当前位置: 首页 > news >正文

【Linux网路编程】传输层协议-----TCP协议

文章目录

  • 一、为什么叫传输控制协议:
  • 二、TCP的报头全解析:
    • 1、整体报文:
    • 2、端口号与校验和
    • 3、四位首部长度
    • 4、十六位窗口大小:
      • 前置知识——流量控制:
      • 前置知识——确认应答机制:
      • 十六位窗口大小:
    • 5、三十二位序号和三十二位确认序号:
      • 前置知识——捎带应答:
      • 前置知识——超时重传:
      • 32位序号:
      • 32位确认序号:
      • 为什么要有两个序号:
    • 6、六个标志位:
      • 为什么要有标志位:
      • ACK:
      • SYN与FIN:
      • PSH:
      • RST:
      • URG & 16为紧急指针:
  • 三、连接管理机制:
    • 1、理解:
      • 为什么握手是三次,挥手是四次?
      • 握手能不能不经过三次:
    • 2、TCP状态介绍:
      • 三次握手的状态:
      • 四次挥手的状态:
      • 全连接与半连接:
      • listen()系统调用的第二个参数backlog:
      • 深入理解TIME_WAIT状态:
  • 四、补充知识:
    • 1、流量控制:
    • 2、滑动窗口:
      • 发送缓冲区结构解析:
      • 向右移动时的窗口变化:
      • 滑动窗口会越界吗
      • 如果丢包了,如何理解滑动窗口
    • 3、拥塞控制:
      • 拥塞窗口:
      • 慢启动:
    • 4、面向字节流:
    • 5、粘包问题:
    • 6、连接异常:

一、为什么叫传输控制协议:

TCP(Transmission Control Protocol)被称为传输控制协议,这一名称源于其核心功能------在计算机网络中对数据传输过程进行精确、可靠的控制

在这里插入图片描述
在TCP中和UDP协议不同的是,TCP具有两个缓冲区,分别是发送缓冲区和接收缓冲区,这个就是我们socket套接字编程时那个sockfd对应的文件缓冲区,在写代码的时候,在上层定义的数组比如说char* buffer[1024]这个就是上述的用户级缓冲区

注意:
我们平时使用的write,read,recv,send这些函数并不是发送函数,而是拷贝函数,将数据从用户级缓冲区拷贝到TCP中的发送缓冲区中,之后就不归我们管了,接下来什么时候发送,发送多少,发送出错了怎么办这些都由TCP决定,这里也就体现了TCP的控制特性
又因为TCP是在传输层进行传输的,所以TCP也具有传输特性

对于TCP的传输实际上是在网络上进行拷贝的,这也是拷贝,只不过这个拷贝是在网络中进行通信的,那么就会有可能会出现错误,所以也就会有许多对应的策略
在这里插入图片描述
当在接收缓冲区中收到数据后,上层如果有read之类的读取数据,那么TCP会在传输层进行报文和有效载荷之间的分离,然后通过端口号传输到对应的进程中

在TCP协议中,各个都是平等的,不存在像应用层中的http/https中存在请求方和响应方

TCP协议和UDP协议一样都是全双工的,指通信双方可以同时双向地发送和接收数据

总的来说:
TCP是进行全双工,具有发送接收缓冲区的,进行数据发送控制的一种协议

二、TCP的报头全解析:

1、整体报文:

在这里插入图片描述

2、端口号与校验和

学习一个协议,我们首先要解决的问题是:报头和有效载荷如何分离,有效载荷应该交付给那一个上层协议
关于报头和有效载荷如何分离,在下面四位首部长度有分析,那么有效载荷应该交付给哪一个上层进程协议,这个就由16位源端口号和16位目的端口号来决定,具体和UDP基本一样,需要理解可前往上一章进行了解

关于16位校验和,这和UDP的校验和作用也是一样的,在TCP中就是检查TCP内容在传输过程中有没有出现偏差

3、四位首部长度

那么首部长度就是解决报头和有效载荷如何分离的关键:
首部长度表示的是报头大小+选项大小

4位首部长度用二进制表示范围是[0000,1111]看着像是从0~15字节,但是这样问题就来了,光光TCP的报头就已经是20字节了,那么连报头都表示不了,所以这是不可能的,因为首部长度是有单位的,它的单位是4字节
所以准确来说:首部长度表示的范围是0~60字节,报头是20字节,选项最多就是40字节

在这里4位的首部长度在计算的时候是有单位4字节的,比如选项的大小是12字节的话,那么首部长度应该就是Yx4 = 20+12—> Y = 8 —>1000,那么如果选项的大小不是4字节的倍数呢?听说就会是4字节对齐的,比如说选项的大小是2字节,那么首部长度就是X*4 = 20 + 2, —>X = 5.5,如果这样的话,那么就不能够用4位首部长度表示了,所以实际上X应该就会进行对齐成6,那么就可以用4位首部长度表示了。所以在读取大小的时候就会读6字节,比实际大小多一点,因为要4字节对齐

这里这个四位首部长度,我们也可以叫他自描述字段,所以报头和有效载荷如何分离的方法就是:固定长度 + 自描述字段

报头和有效载荷如何分离
假设一个 TCP 报文段总长度为 100 字节,首部长度字段值为 5(即 20 字节首部)
前20字节是TCP报头(包含源端口、目的端口、序号、确认号等控制信息)
从第 21 字节到第 100 字节的 80 字节内容,就是需要传递给应用层的有效载荷

通过这种机制,接收方能够准确区分报头和数据,确保控制信息被正确解析,而有效载荷被完整地传递给上层应用

4、十六位窗口大小:

前置知识——流量控制:

什么是流量控制:
TCP流量控制是TCP协议确保数据可靠传输的核心机制之一,其目的是防止发送方发送数据的速率过快,导致接收方缓冲区溢出,从而避免数据丢失。它通过接收方主动告知发送方可接收的数据量,动态调节发送速率,实现发送方与接收方之间的速率匹配

如下是客户端和服务端之间的报文通信交互

