2. TCP协议详解

TCP 这东西,说白了就是网络传输的「定海神针」。我做了这么多年网络编程,踩过的坑有一半都跟 TCP 的特性有关。今天咱们就把它的核心机制掰开揉碎,好好聊一聊。

核心要点:TCP 是面向连接的、可靠的、基于字节流的传输层协议。它的可靠性不是白给的,是靠一套复杂的机制换来的。

一、三次握手:建立连接的门槛

为什么是三次?不是两次,也不是四次?我刚开始学的时候也纳闷。后来在项目中调一个连接超时的问题,才真正理解了这背后的设计哲学。

三次握手的本质,是让通信双方都确认自己和对方的收发能力是正常的。

  1. 第一次握手(SYN):客户端发送 SYN 包,告诉服务器「我想跟你说话」。
  2. 第二次握手(SYN+ACK):服务器回复 SYN+ACK,意思是「收到,我也准备好了」。
  3. 第三次握手(ACK):客户端再发 ACK,表示「好,那咱们开始吧」。

我的经验:我曾经遇到过一个问题,客户端频繁发起连接,服务端 SYN 队列满了,导致新连接被丢弃。那时候我才意识到,三次握手不仅仅是协议层面的规定,还涉及到半连接队列和全连接队列的管理。

你想想看,如果只有两次握手,服务器怎么知道客户端收到了自己的确认?万一第一次握手丢了,客户端重传,服务器就得一直等着。三次握手刚好解决了这个「确认的确认」问题。

二、四次挥手:优雅地告别

断开连接比建立连接更复杂。为什么?因为双方可能还有数据没发完。

四次挥手的过程是这样的:

  1. 第一次挥手(FIN):主动方说「我没数据要发了」。
  2. 第二次挥手(ACK):被动方说「知道了,但我还有数据要发」。
  3. 第三次挥手(FIN):被动方说「我也发完了,拜拜」。
  4. 第四次挥手(ACK):主动方说「收到,再见」。

注意:第四次挥手后,主动方会进入 TIME_WAIT 状态,等待 2MSL(最大报文生存时间)后才真正关闭。这是为了防止最后一个 ACK 丢失,被动方重发 FIN 时还能收到确认。

嗯,这里要注意。我在线上遇到过一个问题,服务端大量连接处于 TIME_WAIT 状态,导致端口被占满。后来调整了系统参数 net.ipv4.tcp_tw_reuse 才解决。不过这个参数现在不建议用了,更好的做法是优化业务逻辑,减少短连接。

三、滑动窗口:流量控制的艺术

滑动窗口是 TCP 实现流量控制的核心机制。说白了,就是接收方告诉发送方「你一次别发太多,我消化不了」。

窗口大小是动态调整的。接收方在 ACK 包里会带上自己的窗口大小,发送方根据这个值控制发送速度。

关键点:窗口大小 = 接收方缓冲区剩余空间。如果窗口为 0,发送方就停止发送,直到收到窗口更新通知。

我记得有一次做文件传输服务,发现传输速度忽快忽慢。排查了半天,原来是接收方处理速度跟不上,窗口频繁缩到 0,导致发送方不断暂停。后来优化了接收方的处理逻辑,速度就稳定了。

四、拥塞控制:别把网络堵死了

流量控制管的是「点对点」,拥塞控制管的是「全局」。网络就像一条高速公路,车太多就会堵。TCP 的拥塞控制就是那个智能交通系统。

它主要靠四个算法:

算法 作用 我的理解
慢启动 连接建立后,拥塞窗口从 1 开始,指数增长 先试探一下,别一上来就猛冲
拥塞避免 达到阈值后,线性增长,避免拥塞 稳扎稳打,慢慢加码
快重传 收到 3 个重复 ACK 时,立即重传,不等超时 别傻等,赶紧补发
快恢复 快重传后,将拥塞窗口减半,然后进入拥塞避免 降速但不归零,保留一点余量

避坑指南:我曾经在弱网环境下测试一个实时通信应用,发现 TCP 的拥塞控制导致延迟飙升。后来改用 KCP 或者 QUIC 协议,才解决了这个问题。所以,选协议要看场景。

五、粘包问题:字节流的陷阱

粘包是 TCP 编程中最常见的坑之一。TCP 是流式协议,它不关心你发的是几个包,只关心字节流的顺序。这就导致多个小包可能被合并成一个包发送(Nagle 算法),或者一个包被拆成多个包接收。

怎么解决?我常用的方法有三种:

  • 固定长度:每个消息固定长度,不够的补零。简单粗暴,但浪费带宽。
  • 分隔符:用特殊字符(如 \n)作为消息边界。适合文本协议,但要注意转义。
  • 长度前缀:在消息头部加上长度字段。这是最通用的做法,我推荐用这个。
// 长度前缀的示例(伪代码)
// 发送
byte[] data = "Hello".getBytes();
int length = data.length;
outputStream.write(intToBytes(length)); // 先写长度
outputStream.write(data);               // 再写数据

// 接收
byte[] lengthBytes = inputStream.read(4);
int length = bytesToInt(lengthBytes);
byte[] data = inputStream.read(length);
String message = new String(data);

注意:不要依赖 Nagle 算法的开关来解决粘包问题。关闭 Nagle 算法(设置 TCP_NODELAY)可以减少延迟,但不能完全避免粘包。正确的做法是在应用层做消息边界划分。

我个人习惯用长度前缀的方式,配合一个简单的协议缓冲区。这样既高效又可靠,线上跑了几年没出过问题。

知识体系总览

下面这张图把 TCP 的核心机制串起来了。你可以看到,三次握手和四次挥手管的是连接生命周期,滑动窗口和拥塞控制管的是数据传输效率,粘包问题是应用层需要解决的。

TCP 协议核心机制 连接管理 三次握手 四次挥手 建立连接 → 数据传输 → 断开连接 TIME_WAIT 状态管理 数据传输 滑动窗口 拥塞控制 流量控制(接收方视角) 全局网络状态感知 应用层问题 粘包问题 拆包问题 解决方案 固定长度 | 分隔符 | 长度前缀 | 应用层协议缓冲区

这张图把 TCP 的核心机制分成了三层:连接管理、数据传输和应用层问题。每一层都有对应的解决方案。你想想看,理解了这些,再遇到网络问题就不会慌了。

最后说一句:TCP 虽然复杂,但它的设计非常精妙。每一个机制背后都有实际的问题在驱动。我建议你在写代码的时候,多想想「为什么 TCP 要这么做」,而不是死记硬背。理解了原理,调起参来才有底气。

可可精品资料库