TCP 协议(二)连接与断开

三次握手与四次挥手

在学习计算机网络之前,我们对于“三次握手”和“四次挥手”有所耳闻,其实这两个名词指的就是 TCP 连接与断开过程。

三次握手过程

三次握手
三次握手是为了让客户端和服务端分别确认自己和对方接收和发送消息的能力是正常的。
一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。
1.第一次握手:客户端会发送 SYN 报文给服务端,TCP 部首 SYN 标志位置为 1,并随机初始化首部序列号 seq=x;表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN_SENT 状态。
2.第二次握手:服务端收到客户端的 SYN 报文后,首先,服务端也随机初始化自己的 TCP 部首序列号 seq=y;其次,把首部的确认号填入 ack=x+1;接着,把部首 SYN 和 ACK 标志位都置为 1;最后、把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN_RCVD 状态。
3.第三次握手:客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次部首确认号填入 ack=y+1 ,最后把报文发送给服务端。这次报文可以携带客户到服务器的数据。
一旦完成三次握手,双方都处于 ESTABLISHED 状态,此致连接就已建立完成,客户端和服务端就可以相互发送数据了。
从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的。

四次挥手过程

TCP是全双工的工作模式,因此每个方向都必须单独进行关闭。当一方完成自己的数据发送任务后,就可以发送一个FIN报文来终止这个方向的连接。
四次挥手

四次挥手也就是客户端与服务器断开连接时,需要一共发送四个报文段来完成断开TCP连接。
初始时,客户端与服务器都处于 ESTABLISHED 状态,假如客户端发起断开连接的请求(服务器也可以发起),四次挥手过程如下:
1.第一次挥手:客户端发送 FIN 报文给服务端,TCP 部首 FIN 标志位置为 1,并随机初始化部首序列号 seq=u。之后客户端处于 FIN_WAIT_1 状态。
2.第二次挥手:服务器收到 FIN 报文后,立即发送一个 ACK 报文,部首确认号为 ack=u+1,序号设为 seq=v。表明已经收到了客户端的报文。之后服务器处于 CLOSE_WAIT 状态。
在第二次挥手和第三次挥手之间的时间段内,由于只是半关闭的状态,数据还是可以从服务器传送到客户端的。
3.第三次挥手:如果数据传送完毕,服务器也想断开连接,那么就发送 FIN 报文给客户端,并重新指定一个序号 seq=w,确认号还是ack=u+1,表明可以断开连接。
4.第四次挥手:客户端收到报文后,发出一个 ACK 报文应答,上一次客户端发送的报文序列号为 seq=u,那么这次序列号就是 seq=u+1,确认号为 ack=w+1。此时客户端处于 TIME_WAIT 状态,需要经过一段时间确保服务器收到自己的应答报文后,才会进入 CLOSED 状态。
服务器收到ACK报文后,就关闭连接,也处于CLOSED状态了。

11种状态名词解析​​​​​​​

LISTEN:等待从任何远端TCP 和端口的连接请求。
SYN_SENT:发送完一个连接请求后等待一个匹配的连接请求。
SYN_RCVD:发送连接请求并且接收到匹配的连接请求以后等待连接请求确认。
ESTABLISHED:表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态。
FIN_WAIT_1:等待远端TCP 的连接终止请求,或者等待之前发送的连接终止请求的确认。
FIN_WAIT_2:等待远端TCP 的连接终止请求。
CLOSE_WAIT:等待本地用户的连接终止请求。
CLOSING:等待远端TCP 的连接终止请求确认。
LAST_ACK:等待先前发送给远端TCP 的连接终止请求的确认(包括它字节的连接终止请求的确认)
TIME_WAIT:等待足够的时间过去以确保远端TCP 接收到它的连接终止请求的确认。TIME_WAIT 两个存在的理由:1.可靠的实现tcp全双工连接的终止;2.允许老的重复分节在网络中消逝。
CLOSED:不在连接状态(这是为方便描述假想的状态,实际不存在)