在这里插入图片描述
双方在进行通信的时候都需要完整的报头,当客户端发送消息进行http请求的时候,需要添加完整的报头,如果有选项也要增加,然后将发送的消息作为数据,将这些进行封装成完整的报头+数据,将这个从自己的发送缓冲区发送给对方端的接收缓冲区,然后对方通过read之类的函数从接收缓冲区进行读取
因为服务端是平等的,以上操作服务端发送给客户端是如此,客户端发送给服务端也是如此

但是这样存在一个问题:假如是从客户端发送给服务端,客户端一直发发发,服务端的上层慢慢读取或者不读取,那么服务端的缓冲区就会逐渐被装满,如果客户端继续向服务端发送消息,那么是不是数据就会写不进去,也就会发生大面积丢包问题,这是不可靠的一种表现,
所以TCP有对应的解决方式:接收方根据自身处理能力(如接收缓冲区的空闲容量),限制发送方的发送速率,当客户端接收到服务端的报文,然后看到服务端的接收缓冲区的大小不足时,就会限制自己发送请求的速度

注意:一般情况下是不会出现丢包情况的,当服务端的接收缓冲区快接收不了的时候,就会“告诉”客户端,让客户端发送消息的速率慢下来

对于发送方来说:发送速率由对方的缓冲区中剩余的大小决定

前置知识——确认应答机制:

什么是确认应答机制:
确认应答机制是TCP等可靠传输协议中确保数据正确、完整送达的核心机制之一。其核心思想是:接收方收到数据后,必须向发送方返回一个“确认报文”(ACK),告知对方 “已成功接收数据”;若发送方未在规定时间内收到确认,则认为数据丢失,会重新发送该数据

生活中的例子:
当我们在和别人说话的时候,当我们对别人说:“吃了吗”,此时就是给别人一个请求,想要知道他有没有吃,然后想要收到的就是别人的回答:“吃了或者没吃”,这样就是一种确认应答机制

回到客户服务端:
当客户端向服务端发送请求的时候,服务端都需要对客户端的请求进行回答,这样就能够保证客户端到服务端方向上的可靠性,同样的,服务端向客户端发送请求的时候,客户端对服务端进行回答,这样也就能够保证服务端到客户端之间的可靠性了

确认应答的局部可靠性:
当服务端给客户端进行应答的时候,我们是无法对服务端保证这个应答被客户端收到了,那么是不是客户端需要向服务端也给一个应答说“我收到你的应答了”,但是这样我们又无法保证这个应答被服务端收到了,那么是不是服务端此时要给客户端回应答呢…这显然是不可能的,因为这样就无限套娃了

所以我们是只能够保证最后一条应答之前的消息可靠性,也就是只能够保证聚不上的消息可靠性

十六位窗口大小:

TCP的16位窗口大小字段是流量控制的基础,通过传递接收方缓冲区容量限制发送方数据量

根据前置知识,我们了解了客户端发送数据的速率,也就是流量控制,是根据对方的接收缓冲区中所剩余的空间大小来决定的,但是如果对于客户端的请求服务端一直不响应,那么客户端怎么知道对方接受缓冲区中还剩多少呢?这就归功于确认应答机制了,当客户端给服务端发送请求,尽管服务端不想给客户端发送消息,但是必须给客户端一个确认应答,这个确认应答中最起码有最基本的报头,里面有16位窗口大小,所以,这个16位窗口大小,填充的就是确认方的接收缓冲区的剩余空间的大小

5、三十二位序号和三十二位确认序号:

前置知识——捎带应答:

什么是捎带应答:
捎带应答(Piggybacking ACK)是TCP协议中一种优化通信效率的机制,其核心思想是:当接收方需要向发送方发送数据时,将确认报文(ACK)“附加” 在该数据报文上一起发送,而非单独发送 ACK 报文,从而减少网络中报文的数量,降低额外开销

简单来说就是当客户端或者服务端需要进行应答并且也想向对方发送数据的时候,将这两者合并为一次发送,这就叫做捎带应答
在这里插入图片描述
这就类似于在生活中:
我—>朋友:吃了吗?
朋友—>我:吃了
朋友—>我:食物是饺子

上述可以优化为:
我—>朋友:吃了吗?
朋友—>我:吃了饺子

将“吃了”,“食物是饺子”融合成一句,提高交流效率

捎带应答原理与场景:
在TCP通信中,双方可能同时向对方发送数据(全双工)
例如:
客户端向服务器发送请求数据(报文 A)
服务器收到后,需要返回确认(ACK),同时可能还要向客户端发送响应数据(报文 B)。
此时,服务器可以将 ACK 信息直接包含在报文 B 中,无需单独发送一个 ACK 报文。这种“借道”数据报文传递确认的方式,就是捎带应答

前置知识——超时重传:

什么是超时重传:
超时重传是 TCP 协议保证数据可靠传输的核心机制之一,用于解决数据在传输过程中可能丢失的问题。其核心逻辑是:发送方在发送数据后启动一个计时器,若在规定时间内未收到接收方的确认应答(ACK),则认为数据已丢失,会重新发送该数据

在这里插入图片描述
像上述这样,如果一方迟迟没有等到对方的应答,那么不会傻傻一直等待,等到计时器时间到了就会认为请求丢失,此时就会重新发送请求,这就是超时重传

关于超时重传有两种丢包情况
一种是发送的数据报文丢失了,此时发送端在一定时间内收不到对应的响应报文,就会进行超时重传
另一种情况是对方发来的响应报文丢包了,此时发送端也会因为收不到对应的响应报文,而进行超时重传
在这里插入图片描述
对于这两种情况来说,都会进行重新发送请求,对于第一种情况,发送的数据丢失理所应当地会进行重发,但是对于第二种情况,如果进行重发是不是就导致同一个数据被多次发送,就会导致服务端接收重复问题,但是不用担心,接收方可以根据报头当中的32位序号来判断曾经是否收到过这个报文,从而达到报文去重的目的

注意:
当发送缓冲区当中的数据被发送出去后,操作系统不会立即将该数据从发送缓冲区当中删除或覆盖,而会让其保留在发送缓冲区当中,以免需要进行超时重传,直到收到该数据的响应报文后,发送缓冲区中的这部分数据才可以被删除或覆盖

超时重传的等待时间:
关于超时重传的等待时间
不能过短,如果短的话,比如说50ms未响应就进行重传,那么如果两者通信时间是100ms,那么就会一直重传,出现大面积的重传问题
也不能过长,如果太长的话,会导致丢包后对方长时间收不到对应的数据,进而影响整体重传的效率

