*4.1* *TCP* 三次握⼿与四次挥⼿⾯试题

任 ***\*TCP\**** 虐我千百遍 ,我仍待 ***\*TCP\**** 如初恋。*TIP*
光看不练 ,⾯试怎么能⾏?
如果你想针对计算机⽹络进展专项的模拟⾯试 ,可以⽤⼩林和朋友合作打造的 Γ⽜⾯*AI*⾯ 试」来检测计算机⽹络的掌握情况:
 点击访问:计算机⽹络专项⾯试
难度是对标互联⽹中⼤⼚的 ,问都是⾼频⾯试知识点 ,来⼀场满头⼤汗的模拟⾯试吧!


|  |  |
| ------------------------------------------------------------ | ------------------------------------------------------------ || | |
| | PS:本次⽂章不涉及 TCP 流量控制、拥塞控制、可靠性传输等⽅⾯知识 ,这些知识在这篇:你还在为 TCP 重传、滑动窗⼝、流量控制、拥塞控制发愁吗?看完图解就不愁了 |
| *TCP* 基本认识 | |
| *TCP* 头格式有哪些?我们先来看看 TCP 头的格式 ,标注颜⾊的表⽰与本⽂关联⽐较⼤的字段 ,其他字段不做详细阐 述。 | |
|  | |
| 序列号:在建⽴连接时由计算机⽣成的随机数作为其初始值 ,通过 SYN 包传给接收端主机 ,每发 送⼀次数据 ,就 Γ累加」⼀次该 Γ数据字节数」 的⼤⼩。⽤来解决⽹络包乱序问题。 | |

控制位:
 ACK:该位为 1 时 , Γ确认应答」 的字段变为有效 ,TCP 规定除了最初建⽴连接时的 SYN 包 之外该位必须设置为 1 。
 RST:该位为 1 时 ,表⽰ TCP 连接中出现异常必须强制断开连接。
 SYN:该位为 1 时 ,表⽰希望建⽴连接 ,并在其 Γ序列号」 的字段进⾏序列号初始值的设 定。
 FIN:该位为 1 时 ,表⽰今后不会再有数据发送 ,希望断开连接。 当通信结束希望断开连接 时 ,通信双⽅的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
为什么需要 *TCP* 协议? *TCP* ⼯作在哪⼀层?
IP 层是 Γ不可靠」 的 ,它不保证⽹络包的交付、不保证⽹络包的按序交付、也不保证⽹络包中 的数据的完整性。

如果需要保障⽹络数据包的可靠性 ,那么就需要由上层(传输层) 的 TCP 协议来负责。


什么是 *TCP* ?
TCP 是⾯向连接的、可靠的、基于字节流的传输层通信协议。

 ⾯向连接:⼀定是 Γ ⼀对⼀」才能连接 ,不能像 UDP 协议可以⼀个主机同时向多个主机发送消 息 ,也就是⼀对多是⽆法做到的;
 可靠的:⽆论的⽹络链路中出现了怎样的链路变化 ,TCP 都可以保证⼀个报⽂⼀定能够到达接 收端;
 字节流:⽤⼾消息通过 TCP 协议传输时 ,消息可能会被操作系统 Γ分组」成多个的TCP 报⽂,
如果接收⽅的程序如果不知道 Γ消息的边界」,是⽆法读出⼀个有效的⽤⼾消息的。并且 TCP 报 ⽂是 Γ有序的」, 当 Γ前⼀个」TCP 报⽂没有收到的时候 ,即使它先收到了后⾯的TCP 报⽂ ,那 么也不能扔给应⽤层去处理 ,同时对 Γ重复」 的 TCP 报⽂会⾃动丢弃。
什么是 *TCP* 连接?
我们来看看 RFC 793 是如何定义 Γ连接」 的:
Connections: The reliabil****ity and flow control mechanisms described above require that TCPs
简单来说就是 ,⽤于保证可靠性和流量控制维护的某些状态信息 ,这些信息的组合 ,包括 *Socket*、序列号和窗⼝⼤⼩称为连接。

**initialize and maintain certain status information for eac****h data stream.** **The combination** **of** **this** **information, including sockets, sequence numbers, a****nd window sizes, is called a** **connection.**所以我们可以知道 ,建⽴⼀个 TCP 连接是需要客⼾端与服务端达成上述三个信息的共识。
 *Socket*: 由 IP 地址和端⼝号组成
 序列号:⽤来解决乱序问题等  窗⼝⼤⼩:⽤来做流量控制
如何唯⼀确定⼀个 *TCP* 连接呢?
TCP 四元组可以唯⼀的确定⼀个连接 ,四元组包括如下:
 源地址
 源端⼝
 ⽬的地址  ⽬的端⼝

源地址和⽬的地址的字段(32 位)是在 IP 头部中 ,作⽤是通过 IP 协议发送报⽂给对⽅主机。
源端⼝和⽬的端⼝的字段( 16 位)是在 TCP 头部中 ,作⽤是告诉 TCP 协议应该把报⽂发给哪个进 程。

服务端通常固定在某个本地端⼝上监听 ,等待客户端的连接请求。
因此 ,客户端 IP 和端⼝是可变的 ,其理论值计算公式如下:
最大TCP连接数=客户端的工P数X客户端的端口数;对 IPv4 ,客户端的 IP 数最多为 2 的 32 次⽅ ,客户端的端⼝数最多为 2 的 16 次⽅ ,也就 是服务端单机最⼤ TCP 连接数 ,约为 2 的 48 次⽅。
当然 ,服务端最⼤并发 TCP 连接数远不能达到理论上限 ,会受以下因素影响:
 ⽂件描述符限制 ,每个 TCP 连接都是⼀个⽂件 ,如果⽂件描述符被占满了 ,会发⽣ Too many open files。 Linux 对可打开的⽂件描述符的数量分别作了三个⽅⾯的限制:
。 系统级: 当前系统可打开的最⼤数量 ,通过 cat /proc/sys/fs/file-max 查看;
。 ⽤户级:指定⽤户可打开的最⼤数量 ,通过 cat /etc/security/limits.conf 查看;
。 进程级:单个进程可打开的最⼤数量 ,通过 cat /proc/sys/fs/nr_open 查看;
 内存限制 ,每个 TCP 连接都要占⽤⼀定内存 ,操作系统的内存是有限的 ,如果内存资源被占满 后 ,会发⽣ OOM。
*UDP* 和****TCP**** 有什么区别呢?分别的应⽤场景是?
UDP 不提供复杂的控制机制 ,利⽤ IP 提供⾯向 Γ⽆连接」 的通信服务。
UDP 协议真的⾮常简 ,头部只有 8 个字节(64 位),UDP 的头部格式如下:

 ⽬标和源端⼝ :主要是告诉 UDP 协议应该把报⽂发给哪个进程。
 包⻓度:该字段保存了 UDP ⾸部的⻓度跟数据的⻓度之和。
 校验和:校验和是为了提供可靠的 UDP ⾸部和数据⽽设计 ,防⽌收到在⽹络传输中受损的 UDP 包。
*TCP* 和 *UDP* 区别:
1. 连接
 TCP 是⾯向连接的传输层协议 ,传输数据前先要建⽴连接。
 UDP 是不需要连接 ,即刻传输数据。
2. 服务对象
 TCP 是⼀对⼀的两点服务 ,即⼀条连接只有两个端点。
 UDP ⽀持⼀对⼀ 、⼀对多、多对多的交互通信
3. 可靠性
 TCP 是可靠交付数据的 ,数据可以⽆差错、不丢失、不重复、按序到达。
 UDP 是尽最⼤努⼒交付 ,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现⼀个可靠 的传输协议 ,⽐如 QUIC 协议 ,具体可以参见这篇⽂章:如何基于 *UDP* 协议实现可靠传输?
4. 拥塞控制、 流量控制
 TCP 有拥塞控制和流量控制机制 ,保证数据传输的安全性。
5. ⾸部开销
 TCP ⾸部⻓度较⻓ ,会有⼀定的开销 ,⾸部在没有使⽤ Γ选项」字段时是 20 个字节 ,如果使 ⽤了 Γ选项」字段则会变⻓的。
 UDP ⾸部只有 8 个字节 ,并且是固定不变的 ,开销较⼩。
6. 传输⽅式
 TCP 是流式传输 ,没有边界 ,但保证顺序和可靠。
 UDP 是⼀个包⼀个包的发送 ,是有边界的 ,但可能会丢包和乱序。
7. 分⽚不同
 TCP 的数据⼤⼩如果⼤于 MSS ⼤⼩ ,则会在传输层进⾏分⽚ , ⽬标主机收到后 ,也同样在传输 层组装 TCP 数据包 ,如果中途丢失了⼀个分⽚ ,只需要传输丢失的这个分⽚。
 UDP 的数据⼤⼩如果⼤于 MTU ⼤⼩ ,则会在 IP 层进⾏分⽚ , ⽬标主机收到后 ,在 IP 层组装完 数据 ,接着再传给传输层。
*TCP* 和 *UDP* 应⽤场景:
由于 TCP 是⾯向连接 ,能保证数据的可靠性交付 ,因此经常⽤于:
 FTP ⽂件传输;
 HTTP / HTTPS;
由于 UDP ⾯向⽆连接 ,它可以随时发送数据 ,再加上 UDP 本⾝的处理既简单⼜⾼效 ,因此经常 ⽤于:
 包总量较少的通信 ,如 DNS 、 SNMP 等;
 视频、⾳频等多媒体通信;
 ⼴播通信;

为什么 UDP 头部没有 Γ⾸部⻓度」字段 ,⽽ TCP 头部有 Γ⾸部⻓度」字段呢?
原因是 TCP 有可变⻓的 Γ选项」字段 ,⽽ UDP 头部⻓度则是不会变化的 ,⽆需多⼀个字段去记录 UDP 的⾸部⻓度。

为什么 UDP 头部有 Γ包⻓度」字段 ,⽽ TCP 头部则没有 Γ包⻓度」字段呢?
先说说 TCP 是如何计算负载数据⻓度:

其中 IP 总⻓度 和 IP ⾸部⻓度 ,在 IP ⾸部格式是已知的。TCP ⾸部⻓度 ,则是在 TCP ⾸部格式已 知的 ,所以就可以求得 TCP 数据的⻓度。
⼤家这时就奇怪了问:“UDP 也是基于 IP 层的呀 ,那 UDP 的数据⻓度也可以通过这个公式计算 呀? 为何还要有 Γ包⻓度」 呢?”
这么⼀问 ,确实感觉 UDP 的 Γ包⻓度」是冗余的。
我查阅了很多资料 ,我觉得有两个⽐较靠谱的说法:
 第⼀种说法: 因为为了⽹络设备硬件设计和处理⽅便 ,⾸部⻓度需要是 4 字节的整数倍。如 果去掉 UDP 的 Γ包⻓度」字段 ,那 UDP ⾸部⻓度就不是 4 字节的整数倍了 ,所以我觉得这 可能是为了补全 UDP ⾸部⻓度是 4 字节的整数倍 ,才补充了 Γ包⻓度」字段。
 第⼆种说法:如今的 UDP 协议是基于 IP 协议发展的 ,⽽当年可能并⾮如此 ,依赖的可能是别 的不提供⾃⾝报⽂⻓度或⾸部⻓度的⽹络层协议 ,因此 UDP 报⽂⾸部需要有⻓度字段以供计 算。
*TCP* 和 *UDP* 可以使⽤同⼀个端⼝吗?
答案:可以的。
在数据链路层中 ,通过 MAC 地址来寻找局域⽹中的主机。在⽹际层中 ,通过 IP 地址来寻找⽹络 中互连的主机或路由器。在传输层中 ,需要通过端⼝进⾏寻址 ,来识别同⼀计算机中同时通信的 不同应⽤程序。
所以 ,传输层的 Γ端⼝号」 的作⽤ ,是为了区分同⼀个主机上不同应⽤程序的数据包。
传输层有两个传输协议分别是 TCP 和 UDP ,在内核中是两个完全独⽴的软件模块。
当主机收到数据包后 ,可以在 IP 包头的 Γ协议号」字段知道该数据包是 TCP/UDP ,所以可以根据 这个信息确定送给哪个模块(TCP/UDP)处理 ,送给 TCP/UDP 模块的报⽂根据 Γ端⼝号」确定送 给哪个应⽤程序处理。
因此 ,TCP/UDP 各⾃的端⼝号也相互独⽴ ,如 TCP 有⼀个 80 号端⼝ ,UDP 也可以有⼀个 80 号 端⼝ ,⼆者并不冲突。
关于端⼝的知识点 ,还是挺多可以讲的 ,⽐如还可以牵扯到这⼏个问题:
 多个 TCP 服务进程可以同时绑定同⼀个端⼝吗?
|  |  |
|  客户端 TCP 连接 TIME_WAIT 状态过多 ,会导致端⼝资源耗尽⽽⽆法建⽴新的连接吗? 上⾯这些问题 ,可以看这篇⽂章:TCP 和 UDP 可以使⽤同⼀个端⼝吗?*TCP* 连接建⽴ *TCP* 三次握⼿过程是怎样的?TCP 是⾯向连接的协议 ,所以使⽤ TCP 前必须先建⽴连接 ,⽽建⽴连接是通过三次握⼿来进⾏ 的。三次握⼿的过程如下图: | |
|  | |
|  ⼀开始 ,客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端⼝ ,处于 LISTEN 状态 | |

 客户端会随机初始化序号( client_isn ),将此序号置于 TCP ⾸部的 Γ序号」字段中 ,同时把 SYN 标志位置为 1 ,表⽰ SYN 报⽂。接着把第⼀个 SYN 报⽂发送给服务端 ,表⽰向服务 端发起连接 ,该报⽂不包含应⽤层数据 ,之后客户端处于 SYN-SENT 状态。

服务端收到客户端的 SYN 报⽂后 ,⾸先服务端也随机初始化⾃⼰的序号( server_isn ),将 此序号填⼊ TCP ⾸部的 Γ序号」字段中 ,其次把 TCP ⾸部的 Γ确认应答号」字段填⼊



 客⼾端收到服务端报⽂后 ,还要向服务端回应最后⼀个应答报⽂ ,⾸先该应答报⽂ TCP ⾸部 ACK 标志位置为 1 ,其次 Γ确认应答号」字段填⼊ server_isn + 1 ,最后把报⽂发送给 服务端 ,这次报⽂可以携带客⼾到服务端的数据 ,之后客⼾端处于 ESTABLISHED 状态。
 服务端收到客⼾端的应答报⽂后 ,也进⼊ ESTABLISHED 状态。
| ------------------------------------------------------------ | ------------------------------------------------------------ |从上⾯的过程可以发现第三次握⼿是可以携带数据的 ,前两次握⼿是不可以携带数据的 ,这也是 ⾯试常问的题。
⼀旦完成三次握⼿ ,双⽅都处于 ESTABLISHED 状态 ,此时连接就已建⽴完成 ,客⼾端和服务端就 可以相互发送数据了。
如何在 *Linux* 系统中查看 *TCP* 状态?
TCP 的连接状态查看 ,在 Linux 可以通过 netstat -napt 命令查看。



为什么是三次握⼿?不是两次、 四次?
相信⼤家⽐较常回答的是:“ 因为三次握⼿才能保证双⽅具有接收和发送的能⼒。”
这回答是没问题 ,但这回答是⽚⾯的 ,并没有说出主要的原因。
在前⾯我们知道了什么是 *TCP* 连接:
 ⽤于保证可靠性和流量控制维护的某些状态信息 ,这些信息的组合 ,包括 *Soc**ket*、序列号和窗 ⼝⼤⼩称为连接。
所以 ,重要的是为什么三次握⼿才可以初始化 *Socket*、序列号和窗⼝⼤⼩并建⽴ *TCP* 连接。
接下来 ,以三个⽅⾯分析三次握⼿的原因:
 三次握⼿才可以阻⽌重复历史连接的初始化(主要原因)  三次握⼿才可以同步双⽅的初始序列号
 三次握⼿才可以避免资源浪费
原因⼀: 避免历史连接
我们来看看 RFC 793 指出的TCP 连接使⽤三次握⼿的⾸要原因:
**The principle reason for the three-way handshake is to preven****t old duplicate connection initiations** **from causing confusion.**简单来说 ,三次握⼿的⾸要原因是为了防⽌旧的重复连接初始化造成混乱。
看看三次握⼿是如何阻⽌历史连接的:
客户端连续发送多次 SYN(都是同⼀个四元组)建⽴连接的报⽂ ,在⽹络拥堵情况下:

 客⼾端收到后 ,发现⾃⼰期望收到的确认号应该是 100 + 1 ,⽽不是 90 + 1 ,于是就会回 RST 报⽂。
 服务端收到 RST 报⽂后 ,就会释放连接。
 后续最新的 SYN 抵达了服务端后 ,客⼾端与服务端就可以正常的完成三次握⼿了。
我们考虑⼀个场景 ,客⼾端先发送了 SYN(seq = 90)报⽂ ,然后客⼾端宕机了 ,⽽且这个 SYN 报⽂还被⽹络阻塞了 ,服务端并没有收到 ,接着客⼾端重启后 ,⼜重新向服务端建⽴连接 ,发送 了 SYN(seq = 100)报⽂( **注意!不是重传** **SYN** **,** **重传的** **SYN** **的序列号是⼀样的**)。上述中的 Γ旧 SYN 报⽂」称为历史连接 ,TCP 使⽤三次握⼿建⽴连接的最主要原因就是防⽌ Γ历 史连接」初始化了连接。

*TIP*
有很多⼈问 ,如果服务端在收到 RST 报⽂之前 ,先收到了 Γ新 SYN 报⽂」,也就是服务端 收到客⼾端报⽂的顺序是: Γ旧 SYN 报⽂」 - > Γ新 SYN 报⽂」,此时会发⽣什么?
当服务端第⼀次收到 SYN 报⽂ ,也就是收到 Γ旧 SYN 报⽂」 时 ,就会回复 SYN + ACK 报⽂给客⼾端 ,此报⽂中的确认号是 91(90+1)。
然后这时再收到 Γ新 SYN 报⽂」 时 ,就会回*Challenge* *Ack*报⽂给客⼾端 ,这个 *ack* 报 ⽂并不是确认收到 Γ新 *SYN* 报⽂」 的 ,⽽是上⼀次的 *ack* 确认号 ,也就是91(90+1)。所 以客⼾端收到此 ACK 报⽂时 ,发现⾃⼰期望收到的确认号应该是 101 ,⽽不是 91 ,于是 就会回 RST 报⽂。
如果是两次握⼿连接 ,就⽆法阻⽌历史连接 ,那为什么 TCP 两次握⼿为什么⽆法阻⽌历史连接 呢?
我先直接说结论 ,主要是因为在两次握⼿的情况下 ,服务端没有中间状态给客户端来阻⽌历史连 接 ,导致服务端可能建⽴⼀个历史连接 ,造成资源浪费。
你想想 ,在两次握⼿的情况下 ,服务端在收到 SYN 报⽂后 ,就进⼊ ESTABLISHED 状态 ,意味着这 时可以给对⽅发送数据 ,但是客⼾端此时还没有进⼊ ESTABLISHED 状态 ,假设这次是历史连接,
客⼾端判断到此次连接为历史连接 ,那么就会回 RST 报⽂来断开连接 ,⽽服务端在第⼀次握⼿的 时候就进⼊ ESTABLISHED 状态 ,所以它可以发送数据的 ,但是它并不知道这个是历史连接 ,它只 有在收到 RST 报⽂后 ,才会断开连接。
可以看到 ,如果采⽤两次握⼿建⽴ TCP 连接的场景下 ,服务端在向客户端发送数据前 ,并没有阻 ⽌掉历史连接 ,导致服务端建⽴了⼀个历史连接 ,⼜⽩⽩发送了数据 ,妥妥地浪费了服务端的资 源。
因此 ,要解决这种现象 ,最好就是在服务端发送数据前 ,也就是建⽴连接之前 ,要阻⽌掉历史连 接 ,这样就不会造成资源浪费 ,⽽要实现这个功能 ,就需要三次握⼿。
所以 ,*TCP* 使⽤三次握⼿建⽴连接的最主要原因是防⽌ Γ历史连接」初始化了连接。

*TIP*