抓包分析

第一次握手:客户端10.180.201.67发送 SYN 报文。
seq=1073647333(x),relative seq=0;
ack=0,relative ack=0。
第一次握手
第二次握手:服务端10.49.44.14发送 SYN / ACK 报文。
seq=326764992(y),relative seq=0;
ack=1073647334(x+1),relative ack=1。
第二次握手
第三次握手:客户端10.180.201.67发送 ACK 报文。
seq=1073647334(x+1),relative seq=1;
ack=326764993(y+1),relative ack=1。
第三次握手
客户端第一次发送数据:客户端10.180.201.67发送数据。
seq=1073647334(x+1),relative seq=1,payload=130,数据流的第一个字节编码为1,也即为relative seq=1;
ack=326764993(y+1),relative ack=1。
客户端第一次发送数据
服务端响应:服务端10.49.44.14发送响应数据。
seq=326764993(y+1),relative seq=1;
ack=1073647464(x+1+130),relative ack=131,字节流编码130之前的数据服务器已收到,请客户端继续发送131及以后的数据,也即为relative ack=131。
服务端第一次响应
服务端第一次发送数据:服务端10.49.44.14发送数据。
seq=326764993(y+1),relative seq=1,payload=261;
ack=1073647464(x+1+130),relative ack=131,还是上次的确认号,没有改变。
服务端第一次发送数据
客户端第二次发送数据:客户端10.180.201.67发送数据。
seq=1073647334(x+1+130),relative seq=131,payload=156;
ack=326765254(y+1+261),relative ack=262,同时确认服务端发送的第一次数据(261字节)。
客户端第二次发送数据
中间经过多次收发数据之后。
四次挥手好像没有完全遵守四次挥手的标准。
第一次挥手,客户端10.180.201.67断开链接。
seq=1073648132,relative seq=799,payload=0;
ack=3296855786,relative ack=90794。
客户端断开链接
第二次挥手,服务端10.49.44.14发送确认报文。
seq=3296855786,relative seq=90794,payload=179;
ack=1073648132,relative ack=799。
第二次挥手
第三次挥手;服务端发送 FIN 报文。
seq=3296855965(3296855786+19),relative seq=90973(90794+179),payload=0;
ack=1073648132,relative ack=799。
第三次挥手

三次握手问题总结

三次握手的作用

1.确认双方的接受能力、发送能力是否正常。
2.指定自己的初始化序列号,为后面的可靠传送做准备。
3.如果是 https 协议的话,三次握手这个过程,还会进行数字证书的验证以及加密密钥的生成到。

为什么是三次握手?不是两次、四次?

相信大家比较常回答的是:“因为三次握手才能保证双方具有接收和发送的能力”。
这回答是没问题,但这回答是片面的,并没有说出主要的原因。
在前面我们知道了什么是 TCP 连接:
用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。
所以,重要的是为什么三次握手才可以初始化Socket、序列号和窗口大小并建立 TCP 连接。
接下来以三个方面分析三次握手的原因:
三次握手才可以阻止重复历史连接的初始化(主要原因)
三次握手才可以同步双方的初始序列号
三次握手才可以避免资源浪费

原因一:阻止重复历史连接的初始化

简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。我们考虑一个场景,客户端先发送了 SYN(seq = 90) 报文,然后客户端宕机了,而且这个 SYN 报文还被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq = 100) 报文(注意!不是重传 SYN,重传的 SYN 的序列号是一样的)。
看看三次握手是如何阻止历史连接的:
三次握手避免历史链接
客户端连续发送多次 SYN (都是同一个四元组)建立连接的报文,在网络拥堵情况下:一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端,那么此时服务端就会回一个 SYN + ACK 报文给客户端,此报文中的确认号是 91(90+1)。客户端收到后,发现自己期望收到的确认号应该是 100+1,而不是 90 + 1,于是就会回 RST(复位链接)报文。服务端收到 RST 报文后,就会释放连接。后续最新的 SYN 抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。上述中的「旧 SYN 报文」称为历史连接,TCP 使用三次握手建立连接的最主要原因就是防止「历史连接」初始化了连接。