所以这个等待时间设置成动态的,随着网路变化的更好

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

32位序号:

什么是32位序号:
在 TCP中,32 位序号是保证数据可靠传输、有序交付的核心机制之一,用于标识 TCP 报文段中数据字节的位置,解决数据乱序、重复和丢失问题

对于确认应答机制并不是一个发送一个应答这样串行发送的,因为这样的效率非常低下,取代而之的是并行——也就是一次发送一批消息,然后服务器依次进行应答

在这里插入图片描述
但是客户端发送了许多消息,服务端是不是依次按照顺序进行接收的呢?

这就好比我们和朋友去旅游,假如是依次前往目的地的,那么是不是先出发人就一定先到呢?这是不一定的,有可能先出发的人驾车,后出发的人坐飞机,所以达到的顺序是不一定的

所以回到我们的网络来:
服务端或者客户端发送的数据,对方是不一定按照顺序收到的,这就导致了数据包的乱序问题,这是一种不可靠的表现
所以就需要解决方法:
这就引入了32位序号来进行解决这个数据包乱序问题,TCP将发送出去的每个字节数据都进行了编号,这个编号叫做序列号

这个32位序号,我们现在理解为是TCP发送缓冲区里面的数据最后一个字节的数组下标

在这里插入图片描述

32位确认序号:

32位确认序号用于向发送方反馈数据的接收状态

定义:确认序号是TCP报文头中的一个32位字段,其值表示接收方期望接收的下一个字节的序号
简单来说就是:通过32位确认序号是告诉对端,我当前已经收到了哪些数据,你的数据下一次应该从哪里开始发

例如:若接收方已成功接收字节流中序号为1~100的所有字节,则会在返回的确认报文中将确认序号设为101,告知发送方:请从序号101开始发送后续数据

确认序号怎么定义的:当收到序号后,将这个序号+1得到的就是确认序号

确认序号的意义:表示确认序号之前的数据已经全部收到了

这样做的意义是什么?
这样能够允许有少量的应答丢失,比如说客户端向服务端发送数据:1000,2000,3000,得到的确认应答只有3001,那么即使没有收到1001和2001,我们也能够保证序号为1000和2000的数据被对方收到了

为什么要有两个序号:

这里通过两个场景来进行理解:

场景一:
客户端给服务器发消息,服务器要给我应答,服务器应答,可能只是应答,也可能是指捎带应答。应答就是纯报头,只是用32位确认序号,捎带应答就是要有数据,并且捎带应答因为也有数据,所以就要有32位序号。也就是说,因为一个报文,他可能有双重身份,既可能是应答,也可能是带有数据,所以序号和确认序号有可能同时存在,所以协议里面必须把这两个分开,不能复用。

场景二:
我们实际在通信的时候客户端给服务器发消息,那么服务器也可能再给客户端发消息,双方地位是对等的,因为地位是对等的,所以这种捎带应答的互发消息的情况是很常见的,所以我们客户端给服务器发消息的同时,服务器也在给客户端发消息
所以序号的问题还是分开好,那么正是因为如此呢,我们就有了序号和确认序号两组序号

6、六个标志位:

在这里插入图片描述
在TCP报头中,有6个标志位,一个标志位的大小是一个比特位,其中0表示当前标志位没有被标记,1表示当前标志位被标记

为什么要有标志位:

在网络编程中,一般来说服务端:客户端是1:n的,所以服务端会被多个客户端进行TCP进行请求,并且每个请求有不同的类型,比如在TCP通信的时候需要进行连接(三次握手),在最后也要进行断开连接的通信(四次挥手),并且也有在通信中的正常数据,所以客户端和服务端之间进行通信是有不同的类型的,也就是说TCP收到的报文一定是有各种类型的,这些不同的类型决定了接收方需要有不同的动作,那么问题来啦,接收方怎么知道报头的类型是什么呢?这就归功于6个标志位了

所以标志位存在的意义是:区分TCP报文的类型

ACK:

ACK标志位用于确认已接收到对方发送的数据,告知发送方:我已成功接收了某部分数据,请继续发送后续数据(或确认连接状态)

一般除了第一个请求是将ACK置为0了,其余应该都是将TCP置为1,因为发送给对方的数据本身就有对上一条的数据进行确认的能力

SYN与FIN:

SYN:用于连接的建立过程,在三次握手会被设置
FIN:用于连接的关闭过程,在四次挥手会被设置
上述二者是TCP实现面向连接特性的核心机制

只有在连接建立阶段,SYN才被设置,正常通信时SYN不会被设置
只有在断开连接建立阶段,FIN才被设置,正常通信时FIN不会被设置

PSH:

作用是:要求接收方立即将接收到的数据提交给应用层处理,而不是等待缓冲区填满后再提交

TCP 在传输数据时,发送方和接收方通常会使用缓冲区来临时存储数据,以提高传输效率
发送方缓冲区:暂时存放应用层交付的数据,积累到一定量后再封装成TCP报文发送
接收方缓冲区:暂时存放接收到的 TCP 数据,积累到一定量或等待超时后,再一次性提交给应用层

主要用于需要实时交互或紧急处理的场景:
当发送方不断地向接收方进行数据发送,接收方的上层却迟迟不读取,那么接收方的缓冲区是不是就会被渐渐打满,那么发送方就会被阻塞,所以就有了PSH这个标志位,催促接收端快点将缓冲区中的数据读上去

总的来说:PSH作用是确保数据快速提交给应用层,适用于对交互及时性要求较高的场景

RST:

我们知道TCP协议是面向连接的,所以在C/S之间需要建立连接,那么是不是只要申请了建立连接就一定连接成功呢?——这是不一定的,TCP确实需要保证可靠性,但是TCP也允许连接建立失败,之后在重新建立连接即可

如何理解连接:
C/S建立连接本质上是在内核中对连接创建一个结构体,这个结构体中有这些连接对应的属性:链接什么时候建立、客户端或者服务端的端口号等等,我们之前又了解一个服务端是对应多个客户端的,那么在服务端的内核中是不是就有许多连接的结构体呢?如果是,服务端要不要对这些连接做管理呢?——既然要管理,怎么进行管理呢?当然是先描述再组织,我们已经通过结构体进行描述起来了,那么将这一个个结构体连接起来形成一个链表,那么对连接的管理就成了对结构体的增删查改

