跳到主要内容

TCP 协议详解

TCP(Transmission Control Protocol,传输控制协议)是传输层最重要的协议,提供面向连接、可靠的字节流传输服务。理解 TCP 的工作原理对于网络编程和问题排查至关重要。

TCP 概述

TCP 是互联网最基础的传输协议之一,由 RFC 793 定义。它的设计目标是适应支持多网络应用的分层协议层次结构,为应用程序提供可靠的通信服务。

TCP 的核心特性

面向连接:通信前需要建立连接,通信结束后释放连接。就像打电话,需要先拨号建立通话,结束后挂断。

可靠传输:通过确认机制、超时重传、差错校验保证数据正确到达。发送的每个字节都会被确认,丢失的数据会自动重传。

面向字节流:TCP 将数据看作无结构的字节序列,不保留报文边界。应用层发送的多次数据可能被合并发送,也可能被拆分。

全双工通信:连接双方可以同时发送和接收数据。建立连接后,双方都有发送缓冲区和接收缓冲区。

流量控制:通过滑动窗口机制,防止发送方发送过快导致接收方缓冲区溢出。

拥塞控制:根据网络状况动态调整发送速率,避免网络拥塞。

TCP 报文段格式

TCP 首部通常为 20 字节(不含选项),结构如下:

 0               16                              31
+----------------+--------------------------------+
| 源端口 | 目的端口 |
+----------------+--------------------------------+
| 序号 |
+------------------------------------------------+
| 确认号 |
+--------+-------+-------+------------------------+
| 数据偏移| 保留 | 标志位| 窗口大小 |
+----------------+--------------------------------+
| 校验和 | 紧急指针 |
+----------------+--------------------------------+
| 选项(可选) |
+------------------------------------------------+

字段详解

源端口和目的端口(各 16 位):标识发送方和接收方的应用进程。端口号范围 0-65535,其中 0-1023 为知名端口。

序号(32 位):数据段中第一个数据字节的序号。TCP 为每个字节编号,初始序号随机生成。

确认号(32 位):期望收到的下一个字节的序号。表示确认号之前的所有数据都已正确接收。

数据偏移(4 位):TCP 首部长度,以 4 字节为单位。最小值 5(20 字节),最大值 15(60 字节)。

保留(6 位):保留供将来使用,目前必须置 0。

标志位(6 位):

标志名称含义
URG紧急紧急指针有效
ACK确认确认号有效
PSH推送接收方应尽快交付应用层
RST重置重置连接
SYN同步发起连接
FIN结束关闭连接

窗口大小(16 位):接收方的接收窗口大小,用于流量控制。最大值 65535,通过窗口扩大选项可以更大。

校验和(16 位):覆盖 TCP 首部和数据,用于检测传输错误。计算时包含伪首部(源 IP、目的 IP、协议号等)。

紧急指针(16 位):与 URG 标志配合使用,指出紧急数据的结束位置。

选项(可变):常用的选项包括:

  • 最大报文段长度(MSS):TCP 载荷的最大长度,通常为 MTU - 40(IP 首部 + TCP 首部)
  • 窗口扩大因子:扩展窗口大小,最大可达 1GB
  • 时间戳:用于计算 RTT 和防止序号回绕
  • 选择确认(SACK):允许确认不连续的数据块

TCP 连接管理

三次握手建立连接

TCP 建立连接需要三次交互,称为三次握手:

客户端                                    服务器
| |
| SYN=1, seq=x |
|--------------------------------------->| (LISTEN -> SYN_RCVD)
| |
(SYN_SENT) |
| SYN=1, ACK=1, seq=y, ack=x+1 |
|<---------------------------------------|
| |
| ACK=1, seq=x+1, ack=y+1 |
|--------------------------------------->|
| | (ESTABLISHED)
(ESTABLISHED) |

第一次握手:客户端发送 SYN 报文,携带初始序号 x,进入 SYN_SENT 状态。

第二次握手:服务器收到 SYN,回复 SYN+ACK 报文,携带自己的初始序号 y 和确认号 x+1,进入 SYN_RCVD 状态。

