TCP 是面向连接、可靠的、基于字节流的传输层协议。
为了达到以上效果,TCP协议有以下规定 数据分片、到达确认、超时重发、滑动窗口、失序处理、重复处理、数据校验。
1. TCP 报文格式
图片源自 wikipedia
- 来源连接端口 \ 目的连接端口。
- 序列号码,一般说的 Seq,是一个相对值,在 TCP 连接过程中赋值。
- 确认号码,一般说的 ACK,也是一个相对值,初始值在 TCP 连接过程中赋值。
- 资料偏移,报文头大小,单位是 4 字节,可以看到使用 4 位,最大值为 15,因此 TCP 头部最多只有 60 字节,固定数据 20 字节,可选项最大 40 字节。
- 控制位/保留位(共12比特),表明报文属性。除了请求连接,ACK 字段必须为 1,因此除了第一个包,每一个包都会携带 ACK 信息。
- 窗口大小,16位,最大值 64K,但是在可选项中有一个缩放因子选项,使用 2 字节表示,最大允许为 14,表示缩放倍数为,因此窗口最大约为 214 + 16 = 2G 大小。
- 校验和,用于确认报文在传输中是否出错。
- 紧急指针,窗口为零时也可发送。
- 可选项
- 窗口扩大选项(type: 3)。见窗口大小
- 时间戳(type: 8)。发送方包含此选项,接收方收到后,将此值复制到应答字段。用于计算 RTT,防止序号绕回。
- NOP(type: 1)。前面说了,头部大小是4字节为单位,不足需要补齐。
- MSS(type: 2)。告知 MSS 大小。
- SACK(type: 4)。支持 SACK 选项。
2. 示例
以下为一次 GET 请求的抓包示例,高亮部分正好是一次重传,和上一个报文相比,只是各校验值和时间戳有变:
截图中第一行内容进行说明:
74 46358 → 80 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM=1 TSval=3532023249 TSecr=0 WS=128
74: 数据包大小,构成是:
- 两个 MAC 地址 0x0800 表示 IPv4 协议,共14字节;
- IP 协议头,20 字节,里面可以看到表示 IP 数据包大小为 60 字节;
- TCP 协议数据,20 字节 TCP 数据头,20 字节可选字段。
46358 -> 80 源端口和目标端口,46358 是客户端,80 是服务端。
[SYN] 表示 TCP 包中的对应标志位为 1。
Seq 序号,Win 窗口大小,Len 数据长度,MSS 最大分段长度,SACK_PERM 支持 SACK,TSval 时间戳,TSecr 对应请求的时间戳,WS 窗口缩放因子。
ACK、SEQ、SYN、FIN 等标志位是以 bit 位形式存在,所以会出现一个数据包包含多种信息。比如说,连接建立过程中的服务端的 SYN+ACK,四次挥手部分情况下优化之后的三次挥手 FIN+ACK
最大传输单元 MTU(Maximum Transmission Unit)
最大分段大小 MSS(Maximum Segment Size)
3. TCP三次握手
- C -> S: SYN SEQ=X(SYN_SEND)截图第一行 Seq 显示为 0,这是一个 SYN 包,Wireshark 将随机的 Seq 视为 0,后续的 Seq 也是计算的相对值。
- C <- S: SYN SEQ=Y ACK=(X+1)(SYN_RECV)截图第二行 Seq 为 0,ACK 为 1,ACK 是相对对方的 Seq 来计算的。ACK 为 1,表示对 SYN 的应答。
- C -> S: ACK=(Y+1) (Established) 截图第三行,Seq 为 1,长度为 0,表示没有数据传输,ACK 为 1,表示对服务端 SYN 的应答。
在 TCP 握手过程中,会将以下参数进行告知对方:
- 交换 MSS 值。
- 滑动窗口大小。
- Seq 初始值,Seq 值减去初始值即为已收到的数据大小
4. TCP 数据传输
记录部分可靠报文的实现,用于帮助理解 TCP/IP 协议的实现。
可靠报文的实现:
1. 停止-等待协议
- 发送一组数据,等待确认,之后发送下一组报文。当一定时间未收到应答,认为数据丢失,进行重发。
- 出现差错。接受方丢弃数据,等待发送方超时重传。
- 超时重传时间。太长,效率低;太短,不必要的重传。
- 发送方发送数据后需要等待接收到应答才删除对应数据。
- 发送方需要对数据分组编号,这样才能根据应答\超时进行重传。
- 接收方收到了重复数据包,需要丢弃数据,同时向发送方发送应答信息。
- 每一组数据发出之后,需要等待确认,一个 RTT(Round Trip Time)只发送一组数据,由此导致信道利用率低下。
这类超时自动发起重传的协议,被称为 自动重传请求 ARQ(Automatic Repeat reQuest)
2. 连续 ARQ 协议
为避免出现一个 RTT 只发送一组数据,可以选择一次发送多组数据,后续会陆续收到应答,通过流水线作业,提高通道的利用率。
比如,有100字节需要传输,每个数据包5字节,每次发送5组数据。这样,接收方收到一组数据之后,陆续返回 5 组数据的应答,这样,一个 RTT 时间加上 4 组数据的处理时间,就成功传输了 5 组数据,以120 km为例,光传输时间需要 4 ms,再加上中间路由的处理,假定总共需要 20 ms,应用层读写数据效率远高于网络传输时间。因此达到了提效的目的。接收方也不需要每接收到一组数据就开始应答,TCP 规定接收方只需要告知发送方最后收到的一组数据编号,由此表明之前的数据全部成功接收。
那么是不是可以一次将所有的数据进行传输呢?一次发送(生产)过多数据,另一端应用层读取(消费)速度过慢,传输层缓存大小有限,无法全部接收,只能丢弃掉数据,发送方未接收到应答包,又会继续重传,这样甚至会导致整个数据链路上的设备(缓存都是有限的)都出现大量的无用数据,数据分发效率降低,导致网络出现拥塞。
3. 滑动窗口
WireShark 截图可以看到,每一个 TCP 数据包都有一个 Win 属性,这个表示的是窗口大小,表示基于此数据包的应答数据,还能接收的数据大小。
窗口的简单理解就是一个首尾双指针,先这么理解,首指针代表刚刚进入的数据,尾指针代表即将移出窗口的数据,当收到应答包,尾指针前移
在 TCP 中,发送方和接收方都存在着对应的缓冲区,比如当前有 100 字节数据需要传输,发送方缓冲区大小为 20字节,已发送 12 字节,此时接收方已确认收到了 10 字节,接收方告知的窗口大小为 15 字节,意味着接收方缓存足够容纳到第 25 字节,那么当前发送方可以将第 10 字节及前面数据清理掉,窗口指向 [11, 30],发送方接下来会将 13-25 字节,当接收方陆续收到数据,会发送应答包告知发送方当前已收到的数据包。
当接收方滑动窗口已填满,接收方告知 Win 为 0,发送方不再发送数据。等待一定时间,发送探测数据包。避免接收方应答包丢失导致互相等待。
注意事项:
- 由于网络滞后,发送窗口和接收窗口大小关系不确定。
- 乱序到达的数据,标准并未规定如何处理。
- TCP 要求接收方必须有累计应答功能。为避免不必要的重传:应答延迟时间不应超过 0.5s。对于一连串的最大长度报文,每隔一个报文段必须发送一个确认。
发送方数据小
由于IPv4 TCP 包数据头 40 字节,如果只传输 1 个字节,会导致传输效率极低。
TCP 数据发送时机:
- 数据大小达到 MSS 大小时发送
- 应用进程指明立即发送
- 计时器
Nagle 算法,如果数据不满一个段,或者是滑动窗口的一半,等接收到 ACK 包再发送。相当于一个 RTT + ACK 延时的计时器。
接收方数据小
应用层每次只处理一个字节,即 Win 大小一直为 1。
接收方等待缓存剩余空间不小于 MSS 或者是滑动窗口的一半,在发送 ACK 包。
4. 拥塞控制
拥塞发生原因:资源需求 > 可用资源
比如说前面提到一次发送所有数据,网络上节点缓存有限,丢掉部分数据,导致后续不停的发送重传数据,加剧网络负载。
AIMD 拥塞控制算法
- 慢开始
- 拥塞避免
- 快速重传
- 快速恢复
慢开始:发送方维持一个拥塞窗口的变量 cwnd(一般为 MSS 大小,RFC 2581规定不能超过 2xMSS)发送窗口等于此值,数据发送之后成功收到应答,cwnd 增加不超过 MSS 大小的值,1个包发送之后,收到应答,cwnd 翻倍,接下来发送数据为两倍,接收到两个应答报文,cwnd 再次翻倍,如此以指数方式增长,慢开始是指启动时的值小,之后指数式增长。如此增长下去,很快就会超过负载,所以还有一个阀值,ssthresh(默认 16*MSS),超过此阀值之后使用拥塞避免方法。
拥塞避免:拥塞避免就是让 cwnd 的增长由指数式增长变为线性增长。每经过一个 RTT 时间,增加 MSS 大小。(一个大概的实现就是每收到一个应答,增加 MSS / cwnd * MSS)
当出现拥塞,即没有及时收到应答,执行以下操作:ssthresh = cwnd / 2,cwnd = 1,重新执行慢开始算法。为什么要从 1 开始?出现拥塞之后,为避免网络情况进一步恶化,使设备有时间处理已有的信息。
快重传:接收方收到乱序报文就发送一个应答,如,第一个包收到,应答,第二个包丢失,第三四五个包收到,重复应答1,这样发送方收到第一个包的应答,就会立即重传第二个包。此时执行 ssthresh = cwnd / 2,cwnd = ssthresh,开始执行拥塞避免算法。因为收到了后续包,认为网络没有那么糟糕。
5. TCP 四次挥手
-
C -> S: FIN
-
C <- S: ACK
-
C <- S: FIN
-
C -> S:ACK
TCP 断开连接通过四次挥手完成,Client 和 Server 分别想对方发送 FIN 指令,接收到之后回复 ACK 应答。FIN 指令的发起没有先后顺序,只是代表自己需要发送的数据已全部完成。协议在设计上允许任何一方先发起断开过程。
发送 FIN 指令之后,并且缓冲区数据已全部发送给应用,应用层再次读取数据,传输层协议,就会告知应用数据已全部接收完成。
套接字删除
由于最后的 FIN 指令对应的 ACK 可以会丢失,FIN 指令会重复收到,此时,为了保证协议栈还有对应的信息来发送 ACK 应答,Socket 不会立马被删除,具体等待时间,协议中没有规定,和重传机制相关,一般时间为2倍最长报文时间 MSL(Maximum Segment Lifetime)。
MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s,可以通过
cat /proc/sys/net/ipv4/tcp_fin_timeout
查看。由于端口会被复用,立马删除套接字,新建立连接正好选择了此端口,重传回来的 FIN 会导致新的套接字执行断开操作。
6. 提问
- TCP 连接中的不同包,经过的路由设备是一样的吗?
TCP 基于 IP 协议完成,IP 协议是基于数据包交换的不可靠协议。在 TCP 传输的过程中,IP 包会出现丢包、延迟、乱序等情况,经过一定时间没有收到对应的 ACK 信息,就会进行重传。IP 通道上的物理设备即便出现掉线,路由器会重新寻找对应的物理链路,重新寻找最短路径,路由重收敛。