这里的三次握手是存在成本问题的(在后面的连接管理机制会详细讲的)

在这里插入图片描述
关于上述是三次握手,在后面会详细讲的,
在这里我们思考:既然是三次握手,那么对于客户端来说,三次握手是将第三个ACK发送出去就证明三次握手成功还是等到第三次ACK被服务端收到才证明握手成功呢?
答案是第三次ACK一发送就证明三次握手成功,是不会得到ACK应答的
所以:TCP三次握手本质上是在赌,赌第三次发送的ACK服务端能够被对方收到,毕竟前两次的连接是能够通过确认应答保证可靠的,不能够保证第三次是不是可靠的

所以如果第三次ACK丢失了,那么服务端就没有收到第三次握手,那么服务端就不会在内核中开辟空间,进而建立连接,所以此时就导致客户端认为建立连接成功,服务端认为还没有建立好连接,就会导致二者认知不一致问题
此时客户端是认为连接成功的,之后就向服务端发送数据,但是服务端却认为连接还未建立,就会在确认应答发送过去的报头中将RST标志位置为1,然后C和S重新建立连接,重新进行三次握手

所以RST标志位是在双方连接不一致,然后连接建立失败或者异常的情况下重新进行连接的

URG & 16为紧急指针:

TCP是可靠的,也就证明TCP通信中是讲究先来后到的,数据是一定被对方有序地进行接收的,但是如果想进行插队,是可以的,将URG设置成1即可

当URG设置成1后,表示报头中1位紧急指针是有效的,紧急指针表示数据中带外数据的偏移量,一般带外数据的大小是1字节,所以偏移量+数据大小进行找带外数据中的数据大小是1字节

那么使用场景呢?
当服务端很卡的时候,会有多种卡顿的方式,IO数据过大,有很多响应等等,那么当客户端想要知道服务端是什么类型的卡顿呢,或者是有没有挂掉呢?这个时候就会发送一个紧急数据,这个外带数据会被服务端紧急读取,(所以服务端需要有支持读取外带数据的功能+卡顿状态编号),当服务端读上来后,就知道客户端需要知道服务端的状态,那么就会返回对应的编号给客户端了

想一想如果不能进行插队,那么客户端的请求服务端的状态就会在最后面,那么服务端就会处理完当前数据在处理这个状态请求,这样显然是不合理的

三、连接管理机制:

1、理解:

TCP是面向连接的,在进行通信之前需要建立连接,也就是需要进行三次握手

流程:
首先客户端进行连接请求,发送SYN的报文给服务端,然后服务端进行接收,进行确认应答并且也建立请求,返回SYN+ACK的报文,最后客户端确认应答ACK
这就是粗略地进行三次握手
对于上述的客户端,只要发送最后一个ACK就认为建立连接成功,对于服务端,需要收到最后一个ACK连接才认为建立连接成功

注意:客户端向服务端发送请求的时候,是建立的客户端到服务端的通信,由于TCP是全双工的,那么也需要建立服务端到客户端之间的通信,所以服务端在接收到客户端的请求的时候,服务端也需要向客户端发送建立连接的请求

在上层编码的时候:
客户端进行连接的就是connect系统调用,由connect创建一个将SYN置为1了的报文发送给服务端,是由connect进行三次握手的,当发生请求之后,connect就会阻塞,直到服务端确认应答后才会返回
服务端的accept是不参与连接的,将底层已经建立好的数据拿上来的

当建立好连接之后就进行正常通信,写的时候通过系统调用接口将数据从用户缓冲区拷贝到内核中的发送缓冲区,然后TCP自主进行发送,这本质上是将发送方的发送缓冲区的数据拷贝到对方的接收缓冲区中,然后由对方的上层进行读取

最后是进行四次挥手,断开连接
首先是客户端向服务端发送将FIN标志位置为1的报头表示想要将客户端方向到服务端方向上的连接断开,然后服务端进行确认应答,发送回ACK,这个时候就能够保证客户端到服务端的方向上连接断开,就证明客户端没有消息发送给服务端了,但是服务端依然可以发送数据给客户端,等到服务端也没有消息发给客户端了,然后服务端就发送将FIN标志位置为1的报头,表示想要将服务端方向到客户端方向上的连接断开,然后客户端就进行确认应答,之后二者状态改变,这样就能够完全将连接断开了,这就是四次挥手

在这里插入图片描述

为什么握手是三次,挥手是四次?

实际上握手挥手都是四次,只不过握手的四次中有两次进行了捎带应答,然后将两次合成了一次,就成了三次握手
为什么可以合成呢?
因为通信是要双方都进行连接的,所以当客户端给服务端发送连接请求的时候,服务端也想给客户端发送连接请求,那么就会在确认应答中在ACK的基础上也将SYN置为1,这样就既能够应答,又能够给服务端发送连接请求,等待回应即可

那么为什么挥手是四次,不能合成呢?
因为客户端向服务端发送断开连接请求的时候,是不能够保证此时服务端也想与客户端断开连接的,毕竟服务端可能还有数据要发送给客户端,所以这两个请求之间可能是有空窗的,所以是不能够保证在客户端想要断开连接的时候服务端也想断开连接,所以挥手是四次

握手能不能不经过三次:

我们在前面学习了解到了如下结论:
TCP三次握手本质上是在赌,赌第三次发送的ACK服务端能够被对方收到,毕竟前两次的连接是能够通过确认应答保证可靠的,不能够保证第三次是不是可靠的

既然连接的建立都不是百分之百成功的,因此建立连接时具体采用几次握手的依据,实际是看几次握手时的优点更多

能不能一次握手呢?

如果进行一次握手,也就是说客户端只要发送给服务端一个请求,就证明客户端到服务端方向上的连接已经建立成功了,并且只要服务端接收到了客户端发来的连接请求,就在内核开辟一块空间用来描述连接。
如果这样的话,那么多个客户端恶意请求服务端,那么服务端就需要一直消耗内存空间来维护连接,这样最后会导致服务端卡顿甚至挂掉,这被称为SYN洪水
在这里插入图片描述