原因二:同步双方初始序列号

TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
1).接收方可以去除重复的数据;
2).接收方可以根据数据包的序列号按序接收;
3).可以标识发送出去的数据包中, 哪些是已经被对方收到的;
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
同步双方初始序列号
四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。
而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。

原因三:避免资源浪费

如果只有「两次握手」,当客户端的 SYN 请求连接在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 ACK 确认信号,所以每收到一个 SYN 就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端的 SYN 阻塞了,重复发送多次 SYN 报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
在这里插入图片描述
即两次握手会造成消息滞留情况下,服务器重复接受无用的连接请求 SYN 报文,而造成重复分配资源。

小结

TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
不使用「两次握手」和「四次握手」的原因:
「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

初始序列号 ISN 是如何随机产生的?

起始 ISN 是基于时钟的,每 4 毫秒 + 1,转一圈要 4.55 个小时。
RFC1948 中提出了一个较好的初始化序列号 ISN 随机生成算法。
ISN = M + F (localhost, localport, remotehost, remoteport)
M 是一个计时器,这个计时器每隔 4 毫秒加 1。
F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。

为什么客户端和服务端的初始序列号 ISN 是不相同的?

三次握手的一个重要功能是客户端和服务端交换 ISN(Initial Sequence Number), 以便让对方知道接下来接收数据的时候如何按序列号组装数据。如果ISN是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。

因为网络中的报文会延迟、会复制重发、也有可能丢失,这样会造成的不同连接之间产生互相影响,所以为了避免互相影响,客户端和服务端的初始序列号是随机且不同的。

什么是半连接队列

服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。
这里在补充一点关于SYN/ACK 重传次数的问题:服务器发送完 SYN/ACK 包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s, 2s, 4s, 8s, …

三次握手过程中可以携带数据吗?

第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。
为什么这样呢?大家可以想一个问题,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。也就是说,第一次握手可以放数据的话,其中一个简单的原因就是会让服务器更加容易受到攻击了。
而对于第三次的话,此时客户端已经处于 established 状态,也就是说,对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据页没啥毛病。

既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?

我们先来认识下 MTU 和 MSS
MTU
MTU:一个网络包的最大长度,以太网中一般为 1500 字节;
MSS:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度;
如果 TCP 的整个报文(头部 + 数据)交给 IP 层进行分片,会有什么异常呢?
当 IP 层有一个超过 MTU 大小的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后,由目标主机的 IP 层来进行重新组装后,在交给上一层 TCP 传输层。
这看起来井然有序,但这存在隐患的,那么当如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。
因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。
当接收方发现 TCP 报文(头部 + 数据)的某一片丢失后,则不会响应 ACK 给对方,那么发送方的 TCP 在超时后,就会重发「整个 TCP 报文(头部 + 数据)」。
因此,可以得知由 IP 层进行分片传输,是非常没有效率的。
所以,为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过 MSS 时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU ,自然也就不用 IP 分片了。
MSS
经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。

什么是 SYN 攻击?如何避免 SYN 攻击?

SYN 攻击

我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的 SYN 接收队列(未连接队列),使得服务器不能为正常用户服务。
SYN 攻击

避免 SYN 攻击方式一

其中一种解决方式是通过修改 Linux 内核参数,控制队列大小和当队列满时应做什么处理。
当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。

// 控制该队列的最大值如下参数
net.core.netdev_max_backlog
// SYN_RCVD 状态连接的最大个数
net.ipv4.tcp_max_syn_backlog
// 超出处理能时,对新的 SYN 直接回 RST,丢弃连接
net.ipv4.tcp_abort_on_overflow

避免 SYN 攻击方式二

