传输层协议--TCP协议

目录

TCP协议

谈谈可靠性

TCP协议格式

序号与确认序号

简单表示确认应答(ACK)机制

16位窗口大小

6位标志位

回谈确认应答机制(ACK)

超时重传机制

连接管理机制

三次握手

四次挥手

流量控制

滑动窗口

流量控制

拥塞控制

延迟应答

捎带应答

面向字节流

粘包问题

TCP异常情况

TCP小结

TCP定时器

基于TCP的应用层协议


TCP协议

TCP全称为"传输控制协议(Transmission Control Protocol)",人如其名,要对数据的传输进行一个详细的控制。

TCP协议是当今互联网当中使用最为广泛的传输层协议,没有之一。因其TCP协议的可靠性,被广泛运用,并因此,在此协议的上层还有非常多的协议,比如说:HTTP协议,HTTPS协议,FTP协议,SSH等等。

补充:TCP协议具有发送、接收缓冲区。默认为65535字节。

谈谈可靠性

为什么网络中会存在不可靠

当今大部分计算机都是基于冯诺依曼体系结构。

虽然,输入设备、输出设备、存储器、运算器、控制器都在一个计算机上,但是这几个硬件设备大体上还是相互独立的,如果想要数据交互,就需要进行数据通信,要进行通信,就需要在其之间建立通信介质,因为这几个设备之间是使用线将其连接起来的,其中连接内存和外设之间的"线"叫做IO总线,而连接内存和CPU之间的"线"叫做系统总线。因其这些线都是在一个计算机上,所以传输的时候出现错误的概率也不是很高。

但是如果通信之间相距十万八千里,如果还用线将其各个设备连接起来,那么传输数据的出错概率也就会变得很高,因此就会出现传输的数据到接收方后,变得不可靠,鉴于此,就引入了一些提高可靠性的协议。

总而言之,网络中不可靠的根本原因,就是长距离传输所谓的线太长,当长距离传输的时候就会出现各种各样的问题,而因此TCP协议就在这样的背景下诞生。--TCP协议是保证可靠的协议。

既如此为什么还会有UDP不可靠的协议

TCP协议是可靠的协议,在数据传输的情况下,可以在一定程度上保证数据的可靠性,而UDP协议是不可靠的协议,那么在这种背景下,为什么还会有UDP协议,并且UDP协议运用如此广泛?

可靠与不可靠是中性词,没有任何贬义之分,它们是用于描述其特点。

  • TCP是可靠的协议,这也就意味着TCP协议在使用的时候就需要花费更多的代价用于保证其可靠性。
  • 比如数据在传输过程中出现了丢包、乱序、检验和失败等,这些都是不可靠的情况。
  • 由于TCP要想办法解决数据传输不可靠的问题,因此TCP使用起来一定比UDP复杂,并且维护成本特别高。
  • UDP协议是不可靠的协议,也就意味着UDP协议不需要考虑数据传输时可能出现的问题,因此UDP无论是使用还是维护都足够简单。
  • 虽然TCP复杂,但TCP的效率不一定比UDP低,TCP当中不仅有保证可靠性的机制,还有保证传输效率的各种机制。

UDP与TCP协议并没有谁更好之分,只有在特定场景下谁更合适,在实际网络中具体使用什么协议还需要考虑上层。如果应用场景严格要求数据在传输过程中的可靠性,那么就必须采用TCP协议,如果应用场景允许数据传输出现少量丢包,那么肯定优先选择UDP协议,因为UDP协议足够简单。

TCP协议格式

TCP协议格式如下:

TCP报头当中各个字段的含义如下:

  • 源/目的端口号:表示数据是从哪个进程来,到发送到对端主机上的哪个进程。
  • 32位序号/32位确认序号:分别代表TCP报文当中每个字节数据的编号以及对对方的确认,是TCP保证可靠性的重要字段。
  • 4位TCP报头长度:表示该TCP报头的长度,以4字节为单位。
  • 6位保留字段:TCP报头中暂时未使用的6个比特位。
  • 16位窗口大小:保证TCP可靠性机制和效率提升机制的重要字段。后面会详细提及。
  • 16位检验和:由发送端填充,采用CRC校验。接收端校验不通过,则认为接收到的数据有问题。(检验和包含TCP首部+TCP数据部分)
  • 16位紧急指针:标识紧急数据在报文中的偏移量,需要配合标志字段当中的URG字段统一使用。
  • 选项字段:TCP报头当中允许携带额外的选项字段,最多40字节。

TCP报头当中的6位标志位(用于区分TCP报文类型):

  • URG:紧急指针是否有效。1表示有效,0无效。
  • ACK:确认序号是否有效。1表示有效,0无效。
  • PSH:提示接收端应用程序立刻将TCP接收缓冲区当中的数据读走。
  • RST:表示要求对方重新建立连接。我们把携带RST标识的报文称为复位报文段。
  • SYN:表示请求与对方建立连接。我们把携带SYN标识的报文称为同步报文段。
  • FIN:通知对方,本端要关闭了。我们把携带FIN标识的报文称为结束报文段。

TCP如何将报头与有效载荷进行分离?

当TCP从底层获得一个TCP报文后,虽然不知道该报头的具体长度是多少字节,但是报头的构成是由20字节的固定基本报头+选项。其中前20字节是已经确定,只需确定选项是多少字节即可。

因此TCP是这样分离报头与有效载荷的:

  • 当TCP获取一个报文后,首先读取前20字节,获取其中的一个字段,该字段为4位TCP报头长度。根据此字段计算的到TCP的报头总字节数。
  • 如果计算的到的TCP的报头总字节数大于20,则减去20则为选项字节数。反之为20则无选项。
  • 到此就可以得知报头的总字节数。从而将报头与有效载荷进行分离。

4位TCP报头长度是以4字节为基本单位。4位TCP报头长度所构成的二进制数字范围为:0000 ~ 1111,其也就是0-15。因其是以4字节为基本单位,则转化而得报头的字节大小范围为:0~60字节。

减去固定的20字节的基本报头,剩下的40字节也就是选项字段的最大长度。

反之如果4位TCP报头长度字段填写的为:0101,则无选项字段。

TCP如何决定将有效载荷交付给上层的哪一个协议?

传输层的上一层为应用层,TCP想要确定将有效载荷交付给应用层的那个协议,就需要读取报头中的目的端口号。借此可以找到应用层的那个进程。

序号与确认序号

序号与确认序号的存在在一定基础上保证了TCP的传输可靠性。

简单表示确认应答(ACK)机制

其本质上,保证TCP的可靠性的最基本的一个特点就是:确认应答(ACK)机制。

什么是真正的可靠性