能不能两次握手

两次握手和一次握手是一样的,当客户端向服务端发起请求,服务端接收后给客户端进行确认应答,对于服务端来说:收到客户端的请求是第一次握手,进行确认应答是第二次握手,那么当服务端进行确认应答后,就会在内核中开辟空间来维护连接,那么此时无论客户端是否收到应答,都不停地进行连接请求,那么也会发生SYN洪水问题,因为客户端不需要开辟空间维护连接,所以攻击成本也很小

其本质是因为服务端会在客户端之前建立连接,所以导致当次连接建立失败后的成本被服务端承担

在这里插入图片描述

三次握手可行性:

如果是三次握手,如果连接建立失败,假如是第一次握手或者第二次握手失败,那么问题不大,毕竟有ACK确认应答的存在,并且此时服务端也不会建立连接,成本不是很高,如果是第三次握手失败,也就是说当客户端给服务端发起ACK确认的时候,服务端并没有接收到ACK,但是此时客户端已经认为三次握手成功,在内核中开辟好了空间,维护好了资源,所以此时连接建立失败的成本就转移到了客户端

当然还有,如果是一次二次握手是无法验证全双工通信能力:
第一次握手后,服务器仅确认客户端能发送,自己能接收
第二次握手后,客户端仅确认服务器能发送,自己能接收

三次握手确保双方的初始序列号都能被确认接收
两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收

其核心原因是两次握手无法完成双向通信链路的完整确认,而 TCP 的全双工特性依赖于通信双方对彼此“发送——接收”能力的双向验证

采取三次握手的理由:

  • 能够保证双方通信的可靠,验证全双工
  • 奇数次的握手,可以确保一般情况下握手失败的连接成本是客户端承担的
  • 三次握手是验证双方通信信道的最小次数,能够让能建立的连接尽快建立起来

拓展了解:ddos攻击

三次握手并不是百分百安全的,比如说大量主机同时发送TCP请求给服务端,这是正常的请求,但是架不住数量多,也会令服务端崩溃,这些让服务端崩溃的客户端叫做肉机

此时如果还有正常的客户端请求服务,服务端就不会提供服务了,毕竟没有资源创建连接了,这种攻击手段就是ddos攻击

2、TCP状态介绍:

在这里插入图片描述

三次握手的状态:

LISTEN

服务端会有的状态
监听状态,当服务端上层调用listen()系统调用的时候,此时将服务端设置成监听状态
这个时候服务端等待客户端发来SYN请求后进入下一个状态

SYN_SEND

客户端会有的状态
当客户端上层进行connect()系统调用的时候,客户端向服务端发送SYN报文,此时客户端进入SYN_SEND状态
此时就阻塞等待,直到服务端进行SYN+ACK应答后进入下一个状态

SYN_RECV

服务端会有的状态
当服务端在LISTEN状态下收到客户端的SYN连接请求之后,也就是收到SYN报文之后,从LISTEN状态变为SYN_RECV状态
之后等待客户端的ACK报文

ESTABLISHED:

客户端和服务端都会有的状态
三次握手完成,TCP 连接正式建立,双方可双向传输数据

客户端:SYN_SENT状态下收到SYN+ACK,发送ACK后进入
服务器端:SYN_RCVD状态下收到ACK后进入

四次挥手的状态:

FIN_WAIT_1

主动关闭方所处的状态(客户端服务端均可)
主动关闭方在ESTABLISHED状态下发送FIN后进入此状态
理解:主动关闭方向对方发送FIN报文(请求关闭自己方向的数据流)后,等待对方的ACK报文(确认收到 FIN)

FIN_WAIT_2

主动关闭方所处的状态(客户端服务端均可)
FIN_WAIT_1 状态下收到对方的 ACK 后进入
理解:主动关闭方已收到对方对自己 FIN 的 ACK(确认自己的数据流已关闭),但对方的数据流仍可能传输数据,需等待对方发送 FIN(关闭其方向的数据流)

CLOSE_WAIT

被动关闭方所处的状态(客户端服务端均可)
被动关闭方在 ESTABLISHED 状态下收到 FIN,发送 ACK 后进入
理解:被动关闭方收到对方的 FIN 报文(对方请求关闭其方向的数据流),已发送 ACK 确认,但应用层尚未调用close()(未准备好关闭自己方向的数据流),需等待应用层处理完成

LAST_ACK

被动关闭方所处的状态(客户端服务端均可)
被动关闭方在 CLOSE_WAIT 状态下发送 FIN 后进入
理解:被动关闭方应用层已调用close(),发送 FIN 报文(关闭自己方向的数据流)后,等待对方的 ACK 报文(确认收到 FIN)

TIME_WAIT

主动关闭方所处的状态(客户端服务端均可)
FIN_WAIT_2 状态下收到对方的 FIN,发送 ACK 后进入
理解:主动关闭方已收到对方的 FIN 并发送 ACK(确认对方的数据流已关闭),但需等待2MSL(报文最大生存时间) 后再关闭连接,避免网络中残留的旧报文干扰新连接

CLOSED

客户端服务端都会进入

主动关闭方:TIME_WAIT 状态等待 2MSL 后进入
被动关闭方:LAST_ACK 状态下收到 ACK 后进入
初始状态:客户端未发起连接、服务器未启动监听时的状态

理解:TCP 连接的初始状态和最终状态,连接完全关闭,无任何资源占用

全连接与半连接:

  • 半连接:TCP 三次握手未完全完成,仅实现单向可达确认的过渡状态。此时一方知道对方能收自己的数据包,但另一方不确定自己能收对方的数据包,未形成完整通信链路
  • 全连接:TCP 三次握手完全完成,实现双向可达确认的稳定状态。通信双方(客户端与服务器)均明确对方能收自己的数据包,自己也能收对方的数据包,可正式传输应用层数据
  • 半连接形成:三次握手的前两步:客户端发起请求——>服务端确认请求,客户端并未对服务端进行最终确认,此时是半连接

  • 全连接形成:当三次握手完全完成,服务端和客户端需要维护连接的结构体,一个连接就需要维护一个结构体,那么就需要对结构体做管理,先通过结构体描述连接,然后通过队列将这些结构体组织起来,这个队列就叫做全连接,那么accept就是从这个队列中拿上来连接,然后进行通信