不是的 ,即使服务端还是在 syn_received 状态 ,收到了客⼾端发送的数据 ,还是可以建⽴ 连接的 ,并且还可以正常收到这个数据包。这是因为数据报⽂中是有 ack 标识位 ,也有确 认号 ,这个确认号就是确认收到了第⼆次握⼿ 。如下图:
所以 ,服务端收到这个数据报⽂ ,是可以正常建⽴连接的 ,然后就可以正常接收这个数据 包了。
原因⼆: 同步双⽅初始序列号
TCP 协议的通信双⽅ , 都必须维护⼀个 Γ序列号」, 序列号是可靠传输的⼀个关键因素 ,它的作 ⽤ :
接收⽅可以去除重复的数据;
接收⽅可以根据数据包的序列号按序接收;
可以标识发送出去的数据包中 , 哪些是已经被对⽅收到的(通过 ACK 报⽂中的序列号知道);
可见 ,序列号在 TCP 连接中占据着⾮常重要的作⽤ ,所以当客⼾端发送携带 Γ初始序列号」 的
SYN 报⽂的时候 ,需要服务端回⼀个 ACK 应答报⽂ ,表⽰客⼾端的 SYN 报⽂已被服务端成功 接收 ,那当服务端发送 Γ初始序列号」给客⼾端的时候 ,依然也要得到客⼾端的应答回应 ,这样 ⼀来⼀回 ,才能确保双⽅的初始序列号能被可靠的同步。

四次握⼿其实也能够可靠的同步双⽅的初始化序号 ,但由于第⼆步和第三步可以优化成⼀步 ,所 以就成了 Γ三次握⼿」。
⽽两次握⼿只保证了⼀⽅的初始序列号能被对⽅成功接收 ,没办法保证双⽅的初始序列号都能被 确认接收。
原因三:避免资源浪费
如果只有 Γ两次握⼿」, 当客⼾端发⽣的 SYN 报⽂在⽹络中阻塞 ,客⼾端没有接收到 ACK 报 ⽂ ,就会重新发送 SYN ,由于没有第三次握⼿ ,服务端不清楚客⼾端是否收到了⾃⼰回复的 *ACK* 报⽂ ,所以服务端每收到⼀个 *SYN* 就只能先主动建⽴⼀个连接 ,这会造成什么情况呢?
如果客⼾端发送的 SYN 报⽂在⽹络中阻塞了 ,重复发送多次 SYN 报⽂ ,那么服务端在收到请求 后就会建⽴多个冗余的⽆效链接 ,造成不必要的资源浪费。


即两次握⼿会造成消息滞留情况下 ,服务端重复接受⽆⽤的连接请求 SYN 报⽂ ,⽽造成重复分 配资源。

*TIP*
很多⼈问 ,两次握⼿不是也可以根据上下⽂信息丢弃 syn 历史报⽂吗?
我这⾥两次握⼿是假设 Γ由于没有第三次握⼿ ,服务端不清楚客⼾端是否收到了⾃⼰发送 的建⽴连接的 ACK 确认报⽂ ,所以每收到⼀个 SYN 就只能先主动建⽴⼀个连接」这个 场景。
当然你要实现成类似三次握⼿那样 ,根据上下⽂丢弃 syn 历史报⽂也是可以的 ,两次握⼿ 没有具体的实现 ,怎么假设都⾏。
⼩结
TCP 建⽴连接时 ,通过三次握⼿能防⽌历史连接的建⽴ ,能减少双⽅不必要的资源开销 ,能帮助双 ⽅同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
不使⽤ Γ两次握⼿」和 Γ四次握⼿」 的原因:
 Γ两次握⼿」:⽆法防⽌历史连接的建⽴ ,会造成双⽅资源的浪费 ,也⽆法可靠的同步双⽅序列 号;
 Γ四次握⼿」:三次握⼿就已经理论上最少可靠连接建⽴ ,所以不需要使⽤更多的通信次数。
为什么每次建⽴ *TCP* 连接时 ,初始化的序列号都要求不⼀样呢?
主要原因有两个⽅⾯:
 为了防⽌历史报⽂被下⼀个相同四元组的连接接收(主要⽅⾯);
 为了安全性 ,防⽌⿊客伪造的相同序列号的 TCP 报⽂被对⽅接收;
接下来 ,详细说说第⼀点。
假设每次建⽴连接 ,客⼾端和服务端的初始化序列号都是从 0 开始:
过程如下:
 客⼾端和服务端建⽴⼀个 TCP 连接 ,在客⼾端发送数据包被⽹络阻塞了 ,然后超时重传了这个 数据包 ,⽽此时服务端设备断电重启了 ,之前与客⼾端建⽴的连接就消失了 ,于是在收到客⼾ 端的数据包的时候就会发送 RST 报⽂。
 紧接着 ,客⼾端⼜与服务端建⽴了与上⼀个连接相同四元组的连接;
 在新连接建⽴完成后 ,上⼀个连接中被⽹络阻塞的数据包正好抵达了服务端 ,刚好该数据包的 序列号正好是在服务端的接收窗⼝内 ,所以该数据包会被服务端正常接收 ,就会造成数据错
乱。


如果每次建⽴连接客⼾端和服务端的初始化序列号都 Γ不⼀样」,就有⼤概率因为历史报⽂的序列 号 Γ不在」对⽅接收窗⼝ ,从⽽很⼤程度上避免了历史报⽂ ,⽐如下图:

相反 ,如果每次建⽴连接客⼾端和服务端的初始化序列号都 Γ ⼀样」,就有⼤概率遇到历史报⽂的 序列号刚 Γ好在」对⽅的接收窗⼝内 ,从⽽导致历史报⽂被新连接成功接收。
所以 ,每次初始化序列号不⼀样很⼤程度上能够避免历史报⽂被下⼀个相同四元组的连接接收 , 注意是很⼤程度上 ,并不是完全避免了( 因为序列号会有回绕的问题 ,所以需要⽤时间戳的机制 来判断历史报⽂ ,详细看篇:*TCP* 是如何避免历史报⽂的?)。
初始序列号 *ISN* 是如何随机产⽣的?
起始 ISN 是基于时钟的 ,每 4 微秒 + 1 ,转⼀圈要 4.55 个⼩时。


o M 是⼀个计时器,这个计时器每隔 4 微秒加 1。
 F 是⼀个 Hash 算法 ,根据源 IP、 ⽬的 IP、源端⼝ 、 ⽬的端⼝⽣成⼀个随机数值。要保证 Hash 算法不能被外部轻易推算得出 ,⽤ MD5 算法是⼀个⽐较好的选择。
可以看到 ,随机数是会基于时钟计时器递增的 ,基本不可能会随机成⼀样的初始化序列号。
既然 *IP* 层会分⽚ ,为什么 *TCP* 层还需要 *MSS* 呢?
我们先来认识下 MTU 和 MSS

 MTU :⼀个⽹络包的最⼤⻓度 ,以太⽹中⼀般为 1500 字节;
 MSS :除去 IP 和 TCP 头部之后 ,⼀个⽹络包所能容纳的 TCP 数据的最⼤⻓度;
如果在 TCP 的整个报⽂(头部 + 数据) 交给 IP 层进⾏分⽚ ,会有什么异常呢?
当 IP 层有⼀个超过 MTU ⼤⼩的数据(TCP 头部 + TCP 数据) 要发送 ,那么 IP 层就要进⾏分⽚ , 把数据分⽚成若⼲⽚ ,保证每⼀个分⽚都⼩于 MTU。把⼀份 IP 数据报进⾏分⽚以后 , 由⽬标主机 的 IP 层来进⾏重新组装后 ,再交给上⼀层 TCP 传输层。
这看起来井然有序 ,但这存在隐患的 ,那么当如果⼀个 *IP* 分⽚丢失 ,整个 *IP* 报⽂的所有分⽚都得 重传。
因为 IP 层本⾝没有超时重传机制 ,它由传输层的TCP 来负责超时和重传。
当某⼀个 IP 分⽚丢失后 ,接收⽅的 IP 层就⽆法组装成⼀个完整的TCP 报⽂(头部 + 数据),也就 ⽆法将数据报⽂送到 TCP 层 ,所以接收⽅不会响应 ACK 给发送⽅ ,因为发送⽅迟迟收不到 ACK
确认报⽂ ,所以会触发超时重传 ,就会重发 Γ整个 TCP 报⽂(头部 + 数据)」。

所以 ,为了达到最佳的传输效能 TCP 协议在建⽴连接的时候通常要协商双⽅的 *MSS* 值 , 当 TCP 层发现数据超过 MSS 时 ,则就先会进⾏分⽚ , 当然由它形成的 IP 包的⻓度也就不会⼤于 MTU ,
⾃然也就不⽤ IP 分⽚了。

经过 TCP 层分⽚后 ,如果⼀个 TCP 分⽚丢失后 ,进⾏重发时也是以 *MSS* 为单位 ,⽽不⽤重传所 有的分⽚ ,⼤⼤增加了重传的效率。
第⼀次握⼿丢失了 ,会发⽣什么?
当客⼾端想和服务端建⽴ TCP 连接的时候 ,⾸先第⼀个发的就是 SYN 报⽂ ,然后进⼊到 SYN_SENT 状态。
在这之后 ,如果客⼾端迟迟收不到服务端的 SYN-ACK 报⽂(第⼆次握⼿),就会触发 Γ超时重传」 机制 ,重传 SYN 报⽂ ,⽽且重传的 *SYN* 报⽂的序列号都是⼀样的。
不同版本的操作系统可能超时时间不同 ,有的 1 秒的 ,也有 3 秒的 ,这个超时时间是写死在内核 ⾥的 ,如果想要更改则需要重新编译内核 ,⽐较⿇烦。
当客⼾端在 1 秒后没收到服务端的 SYN-ACK 报⽂后 ,客⼾端就会重发 SYN 报⽂ ,那到底重发⼏ 次呢?
在 Linux ⾥ ,客⼾端的 SYN 报⽂最⼤重传次数由 tcp_syn_retries 内核参数控制 ,这个参数是可 以⾃定义的 ,默认值⼀般是 5。
\# cat /proc/sys/net/ipv4/tcp_syn_retries5
通常 ,第⼀次超时重传是在 1 秒后 ,第⼆次超时重传是在 2 秒 ,第三次超时重传是在 4 秒后 ,第 四次超时重传是在 8 秒后 ,第五次是在超时重传 16 秒后。没错 ,每次超时的时间是上⼀次的 *2* 倍。
当第五次超时重传后 ,会继续等待 32 秒 ,如果服务端仍然没有回应 ACK ,客⼾端就不再发送 SYN 包 ,然后断开 TCP 连接。
所以 ,总耗时是 1+2+4+8+16+32=63 秒 ,⼤约 1 分钟左右。



具体过程:

(SYN-ACK 报⽂),那么客⼾端就会断开连接。
第⼆次握⼿丢失了 ,会发⽣什么?
当服务端收到客⼾端的第⼀次握⼿后 ,就会回 SYN-ACK 报⽂给客⼾端 ,这个就是第⼆次握⼿ ,此 时服务端会进⼊ SYN_RCVD 状态。
第⼆次握⼿的 SYN-ACK 报⽂其实有两个⽬的 :
 第⼆次握⼿⾥的 ACK , 是对第⼀次握⼿的确认报⽂;
 第⼆次握⼿⾥的 SYN ,是服务端发起建⽴ TCP 连接的报⽂;
