3. TCP连接管理:三次握手与四次挥手、TCP慢启动、Nagle算法对延迟的影响
这一章咱们聊点实际的。你写 SDK 时,天天和 TCP 打交道,但你真的清楚每一次握手的开销吗?
我最早做即时通讯 SDK 时,总被用户抱怨“发一条消息等半天”。查来查去,发现根本不是业务代码慢,而是 TCP 连接建立和传输过程中的“隐形坑”。
今天就把它扒开,看看三次握手、四次挥手、慢启动和 Nagle 算法到底怎么影响你的网络延迟。
3.1 三次握手:建连的成本你算过吗?
先说个常识:客户端和服务器建立 TCP 连接,必须先“握手”三次。
- SYN:客户端发一个 SYN 包,告诉服务器“我想连你”。
- SYN+ACK:服务器回一个 SYN+ACK,“好的,我同意”。
- ACK:客户端再发 ACK,“收到,咱们开始传数据”。
看起来简单,但每次握手都要走一个 RTT(往返时间)。如果客户端在北京,服务器在硅谷,RTT 150ms,三次握手就花掉 450ms——你想想,用户点个按钮,半秒过去了,还没开始传数据。
我在项目里见过不少 SDK 没有做连接复用,每次请求都新建 TCP 连接。结果一个简单接口的响应时间直接翻倍。后来我强制做了连接池,把建连成本从每次请求里去掉,效果立竿见影。
3.2 四次挥手:断开连接也有“延迟尾巴”
断开连接时,双方要“挥手”四次。原因是 TCP 是全双工的,每一方向都要单独关闭。
- 主动方发 FIN,告诉对方“我这边没数据了”。
- 被动方回 ACK,但可能还有数据要发。
- 被动方发完数据后,再发 FIN,“我也没了”。
- 主动方回 ACK,之后进入 TIME_WAIT 状态,等待 2MSL 才彻底释放。
另外,如果一方直接发 RST 强行终止,可以跳过挥手,但可能丢数据。SDK 里慎用。
3.3 TCP 慢启动:为什么刚开始传得那么慢?
TCP 不知道网络有多快,所以它用一个“慢启动”算法慢慢试探。
- 初始拥塞窗口(cwnd)通常只有 10 个 MSS(约 1460 字节)。
- 每收到一个 ACK,cwnd 翻倍。
- 直到发生丢包或达到慢启动阈值,再进入拥塞避免。
说白了,第一个 RTT 只能发 10 个包,第二个 RTT 发 20 个,第三个发 40 个……指数增长。但如果你只发少量数据(比如一个 GET /api 请求),慢启动还没来得及加速,数据已经传完了。结果就是:大部分时间浪费在握手和慢启动,有效吞吐很低。
| RTT 次数 | 拥塞窗口(包数) | 累计发送数据 | 用时 |
|---|---|---|---|
| 1 | 10 | 14.6 KB | 100 ms |
| 2 | 20 | 43.8 KB | 200 ms |
| 3 | 40 | 102.2 KB | 300 ms |
| 4 | 80 | 219.0 KB | 400 ms |
3.4 Nagle 算法:为了省带宽,却增加了延迟
Nagle 算法的初衷是好的:避免发送大量小包浪费网络。规则很简单:
- 发送端有数据要发,但如果已有一个未被确认的小包,就先攒着,直到收到 ACK 或数据凑满一个 MSS 再发。
- 说白了,就是“等一等,凑够一个包再发”。
但在交互式场景下,这个“等一等”会要命。例如你写了一个 SDK,客户端每敲一个字符就发送一个字节给服务器。如果 Nagle 算法开着,第二个字符可能要等第一个字符的 ACK 回来才能发——额外增加一个 RTT 的延迟。
我在做游戏 SDK 时就遇到过类似问题。玩家移动指令每帧都发一个小包,Nagle 算法导致操作有粘滞感。最后强制 TCP_NODELAY + 自定义包聚合,既控制包数量又保证实时性。
3.5 综合来看:连接管理对延迟的影响链路
把上面三个机制串起来,一个典型的 TCP 请求生命周期大概是这样的:
- 三次握手(1.5 RTT)
- 慢启动(前几个 RTT 吞吐很低)
- 数据传输(受 Nagle 影响可能积累小包)
- 四次挥手(最后产生 TIME_WAIT)
每次握手、每次等待 ACK、每次慢启动的窗口调整,都在消耗你的时间。如果你不做任何优化,一个 RTT 100ms 的网络,一次简单请求可能花掉 300ms 以上。
下面这张图可以帮你理清各个阶段的关系:
每个阶段都可能成为延迟瓶颈。我的建议是:
- 能复用连接就复用,避免重复三次握手和慢启动。
- 交互性要求高的场景,关掉 Nagle(TCP_NODELAY)。
- 控制连接生命周期,避免 TIME_WAIT 堆积。
- 必要时改用 UDP,自己实现可靠传输,彻底绕过 TCP 的握手和慢启动。
嗯,今天就讲到这里。记住,TCP 连接管理不是“底层透明”的,它直接影响你 SDK 的响应速度和吞吐。下个话题会更深入到缓冲区管理和丢包重传——到时候我们再细聊。