在进行数据通信的时候,一方发送数据的时候,它并不能百分百保证对方一定可以接受的到。如果在这个过程中出现问题,发送方是不知道的,只有当对端主机向发送端发送信息告诉其,我刚才没有收到你的信息,出问题了,到这里,发送端才知道发送出问题,要重发或者修正错误,这样就在一定程度上就保证了数据可以传送给对端,这样就做到了真正的可靠。

(图中实线表示发送的数据对端能够可靠的接收到,虚线不能保证,可能会丢失或者出问题)

但是数据的传输是双向的,真正的可靠是要保证双端的传输都是可靠的,虽然主机A向主机B发送数据可以可靠的让其收到,但是主机B也同样需要保证自己发送的数据可以可靠的让其主机A接收到。因此当主机B向主机A发送可以后,主机A也应该向主机B响应来向主机B表示:可以可靠的收到主机B发来的数据,但此时有需要保证主机A的响应数据的可靠性,主机B又需要发送响应......这样就会陷入死循环。

因此只有一方收到对方的响应消息的时候才可以保证己方发送的数据的可靠性,但是双方发送消息的时候总有一方是发送最后消息的人,因此按照上面的理论,此刻又需要对方响应,来保证最后消息的可靠性,陷入死循环。但实际设计上,肯定会设计别的结构来打破此死循环,因此无法百分百保证其可靠性。

所以在实际的网络设计上,已经明确表示没有百分百的可靠性,因此双方通信时,总有一些数据是无法保证其可靠性的,此类数据就包括响应数据,在实际情况上,只需要保证一些核心数据可靠性即可。

当对端没有在规定时间内收到响应数据,则发送端直接认为发送的数据丢失或出问题导致不可靠,就会进行重传。

这种机制在TCP就叫做确认应答机制。确认应答机制不是保证双方通信的全部数据的可靠性,而是保证一方收到另一方的应答消息,就说明其它上次发送的数据另一方已经可靠的接收到了。

除此之外还有捎带应答,用上图简单的说就是将收到与等于2,合并打包一次性的发送给对端。既可以保证数据的可靠性还可以返回处理后的数据。

32位序号

在网络通信中,如果只有收到上一次发送数据的响应后才继续发送新的数据,那么此时双方的通信是串行的,此时效率是十分低下的。

因此在设计上,为了保证效率,是允许一方向另一方连续发送多个数据报的,发送的数据报只要在后续收到对应的响应数据报即可,此时即可保证了可靠性又保证了效率。

但是需要额外注意的一点就是,连续发送的数据报,对端收到的连续数据包顺序可能不一样,收到的响应数据报不一定是按照发送数据接收到的,这是因为每个报文在网络中传输时候所选择的路径可能不一样,并且每个报文的传输速度会因外部原因随时发生变化。因此处理保证可靠性的接收到响应外,还需要保证报文的有序。鉴于此,就设计了序号,序号的存在就保证了报文的有序。

TCP将发送出去的每个字节数据都进行了编号,这个编号叫做序列号。

  • 比如现在发送端要发送3000字节的数据,如果发送端每次发送1000字节,那么就需要用三个TCP报文来发送这3000字节的数据。
  • 此时这三个TCP报文当中的32位序号填的就是发送数据中首个字节的序列号,因此分别填的是1、1001和2001。

在对端接收到刚才发送的3个TCP数据报后,就可以根据其中报头中的32位序号字段,对这三个数据报按照发送的顺序进行排序。重排后的结构放入到TCP的接收缓冲区。

还需要额外注意的一点:接收端怎么确定序号一定是1、1001、2001,而不是分六次发送?

关键在于:TCP是基于字节流而不是基于消息的 ,接收端不关心"发送方分几次发送",只关心 字节流的连续性

其是根据序列号+数据报的长度得知该数据报包含字节范围为多少。那么就可以推算得知下一个数据报的序号为多少。

32位确认序号

确认序号 = 已成功接收的连续数据的最后一个字节序号 + 1 或 确认序号 = 下一个期望接收的字节的序列号

TCP的确认序号有两个作用:

  • 累积确认:表示接收方已连续、可靠接收到的数据最高位置。
  • 流控制:隐式地告诉发送方下一次发送应该从哪个字节位置开始(即确认序号的值)。

同样主机B向主机A发送数据时也是按照上面的规定进行的。

需要提醒的一点:响应数据与正常的数据是一样的,都是一个完整的TCP报文,只是响应数据报可能不携带有效载荷,但至少是一个TCP报头。

为什么设置为最后一个字节序号 + 1

原因如下

  1. 允许少量应答丢失
  2. 找到对应的序号

那如果报文丢失了怎么办?

在上面的案例中,如果对端发送三个数据报,其序号分别为1、1001、2001。

如果这三个数据报中序号为1001的丢包。那么接收端按照序号对数据进行重排的时候就会发现,1001~2000字节范围的数据丢失。此时主机B就会向主机A发送确认序号为1001的报文。告诉主机我现在已连续、可靠接收到的数据最高位置为:1000。隐式地告诉发送方下一次发送应该从1001字节位置开始。

与正常认知不同的是,主机B并不会向主机A响应确认序号为3001的报文。因为1001-2000在3001之前,不符合累计确认机制。因此会返回1001的确认序号报文。告诉其序号为1001往后的报文发生了丢失。主机A进行重传。同样如果2001也丢失,正常情况下也是如此。

如果单个报文序号与确认序号都填充

如果单个报文序号与确认序号都填充这种情况下仅需要填充32位序号来表明自己当前发送数据的序号。还需要填充32位确认序号,对对方上一次发送的数据进行确认,告诉对方下一次应该从哪一字节序号开始进行发送。此时就是我们上面提及的捎带应答。

为什么不能合并?

因为它们是两个独立的逻辑时钟:

  • 我的序号空间记录我发了多少字节

  • 你的确认序号记录你收到了我多少字节

  • 这两者是不同步的(网络延迟、乱序等)

  • 有捎带应答的存在

  • A发送B的同时,B也可以向A发送数据。

16位窗口大小

TCP的接收缓冲区和发送缓冲区

TCP本身是具有接收缓冲区和发送缓冲区的:

  • 接收缓冲区用来暂时保存接收到的数据。
  • 发送缓冲区用来暂时保存还未发送的数据。
  • 这两个缓冲区都是在TCP传输层内部实现的。

用UDP来进行比较的解释,当我们在编译器中编写代码后,将想要通信数据传输给对端时。

UDP 的发送流程相对简单直接: 当我们调用 sendto() 时,数据从用户空间复制到内核后,UDP 层会为其封装 UDP 头部,然后立即交给网络层进行后续传输。这个过程几乎不做缓冲,也不保证可靠交付,属于"尽最大努力"式的传输。

