这里写目录标题
- TCP头格式有哪些
- 为什么需要TCP,TCP工作在哪
- 什么是TCP
- 什么是TCP连接
- 如何确定一个TCP连接
- TCP和UDP的区别,以及场景
- TCP和UDP能共用一个端口?
- TCP的建立
-
- TCP三次握手过程
- 为什么是三次握手、不是两次、四次
- why每次建立连接,初始化序列号要求不一样
- 初始化序列号ISN是如何随机产生的
- IP层会分片,whyTCP层还需要MSS
- 第一次握手丢失会发生什么
- 第二次握手丢失了,会发生什么
- 第三次握手丢失了,会发生什么
- SYN攻击,如何避免
-
- 半连接队列和全连接队列
- [避免 SYN 攻击方式](#避免 SYN 攻击方式)
- TCP连接断开
-
- 四次挥手过程
- 为什么挥手需要四次
- 第一次挥手丢失,会发生什么
- 第二次挥手丢失,会发生什么
- 第三次挥手丢失,会发生什么
- 第四次挥手丢失,会发生什么
- 为什么TIME_WAIT等待时间是2MSL
- 为什么要有TIME_WAIT状态
- [TIME_WAIT 多有什么危害](#TIME_WAIT 多有什么危害)
- [如何优化 TIME_WAIT](#如何优化 TIME_WAIT)
- 服务器出现大量TIME_WAIT状态的原因是什么
- [服务器出现大量 CLOSE_WAIT 状态的原因有哪些](#服务器出现大量 CLOSE_WAIT 状态的原因有哪些)
- Socket编程
- 重传机制
- 滑动窗口
- 流量控制
-
- TCP流量控制的主要目标
- [TCP 流量控制的基本原理](#TCP 流量控制的基本原理)
- [TCP 流量控制实现的特性](#TCP 流量控制实现的特性)
- TCP流量控制的过程
- 窗口关闭
- 如何解决窗口关闭潜在的死锁问题
- 拥塞控制
- 半连接队列和全连接队列
TCP头格式有哪些
- 源端口号和目标端口号:16位字段,用于标识TCP连接的源和目标端口号。
- 序列号(Sequence Number):32位字段,用于标识发送的数据字节流中的第一个字节的序号。
- 确认号(Acknowledgment Number):32位字段,确认收到的字节序号,即期望接收的下一个字节的序号。
- 数据偏移:4位字段,指示TCP头部的长度,以32位字为单位。
- 保留、U、A、P、R、S、F:这些标志位用于控制TCP连接的状态和行为,例如URG(紧急指针有效)、ACK(确认号有效)、PSH(接收方应尽快将数据交给应用)、RST(重置连接)、SYN(建立连接请求)、FIN(关闭连接)。
- 窗口大小(Window Size):16位字段,表示接收方的接收窗口大小,用于流量控制。
- 校验和(Checksum):16位字段,用于检测TCP头部和数据是否在传输过程中发生错误。
- 紧急指针(Urgent Pointer):16位字段,仅在URG标志位设置为1时有效,用于指示紧急数据的字节偏移。
- 选项(可选):用于在TCP头部中指定一些附加选项,如最大报文段长度(MSS)等。
- 填充(Padding):用于确保TCP头部达到指定长度的填充字段
为什么需要TCP,TCP工作在哪
IP层不可靠,不保证网络包的交付,不保证网络包的按序交付,也不保证网络包中的数据的完整性。因为 TCP 是一个工作在传输层 的可靠 数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。
什么是TCP
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
- 面向连接:再进行数据传输之前,通信的两端需要建立一个可靠的连接,通过三次握手来建立
- 可靠的:TCP 可以保证一个报文一定能够到达接收端,通过序列号、确认应答和重传等机制来确保数据的可靠传输
- 字节流:指的是在TCP连接中,数据被视为连续的字节流而不是分割成固定大小的数据块或消息。当应用程序发送数据时,TCP会将这些数据拆分成较小的数据块,并为每个数据块添加TCP头部信息,然后将它们作为字节流发送
什么是TCP连接
建立一个TCP连接是需要客户端与服务端达成上述三个信息共识
- Socket:用IP地址和端口号组成
- 序列号:用来解决乱序问题
- 窗口大小:用来做流量控制
TCP连接(Transmission Control Protocol connection)是计算机网络中的一种通信方式,它建立在传输控制协议(TCP)之上。TCP连接是一种面向连接的通信方式,意味着在数据传输之前,通信的两端(通常是客户端和服务器)需要通过一系列握手来建立一个可靠的连接。
如何确定一个TCP连接
源地址、源端口、目的地址、目的端口(地址32位在IP头部,端口16位在TCP头部)
TCP最大连接数=客户端IP数*客户端端口数
TCP和UDP的区别,以及场景
TCP(传输控制协议)和UDP(用户数据报协议)是两种常见的传输层协议,它们在网络通信中有着不同的特点和应用场景。
TCP的特点和应用场景:
- 面向连接:TCP使用三次握手建立连接,确保通信双方建立可靠的连接后再进行数据传输。
- 可靠性:TCP保证数据的可靠传输,通过序列号、确认应答和重传机制来确保数据的完整性和正确性。
- 有序性:TCP会按照发送顺序保证数据包在接收端按序重组,避免数据包乱序的情况。
- 流控制和拥塞控制:TCP通过流控制和拥塞控制算法来平衡发送和接收数据的速率,防止网络拥塞。
- 应用场景:TCP适用于对数据完整性和可靠性要求较高的应用,如网页浏览、文件传输、电子邮件等。
UDP的特点和应用场景:
- 无连接:UDP不需要建立连接,直接将数据包发送出去,适用于无需建立持久连接的通信场景。
- 不可靠性:UDP不保证数据的可靠传输,数据包可能丢失、重复、乱序,需要应用层自行处理丢失情况。
- 无序性:UDP的数据包可能按照发送顺序的不同在接收端乱序到达,应用层需要处理乱序的情况。
- 低延迟:由于不需要建立连接和拥有较少的控制机制,UDP传输具有较低的延迟。
- 应用场景:UDP适用于对实时性要求较高的应用,如视频直播、在线游戏、音频通话等。在这些场景下,实时性更重要,而对于数据丢失的情况,可以通过应用层的处理来进行容忍或纠正。
分片不同
- TCP 的数据大小如果大于 MSS(最大分段大小) 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。MSS:它是TCP协议中的一个参数,用于指示在TCP连接中每个数据包的最大有效载荷大小(即除去TCP头部后的数据部分大小)。
- UDP 的数据大小如果大于 MTU(最大传输单元) 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。MTU :它是指在网络通信中能够承载的最大数据包的大小。MTU是以字节为单位来衡量的。
综上所述,TCP和UDP各有优势,应根据具体应用场景的需求来选择合适的协议。对于重要数据的传输和确保数据可靠性的应用,可以选择TCP。而对于实时性要求较高的应用,可以选择UDP。
TCP和UDP能共用一个端口?
答案:可以
**简介回答:**端口号的目的主要是为了区分同一个主机不同应用程序的数据包,传输层有两个传输协议分别是 TCP 和 UDP,在内核中是两个完全独立的软件模块,当主机收到数据包后,可以在 IP 包头的「协议号」字段知道该数据包是 TCP/UDP,所以可以根据这个信息确定送给哪个模块(TCP/UDP)处理,送给 TCP/UDP 模块的报文根据「端口号」确定送给哪个应用程序处理。
**详细回答:**在一般情况下,TCP和UDP是不能共用一个端口的。TCP和UDP是两种不同的传输层协议,它们使用不同的端口来进行通信。每个网络通信应用都使用特定的端口号来与其他应用进行交流。TCP和UDP各自有独立的端口号空间,这意味着一个特定的端口号在TCP和UDP之间可以同时存在,但它们对应的是不同的应用或服务。例如,HTTP通常使用TCP协议在80端口进行通信,DNS通常使用UDP协议在53端口进行通信。这意味着在同一时间,80端口可能被TCP协议的HTTP使用,而53端口可能被UDP协议的DNS使用,二者不会相互干扰。然而,也有一些特殊情况下,某些应用或协议可以同时使用TCP和UDP共享同一个端口。这被称为"TCP/UDP多路复用"或"TCP/UDP Port Sharing"。这种情况下,应用程序需要负责处理接收到的数据,并根据数据包的内容区分使用TCP还是UDP来处理数据。这样的机制通常由应用层协议自行实现,而不是由TCP和UDP协议栈提供的默认功能。总的来说,大多数情况下,TCP和UDP使用独立的端口来避免混淆和冲突。但特定的应用场景下,TCP/UDP多路复用可以实现在同一个端口上同时使用TCP和UDP。
TCP的建立
TCP三次握手过程
TCP三次握手是建立TCP连接的过程,它用于确保客户端和服务器之间建立可靠的连接。以下是TCP三次握手的过程:
-
第一次握手(SYN):
- 客户端向服务器发送一个连接请求报文段,其中设置了SYN标志位(SYN=1)。
- 客户端选择一个初始序列号(ISN,Initial Sequence Number),用于后续数据的传输。
-
第二次握手(SYN+ACK):
- 服务器收到客户端的连接请求后,回复一个确认报文段作为回应。
- 服务器在回复的报文段中设置了SYN和ACK标志位(SYN=1,ACK=1)。
- 服务器也选择一个初始序列号(ISN)用于后续数据传输,并将确认号(ACK)设置为客户端的初始序列号(ISN + 1)。
-
第三次握手(ACK):
- 客户端收到服务器的确认报文段后,向服务器发送一个确认报文段。
- 客户端在这个报文段中设置了ACK标志位(ACK=1)。
- 客户端的确认号(ACK)被设置为服务器的初始序列号(ISN + 1),表示已收到服务器的回应。
一旦服务器收到客户端的确认报文段,TCP连接就建立成功,可以开始进行数据传输。在这个过程中,客户端和服务器通过交换序列号和确认号来确保彼此能够正确接收数据,从而建立可靠的连接。第三次握手是可以携带数据的,前两次握手是不可以携带数据的
为什么是三次握手、不是两次、四次
- 三次握手才可以阻止重复历史连接的初始化(主要原因)
- 三次握手才可以同步双方的初始序列号
- 三次握手才可以避免资源浪费
- 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
- 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
why每次建立连接,初始化序列号要求不一样
- 为了防止历史报文被下一个相同四元组的连接接收(主要方面);
- 为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收;
增强安全性: 选择不同的初始序列号可以增强安全性。如果每次建立连接时初始序列号都是相同的,那么攻击者可以利用这个预测性来发动网络攻击,例如TCP序列号预测攻击。通过选择不同的初始序列号,可以增加攻击者猜测序列号的难度,提高连接的安全性。
防止旧连接的混淆: TCP协议要求,当处于TIME_WAIT状态的连接结束后,必须经过一段时间的等待才能确保网络上的所有数据包都被丢弃。在此期间,如果有新的连接建立并选择了与已结束的旧连接相同的初始序列号,可能会导致新连接收到旧连接的数据包,从而造成混淆。通过选择不同的初始序列号,可以避免这种情况的发生。
初始化序列号ISN是如何随机产生的
起始 ISN
是基于时钟的,每 4 微秒 + 1,转一圈要 4.55 个小时,RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)。
M
是一个计时器,这个计时器每隔 4 微秒加 1。F
是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。
IP层会分片,whyTCP层还需要MSS
- 如果一个IP分片丢失,整个IP报文的所有分片都得重传,因为IP层本身没有超时重传机制,由传输层TCP来负责超时和重传,
当一个TCP分片丢失,重发时以MSS
- TCP层发现数据超过MSS时,会先进行分片,然后由它形成的IP包的长度也就不会大于MTU
虽然IP层可以对大于MTU的数据包进行分片,但TCP层的MSS(最大分段大小)参数仍然非常重要,因为TCP层的MSS决定了在TCP连接中每个数据包的最大有效载荷大小,而这对于TCP连接的性能和效率至关重要。下面解释为什么TCP层需要MSS:
1. 避免不必要的分片: TCP层的MSS参数允许TCP在发送数据之前,根据网络的MTU,选择一个适当的最大有效载荷大小。如果TCP层的数据包大小超过了MTU,IP层会对数据包进行分片。分片增加了网络传输的复杂性,也可能导致丢失或乱序的问题。通过设置适当的MSS,TCP可以尽量避免数据包被分片,提高数据传输的效率和可靠性。
2. 控制拥塞窗口: TCP使用拥塞控制算法来避免网络拥塞。MSS是拥塞窗口的一个重要参数。拥塞窗口决定了TCP可以发送的未确认数据量,通过调整MSS,可以控制拥塞窗口的大小,从而适应网络的拥塞状况,防止过多的数据注入导致网络拥塞。
3. 保持连接的可靠性: TCP层的MSS也与TCP连接的可靠性密切相关。较大的MSS可以减少连接的握手次数和数据包数量,从而降低连接建立和维护的开销,提高连接的可靠性。
总的来说,虽然IP层可以进行分片,但TCP层的MSS仍然非常重要,它可以避免不必要的分片,控制拥塞窗口,提高连接的可靠性,从而优化TCP连接的性能和效率。通过合理设置MSS,TCP可以更好地适应不同网络环境,实现高效、可靠的数据传输。
第一次握手丢失会发生什么
客户端就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的 。不同操作系统超时时间不同,有的1秒,有的3秒,Linux里客户端SYN报文最大重传次数由(tcp_syn_retries)参数控制,默认值:5。每次超时的时间是上一次的 2 倍
第二次握手丢失了,会发生什么
客户端就会触发超时重传机制,重传 SYN 报文 。服务端这边会触发超时重传机制,重传 SYN-ACK 报文 。在 Linux 下,SYN-ACK 报文的最大重传次数由 tcp_synack_retries
内核参数决定,默认值是 5。
第三次握手丢失了,会发生什么
服务端就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。
SYN攻击,如何避免
半连接队列和全连接队列
正常流程:
- 当服务端接收到客户端的 SYN 报文时,会创建一个半连接的对象,然后将其加入到内核的「 SYN 队列」;
- 接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;
- 服务端接收到 ACK 报文后,从「 SYN 队列」取出一个半连接对象,然后创建一个新的连接对象放入到「 Accept 队列」;
- 应用通过调用
accpet()
socket 接口,从「 Accept 队列」取出连接对象。
不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文。
SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。
避免 SYN 攻击方式
可以有以下四种方法:
- 调大 netdev_max_backlog;当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包,队列长度默认值1000
- 增大 TCP 半连接队列;
- 开启 tcp_syncookies;
- 减少 SYN+ACK 重传次数
TCP连接断开
四次挥手过程
TCP四次挥手是终止TCP连接的过程,用于断开客户端和服务器之间的连接。以下是TCP四次挥手的过程:
-
第一次挥手(FIN):
- 客户端向服务器发送一个连接关闭请求,其中设置了FIN标志位(FIN=1)。
- 客户端不再发送数据,但仍可以接收服务器发送的数据。
-
第二次挥手(ACK):
- 服务器收到客户端的连接关闭请求后,回复一个确认报文段作为回应。
- 服务器在回复的报文段中设置了ACK标志位(ACK=1),确认客户端的关闭请求。
- 服务器可能仍有数据要发送给客户端,所以服务器的FIN标志位尚未设置。
-
第三次挥手(FIN):
- 服务器继续发送数据给客户端,在数据发送完毕后,将自己的连接关闭请求发送给客户端。
- 服务器设置FIN标志位(FIN=1),表示服务器准备关闭连接。
-
第四次挥手(ACK):
- 客户端收到服务器的连接关闭请求后,回复一个确认报文段作为回应。
- 客户端在回复的报文段中设置了ACK标志位(ACK=1),确认服务器的关闭请求。
- 至此,TCP连接正式关闭。
注意:在进行四次挥手过程中,客户端和服务器都可以在最后一次挥手前发送数据,因为FIN标志位只表示自己不再发送数据,而并不表示对方不再发送数据。因此,四次挥手可能是"FIN-WAIT-1","FIN-WAIT-2","TIME-WAIT"等状态之间的过程。等待一段时间后,处于TIME-WAIT状态的一方会关闭连接,完成整个四次挥手过程。
为什么挥手需要四次
- 关闭连接时,客户端向服务端发送
FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据。 - 服务端收到客户端的
FIN
报文时,先回一个ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK
和 FIN
一般都会分开发送,因此是需要四次挥手。特定情况下,四次挥手是可以变成三次挥手的
第一次挥手丢失,会发生什么
会触发超时重传机制,客户端重传 FIN 报文,重发次数由 tcp_orphan_retries
参数控制。
第二次挥手丢失,会发生什么
客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数
第三次挥手丢失,会发生什么
服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retrie
s 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的,FIN_WAIT_2状态,默认60秒,有tcp_fin_timeout参数控制
第四次挥手丢失,会发生什么
如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries
参数控制
为什么TIME_WAIT等待时间是2MSL
MSL:报文最大生存时间
TTL:经过路由跳数
TTL 的值一般是 64,Linux 将 MSL 设置为 30 秒,意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒,如果超过了,就认为报文已经消失在网络中了。
网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间TIME_WAIT等待时间为2倍的MSL(Maximum Segment Lifetime),这个值是为了确保在网络中所有可能的冗余数据包都已经被丢弃,从而保证TCP连接的可靠关闭。
为什么要有TIME_WAIT状态
- 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
- 保证「被动关闭连接」的一方,能被正确的关闭;
- 服务端在关闭连接之前发送的
SEQ = 301
报文,被网络延迟了。 - 接着,服务端以相同的四元组重新打开了新连接,前面被延迟的
SEQ = 301
这时抵达了客户端,而且该数据报文的序列号刚好在客户端接收窗口内,因此客户端会正常接收这个数据报文,但是这个数据报文是上一个连接残留下来的,这样就产生数据错乱等严重的问题。
TIME_WAIT 多有什么危害
- 第一是占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等;
- 第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为
32768~61000
,也可以通过net.ipv4.ip_local_port_range
参数指定范围。 - 如果客户端(主动发起关闭连接方)的 TIME_WAIT 状态过多,占满了所有端口资源,那么就无法对「目的 IP+ 目的 PORT」都一样的服务端发起连接了,但是被使用的端口,还是可以继续对另外一个服务端发起连接的
- 如果服务端(主动发起关闭连接方)的 TIME_WAIT 状态过多,并不会导致端口资源受限,因为服务端只监听一个端口,而且由于一个四元组唯一确定一个 TCP 连接,因此理论上服务端可以建立很多连接,但是 TCP 连接过多,会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等
如何优化 TIME_WAIT
-
打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;,tcp_tw_reuse 功能只能用客户端(连接发起方),因为开启了该功能,在调用 connect() 函数时,内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用,使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持
-
net.ipv4.tcp_max_tw_buckets:这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将后面的 TIME_WAIT 连接状态重置,这个方法比较暴力
-
程序中使用 SO_LINGER ,应用强制使用 RST 关闭。我们可以通过设置 socket 选项,来设置调用 close 关闭连接行为
优化TIME_WAIT状态主要是通过调整TCP连接的参数来减少或优化TIME_WAIT等待时间,以减少资源占用和提高系统性能。以下是一些常见的优化方法:
-
减少TIME_WAIT等待时间: 在Linux系统中,可以通过修改系统的TCP参数来减少TIME_WAIT等待时间。例如,可以通过修改
tcp_fin_timeout
参数来减少TIME_WAIT状态的持续时间。这个参数控制在TCP连接关闭后,套接字保持在TIME_WAIT状态的时间,默认值是60秒。可以根据实际情况适当降低这个值,例如设置为30秒。 -
重用端口: 可以通过设置
SO_REUSEADDR
套接字选项来允许重用处于TIME_WAIT状态的端口。这样可以避免新的连接被拒绝,从而提高系统的连接处理能力。在Go语言中,可以通过设置ReuseAddr
字段为true
来实现端口重用:
go
listener, err := net.Listen("tcp", "localhost:8080")
if err != nil {
// 错误处理
}
listener.(*net.TCPListener).SetReuseAddr(true)
- 快速回收: 在Linux系统中,可以通过设置
tcp_tw_recycle
参数来开启快速回收模式。快速回收模式允许将处于TIME_WAIT状态的套接字快速回收,从而释放系统资源。但需要注意的是,快速回收模式可能会导致一些问题,因此在开启前需要仔细评估网络环境和应用需求。
bash
# 设置快速回收模式
sudo sysctl -w net.ipv4.tcp_tw_recycle=1
- 减少短暂连接: 尽量减少短暂连接的创建,短暂连接容易产生大量TIME_WAIT状态,增加系统负担。可以通过连接池或连接复用的方式来优化短暂连接的管理。
需要注意的是,优化TIME_WAIT状态需要根据具体的系统和网络环境进行调整,并且某些优化方法可能会带来潜在的风险。因此,在进行优化前应该仔细评估系统的性能和稳定性需求,并根据实际情况进行调整。
服务器出现大量TIME_WAIT状态的原因是什么
什么场景下服务端会主动断开连接
- 第一个场景:HTTP 没有使用长连接
- 第二个场景:HTTP 长连接超时
- 第三个场景:HTTP 长连接的请求数量达到上限
服务器出现大量TIME_WAIT状态的主要原因是短暂连接的频繁建立和关闭,通常在高并发的网络环境中或者存在大量短连接的场景下会出现这种情况。以下是一些导致服务器大量TIME_WAIT状态的常见原因:
-
高并发连接: 当服务器面对大量并发连接请求时,每个连接建立后在关闭时会进入TIME_WAIT状态。如果连接的频率很高,TIME_WAIT状态的连接数量就会迅速增加。
-
短暂连接: 一些应用或协议(如HTTP/1.0),特点是在每个请求完成后立即关闭连接,导致大量短暂连接产生。这些短暂连接会在关闭后进入TIME_WAIT状态,从而积累大量TIME_WAIT连接。
-
连接重用不当: 如果服务器没有正确地重用连接,而是频繁地创建新的连接,就会导致大量TIME_WAIT状态的连接堆积。在高并发场景下,应尽量复用现有连接,避免频繁创建和关闭连接。
-
服务器性能不足: 当服务器的性能较低或网络负载很高时,处理连接的速度可能跟不上连接的建立和关闭速度,导致连接在TIME_WAIT状态停留的时间较长。
-
客户端异常: 如果客户端异常地关闭连接而没有经过正常的四次挥手过程,服务器可能会出现大量处于TIME_WAIT状态的连接,因为服务器没有收到客户端的确认。
为了解决大量TIME_WAIT状态的问题,可以考虑以下方法:
- 优化服务器性能,提高服务器的处理能力和吞吐量,以便更快地处理连接的建立和关闭。
- 合理地设置TCP参数,如
tcp_tw_reuse
和tcp_tw_recycle
等,根据实际情况减少TIME_WAIT等待时间。 - 优化应用程序逻辑,尽量复用连接,减少短暂连接的创建和关闭。
- 使用连接池或连接复用等技术,合理管理连接资源。
服务器出现大量 CLOSE_WAIT 状态的原因有哪些
当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接 。当服务端出现大量 CLOSE_WAIT 状态的连接的时候,通常都是代码的问题,这时候我们需要针对具体的代码一步一步的进行排查和定位,主要分析的方向就是服务端为什么没有调用 close
Socket编程
Socket编程是一种用于在计算机网络中实现通信的编程方法。它是基于TCP/IP协议栈的一种编程接口,允许程序通过网络在不同计算机之间进行数据传输和通信。
在Socket编程中,程序可以通过创建Socket对象来建立网络连接。Socket对象提供了一组函数(通常是系统调用)来进行数据的发送和接收。通过Socket编程,程序可以实现客户端和服务器之间的通信,实现数据的传输和交换。
Socket编程通常使用两种类型的Socket:
-
流式Socket(Stream Socket): 使用TCP协议来进行数据传输,提供可靠的、面向连接的通信。流式Socket适用于需要可靠数据传输的场景,如HTTP、FTP等。
-
数据报Socket(Datagram Socket): 使用UDP协议来进行数据传输,提供不可靠的、无连接的通信。数据报Socket适用于对数据传输时延要求较低,但可靠性要求较低的场景,如视频流传输、实时游戏等。
Socket编程常用于网络编程和分布式系统中,它允许不同计算机上的应用程序之间进行通信和数据交换,实现了跨网络的数据传输和通信功能。在不同编程语言中,都提供了对Socket编程的支持,例如C、C++、Python、Java等。
- 服务端和客户端初始化
socket
,得到文件描述符; - 服务端调用
bind
,将 socket 绑定在指定的 IP 地址和端口; - 服务端调用
listen
,进行监听; - 服务端调用
accept
,等待客户端连接; - 客户端调用
connect
,向服务端的地址和端口发起连接请求; - 服务端
accept
返回用于传输的socket
的文件描述符; - 客户端调用
write
写入数据;服务端调用read
读取数据; - 客户端断开连接时,会调用
close
,那么服务端read
读取数据的时候,就会读取到了EOF
,待处理完数据后,服务端调用close
,表示连接关闭。
当涉及到socket编程的面试题时,面试官可能会问一系列问题来评估候选人的网络编程能力。以下是一些常见的socket编程面试题及其答案:
什么是Socket
- 答案:Socket是一种用于实现网络通信的编程接口,它允许进程(程序)通过网络进行数据传输和通信。Socket提供了一种机制,使得不同主机上的进程能够建立连接并进行数据交换。
TCP和UDP的区别
- 答案:TCP(传输控制协议)是一种面向连接的可靠协议,它通过三次握手来建立连接,并使用确认和重传机制来确保数据可靠传输。UDP(用户数据报协议)是一种无连接的不可靠协议,它不进行连接建立和断开,直接将数据发送给目标,不保证数据的可靠传输。
socket()函数的作用
- 答案:socket()函数用于创建套接字,返回一个用于网络通信的文件描述符。它需要指定通信协议(如TCP或UDP)和地址族(IPv4或IPv6)。
bind()函数的作用
- 答案:bind()函数用于将套接字与特定的IP地址和端口号绑定,使其能够在网络上被其他进程找到。
listen和accept函数的区别
- 答案:listen()函数用于服务器端,将套接字设置为监听模式,等待客户端的连接请求。accept()函数用于服务器端,接受客户端的连接请求,创建一个新的套接字用于与客户端通信。
connect()函数的作用
- 答案:connect()函数用于客户端,与服务器建立连接,发起与服务器的三次握手过程。
send()和recv()函数的区别
- 答案:send()函数用于发送数据,recv()函数用于接收数据。send()函数返回发送的字节数,recv()函数返回接收的字节数。
什么是阻塞和非阻塞socket
- 答案:阻塞socket在执行读写操作时,会阻塞当前进程,直到操作完成或出现错误。非阻塞socket在执行读写操作时,不会阻塞当前进程,而是立即返回结果。
为什么TCP连接需要三次握手和四次挥手?
- 答案:三次握手用于确保客户端和服务器之间建立可靠的连接。四次挥手用于安全地关闭连接,确保双方都能确认对方的关闭意图,并避免旧连接的混淆。
TIME_WAIT状态的作用
- 答案:TIME_WAIT状态用于确保在网络中所有可能的冗余数据包都已经被丢弃,从而保证TCP连接的可靠关闭。
如何优化TIME_WAIT状态
- 答案:可以通过减少TIME_WAIT等待时间、重用端口、设置快速回收模式等方法来优化TIME_WAIT状态。
这些问题涵盖了socket编程的一些基本概念、函数调用和网络通信流程。面试时,理解这些问题的答案并能够清晰地解释socket编程的原理和用法,将有助于展示你的网络编程能力和理解水平。
重传机制
超时重传
发送数据时,会设定一个定时器,当超过指定的时间后,没有收到对方的ACK确认应答报文,就会重发数据
超时重传的情况
- 数据包丢失
- 确认应答丢失
RTT:是数据发送时刻到接受到确认的时刻的差值,也就是包的往返时间
超时重传时间是以 RTO
(Retransmission Timeout 超时重传时间)表示(超时重传时间 RTO 的值应该略大于报文往返 RTT 的值)
- 超时时间 RTO 较大时,重发就慢,丢了老半天才重发,没有效率,性能差;
- 当超时时间 RTO 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
快速重传
不以时间为驱动,而是以数据驱动重传
- 第一份 Seq1 先送到了,于是就 Ack 回 2;
- 结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2;
- 后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;
- 发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。
- 最后,收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。
所以,快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。快速重传机制只解决了一个问题,就是超时时间的问题,但是还有另一个问题,就是重传的时候重传一个,还是所有。于是后面有了SACK
SACK(选择性确认)
在TCP头部选项字段里加一个SACK的东西,他可以将已收到的数据的信息发送给发送方,这样发送方就知道哪些数据收到了,哪些没有收到,就可以重传丢失的数据
D-SACK
其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了
好处:
- 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
- 可以知道是不是「发送方」的数据包被网络延迟了;
- 可以知道网络中是不是把「发送方」的数据包给复制了;
滑动窗口
窗口大小:指无需等待确认应答,而可以继续发送数据的最大值
TCP头部有个字段叫Window,也就是窗口的大小,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来,通常窗口的大小是由接受方的窗口大小决定的。
发送方的窗口
- #1 是已发送并收到 ACK确认的数据:1~31 字节
- #2 是已发送但未收到 ACK确认的数据:32~45 字节
- #3 是未发送但总大小在接收方处理范围内(接收方还有空间):46~51字节
- #4 是未发送但总大小超过接收方处理范围(接收方没有空间):52字节以后
TCP滑动窗口方案使用三个指针来跟踪四个传输类别中的每个类别中的字节。其中两个指针是绝对指针(指确定的序列号),一个是相对指针(需要做偏移)
SND.WND
:表示发送窗口的大小(大小是由接收方指定的)SND.UNA
(Send Unacknoleged):是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节SND.NXT
:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节- 指向 #4 的第一个字节是个相对指针,它需要
SND.UNA
指针加上SND.WND
大小的偏移量,就可以指向 #4 的第一个字节了
那么可用窗口大小的计算就可以是:可用窗口大小 = SND.WND -(SND.NXT - SND.UNA)
接受方的窗口
- #1 + #2 是已成功接收并确认的数据(等待应用进程读取);
- #3 是未收到数据但可以接收的数据;
- #4 未收到数据并不可以接收的数据;
接收和发送窗口相等吗
并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。
因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。
流量控制
TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。
TCP 流量控制是指通过一系列的机制来确保发送方与接收方之间的数据传输速率适应接收方的处理能力,以防止过多的数据发送导致接收方无法及时处理而发生数据丢失或网络拥塞的情况。
TCP流量控制的主要目标
- 防止数据丢失和拥塞:如果发送方一次性发送大量数据,可能会导致网络拥塞,数据丢失或延迟增加。
- 适应接收方速率:接收方可能处理数据的速度较慢,流量控制可以使发送方的速率适应接收方的处理能力。
TCP 流量控制的主要机制是通过滑动窗口(Sliding Window)和确认机制来实现的。
TCP 流量控制的基本原理
- 滑动窗口(Sliding Window): 发送方和接收方之间维护一个滑动窗口,该窗口指示了接收方当前能够接受的数据量。发送方根据接收方报告的窗口大小来动态调整发送数据的量,以保证不会超过接收方的处理能力。
- 确认机制: 接收方会周期性地向发送方发送确认(ACK)信息,表明接收方成功接收了某个特定序号的数据。发送方收到确认后,会将对应的数据序号从发送窗口中移除,从而允许发送新的数据。
TCP 流量控制实现的特性
- 滑动窗口大小调整: 接收方可以根据自身的处理能力和缓冲区情况来动态调整窗口大小。如果接收方的缓冲区还有空间,窗口会增大,允许发送更多数据;如果缓冲区满了,窗口会减小,限制发送数据的速率。
- 慢启动和拥塞避免: TCP 在建立连接时采用慢启动算法,初始窗口较小,然后逐渐增加发送窗口,以便探测网络的拥塞情况。一旦网络出现拥塞,TCP 会进入拥塞避免状态,通过线性增加发送窗口来稳定网络拥塞情况。
- 超时重传: 如果发送方没有在一定时间内收到接收方的确认,就会认为数据丢失,触发超时重传机制,重新发送数据。这也有助于控制网络拥塞情况。
TCP流量控制的过程
- 发送方发送数据段给接收方。
- 接收方接收数据并发送确认。
- 发送方根据接收方发送的确认来确定接收窗口的大小,确保发送数据的数量不会超过接收方的处理能力。
- 发送方还根据拥塞窗口的大小来控制发送速率,以避免网络拥塞。
总之,TCP 流量控制确保了在发送方和接收方之间的数据传输速率匹配,从而保证了可靠的数据传输,避免了数据丢失和网络拥塞问题。
窗口关闭
如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。
那么,当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,那麻烦就大了。这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不采取措施,这种相互等待的过程,会造成了死锁的现象。
如何解决窗口关闭潜在的死锁问题
为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。
- 如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器;
- 如果接收窗口不是 0,那么死锁的局面就可以被打破了。
窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST
报文来中断连接。
拥塞控制
为什么需要拥塞控制
尽管TCP已经引入了流量控制机制来确保发送方不会以过快的速率向接收方发送数据,但流量控制仅仅关注于发送方和接收方之间的数据传输。然而,网络中的其他因素,如网络拥塞、延迟、丢包等问题,可能会影响整体的网络性能和吞吐量。这就是为什么TCP还需要引入拥塞控制机制的原因。
拥塞窗口,和发送窗口有什么关系
拥塞窗口 cwnd 是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。
我们在前面提到过发送窗口 swnd
和接收窗口 rwnd
是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。
拥塞窗口 cwnd
变化的规则:
- 只要网络中没有出现拥塞,
cwnd
就会增大; - 但网络中出现了拥塞,
cwnd
就减少;
只要「发送方」没有在规定时间内接收到 ACK 应答报文,当发生了超时重传,就会认为网络出了拥塞
拥塞控制的控制算法
慢启动
TCP刚开始建立连接,一点一点发送数据包,当发送方每收到一个ACK,拥塞控制cwnd的大小就会加1
有一个叫慢启动门限 ssthresh
(slow start threshold)状态变量。
- 当
cwnd
<ssthresh
时,使用慢启动算法。 - 当
cwnd
>=ssthresh
时,就会使用「拥塞避免算法」。
拥塞避免
一般来说 ssthresh
的大小是 65535
字节。
那么进入拥塞避免算法后,它的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。
我们可以发现,拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长,还是增长阶段,但是增长速度缓慢了一些。就这么一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。当触发了重传机制,也就进入了「拥塞发生算法」。
拥塞发生
当发生了「超时重传」,则就会使用拥塞发生算法。这个时候,ssthresh 和 cwnd 的值会发生变化:
ssthresh
设为cwnd/2
,cwnd
重置为1
(是恢复为 cwnd 初始化值,我这里假定 cwnd 初始化值 1)
当发生了「快速重传」,则就会使用拥塞发生算法。这个时候,ssthresh 和 cwnd 的值会发生变化:
cwnd = cwnd/2
,也就是设置为原来的一半;ssthresh = cwnd
;- 进入快速恢复算法
快速恢复
进入快速恢复算法如下:
- 拥塞窗口
cwnd = ssthresh + 3
( 3 的意思是确认有 3 个数据包被收到了); - 重传丢失的数据包;
- 如果再收到重复的 ACK,那么 cwnd 增加 1;
- 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;
半连接队列和全连接队列
在理解TCP半连接队列(SYN队列)和全连接队列(Accept队列)之前,让我们先了解TCP的三次握手过程:
-
客户端发送SYN(同步): 客户端向服务器发送一个SYN包,表示客户端想要建立连接。
-
服务器发送SYN + ACK: 服务器收到客户端的SYN包后,会发送一个带有ACK确认号的SYN包作为响应,表示接受连接请求并准备好建立连接。
-
客户端发送ACK: 客户端收到服务器的SYN + ACK包后,发送一个ACK包,表示确认服务器的响应。此时连接建立完成。
现在,我们可以来看看半连接队列和全连接队列:
-
半连接队列(SYN队列): 在TCP的三次握手过程中,当服务器发送了SYN + ACK包后,它会等待客户端发送最终的ACK包以完成连接的建立。在这个等待期间,服务器将客户端的连接请求(即半连接)放入半连接队列,等待客户端的最终确认。半连接队列的大小通常是有限的,操作系统根据资源和配置来决定。
-
全连接队列(Accept队列): 一旦连接的三次握手完成,服务器将连接从半连接队列移至全连接队列。全连接队列中的连接已经建立并且可以开始进行数据传输。服务器将按顺序从全连接队列中选择连接,并分配资源来处理这些连接。
总结起来,半连接队列(SYN队列)用于存放已经发送了SYN请求但尚未完成三次握手的连接,而全连接队列(Accept队列)则存放已经完成三次握手的连接,等待服务器分配资源来处理。这些队列在TCP服务器中起着重要的作用,帮助管理并发连接并维护网络性能。