第三次握手:客户端收到 SYN+ACK,回复 ACK 报文,确认号 y+1,双方进入 ESTABLISHED 状态。

为什么需要三次握手?

假设只有两次握手,客户端发送的连接请求在网络中滞留,客户端超时重发并完成通信后关闭连接。此时滞留的请求到达服务器,服务器误认为新的连接请求,建立无效连接并等待数据,浪费资源。三次握手可以防止这种情况,因为客户端不会对滞留的请求发送第三次 ACK。

SYN 泛洪攻击

攻击者发送大量 SYN 报文但不完成第三次握手,导致服务器维护大量半开连接,耗尽资源。防御措施包括 SYN Cookie(将连接信息编码到 ISN 中)、SYN Cache(缓存半开连接)等。

四次挥手关闭连接

TCP 关闭连接需要四次交互,称为四次挥手:

客户端                                    服务器
| |
| FIN=1, seq=u |
|--------------------------------------->|
| |
(FIN_WAIT_1) |
| ACK=1, seq=v, ack=u+1 |
|<---------------------------------------|
| |
(FIN_WAIT_2) |
| FIN=1, ACK=1, seq=w, ack=u+1 |
|<---------------------------------------|
| |
| ACK=1, seq=u+1, ack=w+1 |
|--------------------------------------->|
| |
(TIME_WAIT) |
| |
(CLOSED) (CLOSED)

第一次挥手:客户端发送 FIN 报文,表示没有数据要发送了,进入 FIN_WAIT_1 状态。

第二次挥手:服务器收到 FIN,回复 ACK 确认,进入 CLOSE_WAIT 状态。客户端收到 ACK 后进入 FIN_WAIT_2 状态。

第三次挥手:服务器发送 FIN 报文,表示同意关闭,进入 LAST_ACK 状态。

第四次挥手:客户端收到 FIN,回复 ACK,进入 TIME_WAIT 状态。服务器收到 ACK 后进入 CLOSED 状态。

为什么需要四次挥手?

TCP 是全双工通信,每个方向的连接需要单独关闭。服务器收到客户端的 FIN 后,可能还有数据要发送,所以先回复 ACK,等数据发送完毕再发送自己的 FIN。

TIME_WAIT 状态

主动关闭方在发送最后一个 ACK 后进入 TIME_WAIT 状态,等待 2MSL(Maximum Segment Lifetime,通常 2 分钟)后才真正关闭连接。

TIME_WAIT 的作用:

  1. 确保最后的 ACK 到达:如果 ACK 丢失,服务器会重发 FIN,TIME_WAIT 状态可以重发 ACK
  2. 让旧连接的重复数据消失:防止旧连接的延迟数据被新连接接收

大量 TIME_WAIT 连接会占用端口资源,可以通过设置 SO_REUSEADDR 选项复用端口。

连接状态转换

TCP 连接过程中的状态转换:

                              +---------+
| CLOSED |
+---------+
|
passive OPEN
|
+---------+
| LISTEN |
+---------+
主动打开 | 收到 SYN
| v
SYN_SENT <------- SYN_RCVD
| |
收到 SYN+ACK | 收到 ACK
| |
v v
ESTABLISHED -----> ESTABLISHED
| |
主动关闭 | 被动关闭
| |
v v
FIN_WAIT_1 CLOSE_WAIT
| |
收到 ACK | 应用关闭
| |
v v
FIN_WAIT_2 LAST_ACK
| |
收到 FIN | 收到 ACK
| |
v v
TIME_WAIT CLOSED
|
2MSL 超时
|
v
CLOSED

TCP 可靠传输机制

TCP 通过多种机制保证可靠传输。

序号与确认

TCP 为每个字节分配序号,接收方通过确认号告知发送方期望收到的下一个字节序号。这是累积确认,表示确认号之前的所有数据都已正确接收。

发送方发送:seq=1, len=100 的数据(序号 1-100)
接收方回复:ack=101(期望收到序号 101 开始的数据)

超时重传

发送方发送数据后启动定时器,如果在超时前没有收到确认,就重传数据。