半连接队列:SYN_RECV状态,被称为半链接,此时服务端也有维护这个连接状态,这就是半连接队列,这个连接队列不会长时间维护,并且成本不像全连接队列那样高,长度我们这里也不关心

全连接队列:当三次握手成功后,服务端会将一个个连接通过队列的方式维护起来,这个队列就叫做全连接队列,全连接队列是有长度的,就是listen()系统调用的第二个参数backlog

对比维度半连接全连接
握手完成度仅完成三次握手的前两步(SYN → SYN+ACK)完成三次握手的全部三步(SYN → SYN+ACK → ACK)
双方状态服务器:SYN_RCVD;客户端:SYN_SENT(或未响应)客户端:ESTABLISHED;服务器:ESTABLISHED
通信能力确认仅单向确认(服务器知 “客户端能发”,客户端未知)双向确认(双方均知 对方能发 + 自己能收)
数据传输能力不可传输(链路未稳定,无序列号同步保障)可传输(序列号已同步,支持可靠传输)
关联队列(服务器侧)暂存于 半连接队列暂存于 全连接队列
核心作用过渡状态,为全连接做准备稳定状态,支撑实际业务通信

在学校抢课的时候,经常会出现一直刷新刷不进去,这是因为全连接连接被打满了,三次握手没成功无法进行通信,甚至有时候半连接队列都被打满了,此时服务端就会繁忙,但是服务端并未挂掉,而是队列被打满了,我们一直刷新刷新,有时候运气好就会连接成功,然后就会进行通信,这就是运气好正好挤进去全连接队列里了,并且被上层accept拿到成功通信

listen()系统调用的第二个参数backlog:

这就是全连接队列的最大长度,当全连接队列满后,服务端对于客户端的握手请求就不会进行回应了,此时就会维护半连接,处于SYN_RECV状态

既然全连接队列满了后面握手请求就不会发生了,那么能不能把全连接队列长度设置得很长呢?
当服务端上层非常忙的时候,全连接队列的连接来不及被上层处理,就会一直维护在全连接队列中,这是会消耗资源的,并且维护的连接也没有创造什么价值,也就是单纯占着空间不创造价值,所以不能把队列长度设置得太长

那么能不能设置得很短甚至没有呢?
这也是不行的,如果没有维护或者很短,那么当上层突然很闲的时候,如果不通过全连接队列将连接请求留住,那么如果此时却正好没有客户端进行连接请求,那么服务端的资源就闲置了,那么就减少收益了,所以正是因为全连接的存在,能够在上层正好空闲的情况下正好有连接进行请求,这样就会提高效益

这就好比在吃海底捞的时候,有的店非常火爆,就会在旁边设置等待区,那么当里面正好有客人吃完后,就能够正好将等待区的人带进去,等待区就类似我们的全连接队列,如果没有这个等待区,不能够保证里面客人吃完后,正好有客人来,这样就会减少收益

深入理解TIME_WAIT状态:

主动断开连接的一方,在四次挥手完成后不能够直接进入CLOSED状态,需要维护成TIME_WAIT状态,等待若干时长(2MSL)后在自动释放

  • MSL:报文最大生存时间,它是指一个 TCP 报文段在网络中可以存在的最长时间,超过这个时间报文将被丢弃,Linux 系统默认 MSL 为 60 秒

为什么要有TIME_WAIT状态

当主动断开连接的一方进行四次挥手完成后,就证明没有数据要向对方发送了,但是不能够保证对方还有没有要向主动断开连接方发送的数据,并且还有数据从主动断开连接的一方刚刚发送出去,没有得到应答,还有对方发送的应答或者数据等等这些都在网络中,如果不进行处理,那么倘若当一个客户端挥手完成,正好有另一个客户端进行bind,倘若此时新的客户端bind的端口号和断开的是一样的(尽管概率非常低),那么就会收到原来的数据,这是不可靠的一种表现,为了防止这种问题,就需要主动断开连接的一方在断开后维持一个TIME_WAIT状态,等待对方的报文数据然后进行丢弃,也就是为了让这些数据在双方通信信道中进行消散

为什么要等待2MSL这么长的时间

当主动断开连接的一方在最后发送一个ACK应答后,就四次挥手退出了,如果此时ACK应答丢失,那么被动断开连接的一方就会处于LASK_ACK状态(虽然只会维持一段时间,时间到了也会自动退),那么被动断开连接的一方就会进行超时重传,倘若不是2MSL,就会导致时间不够,那么主动断开连接的一方就会进入CLOSED状态,进而无法进行响应应答,所以等待这么长时间的理由是:防止主动断开链接的一方最后一次挥手不能到达,导致提前释放

bind失败问题

在之前实现的TCP通信时,有时会出现bind问题,这就是因为主动断开连接的服务端处于TIME_WAIT状态,还没有完全断开连接,ip和port正在被使用,此时如果重新连接,那么端口就会被占用导致bind失败
在这里插入图片描述

怎么立即重启

那么怎么立即重启呢?-----使用setsockopt函数设置地址复用,这样就可以立即重启了
在这里插入图片描述
参数说明:

  • sockfd:套接字描述符
  • level:选项所在的协议层(如 SOL_SOCKET 表示通用套接字选项,IPPROTO_TCP 表示 TCP 层选项)
  • optname:要设置的具体选项名称
  • optval:指向存储选项值的缓冲区
  • optlen:缓冲区的长度
    在这里插入图片描述

客户端为什么不需要担心重启问题呢

因为客户端每次重启的端口号是随机的,几乎不会出现相同的端口号问题,而服务器的端口号是不能随便改变的

四、补充知识:

1、流量控制:

在之前16位窗口大小那里详细讲到了,这里不过多进行赘述,总的来说就是根据TCP报文中16位窗口大小进行来进行判断接收端的接收能力,来进行发送速率的修改的

那么第一次发送怎么能够保证不会一次性打满对方的接收缓冲区呢?
在进行三次握手的时候不仅仅是进行SYN+ACK的交换,此时也协商了双方的接收能力,这即体现可靠性,又提高了效率