而 TCP 则复杂得多,它通过双缓冲区机制实现可靠传输:

  • 发送数据时,调用 write() 或 send() 将数据从用户空间复制到TCP的发送缓冲区。TCP 协议栈会根据一些机制,在适当的时机从缓冲区取出数据、封装 TCP 头部,再交给网络层发送。
  • 接收数据时,对端传来的数据经过 TCP 协议栈校验、排序后,被存入TCP的接收缓冲区。应用层调用 read() 或 recv() 时,实际上是从该缓冲区中将数据复制到用户空间。

我们使用TCPwrit/send后成功只表示数据进入了发送缓冲区,不代表已发送。同样read/recv是从接收缓冲区取数据,不是直接从网卡读。

TCP的发送缓冲区和接收缓冲区存在的意义

发送缓冲区和接收缓冲区的作用:

  • 数据在网络中传输时可能会出现某些错误,此时就可能要求发送端进行数据重传,因此TCP必须提供一个发送缓冲区来暂时保存发送出去的数据,以免需要进行数据重传。只有当发出去的数据被对端可靠的收到后,发送缓冲区中的这部分数据才可以被覆盖掉。
  • 接收端处理数据的速度是有限的,为了保证没来得及处理的数据不会被迫丢弃,因此TCP必须提供一个接收缓冲区来暂时保存未被处理的数据,因为数据传输是需要耗费资源的,我们不能随意丢弃正确的报文。此外,TCP的数据重排也是在接收缓冲区当中进行的。

类似的生产者与消费者模型

对于发送缓冲区

  • 生产者 :应用程序进程或线程,通过调用write()send()系统调用生产数据

  • 交易场所:TCP发送缓冲区,位于内核空间的有界队列

  • 消费者:下层的网络层不断从发送缓冲区

对于接收缓冲区

  • 生产者:下层的网络层从网络中获取到对端发送的数据

  • 交易场所:TCP接收缓冲区,位于内核空间的有界队列

  • 消费者:上层的应用层护短的从接收缓冲区提取数据

窗口大小

当发送端要将数据发送给对端时,本质是将自己的发送缓冲区内的数据传送给对端的接收缓冲区。但是缓冲区是有大小的,如果接收端的接收缓冲区接收数据的速度大于上层应用层提取接收缓冲区的数据速度,那么总有那么一刻,接收缓冲区会被填满,这是如果发送端还继续向对端发送数据,就会出现错误。导致数据丢失,从而发生一系列不可控错误。

那么为了避免这类错误,就有了一个报头字段--16位窗口大小,其作用是于ACK中响应接收缓冲区还有多少空间,填充的是自己的接收缓冲区还剩多少空间大小。

此时发送端收到ACK后读取报头中的16位窗口大小,从而得知对端接收缓冲区的实际情况。然后据此做出调整。

  • 如果16位窗口大小所表示的值越大,那么表示对端接收缓冲区内的所剩空间越多,此时处理数据的能力越强,表示发送端可以提高发送数据的速度。、
  • 如果16位窗口大小所表示的值越小,那么表示对端接收缓冲区内的所剩空间越少,此时处理数据的能力越弱,表示发送端可以降低发送数据的速度。
  • 如果16位窗口大小所表示的值为0,那么表示对端接收缓冲区内没有剩余空间,无法接收任何发送来的数据,表示发送端应该暂时停止发送数据。

6位标志位

标志位存在的意义

TCP报文的种类多种多样,除了正常通信时发送的普通报文,还有一些特殊的报文,比如说:建立连接时发送的请求建立连接的报文,以及断开连接时发送的断开连接的报文等等。

对于收到不同类型的报文,不仅要识别出其类型,还要对其做出相对应的操作。

这就好比我们日常交流,如果只是简单的文字交流,不附带任何表情、语气、动作等等。那么人与人之间的交流,就变得十分生硬,如同机器人一样。无法表达丰富的内容。

所以没有标志位,TCP就像两个机器人用单调的语气说话,无法表达「我要开始说话了」、「我收到了」、「我生气了要挂电话」等丰富意图。

URG

TCP 的 URG(紧急)标志位就像网络中的「救护车」。当它亮起时,数据会获得特权,可以不按原本的序号排队,直接被对端优先读取和处理,确保紧急信息能够第一时间到达。

其中16位紧急指针代表的就是紧急数据在报文中的偏移量。

打个比方,假设一个实际用例,发送端想要发送一个数据报,但该数据过于长,就将其分为3次发送,我们规定,第二个报文中的有效载荷的第6~50字节为紧急数据。那么此时仅第二个报文的URG被填充为1。

当接收端接收到第二个报文时(无需等待三个报文到齐),立即解析TCP头部,进行报头与有效载荷分离,发现URG=1,然后读取其16位紧急指针。紧急指针的值表示的是从TCP报文起始处(含TCP头部)到紧急数据结束位置的下一个字节的偏移量。假设TCP头部为20字节,紧急数据为有效载荷的第6~50字节(共45字节),那么紧急指针应设置为:20 + 50 = 70(因为紧急指针指向的是紧急数据最后一个字节之后的位置)。接收端根据此值计算出紧急数据范围为TCP报文的第26~70字节,立即将这部分数据提取为紧急数据优先处理,而该报文中其余的数据,则按正常流程放入接收缓冲区排队。

ACK

  • 报文当中的ACK被设置为1,表明该报文可以对收到的报文进行确认。
  • 一般除了第一个请求报文没有设置ACK以外,其余报文基本都会设置ACK,因为发送出去的数据本身就对对方发送过来的数据具有一定的确认能力,因此双方在进行数据通信时,可以顺便对对方上一次发送的数据进行响应。

PSH

提示接收端应用程序立刻将TCP接收缓冲区当中的数据读走。

该情况并不是我们理解的那样,只有接收端的接收缓冲区满的情况下才会将其填写为1,而是当接收缓冲区的数据量达到一个额定值的时候就会被填充为1,这就类似于保护机制,在发生问题前就发现,并向对方提醒,要注意。

剩下的三个没什么要注意的,这里就不提及了。

回谈确认应答机制(ACK)

根据上面的学习,我们了解到了确认应答机制的核心确实由TCP报头中的32位序号与32位确认序号实现,

所以我们对上面的简单总结一下:

  1. 对于发送方来讲,发送速度由对方的接收缓冲区中剩余空间的大小来决定。
  2. 接收了确认应答,则表示发的消息对方可靠的接收到了。
  3. 没有收到确认应答,则无法可靠性,所以,最新一条消息无应答,无法保证发送的消息100%可靠。
  4. 一段时间如果没有收到应答,则认为数据丢失,可能会进行重传。
  5. 没收到之前,发送端的发送缓冲区要维护数据一段时间,为了就是后续的重传。

