文章目录:
TCP 协议
传输控制协议(TCP)是 Internet 协议组的主要协议之一。它位于用于提供可靠交互服务的应用程序和网络层之间。它是一种面向连接的通信协议,有助于在网络上不同设备之间交换消息。TCP 运行在通过 IP 网络通信的主机上的应用程序之间提供可靠的、有序的、错误检查的字节流。主要的互联网应用,如:万维网、电子邮件、远程管理和文件传输都依赖于 TCP,它是 TCP/IP 协议传输层的一部分。
TCP 协议在如今广泛使用,其广泛使用是基于其提供的可靠性保证和强大功能的优势。基于 TCP 上层应用非常丰富多样,如:HTTP、HTTPS、FTP、SSH 等,甚至一些底层的数据库系统,如:MySQL 也是基于 TCP 进行通信,TCP 的可靠性保证对于数据库操作的一致性和可靠性非常重要。
关于可靠性
如今的大部分计算机都是基于冯诺依曼体系结构的。冯诺依曼体系结构的计算机中,各个硬件设备(输入设备、输出设备、内存、CPU)之间通过总线进行通信。在短距离内,数据传输的可靠性通常较高。然而,当设备之间的距离较远时,比如在网络之间进行数据传输,传输路线非常长,导致数据在传输的过程中出现错误的概率增加。
为了解决长距离通信中的可靠性问题,TCP 协议应运而生。通过 TCP 协议,数据可以安全、可靠地在网路中传输,从而满足现代计算机和网路应用中对数据可靠性的需求。
TCP 协议段格式
TCP 协议由报头和数据组成。报头包含10个必选字段和一个可扩展字段(选项)。数据部分在报头之后,是应用程序协议的有效负载数据。
TCP 协议段的格式如下:
TCP 报头中的各个字段的含义如下:
-
源端口(16位)
: 它定义了发送数据的应用程序的端口。即源端口地址。 -
目的端口(16位)
:它定了应用程序在接收端的端口。即目的端口地址。 -
序号(32位)
:该字段包含特定会话中数据字节的序列号。 -
确认序号(32位)
:当设置了 ACK 标志时,它包含数据字节的下一个序列号,并作为对接收到的前一个数据的确认。例如,如果接收方收到段号 ‘x’,那么它响应 ‘x+1’ 作为确认序号。 -
TCP 报头长度(4位)
:它指定报头的长度,由报头中的4字节字表示。报头的大小在20到60之间。因此,该字段的值介于5到15之间。 -
6个标志位
:
1️⃣ URG(Urgent):表示紧急指针。如果设置了,则紧急处理数据。
2️⃣ ACK(Acknowledgment):表示确认字段有效。客户端发送的初始 SYN 包之后的所有包都应该设置此标志。如果 ACK 设置了0,则表示该数据包不包含 ACK。
3️⃣ PSH(Push):如果设置了该字段,则请求接收设备将数据推送到接收应用程序。
4️⃣ RST(Reset):对方要求重新建立连接,我们把携带 RST 标识的报头称为复位报文段。
5️⃣ SYN(Synchronize):请求建立连接,我们把携带 SYN 标识的报文称为同步报文段。
6️⃣ FIN(Finish):用于释放连接,不再进行进一步的数据交换,我们称携带 FIN 标识的报文为结束报文段。 -
窗口大小(16位)
:包含接收方可以接收的数据大小。该数据用于发送方和接收方之间的流量控制,也决定接收方为一个段分配的缓冲区数量。该字段的值由接收方决定。 -
校验和(16位)
:发送端填充,CRC检验。接收端校验不通过,则认为数据有问题。此外的检验和不光包含 TCP 首部,也包含 TCP 数据部分。 -
紧急指针(16位)
:它是一个指针,如果 URG 标志设置为1,则指向紧急数据字节。它定义了一个值,该值将被添加到序列号中以获得最后一个紧急字节的序列号。 -
·
40字节头部选项
:它提供了额外的选项。可选字段用32位表示。如果该字段包含的数据小于32位,则需要填充来获得剩余的位。
TCP 报头是以结构体的形式存储的。在 TCP 协议的实现中,TCP 报头被定义为一个名为 struct tcphdr
的结构体。这个结构体包含了 TCP 的上述字段。每个 TCP 报文都以 struct tcdhdr
结构体的形式存储,以便在网络通信过程中进行解析和处理。在操作系统内核中,TCP 报头的相关信息会被存储在相应的数据结构中,例如 struct sk_buff
。
TCP 是如何将报头与有效载荷进行分离的?
当 TCP 从底层收到一个报文之后,是不知道报文的具体长度的。但是 TCP 报文中的基本报头是20个字节,该报头其中的4位标识了该报文的报头长度。
因此,当 TCP 收到一个报头之后,按照以下步骤进行分离:
- TCP 首先读取报文的前20个字节,这是 TCP 的基本报头部分。
- 在基本报头部分的前4个字节中,包含了4位的首部长度字段。通过读取这4位字段,TCP 即可获得 TCP 报头的大小。
- 如果首部长度字段的值大于20字节,则表示报头中包含了选项字段。TCP 继续从报文中读取额外的字节(首部长度字段的值-20),这部分字段即为 TCP 报头的选项字段。
- 读取完基本报头和选项字段之后,剩余的就是有效载荷部分,即数据。
序号与确认序号
在 TCP 通信中,什么样才是真正的可靠传输?
在进行网络通信时,数据的可靠传输是一个复杂的过程。当一方发送数据之后,并不能直接保证对端已经成功接收到数据,因为在传输过程中可能会发生各种错误。只有当对端主机对发送的消息进行响应时,才能保证主机发送的报文对端接收到了。
TCP 要确保双方通信的可靠性,主机A向主机B发送一个信息,当主机B给主机A回应之后,主机A就可以确保上一次发送的数据被主机B收到了。但主机B也需要保证自己发送的数据被主机A接收到了。因此,在主机A收到报文之后应该再回应一个响应报文,此时又不能保证主机B一定收到,若主机B继续应答就形成了一个循环。
只有当一端收到对方的响应数据时,才能保证自己上一次发送的数据被对端收到了。但双方通信时总会有一条最新的消息,因此不能保证100%可靠。
然而,在实际通信中,并不需要对所有消息都进行严格的可靠性保证。确保通信的可靠性并不意味着每个消息都必须得到响应,而是确保核心数据的可靠传输。核心数据通常是指应用层需要交换的重要信息。例如文件传输中的文件内容、网络会话中的请求和响应内容等。只要这些核心数据能够可靠地传输并得到确认,就可以认为通信是可靠的。
对于一些次要的数据,如响应数据,确实没有必要保证其可靠性。如果对端没有收到这些响应数据,可以推断上一次发送的核心数据丢失,并进行重传。这就是 TCP 中的确认应答机制。
在互联网中,目前无法实现100%可靠性传输,但通过确保核心数据的可靠传输并采用确认应答机制,就可以满足大部分场景下的可靠性通信需求了。
32位序号
在网络通信中,如果一方必须等待上一次发送的数据的响应才发送下一个数据,那么通信过程将会变得串行化,导致效率低下。
为了提高通信效率,TCP 允许发送方连续发送多个报文数据而不需要等待每个报文的响应。关键是要确保每个报文都有对应的响应消息,以保证这些报文被对方接收。然而,由于不同报文在网络传输中可能选择不同的路径,导致报文到达对端主机的顺序与发送顺序可能不同。
为了解决这个问题,TCP 报头的32位序列号起到了非常重要的作用,其中之一就是保证报文的有序性。发送方在发送每个报文时都会为其分配一个唯一的序列号。接收方通过检查报文序号就可以确定报文的顺序并进行重组。如果报文有缺失,可以要求发送发重新发送,以确保报文的有序性和可靠性。
TCP 在数据传输过程中对每个字节进行编号,即为序列号。每个 TCP 报文段都包含一个序列号字段,用于标识该报文中的第一个字节的序列号。
示例:
- 如果发送端要发送3000字节数据,每次发送1000字节数据,那么需要分为三个 TCP 报文段进行发送。
- 这三个报文段的序列号分别为1、1001、2001。发送端会在 TCP 报文段中填写相应的序列号。
- 接收端收到这三个 TCP 报文段后,会根据报文段的序列号对它们进行顺序排列。通过比较报文段的序列号和有效值的字节数,接收端可以确定下一个报文对应的序号,从而恢复原始数据的顺序。
32位确认序号
确认序号(Acknowledgment Number)是 TCP 报头中的另一个32位字段,用于确认接收端期望从发送端接收的下一个字节的序号。接收端会将下一个期望接收的字节的序号放在确认序号中发送回发送端。通过确认序号,发送端可以了解哪些数据已经被接收端成功接收。
在上面的示例中,当主机B已经成功接收并处理了序号为1-1000的字节数据,那么主机B在发送给主机A的响应数据的报头的确认序号字段填写1001。表示主机B已经将序列号在1001之前的字节数据已经收到了。
如果仅有一套序号机制,发送方将序号看作32位序号,接收方对数据进行响应时将序号看作是32位确认序号,这样也是可以完成通信的。 TCP 协议中为什么要使用两套序号机制?
对于 TCP 通信中的双方,每个方向的数据流都需要有自己的序号和确认序号。这是因为在全双工通信中,双方可能同时发送数据,并且需要对对方上次发送的数据进行确认。
发送方使用序号来标识自己当前发送数据的序号,以及期望接收到对端的确认序号。接收方在接收到数据之后,会使用确认序号来告知发送方以及成功接收到的数据字节序号,以及下一次期望接收的数据序号。
通过使用两套序号机制,TCP 可以实现以下功能:
- 发送方使用序号来标识发送的数据段,确保数据的顺序和完整性。
- 接收方使用确认序号来告知发送方已经接收到的数据以及下一次期望接收的数据序号。也可以在32位序号中填写接收方需要发送给对端的数据字节序号。体现了 TCP 全双工的特性。
- 发送方根据接收到的确认序号来确认对方已经成功接收到的数据,数据若有丢失,则进行重传操作。
总结:TCP 使用两套序号机制是为了支持全双工通信和可靠传输。
六个标志位
在 TCP 连接中,标志位被用于指示连接的特定状态,或提供一些额外的有用信息,例如故障排除目的或处理特定连接的控制。常用的标志位如 “SYN”、“ACK”、“FIN” 等,每个标志位占用一个比特位,用0表示假,1表示真。
SYN
TCP中的SYN(同步)标志位在建立两个主机之间的连接时的初始阶段或三次握手过程中使用。只有发送方和接收方的第一个数据包应该设置这个标志位。它用于同步序列号,即告诉对方应该接受哪个序列号。
报文中的 SYN 标志位被设置为1,表明该报文是一个连接请求的报文。
ACK
用于确认主机成功接收到的数据包。如果确认号字段包含有效的确认号,那么该标志位将被设置。
一般情况下,除了第一个请求报文之外,其它报文基本都会设置 ACK 标志位。这是因为发送方发送的数据本身具有对接收方之前发送的数据的确认能力。因此,在进行数据通信时,双方可以同时对彼此上一次发送的数据进行确认和响应。
FIN
用于请求连接终止,即当发送方没有更多数据时,他会请求连接的终止。这是发送方发送的最后一个数据包。它释放了保留的资源并终止连接。
RST
当发送方检测到 TCP 连接有问题或者认为这次通信不应该存在时,使用 RST 来终止连接。当接收方发送给特定主机的报文不被该主机预期时,接收方可能会发送 RST 报文。
RST 标志位用于在发生异常情况或出现严重错误时终止连接。他表示发送方或接收方希望立即中断连接,并且不再进行进一步的通信。RST 报文可以被视为一种强制性的关闭连接的手段。
URG
当发送端发送了一些被标记为紧急数据的内容时,接收端的上层协议需要有机制能够识别和提取这些紧急数据进行优先处理。TCP 协议中,使用了紧急指针(Urgent Pointer)和紧急(URG)标志位来实现这一目的。
当 URG 标志位被设置时,它指示了在报文中存在需要优先处理的紧急数据。紧急指针字段指示了紧急数据的偏移量或位置,以便接收方能够准确地识别和处理这些数据。
当 URG 标志位被设置为1时,表示报文中存在紧急数据,且紧急指针字段的值变得有效。紧急指针字段是一个16位的字段,它指示了紧急数据在报文中的偏移量。
紧急指针字段的值表示从 TCP 报文头部开始的偏移量。由于紧急指针字段只有一个,它只能标识数据段中的一个位置。这意味着紧急数据的长度被限制为一个字节,因为紧急指针只能指示一个字节的位置。具体紧急数据的含义和处理方式应该由应用层协议定义。
在 socket 编程中,recv 函数和 send 函数都提供了一个名为 flags 的参数,用于指定一些特殊的选项。其中,针对紧急数据的处理,可以使用 MSG_OOB
选项。
PUSH
该标志位在 TCP 中用于请求立即将数据传递到接收主机,而不需要等待发送缓冲区中的其它数据。该标志位通常用于实时音频或视频流等应用程序。
当发送端设置 TCP 报文的 PSH 标志位为1时,它是告诉接收端尽快将缓冲区中的数据交互给上层应用程序处理。这表明发送端希望接收端立即将数据传递给上层,而不需要等待更多的数据或缓冲。
在缓冲区中,有一个水位线的概念。当接收缓冲区中的数据达到水位线时,读取操作就能够读取数据并返回给应用层,若没有到达水位线,读取操作可能会被阻塞,等待数据到达水位线才能读取。
这些水位线的设置通常是由操作系统或网络库来管理,以平衡数据传输的效率和延迟。具体的水位线值和行为可能因操作系统和网络库而异。
16位窗口大小
TCP 的接收缓冲区和发送缓冲区
每个 TCP Socket 在内核中都有一个发送缓冲区和一个接收缓冲区,TCP 的全双工的工作模式以及 TCP 的流量(拥塞)控制都是依赖这两个独立的 buffer 的填充状态。
- 对于接收缓冲区,当数据到达接收端的 TCP 连接时,数据会被存储在接收缓冲区中,即使应用程序没有调用 recv() 函数读取数据。无论应用程序是否读取数据,对端发送的数据都会经过内核接收并缓存到接收缓冲区中。recv() 函数的作用是将内核缓冲区中的数据拷贝到应用层用户的缓冲区中,并返回读取的字节数。
- 对于发送缓冲区,当应用程序调用 send() 函数发送数据时,数据一般会被拷贝到发送端的内核发送缓冲区中,然后 send() 函数会在上层返回。即 send() 函数返回时,并不意味着数据一定发送到对端(类似与写文件的行为)。send() 函数仅将应用层缓冲区中的数据拷贝到 TCP 内核发送缓冲区中,实际的发送过程由 TCP 协议负责。
数据在接收缓冲区和发送缓冲区之间的拷贝是通过操作系统内核实现的,应用程序并不直接操作这些缓冲区。TCP 协议负责管理接收缓冲区和发送缓冲区的大小以及数据的读取和发送过程。
为什么要存在发送缓冲区和接收缓冲区,存在的意义是什么?
1️⃣ 发送缓冲区和接收缓冲区的存在可以实现发送端和接收端之间的解耦。发送端将数据放入发送缓冲区之后,可以立即返回给应用程序,而不需要阻塞等待数据发送到接收端。接收端也可以根据自身能力和需求,从接收缓冲区中按需读取数据进行处理。
2️⃣ 发送/接收缓冲区的大小可以用于实现流控制机制。发送端的发送速率可能快于接收端的处理速率,如果没有发送缓冲区,发送端可能需要等待接收端的确认,导致发送速率降低。发送缓冲区可以暂存数据,让发送端可以连续发送一定量的数据。接收缓冲区可以通过控制接收端的处理速率。当接收端的处理能力有限时,可以通过控制接收缓冲区的大小来限制发送端的发送数据,保证数据的可靠传输。
3️⃣ 当网路发生拥堵时,发送缓冲区可以暂存待发送的数据,等待网路恢复时再进行正常发送,从而减轻网络负载。
16位窗口
发送缓冲区和接收缓冲区的存在为窗口的出现提供了基础。TCP 窗口是一种流量控制机制,用于控制发送端发送数据的大小,以适应接收端的处理能力和网路条件。16位窗口大小表示的对端接收缓冲区的剩余空间大小。
接收端对发送端发来的数据进行响应时,它会在确认消息中包含窗口大小字段,告诉发送端当前自己接收缓冲区的剩余空间大小。发送端就可以根据窗口字段来调整自己发送数据的速度。
- 如果接收端的窗口字段较小,表示接收端的接收能力较弱,发送端应该减小发送数据的速度,以避免造成数据丢失或拥塞。
- 如果接收端的窗口字段较大,表示接收端的接收能力较强,发送端可以提高发送数据的速度。可充分利用网络宽带和接收端的处理能力。
- 若窗口大小为0,表示接收缓冲区已经被打满,没有剩余的空间可以接收更多数据。此时发送端应该停止发送,避免数据丢失。
确认应答(ACK)机制
TCP 中的确认应答机制是一种用于保证数据传输可靠性的机制。该机制确保发送方发送的数据能够被接收方正确接收,在并要的时候进行重传。
确认应答机制的流程如下:
- 发送方将数据分隔称为较小的数据段,为每个数据段分配一个序列号,以便接收方可以按序重组;
- 发送方将数据发送,并启动一个定时器来跟踪每个数据段的发送时间;
- 接收方接收数据段,响应 ACK 消息给发送方,确认该数据段已经被正确接收;
- 发送方接收确认消息,确认被接收端接收到的数据段。然后停止相应的定时器,继续发送下一个数据段;
- 若发送方在定时器超时之前都没有接收到响应消息,它会认为数据段可能丢失。因此,发送端重传之前的数据段,并且重启计时器;
- 接收方发送的响应消息也可能丢失。如果在一定时间内发送方没有收到响应消息,它也会进行重传;
- 当接收段将数据报文接收完毕之后,它将按照数据段的序列号进行数据重组。最后,将重组成功之后的数据报文传递给应用程序。
数据接收方发送带有序列号的确认(ACK),以告诉发送方数据已被接收到指定的字节。ACK 并不意味着数据已经交互给应用程序,它仅仅表示现在是接收方负责交互数据。
可靠性是通过发送方检测丢失的数据并重新传输来实现的。TCP 使用两种主要技术来识别丢失。超时重传(RTO)和重复累积确认(DupAcks)。
超时重传机制
在进行网络通信时。当发送方发送一个数据段后,它会启动一个计时器,等待接收方发送确认消息。如果在计时器结束之前没有收到确认消息,那么发送方会认为发送的数据段已丢失,然后将重新发送该数据段。
数据丢包分为两种情况,第一种情况是发送方发送的数据段丢失了,此时发送方如果在特定的时间内收不到响应报文,就会进行超时重传。
- 主机A发送数据给主机B之后,可能因为网络拥堵等原因,数据无法到达主机B;
- 如果主机A在一个特定时间间隔内没有收到主机B发来的确认应答,就会进行数据重发。
另一种情况是,主机B对主机A发送的报文进行了响应,但因为一些原因,响应报文在网络中丢失了,此时发送端也会因为收不到对应的响应报文,而进行超时重传。
基于上面两种情况的数据丢包:
- 当数据段丢失时,发送方无法确认是发送的数据段丢失了还是对端响应的数据段丢失了。在这两种情况下,发送方都无法收到对方的响应。因此,发送方会进行数据段的超时重传;
- 若是因为接收端的响应报文丢失而导致的发送方的超时重传,此时接收端就会收到很多重复的数据。那么 TCP 协议需要能够识别出哪些包是重复的包,并且把重复的丢弃掉。这时,我们就可以利用32位序列号来判断是否出现了重复的报文,从而避免出现重复数据,实现报文的去重;
当发送方的数据段发送出去,在没有收到确认报文之前。操作系统并不会立即将发送出去的数据报文从发送缓冲区中删除,而是继续保留在发送缓冲区中,直到发送方确认接受端已经准确接收到该报文时,发送方才会将该报文从发送缓冲区中移除。这样就能确保在数据在网络传输中丢失之后,能够进行重传。保证数据传输的可靠性。
那么,超时的时间是如何确定的呢?
- 最理想的情况下,找到一个最小的时间,保证 “确认应答” 一定能在这个时间内返回;
- 但是这个时间的长短,随着网络环境的不同,是会有差异的;
- 如果超时时间设置的太长了,会影响整体的重传效率;
- 如果超时时间设置的太短了,有可能会频繁的发送重复的包。
TCP 为了保证无论在任何环境下都能有比较高性能的通信,确保数据的可靠传输,会采用动态计算的方式来确定这个最大超时时间。
- 在 Linux 系统中(BSD Unix 和 Windows 也是如此),超时以 500ms 为一个单位进行控制,每次判定超时重发的超时时间都是 500ms 的整数倍;
- 如果重发一次之后,仍然得不到应答,等待2倍的时间(2500ms)之后再一次进行重传。如果仍然没有收到确认响应,会等待4倍的超时时间(4500ms)再进行重传,以指数形式递增超时时间;
- 如果重传次数累计到一定的阈值,TCP 会认为网络或对端主机出现异常情况,会强制关闭连接,以避免无限重传导致资源浪费和影响通信性能。
这种动态计算超时时间的机制会根据网络环境的不同,自适应地调整超时时间,以提供更好的性能和可靠性。通过指数递增的方式,可以适应不同程度的网络延迟和丢包情况。
往返时间 RTT(Round Trip Time)
初始超时计时器的设置是基于往返时间(RTT)估计。其中初始定时器值是 RTT+max(G,4*RTT 变化),其中 G 是时钟粒度。这样可以防止由于故障或者恶意行为(如中间人拒绝服务攻击者)导致的过多传输流量。
准确的 RTT 估计对于丢失恢复非常重要,因为它允许发送方在足够的时间过去后认为未确认的数据包已经丢失,从而确定重新传输超时(RTO - Retransmission Timeout)的时间。重传歧义会导致发送方对 RTT 的估计不准确。在具有变化的 RTT 环境中,可能会出现虚假超时:如果 RTT 被低估,那么 RTO 会触发不必要的重传或慢启动。在一次虚假的重传之后,当原始传输的确认到达时,发送方可能会错误地认为这些确认是对重传的确认,并错误地得出结论,即在原始传输和重传之间发送的数据段已经丢失,导致进一步不必要的重传,从而使链路真正拥塞。选择性确认可以减少这种效应。RFC 6298 规定,在估计 RTT 时不得使用已经重传的数据段。Karn 算法确保只有在出现明确的确认之前才调整 RTO,从而产生最终良好的 RTT 估计。然而,在虚假重传之后,可能需要很长时间才能获得明确的确认,从而在此期间减低性能。TCP 时间戳也可以解决设置 RTO 时的重传歧义问题,尽管它们不一定能提高 RTT 估计。
连接管理机制
TCP 是面向连接的,如果理解连接呢?
TCP 的可靠性机制是基于连接的,并且与连接密切相关。TCP 通过建立连接来提供可靠的数据传输和其它的可靠性保证。
当一台服务器启动之后,可能会有多个客户端同时连接,而 TCP 的连接机制可以确保每个客户端与服务器之间都建立独立的连接。每个连接都有自己的接收缓冲区,这样客户端发送的数据就可以被正确的接收和处理。
大量的连接,操作系统就需要管理这些连接,如何管理呢?
当操作系统面对大量的连接时,它需要能有效地管理这些连接以确保系统的性能。
- 操作系统通常会使用连接表(Connection Table)来管理连接。连接表是一个数据结构,用于跟踪和储存当前活动的连接信息。每个连接都会在该数据结构中有一个对应的表项,包含该连接的各项信息。通过连接表,操作系统就可以快速检索和管理每一个连接。
- 为了保证系统资源防止资源耗尽,操作系统通常会对连接数进行限制。它可以设置最大连接数或使用其它策略来对连接的数量进行限制。
- 操作系统需要监控连接的活动状态,根据实际情况对超时和空闲连接进行相应管理。
- 操作系统需要有效地处理并发的连接请求和数据传输。它可能使用多线程或多进程技术来处理并发的连接操作,分配适当的资源和调度策略,以提高系统的吞吐量和响应性能。
这些都是操作系统管理连接的一些机制,实际的连接管理方式可能因为操作系统的不同而有所差异。
连接建立(三次握手)
在客户端与服务器建立连接之前,服务器必须先绑定并监听一个端口,以打开它供连接使用,这称为被动打开(passive open)。一旦被动打开建立成功,客户端可以通过启动主动打开(active open)来建立连接,使用三次握手的方式:
SYN
:客户端通过向服务器发送一个 SYN 来执行主动打开。客户端将数据段的序列号设置为一个随机值 X;SYN - ACK
:服务器回复一个 SYN - ACK 作为响应。序列号设置为收到的序列号加1,即 X+1,而确认号设置为收到的序列号加1,即 Y+1;ACK
:最后,客户端向服务器发送一个 ACK 。序列号设置为收到的确认号,即 X+1,而确认号设置为收到的序号加1,即 Y+1;
步骤1和步骤2建立并确认了一个方向(客户端到服务器)的序列号。步骤2和步骤3建立并确认了另一个方向(服务器到客户端)的序列号。完成这些步骤后,客户端和服务器都已经收到了确认,并建立了全双工通信。
为什么TCP建立连接是三次握手?
1️⃣ 通过三次握手,客户端和服务端都可以确认对方具备发送和接收数据的能力。在第一次和第二次握手中,客户端和服务器都发送了 SYN 数据包,对方收到后可以确认对方的接收能力。在第三次握手中,客户端发送了 ACK 数据包,服务器收到后可以确认客户端的发送能力。
2️⃣ 在网络中,可能有已经失效的连接请求滞留在某个网络节点,如果只有两次握手,那么这些失效的连接会被误认为是真实连接,浪费资源。而且两次握手容易遭受到攻击。
3️⃣ 三次握手是确保双方通信信道可靠性的最小次数。
TCP 三次握手时的状态变化
- 最初,客户端和服务端都处于 CLOSE 状态;
- 服务端为了能够接收客户端的连接请求,需要由 COLOSE 状态变为 LISTEN 状态,等待客户端连接;
- 此时,客户端就可以向服务器发起连接请求了,当客户端发起第一次握手后,客户端状态由 CLOSE 状态变为 SYN_SENT 状态;
- 处于 LISTEN 状态的服务器收到客户端的连接请求后,就将该连接放入内核的等待队列中,并向客户端发起第二次握手,状态由 LISTEN 状态变为 SYN_RCVD 状态;
- 客户端收到服务器的第二次握手请求后,向服务器发起第三次握手,此时客户端的连接建立,状态由 SYN_SENT 变为 ESTABUSHED;
- 服务器接收到客户端发来的第三次握手后,服务器也建立连接成功,此时服务器状态由 SYN_RCVD 变为 ESTABLISHED,可以进行数据读写了。
连接终止(四次挥手)
连接终止阶段使用四次挥手进行,连接的每一段独立地终止。
当一个端希望停止它那一半的连接时,他发送一个 FIN 数据包,另一端用 ACK 确认。因此,典型的终止需要每个 TCP 端的一对 FIN 和 ACK 段。发送第一个 FIN 的一方用最后的 ACK 响应后,在最终关闭连接之前等待一个超时,在此期间本地端口不可用于新的连接;这种状态允许 TCP 客户端在 ACK 在传输过程中丢失的情况下向服务器重新发送最终确认。超时的时间长度取决于实现,但一些常见的值是30s、1min 和 2min。超时后,客户端进入 CLOSE 状态,本地端口可供新的连接使用。
我们以服务器和客户端为例,当客户端与服务器通信结束时,需要与服务器断开连接,此时就不要进行四次挥手的过程:
- 第一次挥手(FIN):客户端发送一个 FIN 报文给服务器,表示客户端不会再发数据给服务器了;
- 第二次挥手(ACK):服务器接收到 FIN 报文后,发送一个 ACK 报文给客户端,表示已经收到了客户端的关闭连接请求;
- 第三次挥手(FIN):服务器发送一个 FIN 报文给客户端,表示服务器也不会再向客户端发送数据了;
- 第四次挥手(ACK):客户端收到 FIN 报文后,发送一个 ACK 报文给服务器,表示已经收到了服务器的断开请求;
在四次挥手中,双方都直到彼此已经关闭连接,并且可以安全地终止连接。
一些操作系统,如 Linux 和 HP-UX,实现了半双工关闭序列。如果主机主动关闭连接,但仍有未读的传入数据可用,主机发送信号 RST(丢失任何接收到的数据)而不是 FIN。这确保了 TCP 应用程序意识到数据丢失。
连接可以处于半打开状态,在这种情况下,一端终止了连接,但另一端没有。终止连接的一端不能再向连接发送任何数据,但另一端可以。终止来接的有胆应继续读取数据,直到另一端也终止连。
TCP 四次挥手时的状态变化
- 在断开连接之前客户端和服务器都处于连接建立后的 ESTABLISHED 状态;
- 客户端主动调用 CLOSE 与服务器断开连接,此时客户端状态由 ESTABLISHED 变为 FIN_WAIT_1;
- 服务器收到客户端发来的结束报文,服务器返回确认报文,状态由 ESTABLISHED 变为 CLOSE_WAIT;
- 客户端收到服务器对结束报文的确认, 状态由 FIN_WAIT_1 变为 FIN_WAIT_2 ,开始等待服务器的结束报文段;
- 当服务器没有数据发送给客户端时,服务器会向客户端发送 FIN 报文,服务器状态由 CLOSE_WAIT 变为 LAST_ACK ,等待最后一个 ACK 到来;
- 客户端收到服务器发来的结束报文段,进入 TIME_WAIT 状态,并发出最后一个确认报文 ACK;
- 服务器收到客户端发来的最后一个响应报文时,服务器就会彻底关闭连接,状态变为 CLOSE;
- 客户端在变为 TIME_WAIT 状态之后,需要等待一个 2MSL(Max Segment Life,报文最大生存时间)的时间,才会进入 CLOSE 状态。
TCP 状态变化图如下所示:
CLOSE 是一个假想的起始点,不是真实的状态。
TIME_WAIT 状态
接下来进行一个测试,首先启动 server 程序,然后启动 client 程序。然后使用 Ctrl-C 终止 server 程序,然后立即重启 server 程序,如果如下:
虽然 server 的应用程序终止了,但 TCP 协议层的连接并没有完全断开,因此不能再次监听同样的 server 端口。
使用 netstat 命令查看:
- TCP 协议规定,主动关闭连接的一方要处于 TIME_WAIT 状态,等待两个 MSL(Maximum Segment lifetime)的时间后才能回到 CLOSE 状态;
- 使用 Ctrl-C 终止 server ,所以 server 是主动断开连接的一方,在 TIME_WAIT 期间仍然不能监听一样的 server 端口;
- MSL 在 RFC1122 中规定为 2min,但是各个操作系统的实现不同,在 Centos7 上默认配置的值为 60s;
- 如下所示,通过命令
cat /proc/sys/net/ipv4/tcp_fin_timeout
查看 msl 的默认值。
为什么 TIME_WAIT 的时间是 2MSL?
在 TIME_WAIT 状态下,服务器会等待2倍的最大报文生存时间(MSL),这是为了确保网络中所有的延迟报文段都已经被丢弃。如果服务器立即关闭连接并重启,可能会收到来自上一个连接的迟到报文,这些报文可能数据错误的,因此等待一段时间可以确保这些报文段已经被丢弃。
在 TIME_WAIT 状态下,服务器仍然保持着连接状态,以便接收可能迟到的最后一个 ACK 报文。如果最后一个 ACK 报文丢失,服务器可以重发 FIN 报文,即使客户端的进餐已经关闭,TCP 连接仍然存在,可以重发 LAST_ACK 报文。
通过设置 2MSL 作为 TIME_WAIT 的持续时间,可以确保网络中的所有报文段都被丢弃,防止连接混淆。
解决 TIME_WAIT 状态引起的 bind 失败的方法
在 server 的 TCP 连接没有完全断开之前不允许重新监听,某些情况下是不合理的。
- 服务器需要处理大量的客户端连接请求(每个连接的生存时间可能很短,但是每秒都有大数量的客户端来请求),这个时候如果服务器主动断开连接(比如某些客户端不活跃,就需要被服务器端主动清理掉),就会产生大量的 TIME_WAIT 状态;
- 由于请求量很大,就可能导致 TIME_WAIT 的连接数很多,每个连接都会占用一个通信五元组(源IP、源端口、目的IP、目的端口、协议)。其中服务器的IP和端口和协议都是固定的。如果新来的客户端连接IP和端口号和 TIME_WAIT 占用的链接重复了,就会出现问题。
因此,当服务器端主动关闭连接时,需要使用同样的端口立即重启。可以使用 setsockopt()
函数来设置套接字描述符的选项 SO_REUSEADDR
为1,从而允许在相同的端口号上创建多个套接字描述符,即使IP地址不同也可以。
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
CLOSE_WAIT 状态
在 TCP 连接的四次挥手过程中,如果客户端调用了 close 函数,而服务器没有及时调用 close 函数关闭对应的文件描述符,就会导致服务器进入 CLOSE_WAIT 状态,而客户端则进入 FIN_WAIT_2 状态。但连接只有在完成四次挥手后才算真正断开,并释放相应的连接资源。
如果服务器没有及时关闭不需要的文件描述符,就会导致大量的连接处于 CLOSE_WAIT 状态,每个连接都会占用服务器的资源。这会导致服务器的可用资源逐渐减少,最终可能耗尽服务器的资源。
在这样的情况下,不仅文件描述符泄漏,还可能导致连接资源无法完全释放,从而引发内存泄漏。因此,在网络套接字程序运行之后,如果服务器中有大量处于 CLOSE_WAIT 状态的连接,就需要检测服务器有没有及时关闭文件描述符了。
总结:对于服务器上出现大量的 CLOSE_WAIT 状态,原因就是服务器没有正确的关闭 socket,导致四次挥手没有正确完成。这是一个 BUG,只需要加上对应的 close 即可解决该问题。
滑动窗口(sliding window)
之前说的确认应答策略,对每一个发送的数据段,都要给一个 ACK 确认应答,收到 ACK 后再发送下一个数据段。这样做比较大的一个缺点就是性能较差。尤其是数据往返时间较长的时候。
既然这样一发一收的方式性能较低。那么我们采取一次发送多个数据段,这样就可以大大的提高性能了(即将多个数据段的等待时间重叠在一起了)。
TCP 通信中,虽然可以向对端一次性发送大量的报文,但也不要将自己缓冲区中的数据一次性全部发送给对端,也需要考虑对端的接收能力。
滑动窗口
TCP 滑动窗口可以确认一个主机发送给另一个主机的未确认字节数 x 。有两个因素决定 x 的值:
- 发送主机上的发送缓冲区大小;
- 接收主机上的接收缓冲区的大小和可用空间。
发送主机不能发送超过接收主机的接收缓冲区可用空间的字节数。在接收主机的接收缓冲区中的字节被 TCP 确认之前,发送主机的 TCP 必须等待发送更多的数据。
在接收主机上,TCP 将接收到的数据存储在接收缓冲区中。TCP 确认接收到数据,并向发送主机传达一个新的接收窗口大小。
发送缓冲区中的数据可以被分为以下三个部分:
- 已经发送并且已经收到 ACK 的数据。这些是发送给接收方的数据,并且接收方已经发送了确认应答。这部分数据可以从发送缓冲区中删除了。
- 已经发送但还没有收到 ACK 的数据 。这些数据已经发送给接收方,但还没有收到确认应答。这部分数据仍然需要留在发送缓冲区中,因为发送方需要在超时或接收方重新请求时进行重传。
- 待发送的数据。发送方准备发送但还没有发送的数据。这些数据等待发送,直到发送窗口允许发送。
这样可以更好的帮助发送方跟踪数据的状态,保证数据的可靠传输。
- 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值,下图的窗口大小就是 4000 字节(四个数据段)。
- 发送前四个数据段时,不需要等待任何的 ACK ,直接发送。
- 收到第一个 ACK 后,滑动窗口向右移动,继续发送第五个数据段;以此类推。
- 操作系统为了维护这个滑动窗口,需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答。只有确认应答过的数据,才能从缓冲区中移除掉。
- 窗口越大,则网络的吞吐量越高。
滑动窗口存在的意义在于可以提高发送数据的效率和数据可靠性传输的保证:
- 假设发送方不考虑拥塞窗口并且对端的窗口的接收能力一直为4000字节。根据滑动窗口原理,发送方可以连续发送多个数据段,直到滑动窗口的大小到达4000字节为止。这样就充分利用了网络宽带,提高了数据发送的效率。
- 如上图所示,发送方依次发送了编号 1001 ~ 2000、2001 ~ 3000、3001 ~ 4000、4001 ~ 5000 的四个数据段,而无需等待对方的 ACK 应答。
- 当发送方收到对方的确认序号2001时,说明 1001 ~ 2000 这个数据段已经被对方成功接收。此时滑动窗口向右移动1000个字节,让滑动窗口的左边界为2001(即将该段数据移除掉)。然后将滑动窗口的右边界向右移动1000个字节,表示将 5001 ~ 6000 的数据段发出。以此类推。
- 滑动窗口的大小决定了发送方可以连续发送的数据量,从而影响数据的传输能力。较大的滑动窗口可以提高网路的吞吐量,同时也反应了对端的接收能力。
- 滑动窗口不仅收到对方窗口的限制,还受到网络拥塞控制机制的影响。实际上,需要综合考虑对端窗口大小、网络拥塞情况以及其它影响因素,动态调整滑动窗口的大小,以实现最有的传输效果。
TCP 的重传机制要求保持发出但未收到确认应答的数据,这部分数据就位于滑动窗口中,滑动窗口左侧的数据已被发出确认,可以移除。滑动窗口中之所以要保存发出但未收到确认的数据,是为了支持 TCP 的重传机制。
丢包问题
如果出现了数据丢包,如何进行重传?这里分两种情况讨论。
情况一:数据包已抵达,ACK 丢包了。
发送端发送多个数据段时,部分 ACK 丢失并不会导致问题,因为后续的 ACK 可以确认应答。
上图中,1 ~ 1000、2001 ~ 3000、3001 ~ 4000 的数据段丢失了,但当发送端收到最后 5001 ~ 6000 数据段的响应,此时发送端就确认之前的数据段都收到了。因为如果接收方没有收到 1 ~ 1000、2001 ~ 3000、3001 ~ 4000 的数据段,是不会设置确认序号为6001的,确认序号的意思就是该确认序号之前的数据都已经收到。
情况二:数据包丢失。
- 当某一段报文段丢失后,发送端会一直收到 1001 这样的 ACK,表示这段报文没有收到;
- 如果发送端主机连续收到三次同样一个 “1001” 这样的应答,就会将对应的数据 1001~2000 重新发送;
- 这是如果对端接收到了 1001~2000 的报文之后,再次返回的 ACK 就是 7001 了,因为 2001 ~ 7001 接收端在之前已经收到了,被放到接收端操作系统内核的接收缓冲区中了。
这种机制被称为 “高速重发机制(快重传 - Fast Retransmit)”。
快重传 VS 超时重传
快速重传(Fast Retransmit)和超时重传(Timeout Retransmit)是 TCP 中两种常见的重传机制,都用于处理数据丢包的情况,但它们触发的条件有所不同。
1️⃣ 快速重传
- 触发条件:当发送方连续收到相同的重复 ACK 时,可以推断出前一个数据段丢失。
- 行为:发送方立即进行重传,而无需等待超时定时器触发。它可以快速的重传丢失的数据报文,提高数据传输的速率。
2️⃣ 超时重传
- 触发条件:当发送方发送数据后,在定时器超时之前没有收到 ACK 确认信息,可以认为数据报文丢失。
- 行为:定时器超时后,发送方将丢失的数据进行重传。超时重传时间相对较长,适用于网络延迟较高或不稳定的情况。
实际中,TCP 会结合两种重传机制,以适应不同的网络环境和数据丢失情况。快速重传可以提供更快的重传响应时间,而超时重传可以作为一种保底机制,用于处理更严重的数据丢失情况。
流量控制(Flow control)
TCP 使用端到端的流量控制协议来避免数据发送方发送数据的速度过快,导致 TCP 接收方无法可靠地接收和处理数据。在不同网络速度的机器之间进行网络通信时,流量控制机制至关重要。例如,如果PC向智能手机发送数据,而智能手机正在缓慢地处理接收到的数据,智能手机必须能够调节数据流,以避免被数据淹没。
TCP 使用滑动窗口流量控制协议。在每个 TCP 段中,接收方在接收窗口的字段中指定它愿意为连接缓冲的额外接收数据量(以字节为单位)。发送主机只能发送一定数量的数据,然后必须等待接收主机的确认并接收窗口更新。
当接收方宣布窗口大小为0时,发送方停止发送数据并启动持续计时器(persist time)。持续计时器的目的是保护 TCP 免受死锁状态的情况,如果接收方后续的窗口大小更新丢失,发送方无法发送更多的数据。当持续计时器到期时,TCP 发送方尝试发送一个小数据包来恢复,以便接收方通过发送另一个含新窗口大小的确认来响应。
如果接收方以很小的增量处理传入数据,它可能会重复通告一个较小的接收窗口。这被称为愚蠢窗口综合征(silly window syndrome),因为在 TCP 段中只发送几个字节的数据是低效的,TCP 报头的开销相对较大。
即接收到处理出数据的能力是有限的。如果发送端发送数据过快,导致接收端的接收缓冲区被打满,则时候如果发送端继续发送,就会造成丢包,继而引起一系列的丢包重传等一系列的连锁反应。因此 TCP 支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做 流量控制(Flow Control)。
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段,通过 ACK 端通知发送端;
- 窗口大小字段越大,说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值响应给发送端;
- 发送端接收并提取到这个响应中的窗口大小时,就会按照窗口具体大小来调整自己的发送速度;
- 如果接收端缓冲区满了,就会将窗口设置为0;这是发送方不再发送数据,但需要定期发送一个窗口探测数据段,使接收端将窗口大小告诉发送端。
接收端将窗口大小信息告知发送端是通过 TCP 首部中的16位窗口字段实现的。那么问题来了,16位数字最大表示65535,那么 TCP 窗口最大就是65535字节吗?
实际上,TCP 首部40个字节选项中包含了一个窗口扩大因子(Window Scale Option)M,这个选项用于扩大窗口大小的表示范围。窗口扩大因子是一个8位的字段,它指示了窗口字段的有效位数。通过窗口扩大因子,可以将窗口字段的值左移 M 位,从而实现更多大的窗口大小。示例:如果窗口字段的值为1000,窗口扩大因子 M=2,那么实际窗口大小就是1000左移两位,即4000字节。
拥塞控制(Congestion control)
虽然 TCP 有了滑动窗口这个杀器,能够高效可靠的发送大量的数据。但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题。虽然网络上有很多的计算机,可能当前的网络状态就已经比较拥堵。在不清楚当前网络状态情况下,贸然发送大量的数据,可能雪上加霜。
TCP 使用多种机制实现高性能,并避免拥塞崩溃(Congestive Collapse),即网络性能严重降低的死锁状态。这些机制控制着进入网络的数据速率,使数据流量保持在不会引发崩溃的速率以下。它们还在流量之间产生近似最大最小的公平分配。
数据发送时确认或未确认的情况,被发送方用来推断 TCP 发送方和接收方之间的网络状况。结合定时器,TCP 发送方和接收方可以改变数据流的行为。这通常被称为拥塞控制或拥塞避免。
TCP 引入 慢启动 机制,先发送少量数据探探路,摸清当前网络拥堵状态,再决定按照多大的速度传输数据。
- 此处引入一个概念为 拥塞窗口。拥塞窗口是 TCP 中用于控制发送速率的一个关键参数。
- 发送开始时,拥塞窗口大小被初始化为1。
- 每次收到一个 ACK 应答,拥塞窗口的大小就会增加1;
- 每次发送数据包的时候,发送方会将拥塞窗口的大小与接收端主机反馈的窗口大小进行比较,然后选择较小的值作为实际发送窗口的大小。
像说明这样的拥塞窗口增长速度,是指数级别的,“慢启动” 只是指初始时慢,但是增长速度非常快。
- 为了不增长的那么快,因此不能使拥塞窗口单纯的加倍;
- 此处引入一个叫做慢启动(Slow Start)的阈值(Threshold),初始阈值被设置为一个较小的值,通常是网络的初始拥塞窗口大小;
- 当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长;
- 当 TCP 开始启动的时候,慢启动阈值被设置为拥塞窗口的最大值;
- 当发送超时重传时,慢启动阈值会被设置为原来阈值的一半,并将拥塞窗口重新置为1。这样做的目的是降低发送速率,以避免进一步加重网络的拥塞情况。
在 TCP 通信中,少量的丢包通常被视为随机的噪声而不是网络拥塞的指示。因此,发送少量丢包时,TCP 会通过触发超时重传来进行恢复,即重新发送未确认的数据。
当出现大量丢包时,TCP 会认为是网络拥塞的迹象。为了应对拥塞,TCP 会采取响应的措施来减缓发送速率,避免进一步加重网络拥塞。
在 TCP 通信开始后,网络吞吐量会逐渐上升;随着网络发送拥堵,吞吐量会立刻下降。拥塞控制,其实就是 TCP 协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。
延迟应答(Delayed Acknowledgment)
如果接收数据的主机立刻返回 ACK 应答,这是返回的窗口可能比较小。
- 假设接收端缓冲区为 1M。一次收到了 500K 的数据,如果立即应答,返回的窗口就是 500K;
- 但实际上可能处理端处理数据的速度非常快,10ms 之内就把 500K 数据从缓冲区拿走处理了;
- 在这种情况下,接收端处理还没有达到自己的极限,及时窗口再放大一些,也能处理过来;
- 如果接收端稍微等一会在应答,比如等待 200ms 再应答,那么这时候返回的窗口大小就是 1M;
窗口越大,网络吞吐量就越大,传输效率就越高。我们的目标就是在保证网络不拥塞的情况下尽量提高传输效率。
那么所有的包都可以延迟应答么?肯定也不是。
数量限制:每隔 N 个包就应答一次。当接收端收到 N 个包后,会一次性发送一个 ACK 应答,而不是针对每个包都发送 ACK 应答。
时间限制:超过最大延迟时间就应答一次。
具体的数量限制和超时时间可能因操作系统的不同而有所差异。一般情况下,常见的设置是每隔2个包发送一次 ACK 应答,并设置最大延迟时间为 200ms 。在减少 ACK 应答的同时,尽量保持对发送端的及时反馈。
捎带应答
在应用层进行一发一收的交互时,可以利用 ACK 应答与应用层的数据回复进行优化。当客户端向服务器发送一条请求,例如 “How are you?”,服务器在处理完请求之后,会给客户端回复一条应答 “Fine, thank you!”。这个时候 ACK 可以搭顺风车,和服务器回应的消息一起回给客户端。
服务器在发送应答数据给客户端时,可以在 TCP 层同时发送 ACK 应答。这样客户端接收到应答数据的同时,也会收到 ACK 应答,从而减少额外的 ACK 传输。
通过将 ACK 应答与应用层数据恢复进行合并,可以减少网络上的额外数据传输,提高整体的传输效率和响应速度。
面向字节流
创建一个 TCP 的 socket 时,内核会同时创建一个 发送缓冲区 和 接收缓冲区。
- 当调用 write 时,数据会先写入发送缓冲区中;
- 如果写入的字节数超过一个 TCP 数据包的大小,数据会被拆分成多个 TCP 数据包进行发送;
- 如果写入的字节数较少,数据会在发送缓冲区中等待,等到缓冲区长度差不多了,或者其它合适的实际发送出去;
- 接收数据的时候数据也是从网卡驱动程序到达内核的接收缓冲区;
- 然后应用程序可以调用 read 从接收缓冲区中读取数据;
- TCP 的一个连接,既有发送缓冲区和接收缓冲区,那么对于这一个连接,既可以读取数据,也可以写入数据。这使得连接能够进行全双工通信。
由于缓冲区的存在,TCP 程序的都和写都不需要一一匹配,例如:
- 写100个字节数据时,可以调用一次 write 写100个字节,也可以调用100次 write ,每次写一个字节;
- 读100个字节数据时,也完全不需要考虑写得时候是怎么写的,既可以一个 read 100个字节,也可以一次 read 一个字节,重复100次。
读和写操作的灵活性使得应用程序可以根据自身的需求和逻辑进行数据的读写操作。应用程序可以按照自己的设计和实现,选择适合的读写方式,不需要与之前的写入一一对应。
粘包问题
- 首先要明确,在粘包问题中,“包” 指的是应用层的数据包;
- 在 TCP 的协议报头中,没有像 UDP 一样的 “报文长度” 字段,但是有一个序号这样的字段;
- 从传输层的角度看,TCP 是一个一个报文过来的,按照序号排好序放在缓冲区中;
- 从应用层的角度看,看到的只是一串连续的字节数据;
- 因此,应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分是一个完整的应用层数据包。
即 TCP 中的粘包问题是指发送方连续发送的多个数据包在接收方接收时被看作一个数据包的现象。这可能会导致接收方无法准确地识别每隔数据包的边界,从而造成数据解析错误。
✨那么如何避免粘包问题呢?解决问题的本质就在于明确两个包之间的边界。
定长包
:对于固定长度的包,可以简单地按照固定长度大小读取每个包。例如,在 Request 结构中,大小是固定的,可以从缓冲区的开头开始按照 sizeof(Request) 依次读取即可;变成包(长度字段)
:对于变长的包,可以在包头的位置约定一个字段来表示整个包的长度,从而确定包的结束位置。在读取时,先读取长度字段,然后根据长度读取对应数量的数据,以确保包的完整性;变长包(分隔符)
:可以在包和包之间使用明确的分隔符。应用层协议可以根据自己的需要来定义分隔符,只要确保分隔符不会与正文数据冲突即可。在读取时,可以根据分隔符将缓冲区拆分为多个完整的包。
☑️ 对于 UDP 协议来说, 是否也存在 “粘包问题” 呢?
- 对于 UDP,如果还没有上层交付数据,UDP 的报文长度仍然在。同时,UDP 是一个一个把数据交互给应用层,就有很明确的数据边界;
- 从应用层的角度来看,使用 UDP 时,数据包的交付给应用层是一个一个进行的,这意味着应用层要么收到完整的 UDP 报文,要么不收,不会出现 “半个” 的情况。
TCP 异常情况
在 TCP 中,存在一些异常情况可能会导致连接的中断或异常终止。如下是一些常见的 TCP 异常情况:
进程终止
:当 TCP 连接相关的进程终止时,操作系统会释放相关的文件描述符,并发送一个 FIN 段给对端。这和正常关闭没有什么区别。机器重启
:当机器重新启动时,TCP 连接相关的进程也会被关闭。此时,与进程终止情况类似,操作系统会释放文件描述符并发送 FIN 段给对端。机器断电/网线断开
:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行 reset。即使没有写入操作,TCP 自己也内置了一个保活定时器,会定期向对方发送保活消息,检测对端是否还在。如果对端不在了,也会把连接释放。应用层检测机制
:某些应用层协议也可能具有自己的连接状态检测机制。例如:在 HTTP 长连接中,会定期发送心跳检测请求以检测对方的状态。类似地,一些即使通讯应用如 QQ 在断线后也会尝试定期重新连接。
基于 TCP 应用层协议
下面是几种基于 TCP 的常见应用层协议:
- HTTP(超文本传输协议);
- HTTPS(安全超文本传输协议);
- SSH(安全外壳程序);
- Telnet(远程终端协议);
- FTP(文本传输协议);
- SMTP(简单邮件传输协议);
除了上述列举的常见应用层协议外,还可以根据具体需求自定义开发 TCP 应用层协议,以满足特殊场景的需求。
TCP 小结
TCP 协议之所以复杂,是因为它需要在保证可靠性的同时尽可能提高性能。
可靠性:
- 校验和:检测数据在传输过程中是否发生了错误,以保证数据的完整性。
- 序列号:通过序列号对数据进行编号,确保数据按序到达目的地。
- 确认应答:接收方收到数据,会发送确认应答,告知发送方数据已成功接收。
- 超时重发:发送方发送数据后设置一个定时器,如果在规定时间内未收到确认应答,会重新发送数据。
- 连接管理:TCP 在建立连接和断开连接时进行握手和挥手,确保通信双方的状态同步和可靠连接的建立与关闭。
- 流量控制:使用滑动窗口机制来控制对方发送数据的速率,以避免接收方缓冲区溢出。
- 拥塞控制:通过拥塞窗口和拥塞避免算法来控制网络中的拥塞情况,以避免网络过载和性能下降。
提高性能:
- 滑动窗口:用来实现流量控制和提高传输效率,允许发送方连续发送多个数据包而无需等待确认应答。
- 快速重传:若接收方收到了乱序的数据包或发生丢包,会立即发送重复确认,通知发送方进行快速重传,避免等待超时重传的时间延迟。
- 延迟应答:接收方可以在一定时间内等待多个数据包,然后一次性发送确认应答,以减少网络中的确认报文数量。
- 捎带应答:允许在发送确认应答时携带之前未确认的数据包的确认信息,减少确认报文的数量和网络延迟。
其它:
- 定时器:TCP 使用不同类型的定时器来处理超时重传、保活和 TIME_WAIT 等情况,以确保通信的稳定性和安全性。