前言
TCP 的原始规范是 RFC793,其中的一些错误在 RFC1122中被修正
拥塞控制(RFC5681、RFC3782、RFC3517、RFC3390、RFC3168)、重传超时(RFC6298、RFC5682、RFC4015)、连接管理(RFC5482)等特性在后续一系列的 RFC 文档中也进行了补充设计
这篇只是TCP一些经典机制的概括描述, 详细了解和确认建议阅读相关RFC文档和参考资料中的图解
包结构
- source port, destination port 四元组
- sequence number, 解决乱序
- acknowledge number, 解决丢包
- flags, 状态机
- window, 解决流量控制
建立连接和结束连接
3次握手建立连接
握手是为了: 双方初始化自己的seq number并告知对方, 然后返回ACK
ACK和SYN可以合并为一个请求
4次挥手结束连接
挥手是为了: 双方告知对方自己不再发送数据了(但还可以接收数据)
被动方的ACK和FIN不可以合并: 返回ACK表示收到了FIN, 发送FIN表示自己没有数据要发送
期间的时间段内被动方可以继续发送数据, 发完数据再发FIN
状态变化
发出自己的FIN且接收到对方的ACK后, 进入closing状态
收到对方返回的ACK后, 进入time_wait状态, 等待 2 * max segment life 之后进入closed状态
为什么要有time_wait状态而不是直接closed
1. 本次连接的报文可能有延迟到达, 在time_wait状态将本次连接的报文进行丢弃处理, 如果直接closed会把这些报文当成新连接的
2. 如果被动方没有收到主动方返回的ACK, 被动方会尝试重发FIN, 主动方在time_wait状态等待被动方重发FIN
重传机制-解决丢包
超时重传
采样RoundTripTime动态更新timeout
当前Linux使用 Jacobson / Karels 算法, 根据平滑过的SRTT和当前最新RTT计算timeout
快速重传
数据驱动而不是时间驱动: 有数据包丢失的话, 接收方重复返回连续成功接收的最后一个包的ACK, 发送方连续收到3个ACK后重发丢失的数据包, 而不是等待timeout
SACK
接收方使用新字段SACK告知发送方自己成功接收到的包序列, 能减少发送方重传的数量(节约流量)
D-SACK
出现丢包的话, 接收方告知发起方是(发起方发送的数据包丢失)还是(接收方发送的ACK丢失)还是(网络延迟)
滑动窗口
发送窗口
发送方的缓冲区构成
1. 已发送且收到ACK的数据
2. 已发送且未收到ACK的数据
3. 未发送且接收方可以接收的数据(根据接收方返回的window大小判断)
4. 未发送且接收方不可以接收的数据
接收窗口
接收方通过TCP头中的window字段告知发送方自己的缓冲区可以接收的大小(发送方不需要等收到ACK)
流量控制
避免发送方的数据填满接收方的缓存
os缓冲区/滑动窗口
窗口关闭
糊涂窗口
发送的数据包过小会浪费带宽
1. 如果是因为接收方缓冲区可以接收的数据太少, 接收方直接返回0, 等到可以接收的数据超过一定阈值再返回真实大小
2. 如果是因为发送方可以发送的数据太少, 使用nagle算法
1. 可以发送的数据超过一定阈值再发
2. 收到之前数据的ACK再发
拥塞控制
避免发送方的数据填满整个网络
网络发生拥塞的时候, 不能“只考虑自己,重传自己的包”
慢启动
发送方使用cwnd(congestion window)控制, 初始ssthresh = 65535
- 初始为N, 表示一次传N个MSS大小的数据(MaxSegmentSize, TCP定义1MSS=536byte), N跟随论文研究和Linux版本在更新
- 每收到一个ACK, cwnd++(每经过一个RTT, cwnd = cwnd * 2)
拥塞避免
- 如果(cwnd >= ssthresh), 触发 拥塞避免 , 每收到一个ACK, cwnd += 1/cwnd(每经过一个RTT, cwnd++)
拥塞发生
- 如果出现丢包(快速重传发现数据包丢失)(即收到3个后续包的ACK)
- 如果触发超时重传 ssthresh = cwnd / 2, cwnd = 1
- 如果触发快速重传 ssthresh = cwnd / 2, cwnd = cwnd / 2
快速恢复
4.2.1. 触发快速恢复 cwnd = ssthresh + 3
还有很多其他拥塞避免算法
参考资料
TCP 的那些事儿(上) | 酷 壳 - CoolShell
TCP 的那些事儿(下) | 酷 壳 - CoolShell
你还在为 TCP 重传、滑动窗口、流量控制、拥塞控制发愁吗?看完图解就不愁了
机制应用