额外补充的就是:注意到序号与确认序号是不断增大的,那么肯定会增大到一个最大值,此时如果再增大就会报错,因此在设定上,序号与确认序号增大到一定值后会绕回起始值。

超时重传机制

双方在进行网络通信时,发送方发出去的数据在一个特定的事件间隔内如果得不到对方的应答,此时发送方就会进行数据重发,这就是TCP的超时重传机制。

TCP的可靠性一部分是由协议报头体现出来,另一部分就是TCP协议底层代码设计,比如超时重传,超时重传机制实际就是发送方在发送数据后开启了一个定时器,若是在这个时间内没有收到刚才发送数据的确认应答报文,则会对该报文进行重传。这就是通过TCP的代码逻辑实现的。

  • 主机A发送数据给主机B之后,可能因为网络拥堵等原因,数据无法到达主机B。
  • 如果主机A在一定特定时间间隔内没有收到主机B发来的确认应答,主机A就会认为自己发送的数据丢失,就会进行重传。

但是,主机A并未收到主机B发来的确认应答,也可能是因为ACK丢失了。

因此主机B会收到很多重复数据,那么TCP协议就需要能够识别出那些包是重复的包,并把重复的去掉,这时候我们可以利用前面提及的序列号,就可以很容易做到去重的效果。(这在一定程度上也保证了可靠性)

那么超时的时间如果确认呢?

最理想的情况下,找到一个最小的时间,保证"确认应答一定在这个时间内返回"。但是这个时间的长短,会随着网络的环境的不同,是由差异的。如果超时时间设置太长,会影响整体的重传效率。如果超时时间设置太短,有可能会频繁发送重复的包,造成效率低下。

所以TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。

  • Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。
  • 如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传。
  • 如果仍然得不到应答,等待 4*500ms 进行重传。依次类推,以指数形式递增。
  • 累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。

连接管理机制

TCP是面向连接的

TCP的可靠性不是靠主机与主机之间保证的,而是面向连接的,单个主机可能连接多个服务端,单个服务端也可能连接多个客户端,如果不是面向连接的,那么单个主机只能有一个发送缓冲区与接收缓冲区,这样的设定就会导致数据的混乱,所以TCP是面向连接的,每个连接都是独立存在的,单个连接都会存在一个独属于自己连接的一套资源。

所以TCP在使用前就会先建立连接,此就是可靠性的最基础条件,只有保证建立起连接,才可有接下来的步骤。

操作系统对连接的管理

面向连接是TCP可靠性的一种,只有建立好连接,才可以进行通信,但是一个计算机上有许多资源,此时对于TCP的面向连接资源就需要进行特殊的管理。

  • 操作系统的管理这些连接的时候需要"先描述,在组织",在操作系统中管理连接资源的时候有一个其描述的结构体,该结构体包含了连接的各个属性字段,所以定义出的连接资源最终都会以结构体的形式实例化保存起来,多个连接资源就会以类似链表形式进行排列起来,此时对连接的管理就变为了对链表的增删查改。
  • 建立连接,实际就是在操作系统中用该结构体定义一个结构体变量,然后填充各个属性字段,最后插入到管理连接的结构体内即可。
  • 断开连接,实际就是操作系统将某个连接的结构体从管理连接的结构体内删除,释放该连接曾经占用的资源。
  • 因此连接的管理也是有成本的,这个成本就是管理连接结构体的时间成本,以及存储连接结构体的空间成本。

三次握手

三次握手的过程

双方在进行TCP通信之前需要先建立连接,建立连接的这个过程我们称之为三次握手。

以服务器和客户端为例,当客户端想要与服务器进行通信时,需要先与服务器建立连接,此时客户端作为主动方会先向服务器发送连接建立请求,然后双方TCP在底层会自动进行三次握手。

  • 第一次握手:客户端向服务器发送的报文当中的SYN位被设置为1,表示请求与服务器建立连接。
  • 第二次握手:服务器收到客户端发来的连接请求报文后,紧接着向客户端发起连接建立请求并对客户端发来的连接请求进行响应,此时服务器向客户端发送的报文当中的SYN位和ACK位均被设置为1。
  • 第三次握手:客户端收到服务器发来的报文后,得知服务器收到了自己发送的连接建立请求,并请求和自己建立连接,最后客户端再向服务器发来的报文进行响应。

TCP通信是全双工的,因此建立连接也是双向的,请求方向服务端发送连接请求,服务端也需要向请求方发送连接请求,此时才可以建立完整的TCP连接,保证后续的可靠性。

为什么是三次握手?

为什么是三次握手,而不是四次,不是五次呢?

首先要明确的是,在建立连接的时候,进行握手时,如果三次中任何一次发生了丢失,都是会影响建立连接的,都是会使得建立连接失败。

即使前面所有的握手全部成功也是如此,因此在建立连接的设定上,只要有一次握手发生了丢包,都会使得建立连接失败,所以建立连接都不是百分百成功的。

既然建立连接都是不是百分百成功的,那么关于进行几次握手就要看实际数据,设定为几次握手的优点最多。

经过科学家的测试,三次握手为最优。

这不仅仅是因为三次握手在设定上是建立全双工通信信道的最小握手次数,还是因为三次握手在数学上是确保双向可靠连接的最小次数,在工程上是经过长期实践验证的、在可靠性、性能、实现复杂度之间的最佳平衡点。它既不会因为太少而不可靠,也不会因为太多而浪费资源,是"刚好足够"的艺术。

  • 三次握手时的状态变化

三次握手时的状态变化如下:

  • 在最一开始其双方状态都是CLOSE
  • 服务器端为了能够接收到客户端发来的连接请求,会将自己的状态转变为LISTEN状态。
  • 此时客户端就可以向服务器发送三次握手,当客户端发送第一次握手后,其状态就会变为SYN_SENT。
  • 此时处于LISTEN状态的服务器收到客户端的连接请求后,将该连接放入内核等待队列中,并向客户端发起第二次握手,此时服务器的状态变为SYN_RCVD。
  • 当客户端收到服务器端的第二次握手后,紧接着就会发动第三次握手,此时对于客户端来说,连接已经构建成功,状态会变为ESTABLISHED。
  • 而对于此时的服务器来说,当收到客户端发来的最后一次握手后连接也是建立成功的,状态也会变为ESTABLISHED。

至此三次握手结束,通信双方可以开始进行数据交互了。

对于服务端来说会将状态改变,那对于别的客户端发送的连接有啥影响么?

答案是没有影响,因为每个连接都是独立的。让我详细解释一下:

每个连接都是独立的

服务器会为每个客户端连接创建一个独立的socket和TCP控制块(TCB),每个连接都有自己的状态:

  • 监听socket :始终处于LISTEN状态,负责接受新连接

  • 已连接socket:每个客户端连接对应一个独立的socket,有自己的状态转换

