第12章:TCP(Transmission Control Protocol)
传输控制协议
一、导读
1.1 本章学习目标
第12章是全书最重要的章节,深入讲解TCP(传输控制协议)——Internet上最重要的传输层协议。TCP提供可靠的、面向连接的字节流服务,是HTTP、FTP、SMTP、SSH等几乎所有应用层协议的传输基础。理解TCP的工作原理,是编写高效网络程序、诊断网络问题、优化网络性能的基石。
本章的核心学习目标包括:
- 掌握TCP报文段的完整格式:每个字段的含义和作用
- 深入理解三次握手建立连接:为什么是三次?每个报文的作用?
- 深入理解四次挥手释放连接:为什么是四次?TIME_WAIT状态的作用?
- 掌握TCP的可靠传输机制:序号、确认、重传、校验和
- 深入理解流量控制:滑动窗口机制如何防止发送方淹没接收方
- 深入理解拥塞控制:慢启动、拥塞避免、快重传、快恢复
1.2 为什么本章如此重要
TCP是Internet上使用最广泛的传输层协议。当你浏览网页、下载文件、收发邮件、远程登录时,底层几乎都在使用TCP。TCP的设计精妙之处在于:它在不可靠的IP服务之上,实现了可靠的数据传输。
理解TCP对于以下方面至关重要:
- 网络编程:理解socket API的行为、处理连接异常
- 性能优化:理解延迟的来源、如何调优TCP参数
- 故障诊断:分析连接建立失败、数据传输中断、性能下降等问题
- 协议分析:使用tcpdump/Wireshark分析TCP交互
1.3 核心问题引导
在阅读本章时,请带着以下问题思考:
TCP三次握手中,如果第三次ACK丢失了,会发生什么?为什么需要三次而不是两次?
TCP的序号和确认号是如何工作的?它们如何保证数据的可靠传输?
滑动窗口的大小是固定的吗?它是如何动态调整的?
TIME_WAIT状态为什么要持续2MSL?如果TIME_WAIT太多会有什么问题?
TCP的拥塞控制是如何感知网络拥塞的?丢包和超时分别触发什么行为?
Nagle算法解决了什么问题?它有什么副作用?
二、核心概念详解
2.1 TCP的特点
TCP提供以下服务:
| 特性 | 说明 |
|---|---|
| 面向连接 | 通信前必须建立连接(三次握手) |
| 点对点 | 每条连接只有两个端点 |
| 可靠交付 | 保证数据无差错、不丢失、不重复、按序到达 |
| 全双工 | 双方可以同时发送和接收 |
| 字节流 | TCP把数据视为无结构的字节流 |
2.2 TCP报文段格式
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |C|E|U|A|P|R|S|F| |
| Offset| Res. |W|C|R|C|S|S|Y|I| Window |
| | |R|E|G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options (if Data Offset > 5) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+2.2.1 关键字段
| 字段 | 长度 | 说明 |
|---|---|---|
| Source Port | 16位 | 源端口号 |
| Destination Port | 16位 | 目的端口号 |
| Sequence Number | 32位 | 序号(发送数据的第一个字节的编号) |
| Acknowledgment Number | 32位 | 确认号(期望收到对方的下一个字节编号) |
| Data Offset | 4位 | 头部长度(以4字节为单位) |
| Window | 16位 | 窗口大小(接收方通告的接收窗口) |
| Checksum | 16位 | 校验和(覆盖头部、数据和伪头部) |
| Urgent Pointer | 16位 | 紧急指针 |
2.2.2 控制标志
| 标志 | 名称 | 说明 |
|---|---|---|
| SYN | Synchronize | 建立连接 |
| ACK | Acknowledge | 确认号有效 |
| FIN | Finish | 释放连接 |
| RST | Reset | 重置连接 |
| PSH | Push | 请求推送数据 |
| URG | Urgent | 紧急指针有效 |
2.3 TCP连接管理
2.3.1 三次握手
TCP使用三次握手(three-way handshake)建立连接:
客户端 服务器
│ │
│ ① SYN (seq=x) │
│ 客户端进入SYN_SENT状态 │
│ ──────────────────────────────────→ │
│ │ 服务器进入SYN_RCVD状态
│ ② SYN+ACK (seq=y, ack=x+1) │
│ ←────────────────────────────────── │
│ │
│ 客户端进入ESTABLISHED状态 │
│ ③ ACK (ack=y+1) │
│ ──────────────────────────────────→ │
│ │ 服务器进入ESTABLISHED状态
│ │
│ 连接建立,可以传输数据 │为什么需要三次握手?
核心原因:同步双方的初始序号(ISN),并确认双方的发送和接收能力。
- 第一次:客户端证明"我能发送",服务器证明"我能接收"
- 第二次:服务器证明"我能发送",客户端证明"我能接收"
- 第三次:服务器确认"我知道你能接收"
如果只有两次握手,服务器无法确认客户端收到了自己的SYN+ACK。如果该报文丢失,服务器以为连接已建立,但客户端认为没有。服务器后续发送的数据会被客户端视为无效报文。
初始序号(ISN)的选择:ISN不是随机的0,而是基于时钟的伪随机数。这是为了防止"旧连接的报文被新连接误收"(segment confusion)。
2.3.2 四次挥手
TCP使用四次挥手(four-way handshake)释放连接。由于TCP是全双工的,每个方向的关闭是独立的:
主动方 被动方
│ │
│ ① FIN (seq=u) │
│ 主动方进入FIN_WAIT_1 │
│ ──────────────────────────────────→ │
│ │ 被动方进入CLOSE_WAIT
│ ② ACK (ack=u+1) │
│ ←────────────────────────────────── │
│ │
│ 主动方进入FIN_WAIT_2 │
│ (被动方可能继续发送数据) │
│ │
│ ③ FIN (seq=w) │
│ ←────────────────────────────────── │
│ │ 被动方进入LAST_ACK
│ 主动方进入TIME_WAIT │
│ ④ ACK (ack=w+1) │
│ ──────────────────────────────────→ │
│ │ 被动方进入CLOSED
│ (等待2MSL后) │
│ 主动方进入CLOSED │为什么需要四次而不是三次?
因为TCP是全双工的。收到FIN只表示对方不再发送数据,但本方还可以继续发送。所以ACK和FIN不能合并(不像三次握手中SYN+ACK可以合并)。
TIME_WAIT状态:
主动关闭方进入TIME_WAIT状态,持续2MSL(Maximum Segment Lifetime,通常为60秒,所以TIME_WAIT持续约120秒)。
原因:
确保最后的ACK到达:如果ACK丢失,被动方会重发FIN,主动方需要重发ACK
等待网络中旧报文消亡:防止新连接收到旧连接的报文
2.4 TCP的可靠传输
2.4.1 序号和确认号
TCP为每个字节编号。序号(Sequence Number)表示本报文段数据的第一个字节的编号。确认号(Acknowledgment Number)表示期望收到对方的下一个字节的编号。
发送方发送:seq=100, 数据100字节(字节100-199)
接收方确认:ack=200(期望收到字节200,表示100-199已全部收到)2.4.2 超时重传
发送方发送数据后启动定时器,如果在超时时间内没有收到确认,就重传数据。
超时时间(RTO, Retransmission Timeout)的计算:
- 基于RTT(往返时间)的估计
- 使用Jacobson/Karels算法动态调整
- RTO = SRTT + max(G, 4×RTTVAR)
2.4.3 快速重传
如果收到3个重复ACK(即同一个ACK收到4次),发送方立即重传丢失的报文段,而不等待超时。
发送方发送:seq=100, seq=200, seq=300, seq=400
接收方收到:100, 300, 400(200丢失)
接收方发送:ack=200, ack=200, ack=200(3个重复ACK)
发送方收到3个重复ack=200 → 立即重传seq=2002.5 流量控制
流量控制防止发送方发送过快,导致接收方缓冲区溢出。
2.5.1 滑动窗口
接收方在TCP头部中的Window字段通告自己的接收窗口大小(rwnd)。发送方维护一个"发送窗口",窗口内的数据可以发送但尚未确认:
发送窗口:
|---已发送未确认---|---可以发送---|---不能发送---|
↑ ↑ ↑
窗口左沿 窗口右沿 序号空间窗口大小 = rwnd - (已发送未确认的字节数)
当窗口为0时,发送方停止发送数据,但会定期发送探测报文(zero window probe)。
2.6 拥塞控制
拥塞控制防止过多的数据注入到网络中,导致路由器或链路过载。
2.6.1 拥塞窗口
TCP维护一个拥塞窗口(cwnd),发送窗口的大小取 rwnd 和 cwnd 的最小值:
发送窗口 = min(rwnd, cwnd)2.6.2 慢启动(Slow Start)
连接建立初期,cwnd从1个MSS开始,每收到一个ACK,cwnd加1。因此cwnd呈指数增长:1→2→4→8→16→...
当cwnd达到慢启动阈值(ssthresh)时,转入拥塞避免阶段。
2.6.3 拥塞避免(Congestion Avoidance)
cwnd超过ssthresh后,每个RTT只增加1个MSS(线性增长)。
2.6.4 快重传和快恢复(Fast Retransmit & Fast Recovery)
收到3个重复ACK时:
立即重传丢失的报文(快重传)
ssthresh = cwnd/2
cwnd = ssthresh + 3(快恢复)
收到丢失报文的ACK后,cwnd = ssthresh
2.6.5 超时事件
超时意味着严重的拥塞:
ssthresh = cwnd/2
cwnd = 1 MSS
重新开始慢启动
2.7 TCP的性能优化
2.7.1 Nagle算法
当发送缓冲区中有少量数据且未确认时,Nagle算法延迟发送,等待更多数据积累,减少小报文的数量。
问题:可能导致延迟增加。TCP_NODELAY 选项可以禁用Nagle算法。
2.7.2 延迟确认
接收方收到数据后不立即发送ACK,等待一小段时间(通常200ms),看是否有数据要一起发送。
2.7.3 MSS协商
三次握手时,双方通告自己的MSS(Maximum Segment Size)。MSS = MTU - IP头部(20) - TCP头部(20) = 1460(以太网)。
三、实战案例
3.1 案例一:使用tcpdump观察三次握手
sudo tcpdump -i eth0 -nn 'tcp port 80'输出示例:
12:34:56.789 IP 192.168.1.100.49152 > 93.184.216.34.80:
Flags [S], seq 1234567890, win 65535, options [mss 1460,sackOK,TS val 100 ecr 0,nop,wscale 7]
12:34:56.815 IP 93.184.216.34.80 > 192.168.1.100.49152:
Flags [S.], seq 987654321, ack 1234567891, win 65535, options [mss 1460,sackOK,TS val 200 ecr 100,nop,wscale 7]
12:34:56.816 IP 192.168.1.100.49152 > 93.184.216.34.80:
Flags [.], ack 987654322, win 512, options [nop,nop,TS val 101 ecr 200]分析:
[S]:SYN,seq=1234567890(客户端ISN)[S.]:SYN+ACK,seq=987654321(服务器ISN),ack=1234567891(确认客户端ISN+1)[.]:ACK,ack=987654322(确认服务器ISN+1)mss 1460:MSS协商为1460字节wscale 7:窗口缩放因子为7(窗口可以扩大到65535×128)
3.2 案例二:使用ss查看TCP连接状态
ss -tunap
# State Recv-Q Send-Q Local Address:Port Peer Address:Port
# ESTAB 0 0 192.168.1.100:49152 93.184.216.34:80
# TIME-WAIT 0 0 192.168.1.100:49153 93.184.216.34:80
# CLOSE-WAIT 1 0 192.168.1.100:49154 93.184.216.34:80常见状态:
ESTABLISHED:连接已建立SYN-SENT:已发送SYN,等待SYN+ACKSYN-RECV:已收到SYN,已发送SYN+ACKFIN-WAIT-1:已发送FIN,等待ACKFIN-WAIT-2:已收到ACK,等待对方FINCLOSE-WAIT:已收到对方FIN,等待本方关闭TIME-WAIT:已发送最后的ACK,等待2MSLLAST-ACK:已发送FIN,等待最后的ACK
3.3 案例三:使用Wireshark分析TCP流
Wireshark的"Follow TCP Stream"功能可以重组完整的TCP数据流,显示应用层数据。还可以查看每个报文段的序号、确认号、窗口大小、RTT等详细信息。
四、常见误区与难点
4.1 误区一:"TCP传输的是报文段,不是字节流"
TCP协议层操作的是字节流,没有"消息边界"。发送方调用两次write各发送100字节,接收方可能一次read读到200字节,也可能分两次各读100字节。应用层需要自己处理消息边界。
4.2 误区二:"CLOSE_WAIT是正常状态"
大量CLOSE_WAIT通常说明应用程序没有正确关闭连接(收到FIN后没有调用close)。这是程序bug。
4.3 难点:理解拥塞控制的四个阶段
慢启动→拥塞避免→快重传→快恢复,四个阶段在不同条件下切换。建议通过Wireshark的TCP序列图来可视化观察cwnd的变化。
五、本章小结
第12章深入讲解了TCP协议:
TCP报文段包含源/目的端口、序号、确认号、窗口大小、控制标志等字段。
三次握手建立连接,同步双方ISN;四次挥手释放连接,双方独立关闭。
可靠传输通过序号、确认、超时重传、快速重传实现。
流量控制使用滑动窗口机制,接收方通告窗口大小。
拥塞控制包括慢启动、拥塞避免、快重传、快恢复四个阶段。
TIME_WAIT持续2MSL,确保最后ACK到达和旧报文消亡。
六、思考与练习
思考题
如果TCP三次握手的第二步(SYN+ACK)丢失了,客户端会怎么处理?服务器会怎么处理?最终连接能建立吗?
TCP的拥塞控制算法中,为什么超时后cwnd回到1而不是ssthresh?这体现了什么设计思想?
如果一台服务器有大量TIME_WAIT状态的连接,可能是什么原因?如何解决?
在卫星链路(RTT≈500ms,带宽100Mbps)上,TCP的吞吐量受什么限制?如何优化?
练习题
使用tcpdump捕获一次完整的TCP三次握手和四次挥手,记录每个报文段的序号、确认号和标志位。
使用Wireshark分析一个TCP流,绘制序号-时间图,观察拥塞控制的行为。
编写一个简单的TCP客户端和服务器,使用strace跟踪系统调用,观察connect/send/recv/close的交互过程。