目录

网络-07 TCP 连接管理

前置阅读:网络-06 UDP 与 TCP 对比 TCP 内容较多,本系列拆成四篇:本篇讲连接的建立与关闭网络-08 讲可靠传输,网络-09 讲流量控制与拥塞控制,网络-10 讲线上调优与故障排查。

1. TCP 是什么

TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

2. 三次握手

先举个形象的例子,两个人AB进行电话沟通,为了保证双向是通的,常进行以下对话:

A:你能听到我说话吗?

B:可以,你能听到吗?

A:我也可以。

这样双方就知道无论是“去”还是“来”都是正常的,就可以聊天了。

第一次握手中seq=i的i值是随机的。

./三次握手.png

2.1 为什么是三次,两次不行吗?

打电话那个类比解释了"确认双向通道都是通的",但没解释清楚为什么两次就一定不够。核心原因有两个:

1. 两次握手无法让服务端确认客户端已收到自己的 SYN+ACK

如果只有两次(客户端 SYN → 服务端 SYN+ACK 就建立连接),服务端发出 SYN+ACK 后就单方面认为连接已建立,开始发数据。但这个 SYN+ACK 可能丢了,客户端根本不知道有这个连接存在,服务端却在白白发送数据。第三次握手的意义就在于让服务端确认"客户端确实收到了我的序列号"

2. 防止历史连接(失效的连接请求)造成资源浪费

这是更本质的原因。假设客户端发出的第一个 SYN 因为网络拥塞在某个路由器里滞留了很久,客户端超时后重发 SYN 并正常完成了通信、关闭了连接。此时那个滞留的旧 SYN 才姗姗来迟地到达服务端:

  • 两次握手:服务端无法分辨这是新请求还是旧的,直接就建立了连接并分配缓冲区,而客户端早就不认这个连接了——服务端的资源就这么被白白占住。
  • 三次握手:服务端回 SYN+ACK 后,客户端发现这个 ACK 号与自己当前的状态对不上(不是自己期待的序列号),会回一个 RST 把这个连接终止掉。服务端因此不会陷入错误状态。

所以三次握手的本质是:用三次交互同步双方的初始序列号(ISN),并让双方都确认对方已经收到了自己的 ISN。这也解释了为什么 ISN 要随机——固定的话就无法区分历史连接,还容易被攻击者猜测并伪造报文注入连接。

2.2 半连接队列与全连接队列

三次握手在内核里对应两个队列,排查线上"连接超时"问题时经常要看:

  • 半连接队列(SYN queue):服务端收到 SYN、回了 SYN+ACK,但还没收到第三次握手的 ACK,此时连接处于 SYN_RCVD 状态,放在这里。大小受 net.ipv4.tcp_max_syn_backlog 影响。SYN Flood 攻击打满的就是这个队列。
  • 全连接队列(accept queue):三次握手已完成,连接处于 ESTABLISHED,等待应用进程调用 accept() 把它取走。大小取 listen(fd, backlog) 的 backlog 与 net.core.somaxconn 的较小值。

队列满了会发生什么?

  • 半连接队列满:新来的 SYN 被丢弃(若开启 tcp_syncookies 则不受此限制,见下文 SYN Flood 一节)。
  • 全连接队列满:Linux 默认不会立即回 RST,而可能忽略客户端最后的 ACK、保留握手请求并重传 SYN+ACK,期待应用尽快 accept() 腾出位置;客户端此时可能已经认为连接建立成功,于是发送的数据得不到正常处理并发生重传,最终表现为连接或请求超时。net.ipv4.tcp_abort_on_overflow=1 时内核会更积极地回 RST,让客户端快速失败,但这通常不是首选方案。

这是一个很典型的故障:应用进程 accept() 太慢(比如业务逻辑阻塞在 accept 循环里),全连接队列堆积溢出,客户端大面积超时,而服务端日志上什么都看不到。排查命令:

# 看全连接队列溢出次数(overflowed 计数持续增长就是它)
netstat -s | grep -i "listen"

# LISTEN 状态下 Recv-Q 是当前 accept 队列长度,Send-Q 是队列上限
ss -lnt

3. 四次挥手

Tcp断开连接时需要4次挥手。 还是先举个例子,两个人进行电话沟通时突然有一个想挂断,一般会有如下对话:

A:我不想说了(第一次挥手。A发送 FIN,表示不A不再说什么事情)

B:好的 (第二次挥手。B发送ACK表示知道了,但是他可能有话没说话,会继续向A说)

B:对了,还有一个事情我要说完…(B没说完话的话继续说)

B:说完了,我也不想说了(第三次挥手。B发送FIN表示他也说完了)

A:好的(第四次挥手。A发送ACK表示知道了,自此完全断开)

在释放连接时,由于TCP是全双工的,因此最后要由两端分别进行关闭,这个流程如下:

假设Client端发起中断连接请求,也就是发送FIN报文。Server端接到FIN报文后,意思是说"我Client端没有数据要发给你了",但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。所以你先发送ACK,“告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息”。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文。当Server端确定数据已发送完成,则向Client端发送FIN报文,“告诉Client端,好了,我这边数据发完了,准备好关闭连接了”。Client端收到FIN报文后,“就知道可以关闭连接了,但是他还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。“,Server端收到ACK后,“就知道可以断开连接了”。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!

关闭连接有主动关闭和被动关闭一说,这里为了简化理解,我们以客户端作为主动关闭方,服务器为被动关闭方。

./四次挥手.png

三次握手,四次挥手完整流程:

./1537948069294.png

整个过程Client端所经历的状态如下:

./pic1.gif

而Server端所经历的过程如下:

./pic2.gif

【注意】 在TIME_WAIT状态中,如果TCP client端最后一次发送的ACK丢失了,它将重新发送。TIME_WAIT状态中所需要的时间是依赖于实现方法的。典型的值为30秒、1分钟和2分钟。等待之后连接正式关闭,并且所有的资源(包括端口号)都被释放。

我们不仅疑问,为啥断开的时候会比握手的时候多一次? 第二次握手时,SYN+ACK其实可以放一起,但是第二次挥手时ACK 和 ACK却不能放一起,因为B也许有话没说完

4. SYN Flood 洪泛攻击

原理:攻击者首先伪造地址对 服务器发起SYN请求,服务器回应(SYN+ACK)包,而真实的IP会认为,我没有发送请求,不作回应。服务 器没有收到回应,这样的话,服务器不知 道(SYN+ACK)是否发送成功,默认情况下会重试5次(tcp_syn_retries)。这样的话,对于服务器的内存,带宽都有很大的消耗。攻击者 如果处于公网,可以伪造IP的话,对于服务器就很难根据IP来判断攻击者,给防护带来很大的困难。

linux内核参数调优主要有下面三个:

  • tcp_max_syn_backlog 从字面上就可以推断出是什么意思。在内核里有个队列用来存放还没有确认ACK的客户端请求,当等待的请求数大于tcp_max_syn_backlog时,后面的会被丢弃。 所以,适当增大这个值,可以在压力大的时候提高握手的成功率。手册里推荐大于1024。
  • tcp_synack_retries 这个是三次握手中,服务器回应 SYN+ACK 给客户端时,重试的次数。默认是5。显然攻击者是不会完成整个三次握手的,因此服务器发出的 SYN+ACK 包在没有回应的情况下,会重试发送。当发送者是伪造IP时,服务器的 SYN+ACK 回应自然是无效的。 这个值可以降低以缩短半连接占用时间,但不应把 0 或 1 当作通用推荐。弱网、跨地域链路上的正常客户端也可能丢失 SYN+ACK,设置过低会直接降低建连成功率。防御 SYN Flood 应优先结合 SYN cookies、边界限速/清洗、队列容量和监控,再通过压测决定是否调整重试次数。
  • tcp_syncookies Linux中SYN cookie是非常巧妙地利用了TCP规范来绕过了TCP连接建立过程的验证过程,从而让服务器的负载可以大大降低。 在三次握手中,当服务器回应(SYN + ACK)包后,客户端要回应一个 n + 1 的 ACK 到服务器,其中 n 是服务器自己指定的初始序列号。启用 tcp_syncookies 后,内核不再把这个半连接放入半连接队列(即不为它保存任何状态),而是把连接的关键信息(源/目的 IP 和端口、时间戳、MSS 等)经过哈希编码,塞进 n 这个序列号本身。当客户端提交第三次握手的 ACK 包时,内核从 ack 号中取出并校验这个 cookie,校验通过就地重建连接状态,认为这是一个合法连接。

这样一来,服务器在握手完成前完全不占用内存,半连接队列被打满也就不再是问题——这正是它能缓解 SYN Flood 的原因。代价是序列号空间有限,装不下 TCP 选项的全部信息,所以开启 syncookies 时部分选项(如窗口扩大因子、SACK)可能协商不上,属于高负载下的降级手段。

5. 本章面试题

为什么是三次握手,两次不行吗?

两个原因:

  1. 两次握手时服务端无法确认客户端收到了自己的 SYN+ACK。服务端发出后就单方面认为连接已建立并开始发数据,但这个包可能丢了,客户端根本不知道有这个连接。
  2. 防止历史连接浪费资源(更本质)。滞留在网络中的旧 SYN 迟到抵达时,两次握手下服务端无法分辨新旧,会直接建连并分配缓冲区;三次握手下客户端发现 ACK 号对不上会回 RST 终止它。