所以 ,如果第⼆次握⼿丢了 ,就会发⽣⽐较有意思的事情 ,具体会怎么样呢?
因为第⼆次握⼿报⽂⾥是包含对客⼾端的第⼀次握⼿的 ACK 确认报⽂ ,所以 ,如果客⼾端迟迟没 有收到第⼆次握⼿ ,那么客⼾端就觉得可能⾃⼰的 SYN 报⽂(第⼀次握⼿) 丢失了 ,于是客户端 就会触发超时重传机制 ,重传 *SYN* 报⽂。
然后 ,因为第⼆次握⼿中包含服务端的 SYN 报⽂ ,所以当客⼾端收到后 ,需要给服务端发送 ACK 确认报⽂(第三次握⼿),服务端才会认为该 SYN 报⽂被客⼾端收到了。
那么 ,如果第⼆次握⼿丢失了 ,服务端就收不到第三次握⼿ ,于是服务端这边会触发超时重传机 制 ,重传 *SYN-ACK* 报⽂。
在 Linux 下 ,SYN-ACK 报⽂的最⼤重传次数由 tcp_synack_retries 内核参数决定 ,默认值是 5。
\# cat /proc/sys/net/ipv4/tcp_synack_retries5
因此 , 当第⼆次握⼿丢失了 ,客⼾端和服务端都会重传:
 客⼾端会重传 SYN 报⽂ ,也就是第⼀次握⼿ ,最⼤重传次数由 tcp_syn_retries 内核参数决 定;
 服务端会重传 SYN-ACK 报⽂ ,也就是第⼆次握⼿ ,最⼤重传次数由 tcp_synack_retries 内核 参数决定。
举个例⼦ ,假设 tcp_syn_retries 参数值为 1 ,tcp_synack_retries 参数值为 2 ,那么当第⼆次握⼿ ⼀ 直丢失时 ,发⽣的过程如下图:
具体过程:
 当客户端超时重传 1 次 SYN 报⽂后 , 由于 tcp_syn_retries 为 1 ,已达到最⼤重传次数 ,于是再 等待⼀段时间( 时间为上⼀次超时时间的 2 倍),如果还是没能收到服务端的第⼆次握⼿
(SYN-ACK 报⽂),那么客户端就会断开连接。
 当服务端超时重传 2 次 SYN-ACK 报⽂后 , 由于 tcp_synack_retries 为 2 ,已达到最⼤重传次
数 ,于是再等待⼀段时间( 时间为上⼀次超时时间的 2 倍),如果还是没能收到客户端的第三次 握⼿ (ACK 报⽂),那么服务端就会断开连接。


第三次握⼿丢失了 ,会发⽣什么?
客⼾端收到服务端的 SYN-ACK 报⽂后 ,就会给服务端回⼀个 ACK 报⽂ ,也就是第三次握⼿ ,此 时客⼾端状态进⼊到 ESTABLISH 状态。
因为这个第三次握⼿的 ACK 是对第⼆次握⼿的 SYN 的确认报⽂ ,所以当第三次握⼿丢失了 ,如果 服务端那⼀⽅迟迟收不到这个确认报⽂ ,就会触发超时重传机制 ,重传 SYN-ACK 报⽂ ,直到收到 第三次握⼿ ,或者达到最⼤重传次数。
注意 ,*ACK* 报⽂是不会有重传的 ,当 *ACK* 丢失了 ,就由对⽅重传对应的报⽂。
举个例⼦ ,假设 tcp_synack_retries 参数值为 2 ,那么当第三次握⼿⼀直丢失时 ,发⽣的过程如下 图:
具体过程:
 当服务端超时重传 2 次 SYN-ACK 报⽂后 , 由于 tcp_synack_retries 为 2 ,已达到最⼤重传次
数 ,于是再等待⼀段时间( 时间为上⼀次超时时间的 2 倍),如果还是没能收到客户端的第三次

什么是 *SYN* 攻击?如何避免 *SYN* 攻击?
我们都知道 TCP 连接建⽴是需要三次握⼿ ,假设攻击者短时间伪造不同 IP 地址的 SYN 报⽂ ,服 务端每接收到⼀个 SYN 报⽂ ,就进⼊ SYN_RCVD 状态 ,但服务端发送出去的 ACK + SYN 报⽂, ⽆法得到未知 IP 主机的 ACK 应答 ,久⽽久之就会占满服务端的半连接队列 ,使得服务端不能为 正常⽤⼾服务。

先跟⼤家说⼀下 ,什么是 TCP 半连接和全连接队列。
在 TCP 三次握⼿的时候 ,Linux 内核会维护两个队列 ,分别是:
 半连接队列 ,也称 SYN 队列;
 全连接队列 ,也称 accept 队列;
我们先来看下 Linux 内核的 SYN 队列(半连接队列) 与 Accpet 队列(全连接队列)是如何⼯ 作的?

正常流程:
 当服务端接收到客户端的 SYN 报⽂时 ,会创建⼀个半连接的对象 ,然后将其加⼊到内核的 Γ SYN 队列」;
 接着发送 SYN + ACK 给客户端 ,等待客户端回应 ACK 报⽂;
 服务端接收到ACK 报⽂后 ,从 Γ SYN 队列」取出⼀个半连接对象 ,然后创建⼀个新的连接对 象放⼊到 Γ Accept 队列」;
 应⽤通过调⽤ accpet() socket 接⼝ ,从 Γ Accept 队列」取出连接对象。
不管是半连接队列还是全连接队列 ,都有最⼤⻓度限制 ,超过限制时 ,默认情况都会丢弃报⽂。
SYN 攻击⽅式最直接的表现就会把 TCP 半连接队列打满 ,这样当****TCP**** 半连接队列满了 ,后续再在 收到 *SYN* 报⽂就会丢弃 ,导致客户端⽆法和服务端建⽴连接。
避免 SYN 攻击⽅式 ,可以有以下四种⽅法:
 调⼤ netdev_max_backlog;
 增⼤ TCP 半连接队列;
 开启 tcp_syncookies;
 减少 SYN+ACK 重传次数


当⽹卡接收数据包的速度⼤于内核处理的速度时 ,会有⼀个队列保存这些数据包。控制该队列的 最⼤值如下参数 ,默认值是 1000 ,我们要适当调⼤该参数的值 ,⽐如设置为 10000:
sh

net.core.netdev_max_backlog = 10000⽅式⼆: 增⼤ TCP 半连接队列
增⼤ TCP 半连接队列 ,要同时增⼤下⾯这三个参数:
 增⼤ net.ipv4.tcp_max_syn_backlog  增⼤ listen() 函数中的 backlog
 增⼤ net.core.somaxconn
具体为什么是三个参数决定 TCP 半连接队列的⼤⼩ ,可以看这篇:可以看这篇:*TCP* 半连接队列 和全连接队列满了会发⽣什么?⼜该如何应对?

⽅式三:开启 net.ipv4.tcp_syncookies
开启 syncookies 功能就可以在不使⽤ SYN 半连接队列的情况下成功建⽴连接 ,相当于绕过了 SYN 半连接来建⽴连接。
具体过程:
 当 Γ SYN 队列」满之后 ,后续服务端收到 SYN 包 ,不会丢弃 ,⽽是根据算法 ,计算出⼀个 cookie 值;
 将 cookie 值放到第⼆次握⼿报⽂的 Γ序列号」⾥ ,然后服务端回第⼆次握⼿给客户端;
 服务端接收到客户端的应答报⽂时 ,服务端会检查这个 ACK 包的合法性。如果合法 ,将该连接 对象放⼊到 Γ Accept 队列」。
 最后应⽤程序通过调⽤ accpet() 接⼝ ,从 Γ Accept 队列」取出的连接。
可以看到 , 当开启了 tcp_syncookies 了 ,即使受到 SYN 攻击⽽导致 SYN 队列满时 ,也能保证正常 的连接成功建⽴。
net.ipv4.tcp_syncookies 参数主要有以下三个值:
 0 值 ,表⽰关闭该功能;
 1 值 ,表⽰仅当 SYN 半连接队列放不下时 ,再启⽤它;
 2 值 ,表⽰⽆条件开启功能;
那么在应对 SYN 攻击时 ,只需要设置为 1 即可。
|  |  |
| ------------------------------------------------------------ | ------------------------------------------------------------ || | ⽅式四:减少 SYN+ACK 重传次数 |
 客户端打算关闭连接 ,此时会发送⼀个 TCP ⾸部 FIN 标志位被置为 1 的报⽂ ,也即 FIN 报⽂ ,之后客户端进⼊ FIN_WAIT_1 状态。
 服务端收到该报⽂后 ,就向客户端发送 ACK 应答报⽂ ,接着服务端进⼊ CLOSE_WAIT 状态。  客户端收到服务端的 ACK 应答报⽂后 ,之后进⼊ FIN_WAIT_2 状态。
 等待服务端处理完数据后 ,也向客户端发送 FIN 报⽂ ,之后服务端进⼊ LAST_ACK 状态。  客户端收到服务端的 FIN 报⽂后 ,回⼀个 ACK 应答报⽂ ,之后进⼊ TIME_WAIT 状态
 服务端收到了 ACK 应答报⽂后 ,就进⼊了 CLOSE 状态 ,⾄此服务端已经完成连接的关闭。  客户端在经过 2MSL ⼀段时间后 , ⾃动进⼊ CLOSE 状态 ,⾄此客户端也完成连接的关闭。
你可以看到 ,每个⽅向都需要⼀个 *FIN* 和⼀个 *ACK* ,因此通常被称为四次挥⼿。

| 当服务端受到 SYN 攻击时 ,就会有⼤量处于 SYN_REVC 状态的TCP 连接 ,处于这个状态的 TCP 会 重传 SYN+ACK , 当重传超过次数达到上限后 ,就会断开连接。那么针对 SYN 攻击的场景 ,我们可以减少 SYN-ACK 的重传次数 ,以加快处于 SYN_REVC 状态的 TCP 连接断开。SYN-ACK 报⽂的最⼤重传次数由 tcp_synack_retries 内核参数决定(默认值是 5 次),⽐如将 tcp_synack_retries 减少到 2 次: $ echo 2 > /proc/sys/net/ipv4/tcp_synack_retries ***\*TCP\**** 连接断开 | |
| ***\*TCP\**** 四次挥⼿过程是怎样的?天下没有不散的宴席 ,对于 TCP 连接也是这样 , TCP 断开连接是通过四次挥⼿⽅式。双⽅都可以主动断开连接 ,断开连接后主机中的 Γ资源」将被释放 ,四次挥⼿的过程如下图: | |为什么挥⼿需要四次?
再来回顾下四次挥⼿双⽅发 FIN 包的过程 ,就能理解为什么需要四次了。
 关闭连接时 ,客⼾端向服务端发送 FIN 时 ,仅仅表⽰客⼾端不再发送数据了但是还能接收数 据。
 服务端收到客⼾端的 FIN 报⽂时 ,先回⼀个 ACK 应答报⽂ ,⽽服务端可能还有数据需要处