2、滑动窗口:

发送缓冲区结构解析:

发送缓冲区在这里可看做三种类型:已发送已确认收到应答,已发送未确认应答,未发送,我们上一次理解的是发送缓冲区可看做数组,这里通过两个指针将这三个区域进行划分,win_start指向最早未确认的字节(已发送但未收到确认的第一个字节),win_end指向窗口内最后一个字节的下一个位置,即窗口的边界
win_start等于确认序号,win_end指针就等于确认序号+窗口大小,这个窗口大小就是对方给我们的ACK中一定也存在窗口大小

已发送已确认:这部分可被覆盖,当被覆盖后就叫做从TCP发送缓冲区中移除了
已发送未确认:这部分就是可以发送/已经发送,但是尚未收到应答的区域

这个已发送未确认就叫做滑动窗口,所以:滑动窗口在TCP的发送缓冲区中
滑动窗口的大小是对方接受缓冲区中剩余空间的大小(目前这么理解)

向右移动时的窗口变化:

对于窗口的两个指针(为了方便叙述,这里成为左指针和右指针),都是只能够向右移动的

当窗口的大小等于对方接受缓冲区中剩余空间的最大大小的时候,此时就证明窗口满了,此时右指针就不能够向右移动,因为这样会继续增大窗口大小,确认应答后左指针就会向右移动,此时窗口就不是满的了,

窗口不是满的时候,当收到确认应答后,左指针向右移动,当继续发送新数据的时候,右指针向右移动,他们之间的大小是不会超过最大长度的

当接收方应答非常快的时候,有可能将窗口的大小变为0,此时发送方的发送速率小于接收方的应答速率

所以两个指针都是只能够向右移动的,移动有三种:右不变,左移动;左右都移动,窗口变小;左右都移动,窗口变大
移动的时候大小是动态变化的,变小变大不变都有可能

滑动窗口会越界吗

既然左右指针都是向右移动的,那么滑动窗口会越界吗

这是不会的,TCP采用的是基于数组的环形队列,就像是取模运算,比如说将数组下标进行5的取模,那么这个数组的下标是不会超过4的,这就是一个环形数组

如果丢包了,如何理解滑动窗口

丢包分两种情况:请求丢失和应答丢失

倘若应答丢失:

在这里插入图片描述
像上述,我们的2001,3001,4001的应答都丢失了,只收到了5001的应答,尽管如此,滑动窗口依然是会向右移动的,因为我们收到的确认报文的定义就是当前确认序号之前的数据都收到了,所以只要收到了5001应答就能够保证之前的数据都收到了,即使移动窗口也不会造成数据丢失
在这里插入图片描述

倘若请求丢失:

如下,倘若服务端收到了2000,4000,5000,6000的序号请求,但是没有收到3000序号的请求,那么在返回确认序号报文的时候就会一直发2001,此时滑动窗口向右滑动也就只会滑动到2001,所以不用担心滑动窗口会直接越过序号为3000的报文,正因为有确认序号,才会保证滑动窗口不会发生越界问题
在这里插入图片描述
上述的客户端会收到多次2001的报文,此时客户端就知道有请求丢失了,那么就会重新发送2001~3001的请求报文,这被称为快重传,并不会等到2001 ~ 3001的报文超时了进行超时重传,毕竟超时重传比较浪费时间,所以就规定,一旦有三次重复的确认应答,那么就立刻进行重传报文,这被称为快重传

思考:为什么有快重传,明明这么好用为什么还需要存在超时重传?
这是因为快重传是有前置条件的,需要收到三个相同的确认应答报文,才会进行快重传,其存在的意义是提高效率的,超时重传是最后兜底的,不能被替代

3、拥塞控制:

当发送数据,出现问题的时候,不一定是对方主机出问题,还有可能是网络出现问题

如果通信的时候,出现了少量的丢包,这是常规情况,但是如果出现大量的丢包,这就是网络出问题了
这个时候对于发送方来说,不能够立即对报文进行超时重传了,因为此时网络已经出现拥塞了,不能够继续发送数据了,如果继续发送就会加重网络的拥塞,这就像已经拥堵的十字路口,已经有很多车了,不能够继续运行了,那么此时就不要有车继续进入了

那么客户端要怎么做呢?
客户端发送少量的数据,看看此时网络中拥堵的状态,再根据状态进行发送速率的传输
当网络出现拥塞的时候,发送少量的报文,当几乎都能够得到应答的时候,就证明网络已经趋近于健康了,就应该尽快恢复正常通信了

拥塞窗口:

这里修正一个概念:滑动窗口的大小 = min(窗口大小,拥塞窗口)
其中窗口大小:就是对方主机的接收能力
拥塞窗口:考虑的是动态的,网络的接收能力

对于拥塞窗口:其作用就是主机判断网络健康程度的指标,超过拥塞窗口就会引发网络拥塞,因为网络是动态的,所以拥塞窗口也是动态变化的

慢启动:

当网络状态不好的时候,就会进行慢启动:客户端发送少量的数据,看看此时网络中拥堵的状态,再根据状态进行发送速率的传输

就是一开始定义拥塞窗口的大小为1,每次收到一个ACK应答后,就将拥塞窗口的大小 x2 之后达到一个阈值之后,就以线性的方式继续增加,如果这个拥塞窗口的大小将大于滑动对方主机接收窗口的大小的话,就以对方的窗口大小来进行发送
在这里插入图片描述
慢启动的阈值:最近一次发生网络拥塞时,拥塞窗口大小的一半;一旦网络发送拥塞,此时拥塞多大,然后下一次阈值就是此时拥塞窗口的大小的一半

4、面向字节流:

因为TCP的缓冲区存在,TCP的读写不需要一一匹配,比如可以写的时候一次写100字节,读的时候100次1字节地进行读

和面向字节流不一样的,面向数据报,这就是发送端发送了几次报文,接收方就得接收几次报文,并且发多少接受多少
这就类似于收快递,发了几个快递就要收几个快递

对于面向字节流:
假如在应用层向下发送了5个报文,一共50个字节,在传输层的发送缓冲区或者接收缓冲区是不关心有几个报文的,只关心有多少个字节,然后在发送的时候,就只发送字节相关的,发送多少字节,什么时候发送等等这些都由TCP自己完成,我们是不关心的,然后对方接收缓冲区收到数据,是按照字节的方式存储的,至于报文有多少,报头和有效载荷分离那是上层用户要关心的

