1. TCP 头格式有哪些?
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就【累加】一次该【数据字节数】的大小。用来解决网络包乱序问题。
确认应答号:指下一次【期望】收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
控制位:
ACK
:该位为1
时,【确认应答】的字段变为有效,TCP 规定除了最初建立连接时的SYN
包之外该位必须设置为1
。RST
:为1
时,表示 TCP 连接中出现异常必须强制断开连接。SYN
:为1
时,表示希望建立连接,并在其【序列号】的字段进行序列号初始值的设定。FIN
:为1
时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换FIN
位为 1 的 TCP 段。
2. 为什么需要 TCP 协议?TCP 工作在哪一层?
IP
层【不可靠】,它不保证网络包的按序交付、不保证网络包中的数据的完整性,如果需要保障数据网络包的可靠性,就需要传输层的 TCP
协议来负责。
因为 TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。
/# 3. 什么是 TCP?
TCP 是面向连接的、可靠地、基于字节流的传输层通信协议。
- 面向连接:一对一才能连接,不像 UDP 协议可以一对多发送信息。
- 可靠地:无论网络链路发生怎样的变化,TCP 都可以保证一个报文一定能够到达接收端。(可靠性足够高,但极端情况仍会传输失败)
- 字节流:通过 TCP 协议传输时,消息可能会被【分组】成多个的 TCP 报文,并且 TCP 报文是【有序的】。(面向流的服务,而不是面向消息的服务,意味着数据是连续流动的而不是离散的数据包形式,接收端接收到分割的数据包——TCP segments,会进行重新组装以恢复原始的字节流)。
4. 什么是建立一个 TCP 连接?
建立一个 TCP 连接是需要客户端与服务端达成上述三个信息的共识。
- Socket:由 IP 地址和端口号组成
- 序列号: 用来解决乱序问题
- 窗口大小:用来做流量控制
5. 如何唯一确定一个 TCP 连接呢?
TCP 四元组可以唯一确定一个 TCP 连接:
【源地址,源端口,目的地址,目的端口】
地址字段(32位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。
端口字段(16位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。
最大 TCP 连接数 = 客户端的 IP 数 X 客户端的端口数(理论上,实际上一个客户端通常只有一个网络分配的 IP 地址,某些情况可以用虚拟网络或负载均衡技术得到多个 IP 地址),服务端通常是固定在某个本地端口上监听。
服务端最大并发 TCP 连接数远不能达到理论上限,会受到以下因素影响:
- 文件描述符限制:每个 TCP 连接都是一个文件,如果文件描述符被占满了,会发生 Too many open files。Linux 限制 系统级、用户级、进程级的可打开的文件描述符数量。
- 内存限制:每个 TCP 连接都要占用一定内存,操作系统的内存有限,被占满后会发生 OOM。
6. UDF 和 TCP 的区别,分别的应用场景?
UDP 不提供复杂的控制机制,利用 IP 提供面向【无连接】的通信服务,其头部只有 8
个字节(64位)。
- 目标和源端口:告诉 UDP 协议应该把报文发给哪个进程。
- 包长度:保存了 UDP 首部的长度和数据的长度之和。
- 校验和:提供可靠的 UDP 首部和数据而设计,防止收到在网络传输中受损的 UDP 包。
TCP 和 UDP 区别:
- 连接:
- TCP 是面向连接的传输层协议,传输数据前要先建立连接。
- UDP 是不需要连接,即可传输数据。
- 服务对象:
- TCP 是一对一的两点服务。
- UDP 支持 一对一、一对多、多对多的交互通信。
- 可靠性:
- TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。
- UDP 是尽最大努力交付,不保证可靠交付数据。但是可以基于 UDP 传输协议实现一个可靠地传输协议,比如 QUIC 协议。
- 拥塞控制、流量控制:
- TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
- UDP 则没有,即使网络非常拥堵,也不会影响 UDP 的发送速率。
- 首部开销:
- TCP 首部长度较长,首部在没有使用【选项】字段时是
20
个字节。 - UDP 首部只有 8 个字节,并且固定不变,开销较小。
- 传输方式:
- TCP 是流式传输,没有边界,但保证顺序和可靠。
- UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
- 分片不同:
- TCP 的数据大小如果大于 MSS 大小,则会在传输层分片,目标主机收到后也同样在传输层组装包,如果中途丢失分片,只需要传输丢失的这个分片(TCP 段)。(这里与按序传输不冲突,按序是指一个TCP段的字节流是按序的)
- UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后在 IP 层组装数据,。
也就是说TCP在传输层分片,UDP在IP层分片,接收方也在这个层组装。
TCP 和 UDP 应用场景:
TCP 面向连接,能保证数据的可靠性交付,常用于:
FTP
文件传输(通过 FTP 文件传输协议传输的文件)- HTTP / HTTPS
UDP 面向无连接,可以随时发送数据,其本身的处理既简单又高效,因此经常用于:
- 包总量较少的通信,如
DNS
、SNMP
等 - 视频、音频等多媒体通信;
- 广播通信
为什么 UDP 头部没有【首部长度】字段,而 TCP 头部有?
TCP 有可变长的【选项】字段,而 UDP 头部长度不变化,所以不需要记录首部长度。
TCP 和 UDP 可以使用同一个端口吗?
可以。
传输层的 【端口号】的作用,是为了区分同一个主机上不同应用程序的数据包。传输层有两个传输协议分别是 TCP 和 UDP ,在内核中这两个是完全独立的软件模块。
7. TCP 三次握手过程是怎样的?
TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,建立连接是通过三次握手来进行的。
- 一开始,客户端和服务端都处于
CLOSE
状态。先是服务端主动监听某个端口,处于LISTEN
状态。 - 客户端会随机初始化序列号(
client_isn
),将此序号置于 TCP 首部的 【序号】字段中,同时把SYN
标志位置为1
,表示SYN
报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN-SENT
状态。 - 服务端收到客户端的
SYN
报文后,首先服务端也随机初始化自己的序号(server_isn
),将此序号填入 TCP 首部的【序号】字段中,其次把 TCP 首部的 【确认应答号】字段填入client_isn + 1
,接着把SYN
和ACK
标志位置为1
。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于SYN-RCVD
状态。 - 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部
ACK
标志位为1
,其次【确认应答号】字段填入server_isn + 1
,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于ESTABLISHED
状态。 - 服务端收到客户端的应答报文后,也进入
ESTABLISHED
状态。
前两次握手是不可以携带数据的,第三次握手是可以携带数据的。
8. 如何在 Linux 系统中查看 TCP 状态?
netstat -napt
9. 为什么是三次握手?不是两次、四次?
TCP连接是包括 Socket、序列号和窗口大小称为连接,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱,两次握手连接的情况下,服务端没有中间状态给客户端来阻止历史连接,因为两次握手时服务端收到 SYN 报文直接进入 ESTABLISHED 状态,但是客户端并没有进入,所以会建立一个历史连接造成资源浪费。所以要解决这个现象最好就是在服务端发送数据前,也就是建立连接之前,阻止掉历史连接。
此外,三次握手能帮助双方同步初始化序列号,而三次握手理论上最少可靠连接建立,因此不需要四次握手等更多的通信次数。
10. 为什么每次建立 TCP 连接时,初始化的序列号都要求不一样呢?
- 为了防止历史报文被下一个相同四元组的连接接收(比如先建立了一个 TCP 连接,然后在客户端发送数据包的时候被网络阻塞了,然后超时重传,此时服务端断电重启了,建立的连接就消失了,于是在收到客户端的数据包的时候就会发送 RST 报文,紧接着客户端又建立了一个相同四元组的连接,在新连接完成后,上一个被网络阻塞的数据正好抵达了服务端,刚好序列号是在服务端接收窗口内,被正常接收,造成数据错乱)。
- 为了安全性,防止黑客伪造相同序列号的 TCP 报文被对方接收。
11. 初始序列号 ISN 是如何随机产生的?
起始 ISN
是基于时钟的,每 4 微秒 + 1,转一圈要 4.55 个小时。
ISN = M + F(localhost, localport, remotehost, remoteport)
M
是一个计时器,每隔 4 微秒加 1.
F
是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。
随机数会基于时钟计时器递增,所以基本不可能会随机成一样的初始化序列号。
12. 既然 IP 层会分片,为什么 TCP 层还需要 MSS?
MTU
:一个网络包的最大长度,以太网中一般为1500
字节;MSS
:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度;
如果把整个报文交给 IP 层进行分片,那么当一个 IP 分片丢失,整个 IP 报文的所有分片都得重传,因为 IP 层由传输层的 TCP 来负责超时和重传,这是非常没有效率的。
所以,为了达到最佳的传输效能,TCP 协议在建立连接的时候通常要协商双方的 MSS 值,在 TCP 层对超过 MSS 的数据进行分片后,由它形成的 IP 包长度自然也不用 IP 分片。
这样,如果一个 TCP 分片丢失后,进行重发时是以 MSS 为单位,大大增加了重传的效率。
13. 第一次握手丢失了,会发生什么?
客户端发送 SYN 报文,进入到 SYN_SENT
状态,迟迟收不到服务端的 SYN-ACK 报文,触发超时重传,而且重传的 SYN 报文的序列号都是一样的,最大重传次数由内核参数控制,默认值一般为 5,每次超时的时间是上一次的一倍,1,2,4,8,16,32,如果仍然没有收到 ACK,断开 TCP 连接。
14. 第二次握手丢失了,会发生什么?
服务端收到第一次握手后,回 SYN-ACK 报文,服务端进入 SYN_RCVD
状态,第二次握手里的 ACK 是对第一次握手的确认报文,第二次握手里的 SYN 是对服务端发起建立 TCP 连接的报文。
如果第二次握手丢失了,那么客户端会认为 SYN 报文丢失,所以客户端会触发超时重传,重传 SYN 报文,然后服务端也收不到第三次握手,服务端会触发超时重传,重传 SYN-ACK 报文。
15. 第三次握手丢失了,会发生什么?
客户端收到服务端的 SYN-ACK 报文后,会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端进入 ESTABLISH
阶段,如果第三次握手丢失了,服务端会触发超时重传,直到收到第三次握手,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。
16. 什么是 SYN 攻击?如何避免 SYN 攻击?
假设攻击者短时间伪造不同 IP 地址的 SYN
报文,服务端每接收到一个 SYN
报文,就进入 SYN_RCVD
状态,但服务端发送出去的 ACK + SYN
报文,无法得到未知 IP 主机的 ACK
应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
- 半连接队列,也称 SYN 队列
- 全连接队列,也称 Accept 队列
正常流程:
- 服务端收到客户端的 SYN,创建一个半连接的对象,加入到 SYN 队列
- 接着发送 SYN + ACK,等待回应 ACK 报文
- 服务端接收到 ACK 报文后,从 SYN 队列取出一个半连接对象,然后创建一个新的连接对象放入到 Accept 队列
- 应用通过调用
accept()
socket 接口,从 Accept 队列取出连接对象。
半连接队列和全连接队列都有最大长度限制,超过限制时,默认情况都会丢弃报文,因此, SYN 攻击方式最直接的表现就是把半连接队列打满,这样后续的 SYN 报文就会丢弃。
避免 SYN 攻击,有以下几种方法:
- 调大
netdev_max_backlog
:当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包,可以调大控制该队列的参数的值。 - 增大 TCP 半连接队列,这要求同时增大这三个参数:
net.ipv4.tcp_max_syn_backlog
、listen()
函数中的backlog
、net.core.somaxconn
- 开启
net.ipv4.tcp_syncookies
,开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下建立连接。具体过程是当 SYN 队列满后,后续服务端收到 SYN 包,不会丢弃,而是根据算法计算出一个cookie
值,将 cookie 值放到第二次握手报文的【序列号】里,然后服务端回二次握手给客户端。服务端接收到客户端的应答报文时,服务端会检查这个 ACK 包的合法性。如果合法,将该连接对象放入到 Accept 队列,最后应用程序通过调用accept()
接口,从 Accpet 队列取出连接。net.ipv4.tcp_syncookies
参数主要有以下三个值:0 值,表示关闭该功能;1 值,表示仅当 SYN 半连接队列放不下时再启用;2 值,表示无条件开启功能。实际上就是,如果服务器资源充足,那么就是按本身的三次握手方式来做,如果在高负载情况下,服务器会利用 SYN Cookies,使得允许在半连接队列满时,仍然能够处理新的 SYN 报文,而不是简单丢弃。$ echo 1 > /proc/sys/net/ipv4/tcp_syncookies
。 - 减少 SYN + ACK 重传次数,当服务器受到 SYN 攻击时,就会有大量处于 SYN_REVC 状态的 TCP 连接,服务器会重传 SYN + ACK,那么针对 SYN 攻击的场景,可以减少 SYN-ACK 的重传次数,以加快处于 SYN_REVC 状态的 TCP 连接断开。
$ echo 2 > /proc/sys/net/ipv4/tcp_synack_retries
。
17. TCP 四次挥手过程是怎样的?
- 客户端打算关闭连接,此时会发送一个 TCP 首部
FIN
标志位为1
的报文,之后客户端进入FIN_WAIT_1
状态。 - 服务端收到该报文后,就向客户端发送
ACK
应答报文,接着服务端进入CLOSE_WAIT
状态。 - 客户端收到服务端的
ACK
应答报文后,之后进入FIN_WAIT_2
状态。 - 等待服务端处理完数据后,也向客户端发送
FIN
报文,之后服务端进入LAST_ACK
状态。 - 客户端收到服务端的
FIN
报文后,回一个ACK
应答报文,之后进入了TIME_WAIT
状态。 - 服务端收到了
ACK
应答报文后,就进入了CLOSE
状态,至此服务端已经完成连接的关闭。 - 客户端在经过
2MSL
一段时间后,自动进入CLOSE
状态,至此客户端也完成连接的关闭。 - 注意,主动关闭连接的,才有
TIME_WAIT
状态
18. 为什么挥手需要四次?
服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK
和 FIN
一般都会分开发送,因此是需要四次挥手。在被动关闭方在 TCP 回收过程中,【没有数据要发送】并且 【开启了 TCP 延迟确认机制】,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手,FIN
,FIN-ACK
,ACK
。
19. 第一次挥手丢失了,会发生什么?
客户端调用 close 函数后,就会向服务端发送 FIN 报文,如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,就会触发超时重传机制。
20. 第二次挥手丢失了,会发生什么?
服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 CLOSE_WAIT
状态。如果服务端的第二次挥手丢失了,由于 ACK 报文是不会重传的,因此仍是客户端触发超时重传机制。
这里提示,如果主动关闭方使用 close 函数关闭连接,无法再发送和接收数据,所以 FIN_WAIT2
状态不能持续太久,如果 60秒(默认值)还没收到第三次挥手的 FIN 报文,那么主动关闭方的连接会直接关闭。
如果主动关闭方使用 shutdown 函数关闭连接,指定了只关闭发送方向,而接收方向没有关闭,那么如果一直没收到第三次挥手,那么主动方的连接会一直处于FIN_WAIT2
状态。
21. 第三次挥手丢失了,会发生什么?
当服务端收到客户端的 FIN 后,内核自动发送 ACK,同时连接处于 CLOSE_WAIT
状态,表示等待应用进程调用 close 函数关闭连接,当进程调用 close 函数后,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。
如果迟迟收不到 ACK,服务端就会重发 FIN 报文。
22. 第四次挥手丢失了,会发生什么?
如果客户端收到服务端的第三次挥手的 FIN 报文,会回 ACK 报文,也就是第四次挥手,此时客户端进入 TIME_WAIT
状态,在 Linux 系统, TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。服务端如果没有收到 ACK 报文,那么会超时重传 FIN 报文。
23. 为什么 TIME_WAIT 等待的时间是 2MSL?
MSL
是 Maximum Segment Lifetime,报文最大生存时间,2MSL时长其实是相当于至少允许报文丢失一次,时间是从客户端接收到 FIN 后发送 ACK 开始计时的,如果又接收到服务端重发的 FIN 报文,那么2MSL 时间将重新计时。
24. 为什么需要 TIME_WAIT 状态?
主动发起关闭连接的一方,才会有 TIME-WAIT
状态,需要这个状态主要是两个原因:
- 防止历史连接中的数据,被后面相同四元组的连接错误地接收
序列号是 TCP 一个头部字段,是 TCP 为每个传输方向上的每个字节都赋予的一个编号,以便传输成功后确认、丢失后重传以及在接收端保证不会乱序。序列号是一个 32 位的无符号数,因此在到达 4G 之后再循环回到 0。
初始化序列号是在 TCP 建立连接的时候,客户端和服务端都会生成的一个初始序列号,序列号和初始化序列号并不是无限递增的,会发生回绕为初始值的情况,这意味着无法根据序列号来判断新老数据。
所以如果 TIME_WAIT 没有等待时间或者等待时间过短,那么有可能出现:服务端在关闭连接之前发送的 SEQ = 301
报文被网络延迟了,接着,服务端以相同的四元组重新打开了新连接,前面被延迟的 SEQ = 301
这时抵达了客户端,而且该数据报文的序列号刚好在客户端接收窗口内,因此造成了数据错乱。TIME_WAIT 状态的时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
- 保证【被动关闭连接】的一方,能被正确的关闭
TIME-WAIT 等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭,因为如果客户端没有 TIME_WAIT,而是在发完最后一次 ACK 就进入 CLOSE 状态,如果 ACK 报文丢失了,服务器重传 FIN,这是客户端已经进入关闭了,就会回 RST 报文,服务端收到并将其解释为一个错误,这对于可靠的协议不是一个好的终止方式。
25. TIME_WAIT 过多有什么危害?
过多的 TIME-WAIT 状态主要有两种危害:第一是占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等;第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为32768-61000
,也可以通过net.ipv4.ip_local_port_range
参数指定范围。
如果客户端(主动发起关闭连接方)的 TIME_WAIT 状态过多,沾满了所有端口资源,那么就受到了端口资源限制,无法再跟【目的 IP + 目的 PORT】都一样的服务端建立连接了,但只要连接的是不同服务器,端口是可以重新使用的,因为内核定位连接是根据四元组信息定位。
如果服务端(主动发起关闭连接方)的 TIME_WAIT 状态过多,由于只监听一个端口,不会导致端口受限,但是 TCP 连接过多会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等。
26. 如何优化 TIME_WAIT?
- 打开
net.ipv4.tcp_tw_reuse
和tcp_timestamps
,这样的 Linux 内核参数开启后,则可以复用处于 TIME_WAIT 的 socket 为新的连接所用。 net.ipv4.tcp_max_tw_buckets
,默认值为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值,系统就会将后面的 TIME_WAIT 连接状态重置。- 程序中使用
SO_LINGER
,可以通过设置 socket 选项,来设置调用 close 关闭连接行为,相当于跳过TIME_WAIT
状态。
TIME_WAIT 是有助于连接的,上面的方法都试图越过 TIME_WAIT,是不太好的,如果服务端要避免过多的 TIME_WAIT,就不要主动断开连接,把 TIME_WAIT 让客户端承受。
27. 服务器出现大量 TIME_WAIT 状态的原因有哪些?
服务端主动断开连接:
- HTTP 没有使用长连接
从 HTTP/1.1 开始,默认开启了 Keep-Alive,也就是 HTTP 长连接机制,当客户端和服务端任意一方的 HTTP header 中有 Connection:close
信息,那么就无法使用 HTTP 长连接的机制,在关闭长连接机制后,每次请求都需要经历,建立 TCP -> 请求资源 -> 响应资源 -> 释放连接,也就是 HTTP 短连接。根据大多数 Web 服务的实现,不管哪一方禁用了长连接,都是由服务器主动关闭连接。因此,当服务端出现大量的 TIME_WAIT 状态连接的时候,可以排查下是否客户端和服务端都开启了 HTTP Keep-Alive。
- HTTP 长连接超时
为了避免资源浪费的情况,web 服务软件一般都会提供一个参数,用来指定 HTTP 长连接的超时时间,比如 nginx 提供的 keepalive_timeout 参数。假设设置了 HTTP 长连接的超时时间是 60 秒,nginx 就会启动一个【定时器】,如果客户端在完成一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,定时器的时间一到,nginx 就会触发回调函数来关闭该连接,此时服务端上就会出现 TIME_WAIT 状态的连接。所以可以往网络问题的方向排查,比如是否是因为网络问题,导致客户端发送的数据一直没有被服务端接收到,以至于 HTTP 长连接超时。
- HTTP 长连接的请求数量达到上限
Web 服务端通常会有个参数,来定义一条 HTTP 长连接上最大能处理的请求数量,当超过最大限制时,就会主动关闭,比如 nginx 的 keepalive_requests,默认值是 100,对于一些 QPS(每秒请求数)比较高的场景,比如超过 10000 QPS,这个时候 nginx 就会很频繁的关闭连接,此时服务端会出现大量的 TIME_WAIT 状态。
28. 服务器出现大量 CLOSE_WAIT 状态的原因有哪些?
CLOSE_WAIT 状态是 【被动关闭方】才会有的状态,而且如果【被动关闭方】没有调用 close 函数关闭连接,那么就无法发出 FIN 报文,从而无法使得 CLOSE_WAIT 状态的连接转变为 LAST_ACK 状态。
所以,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接。
一个普通的 TCP 服务端的流程:
- 创建服务端 socket,bind 绑定端口,listen 监听端口
- 将服务端 socket 注册到 epoll
- epoll_wait 等待连接到来,连接到来时,调用 accept 获取已连接的 socket
- 将已连接的 socket 注册到 epoll
- epoll_wait 等待事件发生
- 对方连接关闭时,我方调用 close
导致服务端没有调用 close 函数的原因可能有:
- 第 2 步没有做,没有将服务端 socket 注册到 epoll,这样有新连接到来时,服务端没办法感知这个事件,也就无法获取到已连接的 socket,这种原因发生概率较小,属于明显的代码逻辑 bug
- 第 3 步没有做,有新连接到来时没有调用 accept 获取该连接的 socket,导致当有大量的客户端主动断开了连接,而服务端没机会对这些 socket 调用 close 函数。发生这种情况可能是因为服务端在执行 accept 函数之前,代码卡在某一个逻辑或者提前抛出了异常。
- 第 4 步没有做,没有将已连接的 socket 注册到 epoll,导致后续收到 FIN 报文的时候服务端没办法感知这个事件
- 第 6 步没有做,当发现客户端关闭连接后,可能是代码漏处理,或者是在执行 close 之前,代码卡在某一个逻辑,比如发生死锁。
当服务端出现大量 CLOSE_WAIT 状态的连接的时候,通常都是代码的问题,这时候我们需要针对具体的代码一步一步的排查定位。
29. 如果已经建立了连接,但是客户端突然出现了故障怎么办?
服务端无法感知到客户端宕机,所以服务端的 TCP 连接将一直处于 ESTABLISH
状态,占用着系统资源。为了避免这种情况,TCP 搞了个保活机制,在这个时间段内,如果没有任何连接相关的活动,那么 TCP 保活机制会每隔一个时间间隔,发送一个探测报文,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡。
在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:
net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75
net.ipv4.tcp_keepalive_probes=9
用用程序如果想使用 TCP 保活机制需要通过 socket 接口设置 SO_KEEPALIVE
选项才能够生效。
TCP 保活这个机制检测时间比较长,因此可以自己在应用层实现一个心跳机制,比如 web 服务软件提供的 keepalive_timeout
参数,用来指定 HTTP 长连接的超时时间。
30. 如果已经建立了连接,但是服务端的进程崩溃会发生什么?
TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。
比如,使用 kill -9 来模拟进程崩溃的情况,在 kill 掉进程后,服务端会发送 FIN 报文,与客户端进行四次挥手。
31. 针对 TCP 应该如何 Socket 编程?
- 服务端和客户端初始化
socket
,得到文件描述符; - 服务端调用
bind
,将 socket 绑定在指定的 IP 地址和端口; - 服务端调用
listen
,进行监听; - 服务端调用
accept
,等待客户端连接; - 客户端调用
connect
,向服务端的地址和端口发起连接请求; - 服务端
accept
返回用于传输的socket
的文件描述符; - 客户端调用
write
写入数据;服务端调用read
读取数据; - 客户端断开连接时,会调用
close
,那么服务端read
读取数据,就会读取到了EOF
,待处理完数据后,服务端调用close
,表示连接关闭。
这里需要注意的是,服务端调用 accept
时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。
所以,监听的 socket 和真正用来传送数据的 socket,是【两个】socket,一个叫做监听 socket,一个叫做已完成连接 socket。
32. listen 时候参数 backlog 的意义?
Linux 内核会维护两个队列:
- 半连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态;
- 全连接队列(Accept 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态;
int listen (int socketfd, int backlog)
,参数一 socketfd 为 socketfd 文件描述符,backlog 是 accept 队列,但是上限值是内核参数 somaxconn 的大小,也就说 accept 队列长度 = min(backlog, somaxconn)。
33. accept 发生在三次握手的哪一步?
客户端 connect 成功返回是在第二次握手成功以后,服务端 accept 成功返回是在第三次握手成功以后。
34. 客户端调用 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 状态;
- 客户端经过
2MSL
时间之后,也进入 CLOSE 状态。
35. 没有 accept,能建立 TCP 连接吗?
可以,accept 系统调用并不参与 TCP 三次握手过程,它只是负责从 TCP 全连接队列中取出一个已经建立连接的 socket,用户层通过 accept 系统调用拿到了已经建立连接的 socket,就可以对该 socket 进行读写操作了。相当于accept 系统是用于握手完成以后从全连接队列取出已建立的连接的,不影响连接的握手过程。
36. 没有 listen,能建立 TCP 连接吗?
客户端是可以自己连自己的形成连接(TCP 自连接),也可以两个客户端同时向对方发出请求建立连接(TCP 同时打开),这两个情况都没有服务器参与,也就是没有 listen。