超时时间(RTO)的计算

RTO 基于往返时间(RTT)动态计算。经典算法使用加权移动平均:

SRTT = (1 - α) * SRTT + α * RTT    // 平滑 RTT
RTTVAR = (1 - β) * RTTVAR + β * |SRTT - RTT| // RTT 偏差
RTO = SRTT + 4 * RTTVAR

其中 α = 0.125,β = 0.25。

Karn 算法

计算 RTT 时不使用重传的数据包,因为无法确定确认是对原数据包还是重传数据包的响应。

快速重传

超时重传需要等待较长时间,快速重传可以在超时前触发重传。

当接收方收到失序的数据时,会立即发送重复的 ACK,确认号是期望收到的序号。发送方收到三个重复的 ACK 后,立即重传丢失的数据段,不必等待超时。

发送方发送:seq=1, seq=101, seq=201, seq=301
seq=101 丢失
接收方回复:ack=101(对 seq=1 的确认)
ack=101(重复确认,期望 seq=101)
ack=101(重复确认)
ack=101(重复确认)
发送方收到三个重复 ACK,立即重传 seq=101

选择确认(SACK)

传统 TCP 只能使用累积确认,如果中间有数据丢失,后面的数据即使收到也要重传。SACK 允许接收方告知已收到的非连续数据块,发送方只需重传丢失的部分。

SACK 需要在连接建立时通过选项协商。接收方在 ACK 中携带 SACK 选项,列出已收到的数据块。

TCP 流量控制

流量控制防止发送方发送过快导致接收方缓冲区溢出。

滑动窗口

TCP 使用滑动窗口机制实现流量控制。窗口大小表示接收方当前可以接收的数据量。

发送方缓冲区:
+-----+-----+-----+-----+-----+-----+-----+-----+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
+-----+-----+-----+-----+-----+-----+-----+-----+
^ ^ ^
|-------已确认----------|---已发送---|--可发送--|

已确认:1-3
已发送未确认:4-5
可发送:6-7(窗口大小为 4,减去已发送未确认的 2)
不可发送:8

发送方根据接收方通告的窗口大小控制发送速率。接收方在 ACK 中携带窗口大小,发送方据此调整发送窗口。

零窗口与持续定时器

当接收方缓冲区满时,通告窗口大小为 0,发送方停止发送。接收方处理数据后,发送窗口更新通知。

如果窗口更新通知丢失,会导致死锁。发送方使用持续定时器(Persist Timer)定期发送零窗口探测报文(1 字节),触发接收方重新通告窗口大小。

糊涂窗口综合症

如果接收方每次只通告很小的窗口,或发送方每次只发送很少的数据,会导致网络效率低下。这称为糊涂窗口综合症。

解决方案

  • 接收方:不通告小窗口,等到窗口大小达到 MSS 或缓冲区一半时再通告
  • 发送方:Nagle 算法,将小数据包缓存起来,累积到一定量再发送

Nagle 算法

  1. 如果数据长度达到 MSS,立即发送
  2. 如果之前的数据都已确认,立即发送
  3. 否则缓存数据,等待确认到达后再发送

Nagle 算法可以减少小包数量,但会增加延迟。对于实时性要求高的应用,可以通过 TCP_NODELAY 选项禁用。

TCP 拥塞控制

拥塞控制防止过多数据注入网络,导致网络过载。与流量控制不同,拥塞控制是全局性的,考虑整个网络的状况。

拥塞控制算法

TCP 拥塞控制包含四个核心算法:慢启动、拥塞避免、快速重传、快速恢复。

拥塞窗口(cwnd):发送方维护的窗口,表示网络可以承受的数据量。发送方的实际发送窗口取拥塞窗口和接收窗口的较小值。

慢启动阈值(ssthresh):慢启动和拥塞避免的分界点。

慢启动

连接刚建立或超时后,cwnd 从 1 MSS 开始,每收到一个 ACK,cwnd 加倍(指数增长)。

cwnd = 1 MSS
发送 1 个报文段,收到 ACK,cwnd = 2
发送 2 个报文段,收到 2 个 ACK,cwnd = 4
发送 4 个报文段,收到 4 个 ACK,cwnd = 8
...

