# TCP协议

# 一、TCP三次握手

Q1:请详细说明TCP三次握手的过程及其必要性。

参考答案:

# 三次握手过程

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

步骤说明:

  1. 第一次握手:客户端发送 SYN 报文,进入 SYN_SENT 状态

    • SYN=1 表示建立连接
    • seq=x 为初始序列号
  2. 第二次握手:服务端收到 SYN,回复 SYN+ACK,进入 SYN_RCVD 状态

    • SYN=1, ACK=1 表示确认并同意建立连接
    • seq=y 为服务端初始序列号
    • ack=x+1 确认收到客户端的 SYN
  3. 第三次握手:客户端收到 SYN+ACK,发送 ACK,进入 ESTABLISHED 状态

    • ACK=1 表示确认
    • seq=x+1, ack=y+1 确认收到服务端的 SYN

# 为什么是三次而不是两次?

防止已失效的连接请求报文段突然又传送到了服务端

场景:客户端发送的第一个连接请求报文段在网络中滞留,客户端超时重发,建立连接后完成通信关闭连接。之后滞留的旧报文段到达服务端。

  • 两次握手:服务端收到旧报文段,误认为是新连接请求,建立连接等待客户端数据,造成资源浪费
  • 三次握手:服务端收到旧报文段回复 SYN+ACK,客户端根据状态拒绝,避免错误连接

# 二、TCP四次挥手

Q2:为什么TCP断开连接需要四次挥手?

参考答案:

# 四次挥手过程

客户端                                服务端
  |                                     |
  |  1. FIN=1, seq=u                   |
  |------------------------------------>|  (FIN_WAIT_1)
  |                                     |
  |  2. ACK=1, seq=v, ack=u+1          |
  |<------------------------------------|  (CLOSE_WAIT)
  |                                     |
  |  3. FIN=1, seq=w                   |
  |<------------------------------------|  (LAST_ACK)
  |                                     |
  |  4. ACK=1, seq=u+1, ack=w+1        |
  |------------------------------------>|  (TIME_WAIT)
  |                                     |

# 为什么是四次?

TCP是全双工通信,每个方向的连接需要单独关闭

  1. 第一次挥手:客户端发送 FIN,表示不再发送数据(但可接收)
  2. 第二次挥手:服务端回复 ACK,确认收到 FIN
    • 此时服务端可能还有数据要发送给客户端
    • 客户端进入 FIN_WAIT_2 状态,等待服务端发送剩余数据
  3. 第三次挥手:服务端发送 FIN,表示数据发送完毕
  4. 第四次挥手:客户端回复 ACK,确认收到 FIN

# 为什么客户端要等待 2MSL?

MSL(Maximum Segment Lifetime)= 报文最大生存时间

目的:

  1. 保证客户端发送的最后一个 ACK 到达服务端

    • 如果 ACK 丢失,服务端会重发 FIN
    • 客户端在 2MSL 时间内可以收到重发的 FIN 并重发 ACK
  2. 等待网络中所有旧的报文段消失

    • 防止旧连接的报文段干扰新连接

# 三、TCP拥塞控制

Q3:TCP的拥塞控制机制有哪些?请详细说明。

参考答案:

TCP 拥塞控制包含四个核心算法:

# 1. 慢启动(Slow Start)

目的:在连接建立初期,探测网络承载能力

算法

  • 初始拥塞窗口 cwnd = 1 MSS
  • 每收到一个 ACK,cwnd 加倍(指数增长)
  • 当 cwnd 达到慢启动阈值 ssthresh,转为拥塞避免
RTT 1: cwnd = 1
RTT 2: cwnd = 2
RTT 3: cwnd = 4
RTT 4: cwnd = 8

# 2. 拥塞避免(Congestion Avoidance)

目的:避免 cwnd 增长过快导致拥塞

算法

  • 每个 RTT,cwnd 增加 1 MSS(线性增长)
  • 更加保守的增长策略

# 3. 快速重传(Fast Retransmit)

触发条件:收到 3 个重复 ACK

算法

  • 不等待超时,立即重传丢失的报文段
  • ssthresh = cwnd / 2
  • cwnd = ssthresh + 3(加 3 是因为收到 3 个重复 ACK)

# 4. 快速恢复(Fast Recovery)

算法

  • 进入快速恢复状态
  • 每收到一个重复 ACK,cwnd 增加 1
  • 收到新的 ACK,cwnd = ssthresh,转入拥塞避免