本质是:用三次交互同步双方的初始序列号(ISN),并让双方都确认对方收到了自己的 ISN。这也是 ISN 必须随机的原因——固定的话无法区分历史连接,还容易被伪造报文注入。

为什么挥手要四次,比握手多一次?

因为 TCP 是全双工的,两个方向要独立关闭

握手时服务端的 SYN 和 ACK 可以合并成一个包发出(它没有"还有数据要发"的问题)。而挥手时被动关闭方收到 FIN 后,往往还有数据没发完,所以只能先回 ACK(“我知道了”),等自己的数据发完再单独发 FIN,无法合并。

这中间的状态就是半关闭

什么是半关闭?怎么触发?

TCP 全双工,两个方向可独立关闭。一方发 FIN 表示"我不再发数据了”,但仍可继续接收对方数据——这就是半关闭。

主动触发shutdown(fd, SHUT_WR) 只关写方向(发 FIN),读方向保持可用;这与 close() 同时关闭两个方向不同。

状态上:主动方进入 FIN_WAIT_2,被动方处于 CLOSE_WAIT,此时被动方仍可继续发数据。

如果对方迟迟不发 FIN(常见于代码忘记 close(),表现为 CLOSE_WAIT 堆积),主动方可能长期处于 FIN_WAIT_2。Linux 的 tcp_fin_timeout 主要约束已经脱离应用、成为 orphan 的 FIN_WAIT_2 socket;应用仍持有 fd 时不能指望内核参数替代码收尾。详见 网络-10

TIME_WAIT 为什么要等 2MSL?一个 MSL 行不行?

不行。 MSL 是一个 TCP 报文段在网络中存活的最大时间。

主动关闭方发出最后的 ACK 后,它不知道对方是否收到

  • 若对方没收到,会超时重传 FIN,此时主动方需要还能响应;
  • 若对方收到了,则不会再发任何消息。

两种情况都要等,取最坏情况:去向 ACK 的最大存活时间(1 MSL)+ 来向重传 FIN 的最大存活时间(1 MSL)= 2 MSL

另一个作用:让本次连接的所有残留报文在网络中消亡,避免它们干扰之后复用同一四元组的新连接。如果不等,新连接可能莫名收到上一个连接的 FIN。

半连接队列和全连接队列的区别?队列满了会怎样?
  • 半连接队列(SYN queue):收到 SYN、回了 SYN+ACK,但还没收到第三次握手 ACK,连接处于 SYN_RCVD。受 tcp_max_syn_backlog 影响。SYN Flood 打满的是这个队列。
  • 全连接队列(accept queue):握手已完成、处于 ESTABLISHED,等应用 accept() 取走。大小为 min(listen 的 backlog, net.core.somaxconn)

队列满的后果

  • 半连接满 → 丢弃新 SYN(开了 tcp_syncookies 则不受限);
  • 全连接满 → Linux 默认可能忽略最后 ACK 并重传 SYN+ACK,客户端一侧却可能已进入 ESTABLISHED,随后数据重传并超时;开启 tcp_abort_on_overflow 才会更积极地回 RST。服务端应用日志通常看不到,因为连接还没有被 accept()

排查:ss -lnt 看 Recv-Q 是否顶到 Send-Q,netstat -s | grep listen 看溢出计数。详见 网络-10

RST 是什么?什么情况下会出现?

RST 表示立即终止连接,不走正常的四次挥手。常见成因:

  • 未被监听的端口发送数据(这就是 Connection refused 的来源);
  • 对方已 close() 关闭连接,你还继续发数据;
  • 接收缓冲区仍有未读数据时调用 close(),会发 RST 强制关闭(而非 FIN);
  • 请求超时;
  • 中间设备(防火墙、LB)主动掐断。

排查线上 Connection reset by peer 的关键是判断 RST 是谁发的,方法见 网络-21

SYN Flood 的原理和防御?tcp_syncookies 是怎么工作的?

原理:攻击者用伪造的源 IP 发大量 SYN,服务端回 SYN+ACK 后收不到第三次握手(真实 IP 不会响应),于是半连接队列被占满,并反复重传 SYN+ACK 消耗资源。因为源 IP 是伪造的,很难按 IP 封禁。

防御参数

  • tcp_max_syn_backlog 调大半连接队列;
  • tcp_synack_retries 调小(0 或 1),减少无用重传;
  • tcp_syncookies = 1(最有效)。

syncookies 原理:不再把半连接放入队列(不保存任何状态),而是把连接关键信息(源/目的 IP 端口、时间戳、MSS)哈希编码进初始序列号本身。客户端第三次握手带回 ack 号时,内核从中取出并校验 cookie,通过则就地重建连接。

代价:序列号空间有限,装不下全部 TCP 选项,开启后窗口扩大因子、SACK 等可能协商不上——属于高负载下的降级手段。