我们先来看下Linux 内核的 SYN (未完成连接建立)队列与 Accpet (已完成连接建立)队列是如何工作的?
正常流程
正常流程:
当服务端接收到客户端的 SYN 报文时,会将其加入到内核的「 SYN 队列」;
接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;
服务端接收到 ACK 报文后,从「 SYN 队列」移除放入到「 Accept 队列」;
应用通过调用 accpet() socket 接口,从「 Accept 队列」取出的连接。
应用程序过慢
如果应用程序过慢时,就会导致「 Accept 队列」被占满。
SYN攻击二
如果不断受到 SYN 攻击,就会导致「 SYN 队列」被占满。
tcp_syncookies 的方式可以应对 SYN 攻击的方法:
net.ipv4.tcp_syncookies = 1
启动cookie
当「 SYN 队列」满之后,后续服务器收到 SYN 包,不进入「 SYN 队列」;
计算出一个 cookie 值,再以 SYN + ACK 中的「序列号」返回客户端,
服务端接收到客户端的应答报文时,服务器会检查这个 ACK 包的合法性。如果合法,直接放入到「 Accept 队列」。
最后应用通过调用 accpet() socket 接口,从「 Accept 队列」取出的连接。

如何在 Linux 系统中查看 TCP 状态?

TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。
netstat
Linux netstat 命令用于显示网络状态。利用 netstat 指令可让你得知整个 Linux 系统的网络情况。

语法 netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][–ip] 参数说明:-a,--–all 显示所有连线中的Socket。-A<网络类型>或–<网络类型> 列出该网络类型连线中的相关地址。-c,--continuous 持续列出网络状态。-C,--cache 显示路由器配置的快取信息。-e,--extend 显示网络其他相关信息。-F,--fib 显示FIB。-g,--groups 显示多重广播功能群组组员名单。-h,--help 在线帮助。-i,--interfaces 显示网络界面信息表单。-l,--listening 显示监控中的服务器的Socket。-M,--masquerade 显示伪装的网络连线。-n,--numeric 直接使用IP地址,而不通过域名服务器。-N,--netlink或–symbolic 显示网络硬件外围设备的符号连接名称。-o,--timers 显示计时器。-p,--programs 显示正在使用Socket的程序识别码和程序名称。-r,--route 显示Routing Table。-s,--statistics 显示网络工作信息统计表。-t,--tcp 显示TCP传输协议的连线状况。-u,--udp 显示UDP传输协议的连线状况。-v,--verbose 显示指令执行过程。-V,--version 显示版本信息。-w,--raw 显示RAW传输协议的连线状况。-x,--unix 此参数的效果和指定"-A unix"参数相同。--ip,--inet 此参数的效果和指定"-A inet"参数相同。

四次挥手问题总结

为什么挥手需要四次?

再来回顾下四次挥手双方发 FIN 包的过程,就能理解为什么需要四次了。
关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。 服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,从而比三次握手导致多了一次。

为什么 TIME_WAIT 等待的时间是 2MSL?

MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。
MSL 与 TTL 的区别:MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。
TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是:网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
比如,如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 Fin 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。
2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。
其定义在 Linux 内核代码里的名称为 TCP_TIMEWAIT_LEN:

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT state, about 60 seconds  */

如果要修改 TIME_WAIT 的时间长度,只能修改 Linux 内核代码里 TCP_TIMEWAIT_LEN 的值,并重新编译 Linux 内核。

为什么需要 TIME_WAIT 状态?

主动发起关闭连接的一方,才会有 TIME_WAIT 状态。
需要 TIME_WAIT 状态,主要是两个原因:
1.防止具有相同「四元组」的「旧」数据包被收到; 保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK;
2.能让被动关闭方接收,从而帮助其正常关闭。

原因一:防止旧连接的数据包