# 拥塞控制状态转换图

      ┌─────────────────┐
      │   慢启动        │
      │  (指数增长)      │
      └────────┬────────┘
               │ cwnd >= ssthresh
               ▼
      ┌─────────────────┐
      │  拥塞避免        │
      │  (线性增长)      │
      └────────┬────────┘
               │ 3个重复ACK
               ▼
      ┌─────────────────┐
      │  快速重传/恢复   │
      └─────────────────┘

# 四、TCP与UDP对比

Q4:TCP和UDP有什么区别?各自适用什么场景?

参考答案:

特性 TCP UDP
连接性 面向连接 无连接
可靠性 可靠传输(确认、重传) 不可靠传输
有序性 有序(序号机制) 无序
流量控制 有(滑动窗口)
拥塞控制 有(慢启动、拥塞避免)
传输效率 较低(首部20字节) 较高(首部8字节)
应用场景 文件传输、邮件、HTTP 视频直播、DNS、游戏

# TCP 适用场景

  • 文件传输:FTP、HTTP
  • 邮件传输:SMTP、POP3、IMAP
  • 远程登录:SSH、Telnet
  • 需要可靠性的场景

# UDP 适用场景

  • 实时音视频:视频直播、VoIP
  • DNS 查询:快速响应优先
  • 在线游戏:实时性优先于可靠性
  • 广播/多播:DHCP、IGMP

# 五、TCP粘包/拆包

Q5:什么是TCP粘包/拆包?如何解决?

参考答案:

# 粘包/拆包现象

粘包:多个小数据包被合并成一个大数据包发送

拆包:一个大数据包被拆分成多个小数据包发送

# 产生原因

发送方

  • Nagle 算法:合并小数据包提高效率

接收方

  • 应用层读取速度慢,多个包在缓冲区堆积

# 解决方案

# 1. 固定长度

每个消息固定长度,不足补齐

// 每个消息固定100字节
byte[] buffer = new byte[100];
while (true) {
    int len = inputStream.read(buffer);
    if (len == 100) {
        // 处理完整消息
    }
}

# 2. 分隔符

使用特殊字符分隔消息

// 使用换行符分隔
BufferedReader reader = new BufferedReader(
    new InputStreamReader(inputStream)
);
String line;
while ((line = reader.readLine()) != null) {
    // 处理一行消息
}

# 3. 长度字段

消息头包含消息长度

// 消息格式:[长度(4字节)][内容]
DataInputStream dis = new DataInputStream(inputStream);
while (true) {
    int length = dis.readInt(); // 读取长度
    byte[] data = new byte[length];
    dis.readFully(data); // 读取内容
    // 处理消息
}

# 4. Netty 解决方案

// 使用 LengthFieldPrepender 编码器
pipeline.addLast(new LengthFieldPrepender(4));

// 使用 LengthFieldBasedFrameDecoder 解码器
pipeline.addLast(new LengthFieldBasedFrameDecoder(
    1024,  // 最大帧长度
    0,     // 长度字段偏移
    4,     // 长度字段长度
    0,     // 长度调整值
    4      // 剥离长度字段
));

# 六、TCP保活机制

Q6:TCP的Keep-Alive机制是如何工作的?

参考答案:

# Keep-Alive 参数

# Linux 默认参数
net.ipv4.tcp_keepalive_time = 7200   # 2小时无数据后开始探测
net.ipv4.tcp_keepalive_intvl = 75    # 探测间隔75秒
net.ipv4.tcp_keepalive_probes = 9    # 探测次数9次

# 工作流程

  1. 连接空闲超过 tcp_keepalive_time(2小时)
  2. 发送 Keep-Alive 探测包(ACK包,序列号为当前序列号减一)
  3. 收到 ACK 响应,重置计时器
  4. 未收到响应,每隔 tcp_keepalive_intvl(75秒)重发
  5. 连续失败 tcp_keepalive_probes(9次)后,关闭连接

# 应用层 Keep-Alive

TCP Keep-Alive 的局限性

  • 默认时间太长(2小时)
  • 不能携带应用层数据
  • 中间设备可能不支持

应用层实现

// 心跳机制
public class HeartbeatHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                // 读超时,关闭连接
                ctx.close();
            } else if (event.state() == IdleState.WRITER_IDLE) {
                // 写超时,发送心跳
                ctx.writeAndFlush(new HeartbeatMessage());
            }
        }
    }
}

# 七、TCP滑动窗口

Q7:TCP滑动窗口机制的作用是什么?

参考答案:

# 滑动窗口的作用

  1. 流量控制:防止发送方发送过快导致接收方缓冲区溢出
  2. 提高效率:允许发送方连续发送多个报文段,无需等待每个ACK

# 窗口大小计算

发送窗口 = min(拥塞窗口 cwnd, 接收窗口 rwnd)

