目录
[为什么 TCP 关闭连接为什么要四次而不是三次?](#为什么 TCP 关闭连接为什么要四次而不是三次?)
TCP头部格式
序号:是指本报文段所发送的数据第一个字节的序号。如果,一报文段的序号为300,而且数据共100字节,则下一个报文段的序号就是400。用来解决网络包乱序问题。
确认号:是指期望收到对方下一个报文段的第一个数据字节的序号。若确认号为N,表明:序号N-1为止的所有字节都已正确收到。用来解决丢包的问题。
- ACK :该位为
1
时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的SYN
包之外该位必须设置为1
。 - RST :该位为
1
时,表示 TCP 连接中出现异常必须强制断开连接。 - SYN :该位为
1
时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。 - FIN :该位为
1
时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换FIN
位为 1 的 TCP 段。
为什么需要TCP
IP层是不可靠的,他不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中数据的完整性。
如果需要保障网络数据包的可靠性,就需要由上层协议来负责。
TCP是一个工作在传输层的可靠数据传输的服务,他能确保接收端接受的网络包是无损坏的、无间隔的、非冗余的和按序的。
TCP连接
TCP连接是一条虚连接,也就是逻辑连接,而不是一条真正的物理连接。我们所说的TCP连接是指通信的双方维护的一些状态信息,这些信息的组合包括socket、序列号和窗口大小。
每一条TCP连接唯一的被通信两端的两个端点(即两个套接字)所确定,即:
TCP连接::={socket1,socket2}={(IP1:PORT1),(IP2:PORT2)}
TCP的最大连接数
有一个IP的服务端监听了一个端口,它的最大TCP连接数是多少
对 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。
TCP是怎么工作的
TCP的主要任务是从应用层收集数据,他将数据拆分成多个数据包,为每个数据包分配一个编号,然后将这些数据包发送到它们的目的地。
在将数据包发送到应用层之前,他会重新组合数据包。鉴于TCP是面向连接的协议,这个连接将一直保持,直到发送方和接收方完成数据交换。
他是一种可靠的协议。因为,接收方总是会给发是那个方提供关于数据包的确认消息,要么肯定,要么否定,因此,发送方总是能知道数据包是否到达它的目的地,还是说需要被重新发送。
它保证了数据能到达其目的地,而且到达的顺序与发送时相同。他有一套内置的错误检查和恢复体系,负责提供端到端通信。TCP还提供流量控制和服务质量的访问。
UDP是怎么工作的
UDP中,接收方不生成数据报的确认,发送方也不等待数据包的确认。
他很适用于单向数据流的场景,最适合基于查询的通信。
UDP不保证数据包的有序传递。他是无状态的,不提供任何拥塞控制机制。
TCP和UDP的区别
-
连接:TCP是面向连接的协议,在通信前需要先建立连接。UDP则是无连接的协议。
-
服务对象:TCP 是一对一的两点服务,即一条连接只有两个端点。UDP 支持一对一、一对多、多对多的交互通信
-
可靠性:TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。UDP 是尽最大努力交付,不保证可靠交付数据。
-
TCP是面向字节流,UDP面向报文: 面向字节流是以字节为单位发送数据,并且一个数据包可以以字节大小来拆分成多个数据包,以方便发送。
TCP可以先将数据存放在发送缓冲区中,可以等待数据到一定大小发送,也可以直接发送,没有固定大小可言,会出现拆包和粘包问题。
UDP需要每次发送都需要发送固定长度的数据包。
-
流量控制、拥塞控制:TCP拥有流量控制和拥塞控制,UDP没有。
-
分片方式:TCP在传输层上进行分片,UDP在网络层进行分片。 MTU:最大传输单元,网络能够传输的最大数据包的大小
MSS:TCP数据段的最大分段大小
MSS = MTU - 40 (IP header + TCP header)
以太网最大的数据帧是1518字节,这样刨去帧头14字节和帧尾CRC校验部分4字节,那么剩下承载上层IP报文的地方最大就只有1500字节,这个值就是以太网的默认MTU值。这个MTU就是网络层协议非常关心的地方,因为网络层协议比如IP协议会根据这个值来决定是否把上层传下来的数据进行分片,如果单个IP报文长度大于MTU,则会在发送出接口前被分片,被切割为小于或等于MTU长度的IP包。 -
头部开销:TCP头部的最小开销是20个字节,UDP的头部为8个字节。
-
应用场景:tcp传输数据稳定可靠,适用于对网络通讯质量要求较高的场景,需要准确无误的传输给对方,比如,传输文件,发送邮件,浏览网页等等。udp的优点是速度快,但是可能产生丢包,所以适用于对实时性要求较高但是对少量丢包并没有太大要求的场景。比如:域名查询,语音通话,视频直播等。udp还有一个非常重要的应用场景就是隧道网络,比如:vpn,VXLAN。
题:
UDP如何实现可靠交付?(参考http3的QUIC协议)
UDP为什么比TCP快?
TCP和UDP相比较,它有什么缺点?
TCP和UDP的实时性?
IP层会分片,为什么TCP层还需要MSS?(IP层不可靠,在IP层分片,传输时丢失了包,需要重传整个TCP报文段)
三次握手
序号:是指本报文段所发送的数据第一个字节的序号。
确认号:是指期望收到对方下一个报文段的第一个数据字节的序号。
第一次握手,客户端给服务端发送了一个SYN报文,序号为x,变为SYN_SENT状态,这个报文是发送方向接收方同步自己的初始序列号。
第二次握手,服务端向客户端发送SYN+ACK报文,序号为y,确认号为x+1,变为SYN_RECV状态,SYN是接收方向发送方同步自己的初始序列号y,ACK则是对上一次报文的确认,表示自己已经接收到了seq=x的数据,则希望收到的下一个报文段的第一个字节为x+1,所以ack=x+1。在 TCP 协议中,一个报文可以同时设置 SYN 和 ACK 标志位来执行上述操作。这样子做可以减少了必要的通信次数,从而减少了建立连接所需的往返时间(RTT)。如果 SYN 和 ACK 需要分开发送,那么客户端和服务器之间就需要额外的往返,这将增加连接建立的总延迟。
第三次握手,客户端收到SYN+ACK报文后,就可以进入已连接状态,同时向服务器发送ACK报文,表示对收到服务器的初始序列号的确认,则ack=y+1。服务器收到后,就进入已连接状态。
第一次握手丢失
客户端重发SYN报文至最大重传次数,达到最大重传次数后,不再发送,断开连接。
第二次握手丢失
客户端因为超时仍收不到对第一次握手的确认,会认为第一次握手丢失,进行超时重传,重传SYN报文至最大重传次数。
服务端因为超时仍收到对第二次握手的确认,回认为报文丢失,进行超时重传,重传SYN+ACK报文至最大次数。
第三次握手丢失
ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。
服务端因为超时仍收到对第二次握手的确认,回认为报文丢失,进行超时重传,重传SYN+ACK报文至最大次数。
为什么是三次握手,而不是两次或者四次?
-
防止历史连接请求报文突然传送到服务器,浪费资源 在两次握手即可建立连接的情况下,客户端向服务端发送连接请求,服务端收到后即可进入连接状态,服务端再向客户端发送一个连接请求,即可完成握手。在此基础上,假设客户端发送 A 请求建立连接,由于网络阻塞导致 A 暂时无法到达服务器,客户端触发超时重传,重新发送请求报文 B,这次 B 顺利到达服务器,服务器随即返回确认报文并进入已连接状态,客户端在收到 确认报文后也进入 已连接状态,双方建立连接成功后正常传输数据,之后断开连接。断开连接后, A 请求报文到达服务器,服务器随即返回确认报文并进入 已连接状态,但是已经进入 CLOSED 状态的客户端无法再接收确认报文段,更无法进入 ESTABLISHED 状态,向服务端发送RST报文表示连接异常,服务器收到后,断开TCP连接。这将导致服务器长时间单方面等待,造成资源浪费。
-
三次握手才能让双方均确认自己和对方的发送和接收能力都正常 第一次握手:客户端只是发送处请求报文段,什么都无法确认,而服务器可以确认自己的接收能力和对方的发送能力正常;
第二次握手:客户端可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常;
第三次握手:服务器可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常;
可见三次握手才能让双方都确认自己和对方的发送和接收能力全部正常,这样就可以愉快地进行通信了。
-
告知对方自己的初始序号值,并确认收到对方的初始序号值 TCP 实现了可靠的数据传输,原因之一就是 TCP 报文段中维护了序号字段和确认序号字段,也就是图中的 seq 和 ack,通过这两个字段双方都可以知道在自己发出的数据中,哪些是已经被对方确认接收的。这两个字段的值会在初始序号值得基础递增,如果是两次握手,只有发起方的初始序号可以得到确认,而另一方的初始序号则得不到确认。
三次握手已经可以确认双方的发送接收能力正常,双方都知道彼此已经准备好,而且也可以完成对双方初始序号值得确认,也就无需再第四次握手了。
SYN攻击
服务器在收到客户端的SYN报文后进入SYN_RECV状态,会创建一个半连接的对象,然后将其加入到内核的「 SYN 队列」;接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;服务端接收到 ACK 报文后,从「 SYN 队列」取出一个半连接对象,然后创建一个新的连接对象放入到「 Accept 队列」;应用通过调用 accpet()
socket 接口,从「 Accept 队列」取出连接对象。
不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文。
SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃 ,导致客户端无法和服务端建立连接。
避免 SYN 攻击方式,可以有以下四种方法:
- 调大 netdev_max_backlog;
- 增大 TCP 半连接队列;
- 开启 tcp_syncookies;
- 减少 SYN+ACK 重传次数
四次挥手
FIN报文表示不再向对方发送数据,但是仍可以接收数据
第一次挥手,客户端向服务端发送FIN报文,进入FIN_WAIT状态,表示不再向对方发送数据,但是仍可以接收数据。
第二次挥手,服务器向客户端发送ACK报文,表示收到你的FIN报文,但我暂时还需要想你发送数据,我不发FIN报文那么快,进入closed_wait状态,等待服务器调用close()函数。
第三次挥手,服务器向客户端发送FIN报文,表示没有数据需要发送给你了,可以断开连接了,进入last_ack状态。
第四次挥手,客户段收到服务器发来的FIN报文后,向服务器发送ACK报文,表示收到FIN报文,进入TIME_WAIT状态,等待2MSL时间后关闭连接。服务器收到ACK报文后,关闭连接。
为什么 TCP 关闭连接为什么要四次而不是三次?
服务器在收到客户端的 FIN 报文段后,可能还有一些数据要传输,所以不能马上关闭连接,但是会做出应答,返回 ACK 报文段,否则可能会触发第一次挥手的超时重传,接下来可能会继续发送数据,在数据发送完后,服务器会向客户单发送 FIN 报文,表示数据已经发送完毕,请求关闭连接,然后客户端再做出应答,因此一共需要四次挥手。
time_wait等待的时间为什么是2MSL
MSL是报文的最大生存时间。它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。将time_wait时间设定为2MSL,可以保证「被动关闭连接」的一方,能被正确的关闭;
按照常理,在网络正常的情况下,四个报文段发送完后,双方就可以关闭连接进入 CLOSED 状态了,但是网络并不总是可靠的,如果客户端发送的 ACK 报文段丢失,服务器在接收不到 ACK 的情况下会一直重发 FIN 报文段,这显然不是我们想要的。因此客户端为了确保服务器收到了 ACK,会设置一个定时器,并在 TIME-WAIT 状态等待 2MSL 的时间,如果在此期间又收到了来自服务器的 FIN 报文段,那么客户端会重新设置计时器并再次等待 2MSL 的时间,如果在这段时间内没有收到来自服务器的 FIN 报文,那就说明服务器已经成功收到了 ACK 报文,此时客户端就可以进入 CLOSED 状态了。
TIME_WAIT过多的危害
过多的 TIME-WAIT 状态主要的危害有两种:
- 第一是占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等;
- 第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为
32768~61000
,也可以通过net.ipv4.ip_local_port_range
参数指定范围。
如果客户端的 TIME_WAIT 状态过多,占满了所有端口资源,那么就无法对「目的 IP+ 目的 PORT」都一样的服务端发起连接了,但是被使用的端口,还是可以继续对另外一个服务端发起连接的。因此,客户端(发起连接方)都是和「目的 IP+ 目的 PORT 」都一样的服务端建立连接的话,当客户端的 TIME_WAIT 状态连接过多的话,就会受端口资源限制,如果占满了所有端口资源,那么就无法再跟「目的 IP+ 目的 PORT」都一样的服务端建立连接了。
不过,即使是在这种场景下,只要连接的是不同的服务端,端口是可以重复使用的,所以客户端还是可以向其他服务端发起连接的,这是因为内核在定位一个连接的时候,是通过四元组(源IP、源端口、目的IP、目的端口)信息来定位的,并不会因为客户端的端口一样,而导致连接冲突。
如果服务端(主动发起关闭连接方)的 TIME_WAIT 状态过多,并不会导致端口资源受限,因为服务端只监听一个端口,而且由于一个四元组唯一确定一个 TCP 连接,因此理论上服务端可以建立很多连接,但是 TCP 连接过多,会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等。
服务器出现大量TIME_WAIT状态的原因
TIME_WAIT 状态是主动关闭连接方才会出现的状态,所以如果服务器出现大量的 TIME_WAIT 状态的 TCP 连接,就是说明服务器主动断开了很多 TCP 连接。
服务端会主动断开连接的场景:
第一个场景:HTTP 没有使用长连接,
第二个场景:HTTP 长连接超时
第三个场景:HTTP 长连接的请求数量达到上限
服务器出现大量CLOSED_WAIT状态的原因
CLOSE_WAIT 状态是「被动关闭方」才会有的状态,而且如果「被动关闭方」没有调用 close 函数关闭连接,那么就无法发出 FIN 报文,从而无法使得 CLOSE_WAIT 状态的连接转变为 LAST_ACK 状态。所以,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接。
正常服务端的处理连接流程:
- 创建服务端 socket,bind 绑定端口、listen 监听端口
- 将服务端 socket 注册到 epoll
- epoll_wait 等待连接到来,连接到来时,调用 accpet 获取已连接的 socket
- 将已连接的 socket 注册到 epoll
- epoll_wait 等待事件发生
- 对方连接关闭时,我方调用 close
原因:
- 没有将服务端 socket (监听socket)注册到 epoll,这样有新连接到来时,服务端没办法感知这个事件,也就无法获取到已连接的 socket,那服务端自然就没机会对 socket 调用 close 函数了。
- 有新连接到来时没有调用 accpet 获取该连接的 socket,导致有大量客户端主动断开了连接,而服务端没机会对这些 socket 调用 close 函数,从而导致服务端出现大量 CLOSE_WAIT 状态的连接。
- 通过 accpet 获取已连接的 socket 后,没有将其注册到 epoll,导致后续收到 FIN 报文的时候,服务端没办法感知这个事件,那服务端就没机会调用 close 函数了
- 当发现客户端关闭连接后,服务端没有执行 close 函数
如果已经建立了连接,如果某一端出现断电/断网/宕机的情况
TCP保活机制:定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
有数据传输:
如果有数据发送,由于收不到 ACK,所以会重试,但并不会无限重试下去,达到一定的重发次数之后,如果仍然没有任何确认应答返回,就会判断为网络或者对端主机发生了异常,强制关闭连接。此时的关闭是直接关闭,而没有挥手(数据都发不出去,还挥啥手)
没有数据传输
断网时如果没有数据传输,还得看 TCP 连接的的保活机制是否打开
开启保活机制
如果开启了 TCP 保活,需要考虑以下几种情况:
- 第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
- 第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。一方发送 RST 包给另一方,也是会强制对方断开连接的
- 第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。
开启了 KeepAlive 则会在一定心跳检测后断开连接,这个默认检测时间大概2个多小时,比较久。我们可以自己在应用层实现一个心跳机制。
没有开启保活机制
如果没有数据传输,连接永远不会断开
如果已经建立了连接,但是进程崩溃会发生什么?
TCP 连接断开的挥手,在进程崩溃时,会由操作系统内核代劳。
TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。