多连接并发处理

复制代码
监听socket (始终LISTEN)
    ↓
客户端A连接 → 创建新socket A (状态: SYN_RCVD → ESTABLISHED)
客户端B连接 → 创建新socket B (状态: SYN_RCVD → ESTABLISHED)
客户端C连接 → 创建新socket C (状态: SYN_RCVD → ESTABLISHED)

四次挥手

四次挥手的过程

由于双方维护连接都是需要成本的,因此当双方TCP通信结束之后就需要断开连接,断开连接的这个过程我们称之为四次挥手。

还是以服务器和客户端为例,当客户端与服务器通信结束后,需要与服务器断开连接,此时就需要进行四次挥手。

  • 第一次挥手:客户端向服务器发送的报文当中的FIN位被设置为1,表示请求与服务器断开连接。
  • 第二次挥手:服务器收到客户端发来的断开连接请求后对其进行响应。
  • 第三次挥手:服务器收到客户端断开连接的请求,且已经没有数据需要发送给客户端的时候,服务器就会向客户端发起断开连接请求。
  • 第四次挥手:客户端收到服务器发来的断开连接请求后对其进行响应。

四次挥手结束后双方的连接才算真正断开。

为什么是四次挥手?

其主要原因也如三次握手相似。与之外的是因为:

  • 由于TCP是全双工的,建立连接的时候需要建立双方的连接,断开连接时也同样如此。在断开连接时不仅要断开从客户端到服务器方向的通信信道,也要断开从服务器到客户端的通信信道,其中每两次挥手对应就是关闭一个方向的通信信道,故而要进行四次挥手才可完成断开连接。
  • 除此之外不能向三次握手一样,可以将两次握手合并为一次握手,因为一方向另一方握两次手,进行断开连接,完成了通信,但并不能代表另一方没有任何信息要发送给对端了,其可能还有一些必要信息还要进行发送,因此不会立即进行挥手处理。

四次挥手时的状态变化

四次挥手时的状态变化如下:

  • 在挥手前客户端和服务器都处于连接建立后的ESTABLISHED状态。
  • 客户端为了与服务器断开连接主动向服务器发起连接断开请求,此时客户端的状态变为FIN_WAIT_1。
  • 服务器收到客户端发来的连接断开请求后对其进行响应,会将其自己服务器的状态变为CLOSE_WAIT。
  • 此时当客户端在规定的时间轴内收到服务器发送的响应后,会将自己的状态变为FIN_WAIT_2。
  • 当服务器没有数据需要发送给客户端的时,服务器会向客户端发起断开连接请求,然后将刺激的状态变为LASE_ACK。等待其客户端的响应。
  • 客户端收到服务器发来的第三次挥手后,会向服务器发送最后一个响应报文,此时客户端进入TIME_WAIT状态。
  • 当最后一个响应报文服务器收到后就会将自己的状态变为CLOSE,彻底的关闭连接。
  • 然后客户端在等待一个规定的时间后,也会将自己的状态变为CLOSE,彻底的关闭连接。这个时间为2MSL(Maximum Segment Lifetime,报文最大生存时间)

至此四次挥手结束,通信双方成功断开连接。

半关闭状态

我们给一个场景,即男女朋友的例子解释一下。

建立连接 = 确认关系

你表白 → 对方同意 → 你确认 → 正式恋爱(三次握手)

半关闭状态 = 温和分手模式

场景:你想分手,但还想保持朋友关系

过程

  • 你说:"我们分手吧,但你还可以找我聊天"(发送FIN)

  • 对方回复:"好吧"(回复ACK)

  • 此时状态

    • :已关"说"的通道,只能"听"(FIN_WAIT_2)

    • 对方:还能"说"也能"听"(CLOSE_WAIT)

就像:你提出分手后,不再主动联系对方,但对方还能给你发消息倾诉。

为什么需要半关闭?

  • 文件传输:传完后告诉对方"我发完了",但还能收确认

  • 聊天软件:你说"再见"后,还能看到对方最后回复

  • 避免突然断联:给对方缓冲时间

完全关闭 = 彻底分手

当对方也说分手:

  • 对方:"那我也同意分手"(发送FIN)

  • 你:"保重"(回复ACK)

  • 等待冷静期(TIME_WAIT)

  • 彻底结束(CLOSED)

异常情况

  • 突然拉黑:直接断联(RST复位)

  • 单方面纠缠:你说分手,对方一直不发FIN,连接卡住

  • 分手后立刻找新欢:可能收到前任消息(TIME_WAIT防止这个问题)

简单总结

半关闭就是:"我已经不想和你说话了,但你可以把话说完"

→ 一个方向关闭,另一个方向保持

→ 给关系一个温和的结束过渡期

就像现实中的分手,给对方说完最后一句话的机会,然后各自安好。

CLOSE_WAIT

如果客户端向服务器发送第一个挥手后,客户端也正常接收到了第二次挥手的响应,此时客户端的状态正常进行FIN_WAIT_2,服务器的状态变为CLOSE_WAIT。此时对于服务器来说,还是没有断开连接,连接的相关资源还在占用,但客户端已经进行了必要的释放(释放了一部分,但是连接还没有断开),已经打包好,准备下班了。此时状态也叫做半关闭状态。

但是只有进行完四次挥手才是真正的断开了连接,此时双方才真正的释放了连接,与其资源,但是如果服务器没有主动关闭不需要的文件描述符,此时在服务器端就会存在大量处于CLOSE_WAIT状态的连接,此时就会占用资源,导致不能维持新来的连接。

因此在编写网络套接字代码时,如果发现服务器端存在大量处于CLOSE_WAIT状态的连接,此时就可以检查一下是不是服务器没有及时调用close函数关闭对应的文件描述符。

TIME_WAIT

四次挥手中前三次挥手丢包时的解决方法:

  • 第一次挥手丢包:客户端收不到服务器的应答,进而进行超时重传。
  • 第二次挥手丢包:客户端收不到服务器的应答,进而进行超时重传。
  • 第三次挥手丢包:服务器收不到客户端的应答,进而进行超时重传。
  • 第四次挥手丢包:服务器收不到客户端的应答,进而进行超时重传。

客户端在发出第四次挥手后并不会,立即进入CLOSED状态,因为为了避免出现丢包的问题,会在此状态停留2SML。

因此服务器在经过若干次超时重发后得不到响应,最终也一定会将对应的连接关闭,但在服务器不断进行超时重传期间还需要维护这条废弃的连接,这样对服务器是非常不友好的。

为了避免这种情况,因此客户端在四次挥手后没有立即进入CLOSED状态,而是进入到了TIME_WAIT状态进行等待,此时要是第四次挥手的报文丢包了,客户端也能收到服务器重发的报文然后进行响应。

