TCP详解
一、什么是 TCP
- TCP 是面向连接的(虚连接、逻辑上的连接)传输层协议
- 提供点对点连接、面向字节流的全双工通信协议
- 提供可靠、无差错、不丢不重、按需到达的协议
二、TCP报文段格式
- 有20字节的必填字段,称为固定首部
固定首部每一行32位(8字节),总共5行,所以总共40字节
- 剩下的是可选字段
- 以及填充字段,保证报文长度是4字节的整数倍
- 源端口:本地端口,即本端发起通信的端口
- 目的端口:远程端口,即对端接收通信的端口
- 序号:在一个tcp连接中传送的每一个字节都会被编号,这个序号代表的是本报文段中第一个字节的编号。(发送方发给接收方)
- 确认号:期望收到的下一个报文段的序号。(接收方发给发送方)
- 数据偏移:代表首部长度。首部长度=偏移值*4字节
- 保留
- 控制位
- URG: 紧急位。URG=1时,代表数据在发送方的缓存中应当被优先发送
- ACK: 确认位。连接建立后所有的报文段都必须设置ACK=1
- PSH: 推送位。PSH=1时,代表数据在接收方的缓存中应当被优先处理
- RST: 复位。RST=1时,代表连接出错,双方需要释放并重新建立连接
- SYN: 同部位。SYN=1时,代表这是一个请求、接收建立连接的报文段
- FIN: 终止位。FIN=1时,代表发送方要释放连接
- 窗口: 发送报文端的一方的接收窗口能接收数据的大小。即能够接收对方发送多大的数据量
- 检验和: 检验首部+数据用的
- 紧急指针: URG=1时,紧急指针记录紧急数据的字节数
- 选项: 一些可选字段。最大报文段长度MSS、窗口扩大、时间戳。。。
- 填充: 保证报文长度是4字节的整数倍
二、连接管理
连接的生命周期: 连接建立->数据传输->连接释放
2.1 连接建立
2.1.1 三次握手
- 客户端: SYN=1, seq=x(随机) 进入syn_sent状态
- 服务端: SYN=1, ACK=1, seq=y, ack =x+1, 由listen状态进入syn_recd状态
- 客户端: ACK=1, ack=y+1,seq=x+1 进入established状态, 此时报文段已经可以携带需要传输的数据了
- 服务端: 进入established状态
2.1.2 SYN洪泛攻击
攻击者只发起第一次握手的报文段,服务端接收到后发送第二次握手的报文段然后处于syn_recd状态。
此时如果攻击者不发送第三次握手的报文段,那么服务端就人为攻击者没有收到第二次握手的报文,那么服务端就会重发第二次握手的报文。
攻击者发起大量这样的攻击的话,就会占用服务端大量的资源,甚至导致服务端挂掉。
解决方法是设置SYN Cookie
2.1.3 为什么是三次握手
- 三次握手才可以阻止重复历史连接的初始化(主要原因)
- 三次握手才可以同步双方的初始序列号
- 三次握手才可以避免资源浪费
我们来看看 RFC 793 指出的 TCP 连接使用三次握手的首要原因: The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion. 简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱
2.2 连接释放
2.2.1 四次挥手
- 客户端: FIN=1, seq=u, 进入FIN_WAIT_1状态
- 服务端: ACK=1, seq=v, ack=u+1, 进入CLOSE_WAIT状态,客户端接收到该报文段后进入FIN_WAIT_2状态
- 服务端: 数据传输
- 服务端: FIN=1, ACK=1, seq=w, ack=u+1, 进入LAST_ACK状态, 客户端接收到该报文段后进入TIME_WAIT
- 客户端: ACK=1, seq=u+1, ack=w+1, 服务端接收到该报文段后进入CLOSED状态
- 客户端: 进入TIME_WAIT后,会等待2MSL(两个最长报文段寿命)的时间,然后进入CLOSED状态
2.2.2 为什么TIME-WAIT要等待2MSL才关闭?
因为要可靠的实现双方的连接释放。
如果服务端第三次挥手的报文段丢失(或者客户端第四次挥手的报文段丢失),导致服务端一段时间内没有收到客户端第四次挥手,此时服务端会重发第三次挥手。
- 那么有2MSL等待的情况下,客户端就会等待,并收到服务端重发的报文段
- 没有的情况下,如果是客户端第四次挥手的报文段丢失,客户端在发送完第四次挥手的报文段后不等待2MSL,直接CLOSED,那么就收不到服务端重发的三次挥手的报文段,服务端就无法正常释放连接。
2.2.3 出现大量TIME-WAIT
短时间内服务间出现大量请求,就会建立非常多的连接。在连接释放时,就会出现大量处于TIME_WAIT状态的连接。如果TIME_WAIT状态的连接过多,就会导致一部分内存的占用(一个TIME_WAIT连接占用4k大小)、占用文件描述符、无法创建新连接。
如何解决?
- 如果是http请求,改用长链接能够有效缓解
- 修改linux相关内核参数
- 打开net.ipv4.tcp_timestamps、net.ipv4.tcp_tw_reuse,调大net.ipv4.tcp_max_tw_buckets
- 如果出现TIME_WAIT过多的是nginx导致的,那就修改内核参数net.ipv4.ip_local_port_range,增大可用端口范围,可以短暂缓解
2.3 查看Linux下的TCP状态
使用netstat -na
三、可靠传输
- 校验: 固定首部中的校验和
- 序号: 固定首部的序号。基于序号可以保证数据的有序传输与接收
- 确认: 固定首部的确认号。接收方接收到报文段后,会将报文段的序号作为确认号,发送确认报文段给发送方,发送方收到后会将已确认发送的数据移除缓冲区
- 重传:
- 冗余确认: 发送给接收端的报文段有丢失,那么接收端会重发确认报文段,收到三个相同的确认报文段,发射端就会立刻重发。
- 超时: 发送发如果一直没有收到确认报文段,那它也会重发要发送的报文段
四、流量控制
让发送方慢点,让接收方来得及接收。TCP使用的是滑动窗口机制实现的流量控制。
在数据传输的过程中,接收方根据自己的缓存大小,动态地调整发送方发送窗口的大小,rwnd接收端窗口大小。
接收端将rwnd设置到确认报文段中传输给发送方,来影响发送方发送窗口的大小。
发送窗口大小= MIN(rwnd接收端接收窗口大小, cwnd拥塞窗口大小)
五、拥塞控制
带宽不够时(需求的资源>可用资源),就需要拥塞控制。
拥塞控制也用的是滑动窗口机制
与流量控制不同的是,接收窗口大小取决于接收端自己的缓存大小,而拥塞窗口的大小取决于所在的网络环境
拥塞控制有四种算法,
- 慢开始、拥塞避免:
- 快重传、快恢复:
5.1 慢开始
最开始拥塞窗口大小是1个最大报文段长度,随着传输轮次的增加,窗口大小指数级增加。
直到窗口大小到达慢开始门限(ssthresh), 由于窗口大小是指数级增加的,为了避免造成网络环境的拥塞,达到ssthresh时就会进入拥塞避免
5.2 拥塞避免
每次传输轮次的增加,拥塞窗口大小增加1个单位。
直到出现了网络拥塞,此时拥塞窗口大小重置为1,ssthresh=当前拥塞窗口大小/2,然后重新开始慢开始
5.3 快重传
收到3个相同的确认报文段,说明有报文段丢失。此时发送方就会立刻重传丢失的报文段,不用等待超时的时候再重传。
5.4 快恢复
跟拥塞避免不同的是,拥塞窗口大小重置当前拥塞窗口大小/2,然后进入拥塞避免算法