# 三种窗口

窗口类型 作用 控制方
发送窗口 限制发送方发送速率 发送方
接收窗口 接收方缓冲区大小 接收方
拥塞窗口 网络拥塞程度 发送方

# 滑动过程

发送方窗口:
+----------+----------+----------+----------+
| 已发送ACK | 已发送未ACK | 可发送   | 不可发送 |
+----------+----------+----------+----------+
           ↑                     ↑
        已确认                 窗口前沿

收到 ACK 后窗口右移:
+----------+----------+----------+----------+
|  已发送ACK  | 已发送未ACK | 可发送   | 不可发送 |
+----------+----------+----------+----------+
                      ↑                     ↑
                   已确认                 窗口前沿

# 八、TCP延迟确认与Nagle算法

Q8:TCP的延迟确认和Nagle算法是什么?

参考答案:

# 延迟确认(Delayed ACK)

目的:减少 ACK 报文数量

规则

  • 收到数据后,等待 200-500ms 再发送 ACK
  • 如果在此期间有数据要发送,ACK 捎带在数据包中
  • 如果收到两个连续的数据包,立即发送 ACK

问题

  • 与 Nagle 算法交互可能导致额外延迟

# Nagle 算法

目的:减少小包数量

规则

  • 如果数据小于 MSS,且连接中有未确认的数据,缓存数据
  • 当收到 ACK 或缓存数据达到 MSS,发送数据

伪代码

if (数据长度 >= MSS || 连接中没有未确认数据) {
    立即发送数据
} else {
    缓存数据,等待ACK或积累到MSS
}

# Nagle + Delayed ACK 问题

场景

客户端                              服务端
  |                                   |
  |  1. 发送小数据包1                 |
  |---------------------------------->|
  |                                   | 等待200ms(延迟ACK)
  |  缓存数据包2(Nagle算法)          |
  |                                   |
  |  2. 延迟ACK(200ms后)            |
  |<----------------------------------|
  |                                   |
  |  3. 收到ACK,发送数据包2          |
  |---------------------------------->|

延迟:200ms(延迟ACK)+ RTT

解决方案

// 禁用 Nagle 算法
socket.setTcpNoDelay(true);

// 或禁用延迟ACK(Linux)
echo 1 > /proc/sys/net/ipv4/tcp_delack_min

# 九、TCP TIME_WAIT 状态

Q9:为什么TIME_WAIT状态需要等待2MSL?

参考答案:

详见"TCP四次挥手"章节。

补充:TIME_WAIT 过多的问题及解决方案

# TIME_WAIT 过多的影响

  • 占用端口资源(客户端)
  • 占用内存和连接表项
  • 新连接可能失败(端口耗尽)

# 解决方案

# 1. 设置 SO_REUSEADDR

ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true); // 允许重用处于TIME_WAIT的端口
serverSocket.bind(new InetSocketAddress(8080));

# 2. 调整系统参数

# 允许TIME_WAIT sockets快速回收
net.ipv4.tcp_tw_recycle = 1  # 已废弃(Linux 4.12+)

# 允许TIME_WAIT sockets重用
net.ipv4.tcp_tw_reuse = 1

# 减少TIME_WAIT时间
net.ipv4.tcp_fin_timeout = 30

# 3. 使用长连接

避免频繁创建和关闭连接


# 十、TCP校验和

Q10:TCP校验和如何计算?

参考答案:

# 计算范围

TCP 校验和覆盖三部分:

  1. 伪首部(12字节)

    • 源IP地址(4字节)
    • 目的IP地址(4字节)
    • 保留(1字节,置0)
    • 协议类型(1字节,TCP=6)
    • TCP长度(2字节)
  2. TCP首部(20字节+选项)

  3. TCP数据

# 计算步骤

// 伪代码
sum = 0;

// 1. 计算伪首部
sum += 源IP地址
sum += 目的IP地址
sum += 协议类型
sum += TCP长度

// 2. 计算TCP首部和数据(按16位累加)
for (16) {
    sum += data[i]
}

// 3. 处理进位
while (sum >> 16) {
    sum = (sum & 0xFFFF) + (sum >> 16)
}

// 4. 取反
checksum = ~sum & 0xFFFF

# 特点

  • 端到端校验:每个路由器不会重新计算
  • 弱校验:只能检测部分错误
  • 必须计算:IPv4 中可选,IPv6 中强制

# 参考资料

  • 《TCP/IP详解 卷1:协议》
  • RFC 793: Transmission Control Protocol
  • RFC 6298: TCP Retransmission Timeout
最后更新时间: 2026-05-06 17:25:23