但即便存在了这样的设定,也还是存在特殊情况,在规定的2SML时间内,依旧没有完成四次挥手,此时服务器经过多次超时重传后也会关闭连接。这种情况虽然也让服务器维持了闲置的连接,但毕竟是少数,引入TIME_WAIT状态就是争取让主动发起四次挥手的客户端维护这个成本。

下图是TCP状态转化的一个汇总表

  • 较粗的虚线表示服务端的状态变化情况。
  • 较粗的实线表示客户端的状态变化情况。
  • CLOSED是一个假想的起始点,不是真实状态。

流量控制

TCP支持根据接收端的接收数据的能力来决定发送端发送数据的速度,这个机制叫做流量控制(Flow Control)。

在TCP通信中,接收端处理数据的速度是有限的。如果发送端发送速率过快,导致接收端缓冲区被填满,后续发送的数据包就会丢失,进而触发重传机制,引发性能下降等一系列连锁问题。

为此,TCP引入了流量控制机制,让接收方能够根据自身处理能力动态调节发送方的数据发送速率。具体实现方式如下:

  • 接收端在TCP首部的"窗口大小"字段中声明自己当前可接收的缓冲区容量,并通过ACK报文通知发送端。窗口大小直接反映了网络的吞吐能力:窗口越大,允许同时传输的数据量越高;反之,则需降低发送速率。
  • 当接收端发现缓冲区即将满载时,会通过ACK报文告知发送端一个较小的窗口值,发送端随之调低发送速度。若缓冲区完全占满,接收端将窗口设为 ,此时发送方必须暂停发送数据,但仍需定期向接收端发送窗口探测报文,以等待接收端重新通报可用窗口。

发送端在得知接收端窗口为零后,主要通过以下两种方式重新获得发送许可:

  1. 被动等待通知

    接收端上层应用读取缓冲区数据后,接收端会主动发送一个包含新窗口大小的TCP报文,通知发送端缓冲区已有空间,发送端即可恢复数据传输。

  2. 主动周期探测

    发送端会定时向接收端发送仅含探测信息的报文(不携带有效数据),询问当前窗口状态。一旦探测到接收端缓冲区出现可用空间,发送端便继续发送数据。

报文格式中,窗口大小为16位,16位最大表示为65535,那么TCP窗口最大就为65535字节么?

理论上确实是这样的,其65535 字节(约 64 KB)。由于 64 KB 的窗口在现代高速网络中会成为瓶颈,因此 RFC 1323 引入了 TCP 窗口缩放选项(Window Scale Option),通过缩放因子将实际窗口大小扩展到:实际窗口=窗口字段值×2^(缩放因子)

第一次向对方发送数据时如何得知对方的窗口大小?

其这一问题是在进行通信前的三次握手时就已经解决,在双方握手时就已经进行了相关操作。其为协商了双方的接收缓冲区的大小。

滑动窗口

对每一个发送的数据段,都要给一个ACK确认应答。收到ACK后再发送下一个数据段。这样做有一个比较大的缺点,就是性能较差。尤其是数据往返的时间较长的时候。

既然这样一发一收的方式性能较低,那么我们一次发送多条数据,就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)。

而滑动窗口的设计,使得双方通信时可以连续发送多个数据报

在双方进行TCP通信时,双方的通信是面向字节流的,一次通信可以发送一个完整的数据报,如果单位时间内只允许串联的发送数据,那么效率显然是低下的,所以滑动窗口的存在,就使得双方通信时,可以一次向对方发送多条数据,这样并联式的通信使得可以将多个等待响应的时间叠在一起,进而提高通信效率。

在此过程中,双方虽然可以一次发送多个数据传给对端,但是也不可一股脑的将自己想要通信的数据全部传给对端,毕竟根据前文的流量控制描叙,其传输的速度,还是取决于对端。毕竟传的再多,对端收不到也是无用。

滑动窗口的设计

发送方可以一次发送多个报文给对方,此时也就意味着发送出去的这部分报文当中有相当一部分数据是暂时没有收到应答的。

为此发送端的发送缓冲区的数据可以分为三部分:

  • 已经发送并且已经收到ACK的数据。
  • 已经发送还但没有收到ACK的数据。
  • 还没有发送的数据。

这里发送缓冲区的第二部分就叫做滑动窗口。(也有人称三个整体为滑动窗口,第二部分称为窗口大小)

而滑动窗口的大小表示意义就为:发送方不用等待ACK一次所能发送的数据最大量。(其大小于窗口大小有关)

滑动窗口的核心意义在于显著提升数据传输效率

  • 滑动窗口的大小通常取对方接收窗口与己方拥塞窗口的较小值,这既考虑了接收方的处理能力,也兼顾了当前网络状况。为简化说明,我们暂不考虑拥塞窗口,并假定对方窗口固定为 4000 字节。
  • 在此条件下,发送方可一次性发送 4000 字节数据而无需等待确认,即滑动窗口大小为 4000 字节(相当于四个数据段)。例如,连续发送 1001-2000、2001-3000、3001-4000、4001-5000 这四个段时,不必停顿等待 ACK。
  • 当收到确认序号为 2001 的 ACK 时,表明 1001-2000 段已被对方成功接收,该段便可从发送缓存中释放。此时由于接收窗口仍为 4000,滑动窗口可整体向右移动,并继续发送 5001-6000 段,后续过程依此类推。

滑动窗口越大,网络的吞吐率越高,同时也意味着接收方的处理能力越强。 这一机制使得发送方能更充分地利用网络带宽,减少空闲等待时间,从而整体提升传输性能。

当发送方陆续收到数据段的ACK确认时,滑动窗口会向右移动,已确认的数据段将从发送缓冲区中释放。同时,新的待发送数据会进入窗口右侧,准备发送。

滑动窗口不仅限定了无需等待确认即可连续发送的数据量,还为TCP的重传机制提供了数据缓存基础:

  1. 窗口内的所有已发送但未确认数据必须保留在发送缓冲区中

  2. 只有窗口左侧已确认的数据才能被安全移除

  3. 一旦某数据段超时未确认,TCP可从窗口左侧开始重传

这种设计确保了在持续高效发送数据的同时,保持了对每个数据段的可靠传输保障。

但滑动窗口不一定会整体右移

其滑动窗口的大小表示意义为发送方不用等待ACK一次所能发送的数据最大量。其整体的大小还是需要对端来决定的。

比方说,按照上面的例子来讲,发送端发送了1001-4000的数据,对端也收到了,并且准备对1001-2000的数据段进行了响应,但是接收端的上方由于某些问题一直不从接收缓冲区中提取数据,这就导致发送端收到了1001-2000的响应,得知对端的窗口从4000变为了3000。此时发送端的滑动窗口就不会进行整体的右移,只会将左端口向右移动1000,右端口不变。