虽然叫慢启动,但实际上增长很快。慢是相对于一开始就发送大量数据而言。

当 cwnd 达到 ssthresh 时,进入拥塞避免阶段。

拥塞避免

在拥塞避免阶段,cwnd 线性增长。每经过一个 RTT,cwnd 增加 1 MSS。

cwnd = cwnd + MSS * MSS / cwnd

这样可以避免 cwnd 增长过快导致拥塞。

快速重传与快速恢复

当收到三个重复 ACK 时,执行快速重传和快速恢复:

ssthresh = cwnd / 2
cwnd = ssthresh + 3 MSS
重传丢失的报文段
每收到一个重复 ACK,cwnd 增加 1 MSS
收到新的 ACK 时,cwnd = ssthresh,进入拥塞避免

快速恢复避免了超时后 cwnd 从 1 开始的慢启动过程。

超时处理

如果超时,说明拥塞严重:

ssthresh = cwnd / 2
cwnd = 1 MSS
进入慢启动

拥塞控制状态机

              超时
+----------+
| |
v |
+------+ |
| 慢启动|------+
+------+ |
| |
cwnd >= ssthresh|
v |
+----------+ |
| 拥塞避免 |----+
+----------+ |
| |
3 个重复 ACK |
v |
+----------+ |
| 快速恢复 |----+
+----------+

TCP 编程接口

Socket API

使用 TCP 进行网络编程的基本流程:

服务器端

import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('0.0.0.0', 8080))
server_socket.listen(5)
print('服务器启动,等待连接...')

while True:
client_socket, address = server_socket.accept()
print(f'客户端 {address} 已连接')

data = client_socket.recv(1024)
print(f'收到数据: {data.decode()}')

client_socket.send(b'Hello from server')
client_socket.close()

客户端

import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(('127.0.0.1', 8080))

client_socket.send(b'Hello from client')
data = client_socket.recv(1024)
print(f'收到响应: {data.decode()}')

client_socket.close()

Socket 选项

常用的 Socket 选项:

# 地址复用,允许绑定处于 TIME_WAIT 状态的地址
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# 禁用 Nagle 算法,减少延迟
client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

# 设置发送和接收缓冲区大小
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 65536)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536)

TCP 常见问题排查

连接超时

可能原因:

  • 网络不通
  • 防火墙阻止
  • 服务器未启动
  • SYN 队列满

排查方法:

# 测试连通性
ping server_ip

# 检查端口
telnet server_ip port
nc -zv server_ip port

# 查看连接状态
netstat -an | grep port

连接重置

可能原因:

  • 服务器进程崩溃
  • 连接空闲超时
  • 访问被拒绝

排查方法:

# 查看系统日志
dmesg | grep -i tcp

# 抓包分析
tcpdump -i eth0 port 8080

性能问题

可能原因:

  • 窗口太小
  • Nagle 算法延迟
  • 缓冲区太小

优化方法:

# 调整内核参数
echo "net.ipv4.tcp_window_scaling = 1" >> /etc/sysctl.conf
echo "net.core.rmem_max = 16777216" >> /etc/sysctl.conf
echo "net.core.wmem_max = 16777216" >> /etc/sysctl.conf
sysctl -p

小结

TCP 是可靠传输的核心协议:

  1. 报文格式:序号、确认号、标志位、窗口等关键字段
  2. 连接管理:三次握手建立连接,四次挥手关闭连接
  3. 可靠传输:序号确认、超时重传、快速重传、SACK
  4. 流量控制:滑动窗口机制,防止接收方溢出
  5. 拥塞控制:慢启动、拥塞避免、快速重传、快速恢复

理解 TCP 的工作原理,有助于编写高性能的网络程序和排查网络问题。

练习

  1. 描述 TCP 三次握手和四次挥手的详细过程
  2. 解释 TIME_WAIT 状态的作用和可能的问题
  3. 说明 TCP 滑动窗口的工作原理
  4. 分析 TCP 拥塞控制的四个算法