假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后会发生什么呢?
防止旧链接的数据包
如上图黄色框框服务端在关闭连接之前发送的 SEQ = 301 报文,被网络延迟了。
这时有相同端口的 TCP 连接被复用后,被延迟的 SEQ = 301 抵达了客户端,那么客户端是有可能正常接收这个过期的报文,这就会产生数据错乱等严重的问题。
所以,TCP 就设计出了这么一个机制,经过 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 能让被动关闭方接收,从而帮助其正常关闭。
假设 TIME_WAIT 没有等待时间或时间过短,断开连接会造成什么问题呢?
保证链接正确关闭
如上图红色框框客户端四次挥手的最后一个 ACK 报文如果在网络中被丢失了,此时如果客户端 TIME_WAIT 过短或没有,则就直接进入了 CLOSE 状态了,那么服务端则会一直处在 LASE_ACK 状态。
当客户端发起建立连接的 SYN 请求报文后,服务端会发送 RST 报文给客户端,连接建立的过程就会被终止。
如果 TIME_WAIT 等待足够长的情况就会遇到两种情况:
1.服务端正常收到四次挥手的最后一个 ACK 报文,则服务端正常关闭连接。
2.服务端没有收到四次挥手的最后一个 ACK 报文时,则会重发 FIN 关闭连接报文并等待新的 ACK 报文。
所以客户端在 TIME_WAIT 状态等待 2MSL 时间后,就可以保证双方的连接都可以正常的关闭。

TIME_WAIT 过多有什么危害?

如果服务器有处于 TIME_WAIT 状态的 TCP,则说明是由服务器方主动发起的断开请求。
过多的 TIME_WAIT 状态主要的危害有两种:
1.第一是内存资源占用;
2.第二是对端口资源的占用,一个 TCP 连接至少消耗一个本地端口。
第二个危害是会造成严重的后果的,要知道,端口资源也是有限的,一般可以开启的端口为 32768~61000,也可以通过参数 net.ipv4.ip_local_port_range 设置指定。
如果服务端 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。

如何优化 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_timestamps

如下的 Linux 内核参数开启后,则可以复用处于 TIME_WAIT 的 socket 为新的连接所用。
net.ipv4.tcp_tw_reuse = 1
使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即 net.ipv4.tcp_timestamps=1(默认即为 1)
这个时间戳的字段是在 TCP 头部的「选项」里,用于记录 TCP 发送方的当前时间戳和从对端接收到的最新时间戳。
由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。
温馨提醒: net.ipv4.tcp_tw_reuse 要慎用,因为使用了它就必然要打开时间戳的支持 net.ipv4.tcp_timestamps,当客户端与服务端主机时间不同步时,客户端的发送的消息会被直接拒绝掉。

方式二:net.ipv4.tcp_max_tw_buckets

这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将所有的 TIME_WAIT 连接状态重置。
这个方法过于暴力,而且治标不治本,带来的问题远比解决的问题多,不推荐使用。

方式三:程序中使用 SO_LINGER

我们可以通过设置 socket 选项,来设置调用 close 关闭连接行为。

struct linger so_linger;
so_linger.l_onoff = 1;
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 状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。

如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP 有一个机制是保活机制。这个机制的原理是这样的:
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:

net.ipv4.tcp_keepalive_time=7200; // 表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
net.ipv4.tcp_keepalive_intvl=75; // 表示每次检测间隔 75 秒;
net.ipv4.tcp_keepalive_probes=9; // 表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。

也就是说在 Linux 系统中,最少需要经过2小时11分15 秒才可以发现一个「死亡」连接。
客户端故障
这个时间是有点长的,我们也可以根据实际的需求,对以上的保活相关的参数进行设置。
如果开启了 TCP 保活,需要考虑以下几种情况:
第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。
第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。

socket 编程与状态分析

Socket 编码流程