因此滑动窗口的移动并不是一成不变的整体向右移动,而是根据对方的接收能力而时刻发生变化的。

而在内核中,滑动窗口的实现与算法中的滑动窗口实现的方式一样的,同样是双指针实现,start与end。end指针是由start与窗口大小共同决定。

start是根据确认序号指着。而end是由start + win大小。并且滑动窗口在发送缓冲区内并不会越界,因为在此TCP采用了类似环状的算法。

那么如果出现了丢包,如何进行重传? 这里分两种情况讨论。


情况一 :数据包已经抵达,ACK被丢了。

在发送端连续发送多个报文数据时,部分ACK丢包并不要紧,此时可以通过后续的ACK进行确认。

比如图中2001-3000和4001-5000的数据包对应的ACK丢失了,但只要发送端收到了最后5001-6000数据包的响应,此时发送端也就知道2001-3000和4001-5000的数据包实际上被接收端收到了的,因为如果接收方没有收到2001-3000和4001-5000的数据包是无法设置确认序号为6001的,确认序号为6001的含义就是序号为1-6000的字节数据我都收到了,你下一次应该从序号为6001的字节数据开始发送。


情况二: 数据包丢了。

  • 当某一段报文段丢失之后,发送端会一直收到 1001 这样的ACK,就像是在提醒发送端 "我想要的是 1001"一样。
  • 如果发送端主机连续三次收到了同样一个 "1001" 这样的应答,就会将对应的数据 1001 - 2000 重新发送。
  • 这个时候接收端收到了 1001 之后,再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中。

这种机制也叫做"告诉重发控制"(也叫做快重传)--- 其条件就是连续收到3个同样的确认应答。

流量控制

接收端处理数据的能力是有限的,如果发送端速率过快,将可能导致接收缓冲区满溢,进而引发数据包丢失、重传等一系列性能问题。

因此TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control)。TCP 通过**流量控制(Flow Control)**机制,动态调节发送端的传输速率,使其与接收端的实际处理能力相匹配,从而避免因速率不匹配导致的资源浪费和传输效率下降。

  • 接收端通过 TCP 首部中的"窗口大小"字段,在 ACK 报文中告知发送端自己当前可接收的缓冲区容量。该窗口值反映了接收端的即时处理能力:窗口越大,表明接收能力越强,网络吞吐潜力越高。
  • 当接收端发现缓冲区即将占满时,会通过 ACK 报文通知发送端一个更小的窗口值,发送端随之降低发送速率。若缓冲区完全满载,接收端会将窗口设置为零,此时发送端必须暂停发送数据。
  • 为及时获取接收端窗口的变化,发送端会定期发送窗口探测报文(零窗口探测),以便在接收端缓冲区出现空闲时,能立即恢复数据传输。这一机制确保了发送速率始终与接收端的处理能力动态匹配,既避免了缓冲区溢出,也减少了传输延迟。

接收端如何把窗口大小告诉发送端呢? 回忆我们的TCP首部中,有一个16位窗口字段,就是存放了窗口大小信息。回忆一下16位数字最大表示65535,那么TCP窗口最大就是65535字节么?

实际上,还是我们前文提到的扩大因子。TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移M位。

其首次对于如何保证发送数据量合理的问题,其实是在三次握手的时候就保证了,三次握手并不是简单的建立连接,双方还交换了报文,协商了双方的接收能力。其第三次握手还可以携带数据,此就称之为捎带应答。

拥塞控制

尽管TCP通过滑动窗口机制实现了高效可靠的数据传输,但如果在连接建立之初就立即发送大量数据,仍可能加剧网络拥塞、引发性能问题。

网络中存在众多主机与并发连接,初始阶段难以判断当前链路的拥塞程度。若此时贸然注入大量数据,极有可能导致本就繁忙的网络雪上加霜,造成丢包、延迟上升及吞吐量下降。

为此,TCP 引入了 慢启动(Slow Start) 机制:在连接建立后,发送方先以较小数据量试探性地发送,根据确认情况逐步探测当前网络的承载能力,随后动态调整发送速率,实现平稳加速。这种"先探路、后加速"的方式,有效避免了初期突发流量对网络造成的冲击,提升了整体传输的稳健性。

比方说,第一次时发送了由滑动窗口计算得到的规定内的数据,但是由于某些问题发生了大量丢包,此时TCP就认为网络有问题,此处可能怀疑硬件设备出了问题,或者数据量过大,引起了阻塞。此问题就称之为网络拥塞。在此情况下,发送端不能立刻对报文进行超时重传,毕竟在此情况下无论发送多少都是无用,这就类似于堵车。再发也只会造成问题更加严重。所以对此问题下,TCP反而是智能的少发送。

  • 此处引入一个概念程为拥塞窗口。(其为主机判断网络健康的指标,超过其指标就会发生网络拥塞)大小是动态的。
  • 发送开始的时候,定义拥塞窗口大小为1。
  • 每次收到一个ACK应答,拥塞窗口加1。
  • 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口。

像上面这样的拥塞窗口增长速度,是指数级别的。"慢启动" 只是指初使时慢,但是增长速度非常快。

  • 为了不增长的那么快,因此不能使拥塞窗口单纯的加倍。
  • 此处引入一个叫做慢启动的阈值。
  • 当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长。
  • 当TCP开始启动的时候,慢启动阈值等于窗口最大值。
  • 在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1。

少量的丢包,我们仅仅是触发超时重传。大量的丢包,我们就认为网络拥塞。

当TCP通信开始后,网络吞吐量会逐渐上升。随着网络发生拥堵,吞吐量会立刻下降。

拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。TCP拥塞控制这样的过程,就好像热恋的感觉。

但在实际上并不是经常发生网络拥塞,此策略就是防范于未然。但在实际运用上,是每台主机都需要做的,去识别主机是否拥塞的。

延迟应答

如果接收数据的主机收到数据后立即进行ACK应答,此时返回的窗口可能比较小。

  • 假设对方接收端缓冲区剩余空间大小为1M,对方一次收到500K的数据后,如果立即进行ACK应答,此时返回的窗口就是500K。
  • 但实际接收端处理数据的速度很快,10ms之内就将接收缓冲区中500K的数据消费掉了。
  • 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来。
  • 如果接收端稍微等一会再进行ACK应答,比如等待200ms再应答,那么这时返回的窗口大小就是1M。

需要注意的是,延迟应答的目的不是为了保证可靠性,而是留出一点时间让接收缓冲区中的数据尽可能被上层应用层消费掉,此时在进行ACK响应的时候报告的窗口大小就可以更大,从而增大网络吞吐量,进而提高数据的传输效率。

