1.计算机网络体系结构
OSI七层模型
共分七层,物理层、数据链路层、网络层、传输层、会话层、表示层、应用层
。
TCP/IP模型
5层,`物理层、数据链路层、网络层、传输层、应用层。
2.IP、TCP和UDP
IP是TCP/IP中非常重要的协议,往往用来确定网络中唯一的一台计算设备,它的作用就好比我们现实生活中的电话号码或者或者通讯地址。所以这层负责对数据加上IP地址和其他的数据以确定传输的目标。
TCP和UDP
都是传输层的协议,传输层主要为两台主机上的应用程序提供端到端的通信。
TCP有点类似于我们日常生活中的打电话,电话接通后通过“喂”确认对方身份,听不清会要求对方重说,对方说的太快了会要求对方说慢点,讲完了各说一句“再见”结束通话。
TCP
提供了一种可靠的数据传输服务,TCP是面向连接的字节流,也就是说,利用TCP通信
的两台主机首先要经历一个建立连接的过程,等到连接建立后才开始传输数据,而且传输过程中采用“带重传的肯定确认”
技术来实现传输的可靠性。TCP
还采用一种称为“滑动窗口”
的方式进行流量控制,发送完成后还会关闭连接。
UDP
有点类似于我们日常生活中通过不靠谱的物流系统寄东西。UDP是把数据直接发出去,而不管对方是不是在接收,也不管对方是否能接收的了,也不需要接收方确认,属于不可靠的传输,可能会出现丢包现象,实际应用中要求程序员编程验证。
所以TCP要比UDP可靠的多。
2.1TCP
- 序列号:在建立连接时由计算机生成的随机数作为其初始值。每发送一次数据,就累加一次该数据字节数的大小用来解决网络包乱序问题。
- 确认应答号:指下一次期望收到的数据的序列号。用来解决丢包的问题。
- 控制位:
- ACK:该位为 1 时,「确认应答」的字段变为有效。
- RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
- SYN:该位为 1 时,表示希望建立连接。
- FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。
所以我们可以知道,建立一个 TCP 连接是需要客户端与服务端端达成上述三个信息的共识。
- Socket:由 IP 地址和端口号组成
- 序列号:用来解决乱序问题等
- 窗口大小:用来做流量控制
2.2 UDP
2.3. TCP 和 UDP 区别:
- 连接
TCP 是面向连接的传输层协议,传输数据前先要建立连接。
UDP 是不需要连接,即刻传输数据。 - 服务对象
TCP 是一对一的两点服务,即一条连接只有两个端点。
UDP 支持一对一、一对多、多对多的交互通信 - 可靠性
TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。
UDP 是尽最大努力交付,不保证可靠交付数据。 - 拥塞控制、流量控制
TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。 - 首部开销
TCP 首部长度较长,会有一定的开销,首部在没有使用选项字段时是 20 个字节,如果使用了选项字段则会变长的。
UDP 首部只有 8 个字节,并且是固定不变的,开销较小。 - 传输方式
TCP 是流式传输,没有边界,但保证顺序和可靠。
UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。 - 分片不同
TCP
的数据大小如果大于MSS
大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装TCP
数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
UDP
的数据大小如果大于MTU
大小,则会在IP
层进行分片,目标主机收到后,在IP
层组装完数据,接着再传给传输层。
2.4.TCP/IP网络传输中的数据
- 首先应用程序会进行编码处理产生报文/消息(message)交给下面的TCP层。
- TCP根据应用的指示**,负责建立连接、发送数据以及断开连接**。TCP提供将应用层发来的数据顺利发送至对端的可靠传输,需要将应用层数据封装为报文段(segment)并附加一个TCP首部然后交给下面的IP层。
- IP将TCP传过来的TCP首部和TCP数据合起来当做自己的数据,并在TCP首部的前端加上自己的IP首部生成IP数据报(datagram)然后交给下面的数据链路层。
- 从IP传过来的IP包对于数据链路层来说就是数据。给这些数据附加上链路层首部封装为链路层帧(frame),生成的链路层帧(frame)将通过物理层传输给接收端。
- 用户B主机收到链路层帧(frame)后,首先从链路层帧(frame)首部找到MAC地址判断是否为发送给自己的包,若不是则丢弃数据。如果是发送给自己的包,则从以太网包首部中的类型确定数据类型,再传给相应的模块,如IP、ARP等。
- IP模块接收到数据后也做类似的处理。从包首部中判断此IP地址是否与自己的IP地址匹配,如果匹配则根据首部的协议类型将数据发送给对应的模块,如TCP、UDP。
- 在TCP模块中,首先会计算一下校验和,判断数据是否被破坏。然后检查是否在按照序号接收数据。最后检查端口号,确定具体的应用程序。数据被完整地接收以后,会传给由端口号识别的应用程序。
- 接收端应用程序会直接接收发送端发送的数据。通过解析数据,展示相应的内容。
2.4.1既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?
如果在 TCP
的整个报文(头部 + 数据)
交给 IP 层进行分片,会有什么异常呢?
当 IP 层有一个超过 MTU
大小的数据要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU
。把一份 IP
数据报进行分片以后,由目标主机的IP
层来进行重新组装后,再交给上一层 TCP
传输层。
这看起来井然有序,但这存在隐患的,那么当如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。
因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。
当某一个 IP 分片丢失后,接收方的 IP 层就无法组装成一个完整的 TCP 报文(头部 + 数据),也就无法将数据报文送到 TCP 层,所以接收方不会响应 ACK 给发送方,因为发送方迟迟收不到 ACK 确认报文,所以会触发超时重传,就会重发整个 TCP 报文(头部 + 数据)。
因此,可以得知由 IP 层进行分片传输,是非常没有效率的
。
所以,为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS
值,当 TCP
层发现数据超过 MSS
时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU
,自然也就不用 IP
分片了。经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。
2.5 面试题:为什么端口号有65535个?
因为在TCP、UDP协议报文的开头,会分别有16位二进制来存储源端口号和目标端口号,所以端口个数是2^16=65536个,但是0号端口用来表示所有端口,所以实际可用的端口号是65535个。
- 标准既定的端口号:它是指每个应用程序都有其指定的端口号。例如
HTTP、FTP
等广为使用的应用协议中所使用的端口号就是固定的。 - 时序分配法:服务器有必要确定监听端口号,以让客户端程序访问服务器上的服务。但是客户端没必要确定端口号。在这种方法下,客户端应用程序完全可以不用自己设置端口号,而全权交给操作系统进行分配。
3.TCP特性
3.1.1.SYN 报文什么时候情况下会被丢弃?
3.2.重传机制RTT?
3.3.滑动窗口
4.TCP 三次握手过程是怎样的?
- 第一次握手:客户端将请求报文标志位SYN置为1,请求报文的seq中填入一个随机值J,并将该数据包发送给服务器端,客户端进入
SYN_SENT
状态,等待服务器端确认。 - 第二次握手:服务器端收到数据包后由请求报文标志位SYN=1知道客户端请求建立连接,服务器端将应答报文标志位SYN和ACK都置为1,应答报文的
ack中填入ack=J+1
,应答报文的seq中填入一个随机值K,并将该数据包发送,给客户端以确认连接请求,服务器端进入SYN_RCVD
状态。 - 第三次握手:客户端收到应答报文后,检查ack是否为J+1,ACK是否为1,如果正确则将第三个报文标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入
ESTABLISHED
状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。
4.1.为什么是三次握手?不是两次、四次?
原因:
-
三次握手才可以阻止重复历史连接的初始化(主要原因)
客户端连续发送多次 SYN (都是同一个四元组)建立连接的报文,在网络拥堵情况下: -
旧 SYN 报文比最新的 SYN 报文早到达了服务端,那么此时服务端就会回一个 ·SYN + ACK· 报文给客户端,此报文中的确认号是91。
-
客户端收到后,发现自己期望收到的确认号应该是
101
,而不是91
,于是就会回RST
报文。服务端收到RST
报文后,就会释放连接。
后续最新的SYN
抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。上述中的旧 SYN 报文称为历史连接,TCP 使用三次握手建立连接的最主要原因就是防止历史连接初始化了连接。 -
三次握手才可以同步双方的初始序列号
TCP 连接:Socket、序列号和窗口大小称为连接
。为了实现可靠数据传输,TCP协议的通信双方,都必须维护一个序列号,以标识发送出去的数据包中,哪些是已经被对方收到的。三次握手的过程即是通信双方相互告知序列号起始值,并确认对方已经收到了序列号起始值的必经步骤。两次握手,至多只有连接发起方的起始序列号能被确认,另一方选择的序列号则得不到确认。至于为什么不是四次,很明显,三次握手后,通信的双方都已经知道了对方序列号起始值,也确认了对方知道自己序列号起始值,第四次握手已经毫无必要了。 -
三次握手才可以避免资源浪费
如果只有两次握手,当客户端发生的 SYN 报文在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK 报文,所以服务端每收到一个 SYN 就只能先主动建立一个连接,这会造成什么情况呢?如果客户端发送的 SYN 报文在网络中阻塞了,重复发送多次 SYN 报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
4.2.为什么每次建立 TCP 连接时,初始化的序列号都要求不一样呢?
主要原因有两个方面:
- 为了防止历史报文被下一个相同四元组的连接接收(主要方面);
- 为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收;
4.3.三次握手丢失了,会发生什么?
4.3.1.第一次握手丢失了,会发生什么?
首先第一个发的就是 SYN 报文,然后进入到 SYN_SENT 状态。在这之后,如果客户端迟迟收不到服务端的 SYN-ACK
报文,就会触发超时重传机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的
。
在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries
内核参数控制,这个参数是可以自定义的,默认值一般是 5
。
4.3.2.第二次握手丢失了,会发生什么?
第二次握手,此时服务端会进入 SYN_RCVD
状态。第二次握手的 SYN-ACK 报文其实有两个目的 :
- 第二次握手里的 ACK, 是对第一次握手的确认报文;
- 第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文;
所以,如果第二次握手丢了,就会发生比较有意思的事情,具体会怎么样呢?
客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文
。- 服务端这边会触发超时重传机制,重传 SYN-ACK 报文。
在 Linux 下,SYN-ACK
报文的最大重传次数由 tcp_synack_retries
内核参数决定,默认值是 5。
4.3.3.第三次握手丢失了,会发生什么?
第三次握手,此时客户端状态进入到 ESTABLISH 状态。
因为这个第三次握手的 ACK
是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数
。
注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。
4.4.TCP 半连接队列和全连接队列
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
- 半连接队列,也称 SYN 队列;
- 全连接队列,也称 accept 队列;
我们先来看下 Linux 内核的 SYN 队列(半连接队列)
与 Accpet 队列(全连接队列
)是如何工作的?
正常流程:
- 当服务端接收到客户端的 SYN 报文时,会创建一个半连接的对象,然后将其加入到内核的 SYN 队列;
- 接着发送
SYN + ACK
给客户端,等待客户端回应 ACK 报文
; - 服务端接收到
ACK
报文后,从SYN 队列
取出一个半连接对象,然后创建一个新的连接对象放入到Accept 队列
;
应用通过调用accpet()
,从Accept 队列
取出连接对象。不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文。
4.4.1如何避免 SYN 攻击?
三次握手中有一个第二次握手,服务端向客户端应答请求,应答请求是需要客户端IP的,而且因为握手过程没有完成,操作系统使用队列维持这个状态。假设攻击者短时间伪造不同 IP
地址的 SYN
报文,服务端每接收到一个 SYN
报文,就进入SYN_RCVD
状态,但服务端发送出去的ACK + SYN
报文,无法得到未知 IP
主机的 ACK
应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。
最好的方案是防火墙。
- 防火墙
- 无效连接监视释放:不停监视所有的连接,包括三次握手的,还有握手一次的,反正是所有的,当
达到一定(与)阈值时拆除这些连接
,从而释放系统资源。不管是正常的还是攻击的,所以这种方式不推荐。 - 延缓TCB分配方法: 一般的做完第一次握手之后,服务器就需要为该请求分配一个
TCB
(连接控制资源),通常这个资源需要200多个字节。延迟TCB的分配,当正常连接建立起来后再分配TCB则可以有效地减轻服务器资源的消耗。 - 增大 TCP 半连接队列
- 减少
SYN+ACK
重传次数 - 开启
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 值,表示无条件开启功能;
6.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 状态。
6.1.为什么挥手需要四次?
关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。服务端收到客户端的 FIN
报文时,先回一个 ACK 应答报文
,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN
一般都会分开发送,因此是需要四次挥手。
6.2.四次挥手丢失了,会发生什么?
6.2.1.第一次挥手丢失了,会发生什么?
TCP是全双工的连接,必须两端同时关闭连接,连接才算真正关闭。
主动关闭方调用 close
函数后,就会向服务端
发送 FIN
报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1 状态。
- 正常情况下,如果能及时
收到被动关闭方的 ACK
,则会很快变为FIN_WAIT2
状态。 - 如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,次数由
tcp_orphan_retries
参数控制。当客户端重传 FIN 报文的次数超过tcp_orphan_retries
后,就不再发送FIN
报文,则会在等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到第二次挥手,那么直接进入到close
状态。
6.2.2.第二次挥手丢失了,会发生什么?
服务端收到客户端的第一次挥手后,就会先回一个 ACK
确认报文,此时服务端的连接进入到 CLOSE_WAIT
状态。
ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。
6.2.3.第三次挥手丢失了,会发生什么?
当主动关闭方收到第二次挥手,也就是收到被动关闭方发送的 ACK
报文后,客户端就会处于 FIN_WAIT2
状态,在这个状态需要等服务端发送第三次挥手,也就是服务端的 FIN
报文。
被动关闭方收到主动关闭方的 FIN
报文后,内核会自动回复 ACK
,同时连接处于 CLOSE_WAIT
状态,它表示等待应用进程调用 close 函数关闭连接。对于 close
函数关闭的连接,由于无法再发送和接收数据,所以FIN_WAIT2
状态不可以持续太久,而tcp_fin_timeout
控制了这个状态下连接的持续时长,默认值是 60 秒。这意味着对于调用 close
关闭的连接,如果在 60
秒后还没有收到 FIN 报文,主动关闭方的连接就会直接关闭。
服务端处于 CLOSE_WAIT
状态时,调用了 close 函数,内核就会发出 FIN
报文,同时连接进入LAST_ACK
状态,等待客户端返回 ACK 来确认连接关闭。如果迟迟收不到这个 ACK
,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,这与客户端重发 FIN
报文的重传次数控制方式是一样的。
6.2.4.第四次挥手丢失了,会发生什么?
当客户端收到服务端的第三次挥手的 FIN
报文后,就会回 ACK
报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT
状态。在 Linux 系统,TIME_WAIT
状态会持续 2MSL
后才会进入关闭状态。被动关闭方没有收到 ACK 报文前,还是处于 LAST_ACK
状态。如果第四次挥手的 ACK
报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。
6.3.TIME_WAIT
6.3.1.为什么需要 TIME_WAIT 状态?
主动发起关闭连接的一方,才会有 TIME-WAIT
状态。需要 TIME-WAIT
状态,主要是两个原因:
- 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
我们先来了解序列号(SEQ)和初始序列号(ISN)。序列号,是 TCP 一个头部字段,标识了 TCP 发送端到 TCP 接收端的数据流的一个字节,因为 TCP 是面向字节流的可靠协议,为了保证消息的顺序性和可靠性,TCP 为每个传输方向上的每个字节都赋予了一个编号,以便于传输成功后确认、丢失后重传以及在接收端保证不会乱序。序列号是一个 32 位的无符号数,因此在到达 4G 之后再循环回到 0。
初始序列号,在 TCP 建立连接的时候,客户端和服务端都会各自生成一个初始序列号,它是基于时钟生成的一个随机数,来保证每个连接都拥有不同的初始序列号。初始化序列号可被视为一个 32 位的计数器,该计数器的数值每 4 微秒加 1,循环一次需要 4.55 小时。 - 保证被动关闭连接的一方,能被正确的关闭;
6.3.2.为什么 TIME_WAIT 等待的时间是 2MSL?
MSL
是报文最大生存时间
,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文
基于是 IP 协议
的,而 IP 头中有一个 TTL 字段
,是IP 数据报可以经过的最大路由数
,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃。
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
状态的连接可以应对。
2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。
6.3.3.TIME_WAIT 过多有什么危害?
TIME_WAIT 过多有什么危害?
过多的 TIME-WAIT 状态主要的危害有两种:
- 第一是占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等;
- 第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为 32768~61000,也可以通过 net.ipv4.ip_local_port_range参数指定范围。
客户端和服务端 TIME_WAIT 过多,造成的影响是不同的。
- 客户端(主动发起关闭连接方)的
TIME_WAIT
状态过多,占满了所有端口资源,那么就无法对目的 IP+ 目的 PORT都一样的服务端发起连接了,但是被使用的端口,还是可以继续对另外一个服务端发起连接的。 - 服务端(主动发起关闭连接方)的
TIME_WAIT
状态过多,并不会导致端口资源受限,因为服务端只监听一个端口,而且由于一个四元组唯一确定一个 TCP 连接,因此理论上服务端可以建立很多连接,但是 TCP 连接过多,会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等
。
#如何优化 TIME_WAIT?
6.3.4.如何优化 TIME_WAIT?
这里给出优化 TIME-WAIT
的几个方式,都是有利有弊:
- 打开
net.ipv4.tcp_tw_reuse
和net.ipv4.tcp_timestamps
选项;
如下的Linux
内核参数开启后,则可以复用处于TIME_WAIT
的socket
为新的连接所用。
有一点需要注意的是,tcp_tw_reuse 功能只能用连接发起方,因为开启了该功能,在调用connect()
函数时,内核会随机找一个time_wait
状态超过1
秒的连接给新的连接复用。
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_timestamps=1(默认即为 1)
这个时间戳的字段是在 TCP 头部的选项里,它由一共 8
个字节表示时间戳,其中第一个4
字节字段用来保存发送该数据包的时间,第二个 4
字节字段用来保存最近一次接收对方发送到达数据的时间。
由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。
-
net.ipv4.tcp_max_tw_buckets
这个值默认为18000
,当系统中处于TIME_WAIT
的连接一旦超过这个值时,系统就会将后面的TIME_WAIT
连接状态重置,这个方法比较暴力。 -
程序中使用 SO_LINGER ,应用强制使用 RST 关闭。
6.3.5.服务器出现大量 TIME_WAIT 状态的原因有哪些?
TIME_WAIT
状态是主动关闭连接方才会出现的状态,所以如果服务器出现大量的 TIME_WAIT
状态的 TCP
连接,就是说明服务器主动断开了很多 TCP 连接
。
问题来了,什么场景下服务端会主动断开连接呢?
- HTTP 没有使用长连接
- HTTP 长连接超时
- HTTP 长连接的请求数量达到上限
场景一:HTTP 没有使用长连接
HTTP/1.0 中默认是关闭的,如果浏览器要开启 Keep-Alive
,它必须在请求的 header 中添加:
Connection: Keep-Alive
然后当服务器收到请求,作出回应的时候,它也被添加到响应中 header 里。
从 HTTP/1.1
开始, 就默认是开启了 Keep-Alive
。如果要关闭 HTTP Keep-Alive
,需要在 HTTP
请求或者响应的 header
里添加 Connection:close
信息,也就是说,只要客户端和服务端任意一方的 HTTP header 中有 Connection:close 信息,那么就无法使用 HTTP 长连接的机制。
不过,根据大多数 Web 服务的实现,不管哪一方禁用了 HTTP Keep-Alive,都是由服务端主动关闭连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接
。
问题一:客户端禁用了 HTTP Keep-Alive,服务端开启 HTTP Keep-Alive,谁是主动关闭方?
客户端禁用了HTTP Keep-Alive
, header
就会有 Connection:close
信息,这时服务端在发完 HTTP响应后,就会主动关闭连接。原因:HTTP 是请求-响应模型
,发起方一直是客户端,HTTP Keep-Alive 的初衷是为客户端后续的请求重用连接,如果我们在某次 HTTP 请求-响应模型中,请求的 header 定义了 connection:close
信息,那不再重用这个连接的时机就只有在服务端了,所以我们在 HTTP 请求-响应这个周期的末端关闭连接是合理的。
问题二:客户端开启了 HTTP Keep-Alive,服务端禁用了 HTTP Keep-Alive,谁是主动关闭方?
客户端开启了 HTTP Keep-Alive,而服务端禁用了 HTTP Keep-Alive,这时服务端在发完 HTTP 响应后,服务端也会主动关闭连接。 原因:在服务端主动关闭连接的情况下,只要调用一次close()
就可以释放连接,剩下的工作由内核 TCP 栈
直接进行了处理,整个过程只有一次 syscall
;如果是要求 客户端关闭,则服务端在写完最后一个 response 之后需要把这个 socket
放入readable
队列,调用 select / epoll
去等待事件;然后调用一次 read()
才能知道连接已经被关闭,这其中是两次 syscall
,多一次用户态程序被激活执行,而且socket
保持时间也会更长。
服务端出现大量的 TIME_WAIT 状态连接的时候,可以排查下是否客户端和服务端都开启了 HTTP Keep-Alive
,因为任意一方没有开启 HTTP Keep-Alive
,都会导致服务端在处理完一个 HTTP 请求后,就主动关闭连接,此时服务端上就会出现大量的 TIME_WAIT
状态的连接。
场景二:HTTP 长连接超时
使用了 HTTP 长连接,如果客户端完成一个 HTTP 请求后,就不再发起新的请求,此时这个 TCP 连接一直占用着不是挺浪费资源的吗?,所以为了避免资源浪费的情况,web 服务软件一般都会提供一个参数,用来指定 HTTP 长连接的超时时间,比如 nginx 提供的keepalive_timeout
参数。
第三个场景:HTTP 长连接的请求数量达到上限
Web 服务端通常会有个参数,来定义一条 HTTP 长连接上最大能处理的请求数量,当超过最大限制时,就会主动关闭连接。
比如 nginx 的 keepalive_requests
这个参数,这个参数是指一个 HTTP 长连接建立之后,nginx 就会为这个连接设置一个计数器,记录这个 HTTP 长连接上已经接收并处理的客户端请求的数量。如果达到这个参数设置的最大值时,则 nginx 会主动关闭这个长连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接。
keepalive_requests
参数的默认值是 100
,意味着每个 HTTP 长连接最多只能跑 100
次请求,这个参数往往被大多数人忽略,因为当QPS
(每秒请求数) 不是很高时,默认值 100
凑合够用。
但是,对于一些 QPS
比较高的场景,比如超过 10000
QPS,甚至达到30000 , 50000
甚至更高,如果 keepalive_requests
参数值是 100
,这时候就 nginx 就会很频繁地关闭连接,那么此时服务端上就会出大量的 TIME_WAIT
状态。
6.3.6.在 TIME_WAIT 状态的 TCP 连接,收到 SYN 后会发生什么?
6.4.服务器出现大量 CLOSE_WAIT 状态的原因有哪些?
CLOSE_WAIT 状态是被动关闭方才会有的状态,而且如果被动关闭方没有调用 close 函数关闭连接,那么就无法发出 FIN 报文,从而无法使得 CLOSE_WAIT
状态的连接转变为 LAST_ACK
状态。
**所以,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接。**
我们先来分析一个普通的 TCP 服务端的流程:
- 创建服务端 socket,bind 绑定端口、listen 监听端口
- 将服务端
socket
注册到epoll
epoll_wait
等待连接到来,连接到来时,调用accpet
获取已连接的socket
- 将已连接的
socket
注册到epoll
- epoll_wait 等待事件发生
- 对方连接关闭时,我方调用 close
- 可能导致服务端没有调用 close 函数的原因,如下。
第一个原因:第 2 步没有做,没有将服务端 socket 注册到 epoll,这样有新连接到来时,服务端没办法感知这个事件,也就无法获取到已连接的 socket,那服务端自然就没机会对 socket 调用 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 连接原因分析(opens new window)
第四个原因:第 6 步没有做,当发现客户端关闭连接后,服务端没有执行 close 函数,可能是因为代码漏处理,或者是在执行 close 函数之前,代码卡在某一个逻辑,比如发生死锁等等。
可以发现,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,通常都是代码的问题,这时候我们需要针对具体的代码一步一步的进行排查和定位,主要分析的方向就是服务端为什么没有调用 close。