理和发送 ,等服务端不再发送数据时 ,才发送 FIN 报⽂给客⼾端来表⽰同意现在关闭连接。
从上⾯过程可知 ,服务端通常需要等待完成数据的发送和处理 ,所以服务端的 ACK 和 FIN ⼀般 都会分开发送 ,因此是需要四次挥⼿。
但是在特定情况下 ,四次挥⼿是可以变成三次挥⼿的 ,具体情况可以看这篇:*TCP* 四次挥⼿, 可 以变成三次吗?
第⼀次挥⼿丢失了 ,会发⽣什么?
当客⼾端(主动关闭⽅) 调⽤ close 函数后 ,就会向服务端发送 FIN 报⽂ ,试图与服务端断开连 接 ,此时客⼾端的连接进⼊到 FIN_WAIT_1 状态。正常情况下 ,如果能及时收到服务端(被动关闭⽅) 的 ACK ,则会很快变为 FIN_WAIT2 状态。
如果第⼀次挥⼿丢失了 ,那么客⼾端迟迟收不到被动⽅的 ACK 的话 ,也就会触发超时重传机制, 重传 FIN 报⽂ ,重发次数由 tcp_orphan_retries 参数控制。
当客⼾端重传 FIN 报⽂的次数超过 tcp_orphan_retries 后 ,就不再发送 FIN 报⽂ ,则会在等待 ⼀段时间( 时间为上⼀次超时时间的 2 倍),如果还是没能收到第⼆次挥⼿ ,那么直接进⼊到
close 状态。
举个例⼦ ,假设 tcp_orphan_retries 参数值为 3 , 当第⼀次挥⼿⼀直丢失时 ,发⽣的过程如下图:
具体过程:
 当客户端超时重传 3 次 FIN 报⽂后 , 由于 tcp_orphan_retries 为 3 ,已达到最⼤重传次数 ,于是 再等待⼀段时间( 时间为上⼀次超时时间的 2 倍),如果还是没能收到服务端的第⼆次挥⼿
(ACK报⽂),那么客户端就会断开连接。


第⼆次挥⼿丢失了 ,会发⽣什么?
当服务端收到客⼾端的第⼀次挥⼿后 ,就会先回⼀个 ACK 确认报⽂ ,此时服务端的连接进⼊到 CLOSE_WAIT 状态。
在前⾯我们也提了 ,ACK 报⽂是不会重传的 ,所以如果服务端的第⼆次挥⼿丢失了 ,客⼾端就会 触发超时重传机制 ,重传 FIN 报⽂ ,直到收到服务端的第⼆次挥⼿ ,或者达到最⼤的重传次数。
举个例⼦ ,假设 tcp_orphan_retries 参数值为 2 , 当第⼆次挥⼿⼀直丢失时 ,发⽣的过程如下图:
具体过程:
 当客户端超时重传 2 次 FIN 报⽂后 , 由于 tcp_orphan_retries 为 2 ,已达到最⼤重传次数 ,于是 再等待⼀段时间( 时间为上⼀次超时时间的 2 倍),如果还是没能收到服务端的第⼆次挥⼿
(ACK 报⽂),那么客户端就会断开连接。


对于 close 函数关闭的连接 , 由于⽆法再发送和接收数据 ,所以 FIN_WAIT2 状态不可以持续太 久 ,⽽ tcp_fin_timeout 控制了这个状态下连接的持续时⻓ ,默认值是 60 秒。这意味着对于调⽤ close 关闭的连接 ,如果在 60 秒后还没有收到 FIN 报⽂ ,客户端(主动关闭 ⽅) 的连接就会直接关闭 ,如下图:

但是注意 ,如果主动关闭⽅使⽤ shutdown 函数关闭连接 ,指定了只关闭发送⽅向 ,⽽接收⽅向并 没有关闭 ,那么意味着主动关闭⽅还是可以接收数据的。
此时 ,如果主动关闭⽅⼀直没收到第三次挥⼿ ,那么主动关闭⽅的连接将会⼀直处于 FIN_WAIT2 状态( tcp_fin_timeout ⽆法控制 shutdown 关闭的连接)。如下图:
第三次挥⼿丢失了 ,会发⽣什么?
当服务端(被动关闭⽅) 收到客户端(主动关闭⽅) 的 FIN 报⽂后 ,内核会⾃动回复 ACK ,同时 连接处于 CLOSE_WAIT 状态 ,顾名思义 ,它表⽰等待应⽤进程调⽤ close 函数关闭连接。
此时 ,内核是没有权利替代进程关闭连接 ,必须由进程主动调⽤ close 函数来触发服务端发送 FIN 报⽂。
服务端处于 CLOSE_WAIT 状态时 ,调⽤了 close 函数 ,内核就会发出 FIN 报⽂ ,同时连接进⼊ LAST_ACK 状态 ,等待客户端返回ACK 来确认连接关闭。




 当服务端重传第三次挥⼿报⽂的次数达到了 3 次后 , 由于 tcp_orphan_retries 为 3 ,达到了重传 最⼤次数 ,于是再等待⼀段时间( 时间为上⼀次超时时间的 2 倍),如果还是没能收到客户端的
举个例⼦ ,假设 tcp_orphan_retrie s = 3 , 当第三次挥⼿⼀直丢失时 ,发⽣的过程如下图:第四次挥⼿ (ACK报⽂),那么服务端就会断开连接。
 客户端因为是通过 close 函数关闭连接的 ,处于 FIN_WAIT_2 状态是有时⻓限制的 ,如果
tcp_fin_timeout 时间内还是没能收到服务端的第三次挥⼿ ( FIN 报⽂),那么客户端就会断开连 接。
第四次挥⼿丢失了 ,会发⽣什么?
当客户端收到服务端的第三次挥⼿的 FIN 报⽂后 ,就会回ACK 报⽂ ,也就是第四次挥⼿ ,此时客 户端连接进⼊ TIME_WAIT 状态。
在 Linux 系统 ,TIME_WAIT 状态会持续 2MSL 后才会进⼊关闭状态。
然后 ,服务端(被动关闭⽅) 没有收到ACK 报⽂前 ,还是处于 LAST_ACK 状态。
如果第四次挥⼿的 ACK 报⽂没有到达服务端 ,服务端就会重发 FIN 报⽂ ,重发次数仍然由前⾯介 绍过的 tcp_orphan_retries 参数控制。
举个例⼦ ,假设 tcp_orphan_retries 为 2 , 当第四次挥⼿⼀直丢失时 ,发⽣的过程如下:
具体过程:
 当服务端重传第三次挥⼿报⽂达到 2 时 , 由于 tcp_orphan_retries 为 2 , 达到了最⼤重传次
数 ,于是再等待⼀段时间( 时间为上⼀次超时时间的 2 倍),如果还是没能收到客户端的第四次

中再次收到第三次挥⼿ ( FIN 报⽂)后 ,就会重置定时器, 当等待 2MSL 时⻓后 ,客⼾端就会 断开连接。
为什么 ***\*TIME_WAIT\**** 等待的时间是 ***\*2MSL\****?MSL 是 Maximum Segment Lifetime ,报⽂最⼤⽣存时间 ,它是任何报⽂在⽹络上存在的最⻓时 间 ,超过这个时间报⽂将被丢弃。 因为TCP 报⽂基于是 IP 协议的 ,⽽ IP 头中有⼀个 TTL 字段, 是 IP 数据报可以经过的最⼤路由数 ,每经过⼀个处理他的路由器此值就减 1 , 当此值为 0 则数据 报将被丢弃 ,同时发送 ICMP 报⽂通知源主机。
MSL 与 TTL 的区别: MSL 的单位是时间 ,⽽ TTL 是经过路由跳数。所以 *MSL* 应该要⼤于等于 *TTL* 消耗为 *0* 的时间 ,以确保报⽂已被⾃然消亡。
*TTL* 的值⼀般是 *64* ,*Linux* 将 *MSL* 设置为 *30* 秒 ,意味着 *Linux* 认为数据报⽂经过 *64* 个路由器的 时间不会超过 *30* 秒 ,如果超过了 ,就认为报⽂已经消失在⽹络中了。
TIME_WAIT 等待 2 倍的 MSL ,⽐较合理的解释是: ⽹络中可能存在来⾃发送⽅的数据包 , 当这些 发送⽅的数据包被接收⽅处理后⼜会向对⽅发送响应 ,所以⼀来⼀回需要等待 *2* 倍的时间。
⽐如 ,如果被动关闭⽅没有收到断开连接的最后的 ACK 报⽂ ,就会触发超时重发 FIN 报⽂ ,另 ⼀⽅接收到 FIN 后 ,会重发 ACK 给被动关闭⽅ , ⼀来⼀去正好 2 个 MSL。
可以看到 ****2MSL****时⻓ 这其实是相当于⾄少允许报⽂丢失⼀次。 ⽐如 ,若 ACK 在⼀个 MSL 内丢失, 这样被动⽅重发的 FIN 会在第 2 个 MSL 内到达 ,TIME_WAIT 状态的连接可以应对。
为什么不是 4 或者 8 MSL 的时⻓呢?你可以想象⼀个丢包率达到百分之⼀的糟糕⽹络 ,连续两次 丢包的概率只有万分之⼀ ,这个概率实在是太⼩了 ,忽略它⽐解决它更具性价⽐。
2MSL 的时间是从客户端接收到 *FIN* 后发送 *ACK* 开始计时的。如果在 TIME-WAIT 时间内 ,因为 客⼾端的ACK 没有传输到服务端 ,客⼾端⼜接收到了服务端重发的 FIN 报⽂ ,那么 *2MSL* 时间将 重新计时。
在 Linux 系统⾥ 2MSL 默认是 60 秒 ,那么⼀个 MSL 也就是 30 秒。 ***\*Linux\**** 系统停留在 ***\*TIME_WAIT\**** 的时间为固定的 ***\*60\**** 秒。其定义在 Linux 内核代码⾥的名称为 TCP_TIMEWAIT_LEN:
c
state, about 60 seconds */