对于TCP来说,它并不关心发送缓冲区当中的是什么数据,在 TCP 看来这些只是一个个的字节数据,它的任务就是将这些数据准确无误的发送到对方的接收缓冲区当中就行了,而至于如何解释这些数据完全由上层应用来决定,这就叫做面向字节流

5、粘包问题:

在上层用户层才有报文的概念,用户层从传输层的接收缓冲区读取报文的时候,读取的是字节流,但是当读取上来后,就会对字节流进行分析,提取其中的报文,这个时候就会发生粘包问题

当上层进行读取时,如果不对报文进行一个一个的分离,就可能会多处理或者少处理请求,这种情况,叫做数据报粘包问题

对于应用层:看到的只是一串连续的字节数据,那么应用层看一连串的字节数据,不知道从哪个部分开始到哪个部分是一个完整的应用层数据包,读上来的也许是半个,也许是一个半,无法明确报文和报文之间怎么分隔开的

解决粘包问题——核心思想:定协议,明确报文和报文之间的边界:

  1. 定长报文
  2. 使用特殊字符。就比如今天的报文没有换行符。 所以为了区分报文与报文,我们就可以使用 n来进行区分
  3. 使用自描述字段 + 定长报头
  4. 使用自描述字段 + 特殊字符

6、连接异常:

  • 进程终止:对于连接来说,连接和进程之间不是直接相关的,连接和文件才是,文件的生命周期又是随进程的,所以连接本质还是随进程的,我们知道,进程的退出不管是正常退出还是异常退出,其本质都是退出,都会进行四次挥手然后连接都是正常自动断开的

  • 机器重启:当我们进行关机的时候,有时候发现关机比较慢,这是因为此时有许多进程在运行,OS需要首先将这些进程进行杀死,也就是都进行四次挥手,之后再关机或者重启

  • 机器掉电,网络断开:客户端一旦掉线,客户端时没有机会向服务端进行四次挥手,因为报文发不出去,所以服务端不知道客户端已经挂了,他的链接是正常维护的,此时就发生了连接认知不一致问题,当客户端再重新联网, 就要重新进行连接,这也是认知不一致问题
    但是服务端也不会一直保持连接,因为存在保活机制,当客户端长时间不和服务端进行通信,那么服务端就会认为客户端挂了了,然后主动探测空闲连接的有效性,及时释放无效连接,避免因一方异常离线导致另一方长期持有无效连接资源,造成资源浪费或通信阻塞


文章转载自:

http://MJb9xN1u.dktyc.cn
http://WZbaF4s3.dktyc.cn
http://cDJMMLYg.dktyc.cn
http://dIh7UYzS.dktyc.cn
http://2RqsGXQK.dktyc.cn
http://81FtR2zE.dktyc.cn
http://3LwVM9lF.dktyc.cn
http://SZJuMHMA.dktyc.cn
http://A2bCQ5J9.dktyc.cn
http://xYWAGJav.dktyc.cn
http://mLYADAHW.dktyc.cn
http://btcMQto1.dktyc.cn
http://glTvBKqw.dktyc.cn
http://t1Hv3R92.dktyc.cn
http://qkpp7SAR.dktyc.cn
http://LVuKcsAP.dktyc.cn
http://oDPEizua.dktyc.cn
http://UWMm3KO8.dktyc.cn
http://0Sm5dIoV.dktyc.cn
http://gaKOGMte.dktyc.cn
http://I7XCG8UL.dktyc.cn
http://gLBmWcnn.dktyc.cn
http://57dDY1vi.dktyc.cn
http://TslkLu9q.dktyc.cn
http://Qmo3J4Yo.dktyc.cn
http://f43449NK.dktyc.cn
http://ZV9iftCY.dktyc.cn
http://SDd6OnRQ.dktyc.cn
http://vj8kY24v.dktyc.cn
http://CxUVTusv.dktyc.cn
http://www.dtcms.com/a/387899.html

相关文章:

  • dict电子词典
  • pulsar Error receiving messages.Consumer already closed at
  • 计算机视觉(opencv)实战二十五——摄像头动态轮廓识别
  • 简单易懂的Kafka例子
  • 针对tomcat [/usr/lib64:/lib64:/lib:/usr/lib]上找不到基于APR的Apache Tomcat本机库的处理方法
  • 【js】js实现日期转大写:
  • 番茄时钟小程序版本更新记录(v1.0)
  • css消除图片下的白边
  • 我是如何在electron里安装shadcn ui框架的
  • 【图像理解进阶】如何对猫猫的图片进行细粒度分类?
  • JSCPC/GDCPC 2025 J.Puzzle Competition(解谜游戏)
  • SpringMVC 系列博客(三):进阶功能与 SSM 整合实战
  • 电商网站反爬虫机制详解及应对策略
  • 没了CDN与PCDN,网络会怎样?
  • C++中std::vector Vs std::deque VS std::list对比详解
  • RecyclerView实现流式布局
  • 【连载5】C# MVC 异常处理避坑指南:异步操作与静态资源错误解决方案
  • 当控制器无法上网时,如何利用windows笔记本与控制器共享网络?
  • 企业数字化视角下的项目管理软件市场全景分析(2025版)
  • Python异步编程:asyncio.create_task() 用法解析
  • java面试Day1 | redis缓存穿透、击穿、雪崩、持久化、双写一致性、数据过期策略、数据淘汰策略、分布式锁、redis集群
  • Jenkins运维之路(容器项目的优化)
  • 【精品资料鉴赏】363页智慧旅游大数据平台项目建设设计方案
  • 软考 系统架构设计师系列知识点之杂项集萃(149)
  • MyBatis 中注解操作与 XML 映射文件操作的对比
  • 复杂 PDF 文档如何高效解析?
  • 加密网络流量分类
  • leetcode算法题记录:
  • VS安装后通过vswhere.exe查询显示的 installationVersion数字怎么不是2022?
  • 光伏电站安全 “守护神”:QB800 绝缘监测平台,为清洁能源高效运行筑固防线