此外,不是所有的数据包都可以延迟应答。

  • 数量限制:每个N个包就应答一次。
  • 时间限制:超过最大延迟时间就应答一次(这个时间不会导致误超时重传)。

延迟应答具体的数量和超时时间,依操作系统不同也有差异,一般N取2,超时时间取200ms。

捎带应答

在延迟应答机制的基础上,我们注意到客户端与服务器在应用层通信时,常常遵循"一问一答"的模式。例如,客户端发送"How are you?",服务器通常会回复"Fine, thank you"。

在这种情况下,ACK 确认报文可以借助服务器响应的数据报文"搭顺风车",与"Fine, thank you"一起返回给客户端。这种捎带确认(Piggybacking ACK)的方式,既减少了纯确认报文的传输数量,也提升了网络带宽的利用效率,进一步优化了 TCP 的整体传输性能。

面向字节流

当创建TCP socket时,内核会为该连接创建发送缓冲区接收缓冲区。因此每个客户端连接都是完全独立的,拥有独立的管理结构和缓冲区。

发送数据时

调用write()将数据放入发送缓冲区后函数即可返回,TCP会在后台自动发送数据。数据可能被拆分或合并后发送。

接收数据时

数据到达网卡后进入接收缓冲区,调用read()从中读取数据,可按任意字节数读取。

特点

  • 写入100字节可以一次write(100),也可以100次write(1)

  • 读取时同样灵活,不受写入方式限制

  • TCP只负责可靠传输字节流,不关心数据内容,由上层应用解析数据含义

这就是面向字节流:TCP把数据看作无结构的字节序列,保证可靠传输,而数据含义由应用层解释。

粘包问题

什么是粘包?

  • 首先要明确,粘包问题中的"包",是指的应用层的数据包。
  • 在TCP的协议头中,没有如同UDP一样的"报文长度"这样的字段。
  • 站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中。
  • 但站在应用层的角度,看到的只是一串连续的字节数据。
  • 那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。

所以在用户角度来说,对于报文的处理必须是一个一个来的,将字节流变为一个一个完整的请求。

如何解决粘包问题

归根结底就是一句话,明确两个包之间的边界。

  • 对于定长的包,保证每次都按固定大小读取即可。
  • 对于变长的包,可以在报头的位置,约定一个包总长度的字段,从而就知道了包的结束位置。比如HTTP报头当中就包含Content-Length属性,表示正文的长度。
  • 对于变长的包,还可以在包和包之间使用明确的分隔符。因为应用层协议是程序员自己来定的,只要保证分隔符不和正文冲突即可。

UDP是否存在粘包问题?

  • 对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在,同时,UDP是一个一个把数据交付给应用层的,有很明确的数据边界。
  • 站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出现"半个"的情况。

因此UDP是不存在粘包问题的,根本原因就是UDP报头当中的16位UDP长度记录的UDP报文的长度,因此UDP在底层的时候就把报文和报文之间的边界明确了,而TCP存在粘包问题就是因为TCP是面向字节流的,TCP报文之间没有明确的边界。

TCP异常情况

进程终止

当客户端正常访问服务器时,如果客户端进程突然崩溃了,此时建立好的连接会怎么样?

当一个进程退出时,该进程曾经打开的文件描述符都会自动关闭,因此当客户端进程退出时,相当于自动调用了close函数关闭了对应的文件描述符,此时双方操作系统在底层会正常完成四次挥手,然后释放对应的连接资源。也就是说,进程终止时会释放文件描述符,TCP底层仍然可以发送FIN,和进程正常退出没有区别。

机器重启

当客户端正常访问服务器时,如果将客户端主机重启,此时建立好的连接会怎么样?

当我们选择重启主机时,操作系统会先杀掉所有进程然后再进行关机重启,因此机器重启和进程终止的情况是一样的,此时双方操作系统也会正常完成四次挥手,然后释放对应的连接资源。

机器掉电/网线断开

此时接收端认为连接还在, 一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset. 即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在。如果对方不在,也会把连接释放。

其中服务器定期询问客户端的存在状态的做法,叫做基于保活定时器的一种心跳机制,是由TCP实现的。此外,应用层的某些协议,也有一些类似的检测机制,例如基于长连接的HTTP,也会定期检测对方的存在状态。

TCP小结

TCP协议这么复杂就是因为TCP既要保证可靠性,同时又尽可能的提高性能。

可靠性:

  • 检验和。
  • 序列号。(按序到达)
  • 确认应答。--- 核心
  • 超时重传。
  • 连接管理。
  • 流量控制。(可靠性为主,在一定程度上也提高了效率)
  • 拥塞控制。(作用于两端机器上,TCP为其考虑了网络)

提高性能:

  • 滑动窗口。
  • 快速重传。
  • 延迟应答。
  • 捎带应答。

TCP定时器

此外,TCP当中还设置了各种定时器。

  • 重传定时器:为了控制丢失的报文段或丢弃的报文段,也就是对报文段确认的等待时间。
  • 坚持定时器:专门为对方零窗口通知而设立的,也就是向对方发送窗口探测的时间间隔。
  • 保活定时器:为了检查空闲连接的存在状态,也就是向对方发送探查报文的时间间隔。
  • TIME_WAIT定时器:双方在四次挥手后,主动断开连接的一方需要等待的时长。

基于TCP的应用层协议

常见的基于TCP的应用层协议如下:

  • HTTP(超文本传输协议)。
  • HTTPS(安全数据传输协议)。
  • SSH(安全外壳协议)。
  • Telnet(远程终端协议)。
  • FTP(文件传输协议)。
  • SMTP(电子邮件传输协议)。

当然,也包括你自己写TCP程序时自定义的应用层协议。

相关推荐
pixcarp2 小时前
Golang web工作原理详解
开发语言·后端·学习·http·golang·web
Elieal2 小时前
零基础入门 WebSocket:从原理到 Java 实战
java·websocket·网络协议
芯有所享2 小时前
【芯片设计中的ARM CoreSight IP:全方位调试与追踪解决方案】
arm开发·经验分享·网络协议·tcp/ip
终端域名2 小时前
如何保障网络架构变革下物联网设备的安全?
网络·物联网·架构·区块链
小宇的天下2 小时前
Calibre nmDRC-H 层级化 DRC
java·服务器·前端
灰鲸广告联盟2 小时前
APP广告变现数据分析:关键指标与优化策略
大数据·网络·数据分析
万叶学编程2 小时前
Navicat连接Linux主机(MySQL)失败
linux·运维·服务器
多多*2 小时前
程序设计工作室1月21日内部训练赛
java·开发语言·网络·jvm·tcp/ip
Coder个人博客2 小时前
Linux6.19-ARM64 crypto NH-Poly1305 NEON子模块深入分析
linux·网络·算法·车载系统·系统架构·系统安全·鸿蒙系统