网络编程
服务端和客户端初始化 socket,得到文件描述符;
服务端调用 bind,将绑定在 IP 地址和端口;
服务端调用 listen,进行监听等待客户端请求;
客户端调用 connect,向服务器端的地址和端口发起连接请求;
服务端调用 accept,接受客户端连接,accept 返回用于传输的 socket 的文件描述符;
客户端调用 write 写入数据;
服务端调用 read 读取数据;
客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。
注意: 服务端调用 accept 时,会返回一个已完成连接的 socket,用于后续传输数据。所以,服务端监听的 socket 和真正用来传送数据的 socket,是两个socket,一个叫作监听 socket,一个叫作已完成连接 socket。

内核SYN队列和Accept队列

Linux 内核中会维护两个队列:
未完成连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态;
已完成连接队列(Accpet 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态;
内核维护的网络队列

int listen (int socketfd, int backlog)
// 参数一:socketfd 为 socketfd 文件描述符
// 参数二:backlog,这参数在历史有一定的变化
// 在早期 Linux 内核 backlog 是 SYN 队列大小,也就是未完成的队列大小。
// 在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 accept 队列。

accept 和 close 处理时机?

在这里插入图片描述

accept 在三次握手的哪一步?

客户端向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 seq=x,客户端进入 SYNC_SENT 状态;
服务器端收到这个包之后,返回 ACK 应答,应答的值为ack=x+1,表示对客户端 SYN 包的确认,同时服务器也告诉客户端当前发送序列号为 seq=y,服务器端进入 SYNC_RCVD 状态;
客户端收到 ACK 之后,应用程序从 connect 调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端也会对服务器端的 SYN 包进行确认应答,确认号 ack=y+1;
应答包到达服务器端后,服务器端程序从 accept 调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入 ESTABLISHED 状态。
从上面的描述过程,我们可以得知客户端 connect 成功返回是在第二次握手,服务端 accept 成功返回是在三次握手成功之后。

客户端调用 close 了,连接断开的流程是什么?

客户端调用 close,表明客户端没有数据需要发送了,则此时会向服务端发送 FIN 报文,并进入 FIN_WAIT_1 状态;
服务端接收到了 FIN 报文,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,应用程序可以通过 read 调用来感知这个 FIN 包。文件结束符 EOF 会被放在排队等候的已接收数据之后,这就意味着服务端需要处理这种异常情况,因为 EOF 表示在该连接上再无额外数据到达。此时,服务端进入 CLOSE_WAIT 状态;
当已数据处理完之后,自然就会读到 EOF,于是调用 close 关闭已完成连接 socket,并发出一个 FIN 包,之后处于 LAST_ACK 状态;
客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
客户端进过 2MSL 时间之后,也进入 CLOSED 状态。

参考:
https://blog.csdn.net/qq_32907195/article/details/108335868
https://blog.csdn.net/m0_38106923/article/details/108292454
https://blog.csdn.net/sutong_first/article/details/126540687
https://blog.csdn.net/weixin_52109884/article/details/119885387

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/18171.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vulnhub靶机渗透:PWNLAB: INIT

PWNLAB: INIT 靶机环境介绍nmap扫描端口扫描服务扫描漏洞扫描扫描总结 80端口目录爆破LFI利用 3306端口回到80端口文件上传 获得立足点横向移动提权总结参考 靶机环境介绍 https://www.vulnhub.com/entry/skytower-1,96/ 靶机IP&#xff1a;192.168.56.103 kali IP&#xff…

【Redis基础】快速入门

一、初识Redis 1. 认识NoSQL 2. 认识Redis Redis诞生于2009年&#xff0c;全称是Remote Dictionary Server&#xff08;远程词典服务器&#xff09;&#xff0c;是一个基于内存的键值型NoSQL数据库特征 &#xff08;1&#xff09;键值&#xff08;key-value&#xff09;型&am…

在vite创建的vue3项目中使用Cesium加载纽约建筑模型、设置样式,划分城市区域并着色

在vite创建的vue3项目中使用Cesium加载纽约建筑模型、设置样式&#xff0c;划分城市区域并着色 使用vite创建vue3项目 npm create vitelatestcd到创建的项目文件夹中 npm install安装Cesium npm i cesium vite-plugin-cesium vite -D配置 vite.config.js文件&#xff1a;添加Ce…

基于OpenCV 实现车牌号码识别--附免费源码

在本教程中,您将学习如何使用 OpenCV 和 EasyOCR 包自动执行车牌/车牌识别 (LPR/NPR)。 EasyOCR是一个开源 Python 包,用于执行光学字符识别 - OCR(从图像中提取文本)。 该软件包非常易于使用,在撰写本文时,它支持 80 多种语言,包括中文、阿拉伯语、法语、英语、西里尔…

【Linux】- 常用指令和运行级别

运行级别 1.1&#x1f69e;指定运行级别1.2&#x1f68a;帮助指令1.3&#x1f694;文件目录类指令2.1 **ls 指令**2.2 **cd 指令**2.3 **mkdir 指令**2.4 **rmdir 指令**3.1 **touch 指令**3.2 **cp 指令**3.3 **rm 指令**3.4 **mv 指令**4.1 **cat 指令**4.2 **more 指令**4.3…

【C++学习笔记】C++如何规范C语言中的类型转换

C的类型转换 1 C语言中类型转换的缺陷2 为什么C要规范C的类型转换3 C强制类型转换3.1 static_cast3.2 reinterpret_cast3.3 const_cast3.4 dynamic_cast 1 C语言中类型转换的缺陷 在C语言中&#xff0c;如果赋值运算符左右两侧类型不同&#xff0c;或者形参与实参类型不匹配&a…

【unity小技巧】委托(Delegate)的基础使用和介绍

文章目录 一、前言1. 什么是委托&#xff1f;2. 使用委托的优点 二、举例说明1. 例12. 例2 三、案例四、泛型委托Action和Func1. Action委托2. Func委托 五、参考六、完结 一、前言 1. 什么是委托&#xff1f; 在Unity中&#xff0c;委托&#xff08;Delegate&#xff09;是一…

【分布式】 ELK 企业级日志分析系统 二

目录 一、FilebeatELK 部署1.1 环境部署 二、grok 正则捕获插件mutate 数据修改插件multiline 多行合并插件date 时间处理插件 一、FilebeatELK 部署 1.1 环境部署 Node1节点&#xff08;2C/4G&#xff09;&#xff1a;node1/192.168.137.101 Elasticsearch Node2节点&…

【Distributed】分布式ELK日志文件分析系统(二)

文章目录 一、FilebeatELK 部署1. 环境部署2. 在 Filebeat 节点上操作2.1 安装 Filebeat2.2 设置 filebeat 的主配置文件 3. 在 Apache 节点上操作3.1 在 Logstash 组件所在节点上新建一个 Logstash 配置文件 3. 启动3.1 在Logstash 组件所在节点启动3.2 在 Filebeat 节点 启动…

git报错:remote: Access denied (URL 403)

git报错&#xff1a;remote: Access denied fatal: unable to access ‘ https:/ /gitee. cohe requested URL 403 大概的原因&#xff0c;是之前更改了 可能因为我之前在git bash中配过ssh&#xff0c;系统已经将指向git的用户设置了别的位置&#xff0c;所以…

基础篇--单片机简介

单片机简介 视频教程 单片机是什么 单片机&#xff1a;Single-Chip Microcomputer 单片微型计算机&#xff0c;是一种集成电路芯片 单片机有什么用&#xff1f; 单片机发展历程 单片机发展超势 CISC Vs RISC CISC和RISC举例 https://wenku.baidu.com/view/b074b0ed998fcc22b…

使用均值漂移来量化带宽分类数据

均值漂移概念 均值漂移的基本概念&#xff1a;沿着密度上升方向寻找聚簇点&#xff0c;其计算过程如下&#xff1a; 1 均值漂移算法首先找到一个中心点center&#xff08;随机选择&#xff09;&#xff0c;然后根据半径划分一个范围 把这个范围内的点输入簇x的标记个数加1 2 在…