\#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
为什么需要 ***\*TIME_WAIT\**** 状态?主动发起关闭连接的⼀⽅ ,才会有 TIME-WAIT 状态。
需要 TIME-WAIT 状态 ,主要是两个原因:
 防⽌历史连接中的数据 ,被后⾯相同四元组的连接错误的接收;
 保证 Γ被动关闭连接」 的⼀⽅ ,能被正确的关闭;
原因⼀: 防⽌历史连接中的数据, 被后⾯相同四****元组的连接错误的接收
为了能更好的理解这个原因 ,我们先来了解序列号(SEQ)和初始序列号( ISN)。
 序列号 ,是 TCP ⼀个头部字段 ,标识了TCP 发送端到TCP 接收端的数据流的⼀个字节 ,因为 TCP 是⾯向字节流的可靠协议 ,为了保证消息的顺序性和可靠性 ,TCP 为每个传输⽅向上的每 个字节都赋予了⼀个编号 ,以便于传输成功后确认、丢失后重传以及在接收端保证不会乱序。 序列号是⼀个 *32* 位的⽆符号数 ,因此在到达 *4G* 之后再循环回到 *0*。
 初始序列号 ,在 TCP 建⽴连接的时候 ,客⼾端和服务端都会各⾃⽣成⼀个初始序列号 ,它是基 于时钟⽣成的⼀个随机数 ,来保证每个连接都拥有不同的初始序列号。初始化序列号可被视为 ⼀个 *32* 位的计数器,该计数器的数值每 *4* 微秒加 *1* ,循环⼀次需要 *4.55* ⼩时。
给⼤家抓了⼀个包 ,下图中的 Seq 就是序列号 ,其中红⾊框住的分别是客⼾端和服务端各⾃⽣成 的初始序列号。



假设 TIME-WAIT 没有等待时间或时间过短 ,被延迟的数据包抵达后会发⽣什么呢?

如上图:
 服务端在关闭连接之前发送的 SEQ = 301 报⽂ ,被⽹络延迟了。

⽂ ,但是这个数据报⽂是上⼀个连接残留下来的 ,这样就产⽣数据错乱等严重的问题。
为了防⽌历史连接中的数据 ,被后⾯相同四元组的连接错误的接收 ,因此 TCP 设计了 TIME_WAIT 状态 ,状态会持续 2MSL 时⻓ ,这个时间⾜以让两个⽅向上的数据包都被丢弃 ,使得原来连接的 数据包在⽹络中都⾃然消失 ,再出现的数据包⼀定都是新建⽴连接所产⽣的。
原因⼆: 保证 Γ被动关闭连接」 的⼀⽅ , 能被正确的关闭
在 RFC 793 指出 TIME-WAIT 另⼀个重要的作⽤是:
**TIME-WAIT - represents waiting for enough time to pass to** **be sure** **the** **remote** **TCP** **received** **the** **acknowledgment** **of** **its** **connection** **termination** **request****.**也就是说 ,TIME-WAIT 作⽤是等待⾜够的时间以确保最后的 *ACK* 能让被动关闭⽅接收 ,从⽽帮助 其正常关闭。
如果客⼾端(主动关闭⽅) 最后⼀次 ACK 报⽂(第四次挥⼿) 在⽹络中丢失了 ,那么按照 TCP 可 靠性原则 ,服务端(被动关闭⽅) 会重发 FIN 报⽂。
假设客⼾端没有 TIME_WAIT 状态 ,⽽是在发完最后⼀次回ACK 报⽂就直接进⼊ CLOSE 状态 ,如 果该 ACK 报⽂丢失了 ,服务端则重传的 FIN 报⽂ ,⽽这时客⼾端已经进⼊到关闭状态了 ,在收到 服务端重传的 FIN 报⽂后 ,就会回 RST 报⽂。
服务端收到这个 RST 并将其解释为⼀个错误(Connection reset by peer),这对于⼀个可靠的协议 来说不是⼀个优雅的终⽌⽅式。
为了防⽌这种情况出现 ,客户端必须等待⾜够⻓的时间 ,确保服务端能够收到 ACK ,如果服务端 没有收到 ACK ,那么就会触发 TCP 重传机制 ,服务端会重新发送⼀个 FIN ,这样⼀去⼀来刚好两 个 MSL 的时间。

客⼾端在收到服务端重传的 FIN 报⽂时 ,TIME_WAIT 状态的等待时间 ,会重置回 2MSL。
***\*TIME_WAIT\**** 过多有什么危害?过多的TIME-WAIT 状态主要的危害有两种:
 第⼀是占⽤系统资源 ,⽐如⽂件描述符、 内存资源、CPU 资源、线程资源等;
 第⼆是占⽤端⼝资源 ,端⼝资源也是有限的 ,⼀般可以开启的端⼝为 32768~61000 ,也可以 通过 net.ipv4.ip_local_port_range 参数指定范围。
客⼾端和服务端 TIME_WAIT 过多 ,造成的影响是不同的。
如果客户端(主动发起关闭连接⽅) 的 *T**IME_WAIT* 状态过多 , 占满了所有端⼝资源 ,那么就⽆法 对 Γ⽬的 IP+ ⽬的 PORT」都⼀样的服务端发起连接了 ,但是被使⽤的端⼝ ,还是可以继续对另外 ⼀个服务端发起连接的。具体可以看我这篇⽂章:客户端的端⼝可以重复使⽤吗?
因此 ,客⼾端(发起连接⽅) 都是和 Γ⽬的 IP+ ⽬的 PORT 」都⼀样的服务端建⽴连接的话 , 当 客⼾端的 TIME_WAIT 状态连接过多的话 ,就会受端⼝资源限制 ,如果占满了所有端⼝资源 ,那么 就⽆法再跟 Γ⽬的 IP+ ⽬的 PORT」都⼀样的服务端建⽴连接了。
不过 ,即使是在这种场景下 ,只要连接的是不同的服务端 ,端⼝是可以重复使⽤的 ,所以客⼾端 还是可以向其他服务端发起连接的 ,这是因为内核在定位⼀个连接的时候 ,是通过四元组(源IP、 源端⼝ 、 ⽬的IP、 ⽬的端⼝) 信息来定位的 ,并不会因为客⼾端的端⼝⼀样 ,⽽导致连接冲突。

很多连接 ,但是 TCP 连接过多 ,会占⽤系统资源 ,⽐如⽂件描述符、 内存资源、CPU 资源、线程 资源等。
如何优化 ***\*TIME_WAIT\****?这⾥给出优化 TIME-WAIT 的⼏个⽅式 ,都是有利有弊:
 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;  net.ipv4.tcp_max_tw_buckets
 程序中使⽤ SO_LINGER ,应⽤强制使⽤ RST 关闭。
⽅式—:net.ipv4.tcp_tw_reuse 和 **tcp_timest**amps
如下的 Linux 内核参数开启后 ,则可以复⽤处于 ***\*TIME_WAIT\**** 的 ***\*socket\**** 为新的连接所⽤。
有⼀点需要注意的是 ,***\*tcp_tw_reuse\**** 功能只能⽤客户端(连接发起⽅),因为开启了该功能 ,在调 ⽤ ***\*connect()\**** 函数时 ,内核会随机找⼀个 ***\*time\*******\*_wait\**** 状态超过 ***\*1\**** 秒的连接给新的连接复⽤。
net.ipv4.tcp_tw_reuse = 1使⽤这个选项 ,还有⼀个前提 ,需要打开对 TCP 时间戳的⽀持 ,即
net.ipv4.tcp_timestamps=1(默认即为 1)这个时间戳的字段是在 TCP 头部的 Γ选项」⾥ ,它由⼀共 8 个字节表⽰时间戳 ,其中第⼀个 4 字 节字段⽤来保存发送该数据包的时间 ,第⼆个 4 字节字段⽤来保存最近⼀次接收对⽅发送到达数 据的时间。
由于引⼊了时间戳 ,我们在前⾯提到的 2MSL 问题就不复存在了 ,因为重复的数据包会因为时间 戳过期被⾃然丢弃。
**⽅式⼆:****net.ipv4.tcp_max****_tw_buckets**这个值默认为 18000 ,当系统中处于 *TIME_WAIT* 的连接⼀旦超过这个值时 ,系统就会将后⾯的 *TIME_WAIT* 连接状态重置 ,这个⽅法⽐较暴⼒。
⽅式三:程序中使⽤ SO_LINGER
我们可以通过设置 socket 选项 ,来设置调⽤ close 关闭连接⾏为。

三
_li l_ ff so_linger.l_linger = 0;
setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger,sizeof(so_linger));
如果 l_onoff 为⾮ 0 , 且 l_linger 值为 0 ,那么调⽤ close 后 ,会⽴该发送⼀个 RST 标志给对 端 ,该 TCP 连接将跳过四次挥⼿ ,也就跳过了 TIME_WAIT 状态 ,直接关闭。但这为跨越 TIME_WAIT 状态提供了⼀个可能 ,不过是⼀个⾮常危险的⾏为 ,不值得提倡。
前⾯介绍的⽅法都是试图越过 TIME_WAIT 状态的 ,这样其实不太好。虽然 TIME_WAIT 状态持续 的时间是有⼀点⻓ ,显得很不友好 ,但是它被设计来就是⽤来避免发⽣乱七⼋糟的事情。
《UNIX⽹络编程》 ⼀书中却说道:***\*TIME_WAIT\**** 是我们的朋友 ,它是有助于我们的 ,不要试图避免 这个状态 ,⽽是应该弄清楚它。
如果服务端要避免过多的 ***\*TIME_WAIT\**** 状态的连接 ,就永远不要主动断开连接 ,让客户端去断开, 由分布在各处的客户端去承受 ***\*TIME_WAIT\****。
服务器出现⼤量 ***\*TIME_WAIT\**** 状态的原因有哪些?⾸先要知道 TIME_WAIT 状态是主动关闭连接⽅才会出现的状态 ,所以如果服务器出现⼤量的 TIME_WAIT 状态的TCP 连接 ,就是说明服务器主动断开了很多 TCP 连接。
问题来了 ,什么场景下服务端会主动断开连接呢?
 第⼀个场景: HTTP 没有使⽤⻓连接  第⼆个场景: HTTP ⻓连接超时
 第三个场景: HTTP ⻓连接的请求数量达到上限
接下来 ,分别介绍下。
**第⼀个场景:**[**HTTP**](HTTP) **没有使⽤⻓连接**
我们先来看看 [HTTP](HTTP) ⻓连接( Keep-Alive)机制是怎么开启的。
在 [HTTP/1.0](HTTP/1.0) 中默认是关闭的,如果浏览器要开启 Keep-Alive, 它必须在请求的 header 中添加:Connection: Keep-Alive
然后当服务器收到请求 ,作出回应的时候 ,它也被添加到响应中 header ⾥:


这样做 ,TCP 连接就不会中断 ,⽽是保持连接。 当客⼾端发送另⼀个请求时 ,它会使⽤同⼀个 TCP 连接。这⼀直继续到客⼾端或服务器端提出断开连接。
关闭 HTTP ⻓连接机制后,每次请求都要经历这样的过程:建⽴ TCP -> 请求资源 -> 响应资源 -> 释放连接,那么此⽅式就是 *HTTP* 短连接,如下图:
从 [***\*HTTP/1.1\****](HTTP/1.1) 开始, 就默认是开启了 ***\*Keep-Alive\****,现在⼤多数浏览器都默认是使⽤ [HTTP/1.1](HTTP/1.1),所 以 Keep-Alive 都是默认打开的。⼀旦客⼾端和服务端达成协议 ,那么⻓连接就建⽴好了。
如果要关闭 [HTTP Keep-Alive](HTTPKeep-Alive),需要在 [HTTP](HTTP) 请求或者响应的 header ⾥添加 Connection:close 信息,也就是说,只要客户端和服务端任意⼀⽅的 [***\*HTTP\**** ***\*he\*******\*ader\****](HTTPheader) 中有 ***\*Connection:close\**** 信息, 那么就⽆法使⽤ [***\*HTTP\****](HTTP) ⻓连接的机制。
⻓连接机制,这样在完成⼀次 [HTTP](HTTP) 请求/处理后,就会关闭连接。问题来了 ,这时候是客户端还是服务端主动关闭连接呢?
在 RFC ⽂档中 ,并没有明确由谁来关闭连接 ,请求和响应的双⽅都可以主动关闭 *TCP* 连接。


HTTP ⻓连接的特点是,只要任意⼀端没有明确提出断开连接,则保持 TCP 连接状态。
不过,根据⼤多数 ***\*Web\**** 服务的实现,不管哪⼀⽅禁⽤了 [***\*HTTP\**** ***\*Keep-Alive\****](HTTPKeep-Alive),都是由服务端主动关 闭连接 ,那么此时服务端上就会出现 TIME_WAIT 状态的连接。
客⼾端禁⽤了 [HTTP Keep-Alive](HTTPKeep-Alive),服务端开启 [HTTP Keep-Alive](HTTPKeep-Alive),谁是主动关闭⽅?
当客⼾端禁⽤了 [HTTP Keep-Alive](HTTPKeep-Alive),这时候 [HTTP](HTTP) 请求的 header 就会有 Connection:close 信 息,这时服务端在发完 [HTTP](HTTP) 响应后,就会主动关闭连接。
为什么要这么设计呢? [HTTP](HTTP) 是请求-响应模型,发起⽅⼀直是客⼾端, [HTTP Keep-Alive](HTTPKeep-Alive) 的初衷是 为客户端后续的请求重⽤连接,如果我们在某次 [***\*HTTP\****](HTTP) 请求***\*-\****响应模型中,请求的 ***\*header\**** 定义了 ***\*connection\****:***\*close\**** 信息,那不再重⽤这个连接的时机就只有在服务端了,所以我们在 [HTTP](HTTP) 请 求-响应这个周期的 Γ末端」关闭连接是合理的。
客⼾端开启了 [HTTP Keep-Alive](HTTPKeep-Alive),服务端禁⽤了 [HTTP Keep-Alive](HTTPKeep-Alive),谁是主动关闭⽅?
当客⼾端开启了 [HTTP Keep-Alive](HTTPKeep-Alive),⽽服务端禁⽤了 [HTTP Keep-Alive](HTTPKeep-Alive),这时服务端在发完 [HTTP](HTTP) 响应后 ,服务端也会主动关闭连接。
为什么要这么设计呢?在服务端主动关闭连接的情况下 ,只要调⽤⼀次 close() 就可以释放连接, 剩下的⼯作由内核 TCP 栈直接进⾏了处理 ,整个过程只有⼀次 syscall;如果是要求 客⼾端关闭, 则服务端在写完最后⼀个 response 之后需要把这个 socket 放⼊ readable 队列 ,调⽤ select /
epoll 去等待事件;然后调⽤⼀次 read() 才能知道连接已经被关闭 ,这其中是两次 syscall ,多⼀次 ⽤⼾态程序被激活执⾏ ,⽽且 socket 保持时间也会更⻓。
因此 ,当服务端出现⼤量的 ***\*TIME_WAIT\**** 状态连接的时候 ,可以排查下是否客户端和服务端都开启 了 [***\*HTTP\**** ***\*Keep-Alive\****](HTTPKeep-Alive), 因为任意⼀⽅没有开启 [HTTP Keep-Alive](HTTPKeep-Alive),都会导致服务端在处理完⼀个
[HTTP](HTTP) 请求后,就主动关闭连接,此时服务端上就会出现⼤量的 TIME_WAIT 状态的连接。
针对这个场景下,解决的⽅式也很简单,让客⼾端和服务端都开启 [HTTP Keep-Alive](HTTPKeep-Alive) 机制。
**第⼆个场景:**[**HTTP**](HTTP) **⻓连接超时**的请求 ,此时这个 TCP 连接—直占⽤着不是挺浪费资源的吗?
假设设置了 HTTP ⻓连接的超时时间是 60 秒,nginx 就会启动—个Γ定时器」, 如果客户端在完后 ⼀个 *HTTP* 请求后,在 *60* 秒内都没有再发起新的请求,定时器的时间⼀到,*nginx* 就会触发回调 函数来关闭该连接 ,那么此时服务端上就会出现 *TIME_W**AIT* 状态的连接。
当服务端出现⼤量 TIME_WAIT 状态的连接时 ,如果现象是有⼤量的客户端建⽴完 TCP 连接后 ,很 ⻓⼀段时间没有发送数据,那么⼤概率就是因为 HTTP ⻓连接超时,导致服务端主动关闭连接,产 ⽣⼤量处于 TIME_WAIT 状态的连接。


对没错,所以为了避免资源浪费的情况,web 服务软件—般都会提供—个参数,⽤来指定 [HTTP](HTTP) ⻓连接的超时时间 ,⽐如 nginx 提供的 keepalive_timeout 参数。
**第三个场景:**[**HTTP**](HTTP) **⻓连接的请求数量达到上限**
Web 服务端通常会有个参数,来定义⼀条 [HTTP](HTTP) ⻓连接上最⼤能处理的请求数量, 当超过最⼤限 制时 ,就会主动关闭连接。
⽐如 nginx 的 keepalive_requests 这个参数,这个参数是指⼀个 [HTTP](HTTP) ⻓连接建⽴之后,nginx 就 会为这个连接设置⼀个计数器,记录这个 [HTTP](HTTP) ⻓连接上已经接收并处理的客⼾端请求的数量。如 果达到这个参数设置的最⼤值时 ,则 ***\*nginx\**** 会主动关闭这个⻓连接 ,那么此时服务端上就会出现 TIME_WAIT 状态的连接。
keepalive_requests 参数的默认值是 100 ,意味着每个 [HTTP](HTTP) ⻓连接最多只能跑 100 次请求,这个 参数往往被⼤多数⼈忽略 ,因为当 QPS (每秒请求数) 不是很⾼时 ,默认值 100 凑合够⽤ 。但是 ,对于⼀些 *QPS* ⽐较⾼的场景 ,⽐如超过 *10000* *QPS* ,甚⾄达到 *3000**0* *,* *50000* 甚⾄更⾼,
如果 ***\*keepalive_requests\**** 参数值是 ***\*100\**** ,这时候就 ***\*nginx\**** 就会很频繁地关闭连接 ,那么此时服务端 上就会出⼤量的 ***\*TIME_WAIT\**** 状态。针对这个场景下 ,解决的⽅式也很简单 ,调⼤ nginx 的 keepalive_requests 参数就⾏。
服务器出现⼤量 ***\*CLOSE_WAIT\**** 状态的原因有哪些?CLOSE_WAIT 状态是 Γ被动关闭⽅」 才会有的状态 ,⽽且如果 Γ被动关闭⽅」 没有调⽤ close 函数 关闭连接 ,那么就⽆法发出 FIN 报⽂ ,从⽽⽆法使得 CLOSE_WAIT 状态的连接转变为 LAST_ACK 状态。
所以 ,当服务端出现⼤量 ***\*CLOSE_WAIT\**** 状态的连接的时候 ,说明服务端的程序没有调⽤ ***\*close\**** 函 数关闭连接。那什么情况会导致服务端的程序没有调⽤ close 函数关闭连接?这时候通常需要排查代码。
我们先来分析⼀个普通的TCP 服务端的流程:
\1. 创建服务端 socket ,bind 绑定端⼝ 、listen 监听端⼝
\2. 将服务端 socket 注册到 epoll
\3. epoll_wait 等待连接到来 ,连接到来时 ,调⽤ accpet 获取已连接的 socket\4. 将已连接的 socket 注册到 epoll
\5. epoll_wait 等待事件发⽣
\6. 对⽅连接关闭时 ,我⽅调⽤ close
可能导致服务端没有调⽤ close 函数的原因 ,如下。

close 函数了。
不过这种原因发⽣的概率⽐较⼩ ,这种属于明显的代码逻辑 bug ,在前期 read view 阶段就能发现 的了。
第⼆个原因: 第 3 步没有做 ,有新连接到来时没有调⽤ accpet 获取该连接的 socket ,导致当有⼤ 量的客⼾端主动断开了连接 ,⽽服务端没机会对这些 socket 调⽤ close 函数 ,从⽽导致服务端出 现⼤量 CLOSE_WAIT 状态的连接。
发⽣这种情况可能是因为服务端在执⾏ accpet 函数之前 ,代码卡在某⼀个逻辑或者提前抛出了异 常。
第三个原因:第 4 步没有做 ,通过 accpet 获取已连接的 socket 后 ,没有将其注册到 epoll ,导致 后续收到 FIN 报⽂的时候 ,服务端没办法感知这个事件 ,那服务端就没机会调⽤ close 函数了。
发⽣这种情况可能是因为服务端在将已连接的 socket 注册到 epoll 之前 ,代码卡在某⼀个逻辑或 者提前抛出了异常。之前看到过别⼈解决 close_wait 问题的实践⽂章 ,感兴趣的可以看看:⼀次 *Netty* 代码不健壮导致的⼤量 *CLOSE_WAIT* 连接原因分析
第四个原因:第 6 步没有做 , 当发现客⼾端关闭连接后 ,服务端没有执⾏ close 函数 ,可能是因为 代码漏处理 ,或者是在执⾏ close 函数之前 ,代码卡在某⼀个逻辑 ,⽐如发⽣死锁等等。
可以发现 ,当服务端出现⼤量 ***\*CLOSE_\*******\*WAIT\**** 状态的连接的时候 ,通常都是代码的问题 ,这时候我 们需要针对具体的代码⼀步⼀步的进⾏排查和定位 ,主要分析的⽅向就是服务端为什么没有调⽤ ***\*close\****。如果已经建⽴了连接 ,但是客户端突然出现故障了怎么办?
客⼾端出现故障指的是客⼾端的主机发⽣了宕机 ,或者断电的场景。发⽣这种情况的时候 ,如果 服务端⼀直不会发送数据给客⼾端 ,那么服务端是永远⽆法感知到客⼾端宕机这个事件的 ,也就 是服务端的 TCP 连接将⼀直处于 ESTABLISH 状态 , 占⽤着系统资源。
为了避免这种情况 ,TCP 搞了个保活机制。这个机制的原理是这样的:
定义⼀个时间段 ,在这个时间段内 ,如果没有任何连接相关的活动 ,TCP 保活机制会开始作⽤ ,每 隔⼀个时间间隔 ,发送⼀个探测报⽂ ,该探测报⽂包含的数据⾮常少 ,如果连续⼏个探测报⽂都
没有得到响应 ,则认为当前的TCP 连接已经死亡 ,系统内核将错误信息通知给上层应⽤程序。
在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔 ,以下 都为默认值:

 tcp_keepalive_time=7200:表⽰保活时间是 7200 秒(2⼩时),也就 2 ⼩时内如果没有任何连 接相关的活动 ,则会启动保活机制
 tcp_keepalive_intvl=75 :表⽰每次检测间隔 75 秒;
 tcp_keepalive_probes=9:表⽰检测 9 次⽆响应 ,认为对⽅是不可达的 ,从⽽中断本次的连接。 也就是说在 Linux 系统中 ,最少需要经过 2 ⼩时 11 分 15 秒才可以发现⼀个 Γ死亡」连接。

i _k li _i l net.ipv4.tcp_keepalive_probes=9注意 ,应⽤程序若想使⽤ TCP 保活机制需要通过 socket 接⼝设置 SO_KEEPALIVE 选项才能够⽣ 效 ,如果没有设置 ,那么就⽆法使⽤ TCP 保活机制。
如果开启了TCP 保活 ,需要考虑以下⼏种情况:
 第⼀种 ,对端程序是正常⼯作的。 当 TCP 保活的探测报⽂发送给对端, 对端会正常响应 ,这样 *TCP* 保活时间会被重置 ,等待下⼀个 TCP 保活时间的到来。
 第⼆种 ,对端主机宕机并重启。 当 TCP 保活的探测报⽂发送给对端后 ,对端是可以响应的 ,但
由于没有该连接的有效信息 ,会产⽣⼀个 *RST* 报⽂ ,这样很快就会发现 TCP 连接已经被重置。
 第三种 ,是对端主机宕机( 注意不是进程崩溃, 进程崩溃后操****作系统在回收进程资源的时候,
会发送 FIN 报⽂, ⽽主机宕机则是⽆法感知的, 所以需要 TCP 保活机****制来探测对⽅是不是发⽣ 了主机宕机),或对端由于其他原因导致报⽂不可达。 当 TCP 保活的探测报⽂发送给对端后 ,⽯ 沉⼤海 ,没有响应 ,连续⼏次 ,达到保活探测次数后 ,*TCP* 会报告该 *TCP* 连接已经死亡。
TCP 保活的这个机制检测的时间是有点⻓ ,我们可以⾃⼰在应⽤层实现⼀个⼼跳机制。
⽐如,web 服务软件⼀般都会提供 keepalive_timeout 参数,⽤来指定 HTTP ⻓连接的超时时 间。如果设置了 HTTP ⻓连接的超时时间是 60 秒,web 服务软件就会启动⼀个定时器,如果客⼾

|  |  |
| ------------------------------------------------------------ | ------------------------------------------------------------ || 如果已经建⽴了连接 ,但是服务端的进程崩溃会发⽣什么?TCP 的连接信息是由内核维护的 ,所以当服务端的进程崩溃后 ,内核需要回收该进程的所有 TCP 连接资源 ,于是内核会发送第⼀次挥⼿ FIN 报⽂ ,后续的挥⼿过程也都是在内核完成 ,并不需要 进程的参与 ,所以即使服务端的进程退出了 ,还是能与客⼾端完成 TCP 四次挥⼿的过程。我⾃⼰做了个实验 ,使⽤ kill -9 来模拟进程崩溃的情况 ,发现在 *kill* 掉进程后 ,服务端会发送 *FIN* 报⽂ ,与客户端进⾏四次挥⼿。 | |
| | ****TIP****关于进程崩溃和主机宕机的区别 ,可以参考这篇:TCP 连接,⼀端断电和进程崩溃有什么 区别?还有⼀个类似的问题: Γ拔掉⽹线后 , 原本的TCP 连接还存在吗?」,具体可以看这篇:拔掉⽹线后, 原本的 TCP 连接还存在吗? |
| *Socket* 编程 | |
| 针对 *TCP* 应该如何 *Socket* 编程? | |
 服务端和客户端初始化 socket ,得到⽂件描述符;
 服务端调⽤ bind ,将 socket 绑定在指定的 IP 地址和端⼝;
 服务端调⽤ listen ,进⾏监听;
 服务端调⽤ accept ,等待客户端连接;
 客户端调⽤ connect ,向服务端的地址和端⼝发起连接请求;
 服务端 accept 返回⽤于传输的 socket 的⽂件描述符;
 客户端调⽤ write 写⼊数据;服务端调⽤ read 读取数据;
 客户端断开连接时 ,会调⽤ close ,那么服务端 read 读取数据的时候 ,就会读取到了 EOF ,待处理完数据后 ,服务端调⽤ close ,表⽰连接关闭。
这⾥需要注意的是 ,服务端调⽤ accept 时 ,连接成功了会返回⼀个已完成连接的 socket ,后续 ⽤来传输数据。
所以 ,监听的 socket 和真正⽤来传送数据的 socket ,是 Γ两个」 socket ,⼀个叫作监听 *socket*, ⼀个叫作已完成连接 *socket*。


***\*listen\**** 时候参数 ***\*backlog\**** 的意义?Linux内核中会维护两个队列:
 半连接队列(SYN 队列):接收到⼀个 SYN 建⽴连接请求 ,处于 SYN_RCVD 状态;  全连接队列(Accpet 队列):已完成 TCP 三次握⼿过程 ,处于 ESTABLISHED 状态;

c
 参数⼀ socketfd 为 socketfd ⽂件描述符
 参数⼆ backlog ,这参数在历史版本有⼀定的变化

int listen (int socketfd, int backlog)在 Linux 内核 2.2 之后 ,backlog 变成 accept 队列 ,也就是已完成连接建⽴的队列⻓度 ,所以现在 通常认为 *backlog* 是 *accept* 队列。
想详细了解 TCP 半连接队列和全连接队列 ,可以看这篇:*TCP* 半连接队列和全连接队列满了会发 ⽣什么?⼜该如何应对?
但是上限值是内核参数 ***\*somaxconn\**** 的⼤⼩ ,也就说 ***\*accpet\**** 队列⻓度 ***\*=\**** ***\*min(backlog,\**** ***\*somaxconn)\****。*accept* 发⽣在三次握⼿的哪⼀步?
我们先看看客户端连接服务端时 ,发送了什么?

 客户端的协议栈向服务端发送了 SYN 包 ,并告诉服务端当前发送序列号 client_isn ,客户端进⼊ SYN_SENT 状态;
 服务端的协议栈收到这个包之后 ,和客户端进⾏ ACK 应答 ,应答的值为 client_isn+1 ,表⽰对 SYN 包 client_isn 的确认 ,同时服务端也发送⼀个 SYN 包 ,告诉客户端当前我的发送序列号为

向连接建⽴成功 ,客户端的状态为 ESTABLISHED ,同时客户端协议栈也会对服务端的 SYN 包进 ⾏应答 ,应答数据为 server_isn+1;
 ACK 应答包到达服务端后 ,服务端的TCP 连接进⼊ ESTABLISHED 状态 ,同时服务端协议栈使 得 accept 阻塞调⽤返回 ,这个时候服务端到客户端的单向连接也建⽴成功。⾄此 ,客户端与 服务端两个⽅向的连接都建⽴成功。
从上⾯的描述过程 ,我们可以得知客户端 ***\*c\*******\*onnect\**** 成功返回是在第⼆次握⼿ ,服务端 ***\*accept\**** 成功 返回是在三次握⼿成功之后。客户端调⽤ *close* 了 ,连接是断开的流程是什么?
我们看看客户端主动调⽤了 close ,会发⽣什么?
 客户端调⽤ close ,表明客户端没有数据需要发送了 ,则此时会向服务端发送 FIN 报⽂ ,进⼊ FIN_WAIT_ 1 状态;
 服务端接收到了 FIN 报⽂ ,TCP 协议栈会为 FIN 包插⼊⼀个⽂件结束符 EOF 到接收缓冲区
中 ,应⽤程序可以通过 read 调⽤来感知这个 FIN 包。这个 EOF 会被放在已排队等候的其他 已接收的数据之后 ,这就意味着服务端需要处理这种异常情况 ,因为 EOF 表⽰在该连接上再⽆ 额外数据到达。此时 ,服务端进⼊ CLOSE_WAIT 状态;
 接着 , 当处理完数据后 , ⾃然就会读到 EOF ,于是也调⽤ close 关闭它的套接字 ,这会使得 服务端发出⼀个 FIN 包 ,之后处于 LAST_ACK 状态;
 客户端接收到服务端的 FIN 包 ,并发送 ACK 确认包给服务端 ,此时客户端将进⼊ TIME_WAIT 状态;
 服务端收到ACK 确认包后 ,就进⼊了最后的 CLOSE 状态;

没有 *accept* ,能建⽴ *TCP* 连接吗?
答案:可以的。
accpet 系统调⽤并不参与 TCP 三次握⼿过程 ,它只是负责从 TCP 全连接队列取出⼀个已经建⽴连 接的 socket ,⽤户层通过 accpet 系统调⽤拿到了已经建⽴连接的 socket ,就可以对该 socket 进 ⾏读写操作了。

