【Linux】TCP协议

文章目录

  • [📖 前言](#📖 前言)
  • [1. TCP协议格式](#1. TCP协议格式)
  • [2. 确认应答机制](#2. 确认应答机制)
  • [3. 16位窗口大小](#3. 16位窗口大小)
  • [4. 6个标记位](#4. 6个标记位)
    • [4.1 URG紧急指针标记位:](#4.1 URG紧急指针标记位:)
  • [5. 超时重传机制:](#5. 超时重传机制:)
  • [6. 连接管理机制](#6. 连接管理机制)
  • [7. 滑动窗口](#7. 滑动窗口)
    • [7.1 如何理解滑动窗口:](#7.1 如何理解滑动窗口:)
    • [7.2 滑动窗口移动和大小问题:](#7.2 滑动窗口移动和大小问题:)
  • [8. 高速重发控制(快重传)](#8. 高速重发控制(快重传))
  • [9. 流量控制](#9. 流量控制)
  • [10. 拥塞控制](#10. 拥塞控制)
    • [10.1 慢启动机制:](#10.1 慢启动机制:)
  • [11. 延迟应答](#11. 延迟应答)
  • [12. 捎带应答](#12. 捎带应答)
  • [13. 再谈面向字节流](#13. 再谈面向字节流)
  • [14. 粘包问题](#14. 粘包问题)

📖 前言

从上一章开始,我们正式进入传输层的学习,该层两个重要的协议TCP协议和UDP协议。上一章我们主要学习了UDP,认识了其报头和各个字段。TCP协议因为要维持可靠性传输,所以要做很多工作,同时也很难理解,本章我们将重讲TCP协议。

整个网络协议栈就叫做TCP/IP协议栈,可想而知TCP/IP协议的重要性!!

整个网络协议栈叫做TCP/IP协议,用两个协议把整个网络协议栈命名了,但是可不止这两个协议,说明这两个协议非常重要。


1. TCP协议格式

TCP传输控制协议,主要解决网络通信过程中,可靠性的问题。

同时也帮我们]解决了效率的问题。

我们之前用TCP协议写过一个聊天程序,并以守护进程的方式布置在了服务器上:👉 TCP的服务端(守护进程) + 客户端

应用层传输到传输层的时候(从上往下交付时),和UDP一样,也是要填写TCP报文的格式的。

报头长度:

  • 一行四个字节,一共五行。
  • TCP的标准长度是20个字节。

16位源端口和目的端口:

  • 在端口这件事情上,传输层协议,报文必须携带端口。
  • 因为端口解决的是(尤其是目的端口号),解决的是这个报文将来被分包之后如何交付给上层的问题。

4位首部长度:

  • TCP的4位首部长度字段,也被称为"Data Offset"字段,用于指示TCP报头的长度。
  • 因为TCP报头有标准长度,所以这个数字一定有自己的最小范围。
  • 从某一个特定的值开始到1111(二进制)也就是15(十进制)
  • 首部长度不是字面上的字节的概念,长度是有单位的,单位是4字节。
  • 所以4位首部长度至少101(20Byte)(固定的报头字段),是最长是1111(60Byte)(报头选项字段大小不固定)。

范围:5 ~ 15 < == > 101 ~ 1111

16位校验和:

  • 16位校验和不考虑,无非就是保证报文在发送前和发送后是没有被修改过,类似于之前学习的https中的数字签名。
  • 重点是验证数据在网络传输过程中有没有出现问题,如果出问题了直接就将包丢弃了。
  • 如果校验和失败了,数据包直接就被丢弃了,丢弃了大不了进行重传。
  • 校验和可以保证数据不会出现偏差,比如比特位反转这样的问题。

6个保留位:

  • 它们用于未来的扩展,目前没有具体的用途。
  • 这些保留位被标记为"RESERVED",并且必须设置为0以确保与未来的版本兼容。
  • 由于这些保留位是为了以后的扩展而存在的,因此不能将它们用于其他目的,否则可能会导致不可预测的结果。

TCP协议如何将报头和有效载荷如何分离的问题(如何分用):

  • 根据目的端口号向上交付。
  • tcp的报头在大部分情况下是标准的,但是有一些情况是报头超过20字节的。
  • tcp报头是变长的,那么如何确认报头长度呢,因为要将报头和有效载荷分离,如何解包呢?
    • 首先将前20个字书解包出来,在前20个字节中,字段的位置是标准的。
    • 所以在前20个字节当中,去提取4为首部长度一定能拿到。
    • 对于tcp协议可以根据报头字段,可以将报文解包的。

TCP报头是否关心数据(重点):

  • tcp不需要知道数据有多少字节,因为和tcp没关系。
    • 只需要将报头去掉了,然后把数据拷贝到接收缓冲区里,按照顺序一个一个拷贝,任务就完成了。
    • 有效数据是多少不用关心,因为tcp是面向字节流的,这个关心的工作是由应用层来关心的。
    • 所以tcp报头里没有设计报文总长度,只需要将数据放到缓冲区中,任务就完成了。
  • tcp不负责数据有多长的话题,数据有多长是应用层决定的。
    • tcp提供的是流式的套接字服务,只负责将从客户端收到的数据按照顺序放好。
    • 至于tcp怎么解释,是用户决定,这就叫做流式套接。

关于流的概念:

  • 文件被打开之后,如果要读取指定字节的话,要有严格的格式控制,必须按行读或者干脆用循环的方式将文件内容全部读取出来。
  • 文件流:
    • 以前我们学习的管道也是文件,客户端发一条消息,服务端读一条消息,也可能客户端给服务端一次发送了很多条消息。
    • 但是服务端只能一次全读上来,不能一条一条读取,因为管道做不到。
    • 管道只负责将数据放到缓冲区里,至于将来怎么读,是服务端的事情。
    • 这就意味着之前学习的文件或是管道,提供的也是流式服务。
    • 这也就是将文件打开称之为文件流的原因。
  • 制定协议:
    • 如果在文件当中,遵从某些需求,按照一份一份的去读数据。
    • 那么就要在文件内部定协议,序列化反序列化,给字符串设置报文长度。

2. 确认应答机制

在平时生活中,假设有两个人隔着一扇门对话,那么我们是如何确认对方听到了我说的话呢?一定是我收到了回复!!

例如:A对B说:吃了吗?

此时,B回复A说:吃过了,那你呢?

A收到了之前对B提问的应答,就可以确认B收到了A的提问。

那么B如何确认自己的提问被A收到了呢?那么一定是B也收到了A的应答!!

我们发现在上述过程中,只有得到了对方的应答,才能确认对方是否收到我发出去的消息,并且始终是由一条消息是没有得到应答的!
同样的在网络通信中,我们如果收到了对方的应答,我们确认是没有丢包的,否则就是不确定!

如果发出去的报文有很多,那么如何确认这个确认应答是对哪个报文的确认呢?

  • 收到应答时,要确认应答和发出去的报文哪一些是对应的,必须要对应起来。
  • 如果不对应,就很难确认哪个消息被对方收到了。
  • 所以tcp报头中包含了重要字段叫序号字段和确认序号
  • 序号用来标定一个对应报文的序号,确认序号用来确认的是特定收到的报文之前的特定报文全部收到了。

小试牛刀:

  • 如果服务端收到了若干个报文,它们的序号分别为1,2,3,5,6,7
  • 那么服务端给客户端响应的时候,确认信号应该填4。

解释:

  • 确认序号的含义是,一旦收到了若干个报文,确认序号的含义是告诉发送方,特定序号之前的已经全都收到了。
  • 1,2,3,5,6,7这几个报文中,少了序号为4的报文,没有全都收到。
  • 所以只能回复保证在序号为4的报文之前的报文已经收到了,但是序号为4的报文还没收到。
  • 对方再开始发报文响应的时候,就要从序号为4的报文开始发。

TCP必须保证客户端到服务端的可靠性,也必须保证从服务端到客户端的可靠性!

在大部分情况下客户端和服务端在通信时,报头中序号填的是自己的,确认序号确认的是对方的,对于客户端和服务端都是如此。

  • 序号和确认序号是为了保证可靠性的,核心的可靠性叫做确认应答,序号和确认序号是为了支持确认应答而诞生的。
  • 序号是为了让对方确认,确认序号是对方给我确认的,所以最后要根据确认信号来确定对方已经收到了我所发出去的多少报文。

没有百分之百的可靠协议,但是有局部百分之百的可靠协议,虽然最新的消息没有被应答,但是之前的消息可以保证被应答。

为什么tcp报文有两组序号?也就是说为什么既有序号也有确认序号呢?

  • 用一个序号也可以从客户端发送给服务端,也能够进行响应,为什么要有两个序号呢?
  • 如果服务端想给客户端应答,并且同时想给客户端发消息呢?
    • 如果服务端想给客户端应答,就必须必须得填充确认信号。
    • 如果服务端想给客户端发消息,就必须携带自己的序号。
  • 所以服务端必须同时设置序号和确认序号,两个独立的序号。

客户端用自己的序号和服务端的确认序号构成从左向右的可靠性,服务端用自己的序号和客户端的确认序号构成从右向左的可靠性。

所以就可以用序号机制,保证双向方向上的全双工的确认应答机制,TCP通信的时候是全双工的!!

序号这个数字是怎么来的呢?

  • 起始序号是随机生成的,往后序号在递增的时候,和报文是有关系的。
  • 序号一直在递增,一旦出现溢出,会进行回绕的,就像之前学的进程pid一样。

注意:

  • 可靠性指的是被确认过的报文,我们能保证其可靠性,没有被确认的报文我们无法保证。
  • 确认应答机制不是解决去确定数据什么原因丢的,而是解决数据能够被收到的问题。

小结:

在长距离交互的时候,永远有一条最新的数据是没有应答的!发送方无法确定这条消息对方是否收到。但是,只要发送的消息有对应的应答,我们就认为我们发送的消息,对方是收到的!!


3. 16位窗口大小

TCP是既有发送缓冲区,又有接收缓冲区的。

我们之前学的接口都是将数据从用户拷贝到内核,再从内核拷贝到用户。

数据什么时候发,发多少,出错了怎么办,要不要添加提高效率的策略都是由OS内的TCP自主决定的。不是应用层程序员决定的,是由操作系统内的自主决定的。

所以叫做传输控制协议!TCP对于数据发送是具有传输控制的权力和能力的!

那么凭什么呢?

  • 凭的就是用户是将数据拷贝到缓冲区里的。
  • 至于这个缓冲区如何处置这些数据完全是由操作系统决定的。
  • 所以将tcp协议称之为传输控制协议。

如果有两台主机要通信,本质是两个TCP协议在互相通信:

  • 发送消息的时候,是应用层将数据拷贝到发送缓冲区。
  • tcp再把发送缓冲区的数据刷新经过网络推送到服务端tcp的接收缓冲区,服务端再从接收缓冲区读。
  • 如果接收缓冲区没有数据,上层的read或者recvfrom接口会阻塞。
  • 因为有成对的发送和接收缓冲区,所以tcp可以做到全双工!!

那么问题来了,只要是缓冲区就会有大小,所以发送和接收缓冲区是有上限的,如何知道对方的接收缓冲区剩余空间大小呢?

  • 如果发送报文,丟包了再重传,可以但是不可行,因为传输的路上消耗了大量的资源。

  • 如果发送的太快了导致服务端来不及接受,怎么办?等一系列问题都需要TCP来解决。

所以只要是客户端知道了服务端的接收能力,客户端就可以根据服务端的接收能力,来动态的调整自己的发送策略。

  • clietn可以计算出自己缓冲区剩余空间大小,但是client怎么能知道server的接收能力?
    • 发送都会有应答!应答本质就是要发送自己的TCP报头。
    • TCP报头可以有保存server接受能力的属性字段 ------ 16位窗口大小!
  • server的接收能力,哪一个指标表示接收缓冲区剩余空间的大小?
    • 响应给client的TCP报头字段:16位窗口大小!
  • 如果server给client发报文,填充的窗口大小,是server的还是client的?
    • 一定是server的!!

双方基于两个缓冲区,基于得知对方接受能力的前提条件下,进行数据通信,就可以保证给对方发送消息时,对方也给我不断地应答,每次应答都更新对方窗口大小,根据窗口大小发送合适的数据大小!!

如果我们是第一次发消息,我怎么确定对方的接受能力呢?

  • 极端情况下,客户端还没得到服务端的应答,客户端一开始就给服务端就塞了个大的数据,这种情况怎么处理?
    • 此时已经是握手建立成功之后,客户端再给服务端发消息,这个时候才算是正常的通信。
    • 三次握手之后的正常通信(后面讲),双方已经互相拿到了对方的窗口大小。

选项字段的用途:

  • 16位窗口大小,意味着对方接受缓冲区最大也就是2^16 - 1,也就是64KB
  • 这个大小目前来讲大部分情况下是够了的,如果不够了,也不用担心。
  • 因为tcp报头里也有对应的选项 ,这些选项里也有窗口的扩大因子

发送和接收缓冲区:

  • 上层将数据拷贝到发送缓冲区的时候,每段数据就天然的具备了在缓冲区数组当中的下标(缓冲区看作是数组)。
  • 就可以通过下标来充当字节流数据每一个字节的序号,所以要发1 ~ 1000时拿最大下标充当报文的序号就发出去。
  • 当对方ACK时,收到的报文的确认序号是1001,那么下次再去发就拿着数组的下标定位到1001,就可以再发数据了。

4. 6个标记位

TCP通信的完整结构:

如果两个主机想通过tcp进行正常通信,必须先要进行三次握手。通常由客户端主动发起,发起第一次握手,客户端发起完毕之后,服务端进行响应,只有第三次成功之后,才完成三次握手。

客户端发送完毕了,双方要断开连接,要经历四次挥手。每次通信必须保证连接建立成功,然后才是正常通信,然后当我们在通信完毕再进行四次挥手,这才是TCP通信的完整的基本结构。

服务器的视角,它收到的TCP报文,有的是用来建立连接的,有的是正常的数据报文,有的是用来断开连接的,所以报文也是有类别的。
服务器可能在一瞬间内有多个服务器向它发送大量的报文,不同的报文有不同的诉求。

那么作为服务端,要不要区分收到的TCP报文属子哪个类别呢?答案是肯定的,必须得区分!!

不同的报文是有不同的类别的!!这也就是在回答为什么会有若千个标记位的原因。

  • SYN(Synchronous): 只要报文是建立链接的请求,SYN需要 被设置为1(同步标志位)。
  • FIN(Finish): 该报文是一个断开链接的请求报文。
  • ACK(Acknowledgment): 确认标记位,表示该报文是对历史报文的确认。
  • PSH(Push): 提示接收端应用程序立刻从TCP缓冲区把数据读走。

还剩两个后面讲...

带有ACK的报文可能也会携带服务端给客户端发送的消息,在确认的同时也发数据:

  • 比如说客户端给服务端发了一个数据报文序号为10。
  • 服务端给客户端回消息的时候,就会把自己报文的ACK标志位设置为1。
  • 当客户端收到了ACK为1的报文时,就会去报头当中提取确认序号。
  • 就知道了当前服务端已经收到了哪些报文了。
  • 只要是正常通信阶段,ACK都是1。

对PSH标志位的理解:

  • 目前我们学到的所有接口,全部都是主动的,由进程自己去轮询检测的。
  • 如果tcp接收缓冲区收到了一条数据,并且收到的tcp报文中携带了PSH标志位。
  • 那么就会影响服务端的操作系统,让操作系统尽快通知上层:数据已经有了赶紧来读取吧。
  • 这就是PSH状态标志位的含义。

如果有一天无法阻塞式的调用read接口,想要立即让上层开始读取数据,就可以携带PSH标记位,此时数据被服务端收到后放在接收缓冲区里,通知上层来读取。

假如服务端接收缓冲区满了,客户端再发消息时被服务端响应并且tcp报头中ACK是0,于是客户端就再给服务端发送tcp报头是就带上PSH标志位。

4.1 URG紧急指针标记位:

为什么TCP在正常通信时要有序号呢?

序号是确认应答的基石,但是序号不仅仅只有确认应答的功能。

  • 如果客户端给服务端发送序号为1 ~ 10一共10个报文,那么接收方接收到的数据就一定是按照1 ~ 10接收到呢吗?(假设不丢包的情况下)
    • 发是按照顺序发的,但是收不一定是按照顺序收的。
    • 每个报文经过的路由器结点,经过的路径选择,不同设备的效率,路程的长短等。
    • 这些因素都可能影响,每个报文在网络当中传送的时长问题。
  • 首先序号是递增的,所以可以根据序号来排序,在TCP缓冲区内部做了数据顺序的调整,序号的策略可以保证按序到达。
  • 如果数据必须在TCP中进行按序到达的话,也就是说如果有一些数据优先级更高,但是序号较晚,无法做到数据被紧急处理!!
  • TCP可能有些数据,可能是让server端优先去读取的,就可以设置URG标志位,代表该数据直接忽略它的序号而被上层直接读取。

URG紧急指针的全称是"Urgent Pointer"。

如何知道紧急数据在哪呢?

  • 这个16位紧急指针标定了,数据最终在整个报文的偏移量处。
  • 紧急指针只能读取一个字节,换言之,紧急数据只能发送一个字节。
  • 紧急指针指向的那个字节,就是我们将来要读取的紧急数据。

只要URG标志位为1,就代表了有效载荷里携带了紧急数据,由16为紧急指针以偏移量的方式找到数据,并且该数据只有一个字节。

什么时候用紧急指针呢?

  • 大部分情况下紧急指针都是用不到的,紧急指针传过去的大部分已经不再是数据内容本身了。
  • 一般用紧急指针传的是具有额外含义的数据。
  • 如果服务端出问题,一直没响应(如服务端的接收缓冲区满了),此时客户端就想知道服务端到底怎么了。
    • 所以客户端就可以给服务端发一个紧急数据,被服务端优先读取。
  • 发送一个正常的询问报文可以吗?
    • 正常对的询问报文还要在接收缓冲区里排队,前面的数据处理完才能处理这个询问报文。
    • 到那个时候黄花菜都凉了。

当服务端接收到紧急指针后,也以紧急指针的方式返回给客户端(客户端服务端提前约定好紧急指针指向的数值的含义)。

  • 以非常小的字节个数,超过缓冲区之外来进行很少字节的通信,目的主要是未来应用场景是获得主机或服务的状态的。
  • 所以我们把URG标志位表示的数据(紧急指针指向的数据)一般把他称之为带外数据
  • 用同一个tcp连接,不走接收缓冲区,而是优先直接被上层优先处理,这就是带外数据。
  • 用来检测一些毫无反映的机器的状态,URG标志位通常用来进行状态询问的。

5. 超时重传机制:

因为每个数据都要有应答,当把数据发出去,如果在一段时间之内没有收到直接或间接的确认应答的话,对方就会认为要收到的报文就是丢了,至于是不是真的丢了就不管了。

丢了之后就重新发送,这就叫做 ------ 超时重传机制。

潜台词:

  • 如果将一个数据发出去了,在没有收到对方的应答之前,已经发送的数据要在发送端主机保留起来。
  • 要不然后面万一丢了,怎么重传呢?
    • 如果发送端在超时之前没有收到对方的应答或者收到了确认发生丢包的消息。
    • 它会重新发送之前发送过的数据。
  • 那这个数据被保留在哪里呢?
    • 一旦数据被发送到网络中,它将继续留在发送缓冲区中。
    • 直到收到接收端的确认(ACK)或超时发生。


B收到A发来的报文后给A的应答没有被A收到,和A发给B的报文丢了没有收到B的应答,在A的视角看来都是一样的,A都会认为发给B的报文丢了。

如果B给A的应答给丢了怎么办?

  • 应答丢了在主机A看来还是和A发给B的报文丢了是一回事,主机A只知道自己没有收到应答。
  • 实际上B已经收到了,但是发过来的应答丢了,主机A依旧认为数据是丢的,那么A就超时重传。
  • 如果A重传了,此时B就相当于收到了两个一模一样的报文,这种情况是存在的,但是不允许向上层传递。
  • 所以TCP还要具备可靠性机制 ------ 去重。

如何去重呢?TCP序号的第三个作用去重:

  • 序号不仅仅有确认应答,按序到达的作用,还有去重的作用。
  • 所以重复报文直接就被丢弃了。

超时重传的时间是多少呢,也就是说超出多少时间才重传呢?

  • 最理想的情况下,找到一个最小的时间,保证确认应答一定能在这个时间内返回。
  • 因为网络的情况是变化的,所以特定的超时时间间隔也应该是变化的。
  • 不能是确定的值必须根据网络的情况进行调整。
  • 如果网络状况非常好,时间间隔就要很短,如果网络状况很差,时间间隔就要长一些。
    • 如果超时时间设的太长,会影响整体的重传效率。
    • 如果超时时间设的太短,有可能会频繁发送重复的包。

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

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

如果对方主机没有出异常,单纯的只是网络出问题了,服务端强制关闭了连接:

  • 这时就出现了客户端给服务端发的报文服务端没收到的问题。
  • 服务端认为连接已经关了,客户端认为连接还好着呢。
  • 双方再通信,服务端给客户端发送RST,那么连接直接被重置了。

6. 连接管理机制

6.1 TCP三次握手(重点):


三次握手过程:

  • 建立连接的时候,客户端发送出去SYN之后,只要发出去,客户端的状态就叫做SYN_SENT同步发送)
  • 服务端收到了SYN,紧接着发送完SYN + ACK之后,立马就变成了同步收到,状态就变成了SYN_ RCVD(同步收到)
  • 当客户端一旦收到了SYN + ACK之后,再进行ACK发给服务端,服务端此时叫做ESTABUSHED(连接建立)
  • 最后一次ACK是没有应答的。

建立连接就是通过三次握手来完成的,所以对应的TCP连接维护,双方是基于状态变化的。

客户端并不是收到了服务端的ACK,状态才变成了SYN_SENT,而是只要将SYN发出去,状态就变成了SYN_SENT。图中线是斜着的,说明数据包在流动的时候是需要花费时间的。

TCP面向连接的,如何理解连接呢?

  • 大量的连接,OS就需要管理这些连接,如何管理??先描述,再组织!!
  • 连接双方一定要维护该连接创建对应的数据结构。
  • 操作系统建立好连接,双方就要维护连接结构体。

虽然TCP是保证可靠性的,但是并不代表三次握手一定是成功的:

  • 服务器可能会被大量客户端连接的,也就意味着有大量的成本,因为维护连接要有对应的结构体。
  • 所以服务器如果有非常庞大的客户端来连接就会挂掉了。

6.1 - 1 三次握手的原因

在我们熟悉了三次握手过程之后,不禁会有以下问题:

为什么要是三次握手?一次握手可以吗?两次握手可以吗?接下来我们来挨个分析一下。

一次握手可以吗?(有安全问题)

  • 也就是说只要客户端给服务端发送个SYN,双方连接就建立好了,就可以继续通信了。
  • 一次握手和三次握手成功率差不多,都是最后一次握手决定的,但是一次握手成功非常容易受到攻击。
    • 如果客户端循环式的向服务端发起SYN请求(或者构造假的SYN请求),再加上一次握手就能成功建立连接。
    • 服务端会创建数据结构来维护连接,这就会导致一个客户端就可以很快将服务端打满,进而让正常的连接无法进行。
  • 这种攻击方式叫做SYN洪水,把服务端机器搞挂掉。

两次握手可以吗?(有安全问题)

  • 客户端发送一个SYN,服务端必须响应ACK,一来回两次握手。
  • 两次握手和一次握手其实是类似的,服务端只要给客户端发了ACK,那么服务端也认为连接已经建立好了。
    • 同样服务端也要做维护连接的工作。
    • 如果客户端也大量的给服务端发送大量的SYN,服务器收到之后,只要将ACK报文给客户端发过去,服务端也认为连接建立好了。
    • 那么服务端给客户端给发送的第二次握手报文,恶意客户端直接丢弃,就类似于一次握手。
  • 会产生与一次握手同样的安全问题,把服务端机器搞挂掉。

三次握手可以吗?(可以的)

  • 一次和两次握手不行,根本在于连接建立的时候,每一次都是让服务端先认为连接已经建立好了。
  • 三次握手之后,可以把最后一次确认的机会交给服务端。
  • 因为只有三次握手,最后一次握手成功的时候,服务端收到了最后一个报文。
  • 由服务端结束三次握手,只有它最后一次确定的收到了ACK才认为连接建立好了。

只要服务端有了维护连接的结构体,客户端也必定有结构体维护!!

  • 如果想拿一台机器对服务端发起大量的SYN请求连接,服务端也会把这台机器拉下水。
  • 因为客户端给服务端挂多少连接,服务端也要给客户端挂多少连接。
  • 因为资源在不断减少,必须双方都维护链接。

如果三次握手最后一次带有ACK报文丢了,会怎么样?

  • 并不影响服务端,只影响客户端,客户端只要发出去ACK就认为连接建立好了。
  • 只要是客户端最后一次握手发出去了ACK,不管服务端是否收到这个ACK,客户端一定要维护连接!!
  • 服务端认为三次握手没有成功,建立连接失败,最多维护一个半连接,不会用全连接的方式把对应的连接结构体在服务端维护好。
  • 所以三次握手这种奇数次握手,把最后一次报文丢失的成本嫁接给了客户端。

同时三次握手也验证了客户端和服务端输入输出是否正常,俗称验证全双工。

更多次握手可以吗?(没意义)

  • 三次往上握手,是没意义的。
  • 增加更多次握手只会徒增数据交互频次,导致效率降低的问题。
  • 所以三次握手是一个比较合适的方案。

三次握手时不仅仅是只进行三次握手,除此之外双方还有很多协商工作:

  • 比如说互相通告双方的数据接收能力。
  • 只有通告了数据接收能力之后,在正式发送数据的时候,就知道数据接收情况了。
  • 三次握手最后一次ACK也可以携带数据。

如果在TCP三次握手的最后一次握手中,服务器端的确认包(ACK) 丢了,客户端将不会收到服务器端的响应。在这种情况下,客户端会尝试重新发送第三次握手的请求包。

客户端会等待一个合理的时间来接收服务器端的确认包,这个时间通常由操作系统设定。如果超过了这个时间,客户端将认为连接建立失败,并终止连接请求。

6.1 - 2 RST复位标志位

当三次握手,最后一次握手,客户端发送完带有ACK的报文就认为连接已经建立好,就开始向服务端发送消息。

  • 但是发给服务端丢包了,服务端认为连接根本没有建立好,而客户端则认为连接已经建立好了。
  • 服务端丢包了没收到客户端的ACK,服务端就会立马给客户端进行ACK(为0)响应,将响应报文的RST标志位置为1。

TCP中的RST全称为"Reset"。

RST是告知客户端将连接进行重置,只要收到了TCP报文中带有RST标志位设置为1,就代表要关闭连接,进行重新连接。

6.2 TCP四次挥手(重点):

三次握手和四次挥手图用的是上面同一个图。

四次挥手(一个close对应两次挥手):

  • TCP是面向连接的,建立连接三次握手,断开是四次挥手。
  • 比如客户端主动断开连接,首先给服务端发送FIN。服务端再响应返回ACK,一来一回就是两次挥手。
  • 只要给对方发送FIN就代表要和对方断开连接,四次挥手分别由客户端和服务端各自要主动触发一次。
  • 如果客户端要断开连接但是服务端还没有发送FIN,此时客户端就无法再向服务端发送正常数据了。
  • 当然ACK报文可以发,但一般的数据就没法再发了。
  • 而服务端依旧可以向客户端发消息,只有服务端向客户端消息也发完了,再发送FIN,客户端再ACK,双方连接就断开了。

为什么是四次挥手呢?

  • 原因很简单,无论是客户端还是服务端只要想向对方断开连接。
  • 只需要发送一次FIN就够了。
  • 但是要保证对方收到了断开请求,所以对方一定要返回ACK
  • 这就产生了四次挥手。

三次挥手可以吗?(可以的)

  • 三次挥手也是可以的,第二次和第三次挥手并在了一起。
  • 服务端发送给客户端的是FIN + ACK
  • 这是特殊情况,双方要同时断开彼此时才会发生。

所有的FIN的发送都是由上层调用了close来触发的。无论是三次握手还是四次挥手,上层接口也就一个函数,而三次握手四次挥手是tcp协议自主自动完成的。

6.2 - 1 CLOSE_WAIT 和 TIME_WAIT状态

双方状态变化:

  • CLOSE_WAIT / CLOSED / LAST_ACK:
    • CLOSE_WAIT状态是指在一个连接被关闭时,等待关闭的一方的状态,半关闭状态,说白了就是没关。
    • 这种没关的状态依旧会占用连接资源,因为服务端还要向客户端发消息。
    • 具体来说,如果主机A给主机B发送了关闭连接的请求,而主机B同意关闭后,就会进CLOSE_WAIT状态。
    • CLOSE_WAIT状态下,主机A不能再向主机B发送数据,但是仍然可以接收主机B发送的数据。
    • 如果主机B在收到对方关闭请求并响应ACK后,没有继续发送数据或发送了FIN(用于完全关闭连接)。
      • 则会进入CLOSED状态,表示连接已经完全关闭。
    • 如果主机B给主机A发送了FIN,表示它要关闭连接,那么主机B会进入LAST_ACK状态。
      • LAST_ACK状态下,主机B等待来自主机A的确认(ACK),确认收到FIN
      • 一旦主机B收到了确认(ACK),主机B就会进入CLOSED状态,表示连接已经完全关闭。
  • TIME_WAIT:
    • 主动断开连接的一方要进入TIME_WAIT的状态,谁先断开谁就进入TIME_WAIT
    • 具体来说主机A先给主机B发送FIN,一旦主机A收到了主机B的ACK,它会进入TIME_WAIT状态。
    • TIME_WAIT特点是发出去最后一个ACK之后,理论上客户端可以断开连接了。
    • 但是主动断开的一方并不会立马释放连接资源。
    • 而是要等一段时间之后才进入CLOSED状态。

为什么要编码时要注意close:

  • 如果服务端不关闭文件描述符最终要进入的状态就是CLOSE_WAIT状态。
  • 一个已经建立好的连接,只要客户端退出了,无论服务端曾经有没有accept
  • 只要服务端最终没有close,就处于CLOSE_WAIT状态。
  • 只要走到了TIME_WAIT,就一定意味着,主动断开连接的一方就认为,自己把四次挥手的动作做完了。
  • 如果服务器上挂满了CLOSE_WAIT这样的连接,大概率是没有调用close,半关闭状态也消耗资源,服务器会变卡。

TCP维护连接双方都要维护,而且TCP是全双工通信的,指的是客户端和服务端都是全双工的:

  • 断开连接的潜台词是关闭全双工的发送能力。
  • 但是并没有关闭接收能力,只有收到第三次挥手和第四次ACK之后。
  • 双发才把各自的IO能力给关掉,才叫做真正的关闭连接。

当四次挥手都结束了,为什么还停留在TIME_WAIT呢?

  • 主要原因是,最后一次ACK是否被对方收到,我们是不确定的,如果这个ACK丢了呢?
  • 主动断开连接的一方最后将ACK发出去,它认为自己四次挥手完成了。
  • 但是ACK在发送途中也要花费时间,服务端照样四次挥手没有完成。
  • 而客户端那可能主动地先进入断开连接的环节,将维护连接的结构体释放了。
  • 如果服务端没有收到ACK,那么对于服务端来讲四次挥手就没有完成,就要持续一段时间的连接保持的情况。
  • 一旦ACK丢了,就必定要以异常情况关闭连接(这样不太好)。
  • 所以我们让主动发起断开连接的一方发出最后一个报文时,先不要从TIME_WAIT变成CLOSED最后关闭连接的状态。
  • 要先等一等,要尽量保证ACK被对方收到。

三次握手不是百分之百成功的,同样的四次挥手也不是百分之百成功的,所以最后一个ACK被对方收到没有,并不确定。当ACK发出去了,无论有没有丢失,只要发送方等一会,也就相当于是一种间接的方式得到ACK是否被对方收到。

如果在TIME_WAIT这个特定时间内,如果没有收到来自对方的超时重传FIN,那么就认为发送出去的ACK已经被对方收到了。

MSL最大传送时间,TIME_ WAIT等待的时间基本上就是二倍的MSL,至少要报文保证一来一回的时间。

也就至少能保证一个FIN,一个ACK,也就能保证从左向右或从右向左两个方向上的数据能够尽可能消散。

查看MSL的值:

(1)解决TIME_WAIT状态引起的bind失败的方法

TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能到CLOSED状态。

  • 我们使用Ctrl+C终止了server,所以server是主动关闭连接的一方。
  • TIME_WAIT期间不能再次监听同样的server端口。
  • 在服务端的TCP连接没有完全断开之前不允许重新监听,在某些情况下可能是不合理的。

TIME_WAIT期间,如果服务端主动关闭了,重启的话是无法立即重启的,连接依旧存在,被绑定的ip和端口依旧被占用!!

  • 文件描述符的生命周期是随进程的,虽然我们服务端没在代码里close掉,但是将进程Ctrl+C了。
  • 操作系统终止杀掉进程时,底层会自动实现挥手过程。
  • 操作系统会自动帮我们关闭文件描述符,关闭的时候服务端会自动发送FIN
  • 然后客户端ACK,完成前两次挥手,服务端一关闭,先进入TIME_WAIT状态。

比如一个公司的服务器崩了,它首先要做的最重要的事情不是要查出崩了的原因,而是要立刻将服务器重启。

查看TIME_WAIT状态:

我们看到在服务端Ctrl C之后服务端还处于TIME_WAIT的状态。

就这一个函数就能完成服务器崩溃时,就能立马重启的功能:

  • 第一个参数:是要设置哪个套接字的属性。
  • 第二个参数:直接设置SOL_SOCKET
  • 第三个参数:optname是我们要设置的名称,一般用SO_REUSEADDR
  • 后面两个参数是想设置的值和长度。

使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。


这两个随便用哪个都可以。

我们看到服务可以立刻重启:

这两个看到两个进入到了TIME_WAIT的状态:

(2)查看在accept之前三次握手建立的连接

accept不参与三次握手,即便是不调用accept,底层也会三次握手成功。

我们用之前写的TCP服务端演示一下:


查看状态,握手成功:

6.2 - 2 listen的第二个参数

将来服务器可能比较忙,一瞬间又来了成百上千个连接,服务端来不及accept,底层的链接就要在操作系统的底层进行排队。

listen的第二个参数,叫做底层的全连接队列的长度,用户传的值然后加,表示在不accept的情况下,服务器最多能够维护多少个链接。

当listen第二个参数是2时,最多能维护三个连接:


listen的第二个参数,叫做底层的全连接队列的长度,算法是:n + 1,表示在不accept的情况下,最多能够维护多少个链接。

  • 如果第二个参数是2,所以最终我们的服务器在底层自动建立好全连接队列当中的连接个数是3个。
  • 如果再来更多,tcp不再自动进行三次握手。
  • tcp收到请求,暂时以半连接的形式存在着,等到后续有连接关闭退出了,再把这个连接建立成功。

listen的第二个参数不要太大,根据不同的场景来设定。

为什么第二个参数不要太大呢?

  • 服务器可用的硬件资源是确定的,多大的内存,多少的CPU是确定的。
  • 队列如果维护很长,让用户在排队,为什么不把队列设置短一点,让节省出来的资源可以尽快对外提供服务呢!!
  • 在服务器效率并没有怎么提升的前提下,让用户的体验反而会更差。
  • 短的队列可以让服务器在满载的情况下可以一直满载。

7. 滑动窗口

A给B发送一个消息,那么B在收到一个报文之后要给A发送ACK确认应答。整个数据发送的过程,是发一个消息给一个应答,是串行的。

  • 也就是A发送第一个报文,不发送第二个报文,在收到了第一个报文的应答之后,才开始发第二个报文。
  • 这样一来一回就能保证数据通信的可靠性,但是这种做法效率很低。
  • 实际上TCP是用了确认应答的思想,但是并没有采用发第一个不能发第二个,然后再等第一个ACK再发第二个,不是这样的。

既然一发一回的效率太低了,那么就可以一次给主机B塞满大量的报文,此时效率就提高了。以前是串行的,现在是并行的。

  • 那么主机A是如何知道主机B的接收能力呢?响应报文中的窗口大小!!
  • 一并发很多数据,报文就必须携带序号,不然主机B就无法区分,无法进行常规ACK了。

接收缓冲区的内容是从客户端拷贝下来的,再由TCP来决定什么时候发,怎么发,发多少,出错了怎么办之类的问题,所以TCP叫传输控制协议。

当A把数据发出去的时候,因为有可能会丢包,可能会再进行超时重传,所以A的发送中就不能直接将发出去的数据给清掉,得暂时保存在特定的内存区域当中,以支持超时重传。

那么这些临时被保存的数据被保存在哪里了呢?(以前接收窗口谈论的是接收缓冲区,现在谈的是发送缓冲区),所以就引入了 ------ 滑动窗口!!

为了能够更好的支持,高性能的进行数据发送,一次批量化发送大量的数据。当数据发送不成功时,要支持各种超时重传,各种延迟应答和其他的一些后续策略。所以TCP有一种策略叫做 ------ 滑动窗口。


在一大块发送缓冲区当中截取一段区域,来表征可以立即发送暂时不要应答的数据的容量。

操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答。只有确认应答过的数据,才能从缓冲区删掉,窗口越大,则网络的吞吐率就越高。

滑动窗口将发送缓冲区划分成了三个部分:

  • 中间白色区域: 暂时不需要应答,可以立马发送的数据区。
  • 左侧: 已经发送,并且已经收到应答的报文数据。
  • 右侧: 准备发送区域,当然也有可能有没有占满的空间。

放在滑动窗口里的数据:

  • 暂时不要ACK的,立马可以直接给对方发送的数据的集合就放在滑动窗口里。

那么滑动窗口的大小由谁决定?

  • 一次可以给对方发送多少消息,就决定了滑动窗口的大小。
  • 发送的数据前提要保证对方能够来得及接收。

7.1 如何理解滑动窗口:

发送缓冲区:

发送缓冲区可以看做是一个大的数组,而滑动窗口可以理解为,该数组中的一段区域,两个整数(类比指针)维护起始位置和结束位置。

窗口滑动的过程本质就是:

  • start_index += ?
  • end_index += ?

TCP滑动窗口看起来是线性的其实是被设计成:其实是被设计成为环状结构的!!

7.2 滑动窗口移动和大小问题:

学习滑动窗口我们不禁会有疑问,滑动窗口一定是向右滑动吗?滑动窗口可以变大吗?可以变小吗?

滑动窗口的大小由谁决定?是由对方的接受的能力决定!我收到的TCP数据报头中的窗口大小!!

假如滑动窗口的大小是4KB,对方的接收缓冲区当中剩余空间大小也是4KB:

  • 如果一次发送了4KB,但是对方上层根本不取数据,缓冲区被打满了,所以响应报文的ACK为是0。
  • 那么此时滑动窗口会向右移动吗?
    • 此时发送缓冲区滑动是:右侧指针根本不动,而是收到一个ACK(为1)确认左指针向右移动,收到一个ACK(为1)确认左指针向右移动。
  • 如果收到对刚刚4KB确认了,那么就直接将左指针与右指针重合,此时就代表发送窗口为0,也就意味着无法直接向对方发消息了。
    • 这就叫做停止发送,而间接的就是窗口根本没有向右滑动。

所以滑动窗口不一定会向右滑动。

发送缓冲区的滑动窗口,衡量的是对方的接收能力的话,如果对方的接收能力越来越小,那么只有左侧的start_index不断地确认,而end_index一直没有后移。

后续:

  • 过了一会对方的上层取走了8KB的报文,然后对方同步过来了一个TCP报文,通告窗口大小是8KB。
  • 当发送发收到了窗口为8KB的报文,那么此时end_index就向后加上8KB,直接就又扩展出来了一个滑动窗口。
  • 那么此时就可以继续向对方再可以发送了。

如果在通信的时候,如果发送方发送了4KB报文,而对方返回的窗口是16KB。那么此时的滑动窗口就变成了,可以变大,也可以变小。

滑动窗口减小:

  • 如果对方不取数据了,start_index向后移动,end_index指针不动。
  • 代表对方的接收能力在收缩。

滑动窗口变大:

  • 发送方发送了4KB的内容,收到对方的TCP报文中窗口大小是16KB。
  • 那么滑动窗口左指针向右加4KB,而右指针向右加16KB。

正常向右滑动,指的是对方的接收能力比较稳定,发的时候,对方也一直在取。

滑动窗口不会跳过一些没有确认的报文然后向后滑动,因为确认序号规定了,没有办法跳过一些没有确认的序号就向后滑动。

如何理解发送缓冲区发送完毕数据?

  • 滑动窗口向后滑动,也就意味着左侧的数据自然就被淘汰了。
  • 可以被来自上层的数据直接覆盖了。

滑动窗口的最大意义在于:

  • 在保证基本安全的前提条件下,以较高的效率向对方发送数据。
  • 因为滑动窗口的存在允许,一次可以批量化的发送多量的数据。
  • 而不再是之前讲的TCP确认应答,发一个信息确认一下,发一个信息确认一下。
  • 可以并发的让所有的报文在时间上重叠起来,从而提高效率。
  • 所以滑动窗口在TCP的使用场景中,更多的侧重点解决的是效率问题。

8. 高速重发控制(快重传)

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

  • 情况一: 数据报文已经抵达,响应带有ACK的报文丢了。

这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进行确认。

  • 情况二: 发送的报文数据包就直接丢了。


应答当中哪些ACK丢失不影响,发送过去的报文丢失了也不影响,主要还是看响应报文的确认序号!有对应的超时重传机制,如果超时重传收不到ACK,那么就是对方主机的问题了。

如第二个图所示:

  • 当主机A收到了大量确认信号相同的报文,那么主机A立马就意识到了,是发送的1001 ~ 2000报文丢失了。
  • 所以主机A就立马补发1001 ~ 2000然后再进行ACK。
  • 因为之前发送的数据例如3001 ~ 40004001 ~ 5000主机B都收到了。
  • 所以再次响应时确认序号直接干到了7001,于是主机A就继续从7001 ~ 8000进行发送。
  • 同样的道理中间的报文要是丢失了,也是按照上述过程来进行重传。

这种机制被称为 ------ 高速重发控制(也叫快重传)。

然而,如果发送方连续多次重发同一个数据包(通常是3次),仍然没有收到确认,就会认为网络非常拥塞或者接收方已失去响应能力。在这种情况下,发送方将放弃高速重发控制机制,转而使用"超时重传"的方式。


9. 流量控制

如何进行流量控制:

  • 主机A向主机B发送消息时,不能给对方发送数据太快,而导致主机B来不及接收。
  • 所以在给主机B发送消息时,要发送主机B能接受的数据量。
  • A与B在双方握手的时候,会交换双方的窗口大小(各自接收缓冲区中剩余空间的大小)。
  • 如果主机B响应的窗口大小是0,那么主机A就不发了。主机A会发送一些窗口探测报文(就是没有正文的,没有有效载荷的TCP报头)。
  • A与B是双向奔赴的策略,不是主机A一直去问,也不是主机B更新了才去通知A。
  • 而是主机A问着,主机B更新了再和A说。
  • 这样双方可以以最小的时间成本快速重新形成共识,然后继续在进行数据发送。

10. 拥塞控制

要学TCP必学三大机制,分别是:滑动窗口,流量控制,拥塞控制。

发送方考虑了对方接受能力的问题,发送报文丢包的问题,对方乱序的问题,对方效率的问题,对方重复报文去重的问题。有了上述考虑,对方主机可以以很舒服的状态来接收数据了。

但是漏掉了一个非常重要的角色,发送数据给对方主机,但不是直接将数据发送给对方主机,而是现将数据交给了网络。

之前的所有策略都是在考虑对方主机的感受,那网络的感受谁来管呢?所以TCP不仅考虑了对方主机的问题,还考虑了中间路上的问题(网络的问题)。

  • 软件性的,网络拥塞的问题。
  • 比如网线断了,路由器故障了TCP再怎么控制也做不到缓解了。
  • 只能解决设备没问题的情况下,单纯的数据太多的导致的网络拥堵的问题。

假设发送方发送一千个报文,但是有九百多个报文都丢了,对方只收到了几个报文,那么此时肯定不是发送和接收端的问题(因为之前有那么多的机制来保证可靠性)。

那么此时就只能是路上的问题了,如果发送报文丢了一两个,网络是没有问题的,如果是丢了大量的报文,那就是要怀疑是网络的问题了。

如何确认网络出现问题,方法是什么呢?

  • 两端通信时,两端的可靠性机制已经相当完善了。
  • 少量报文丢失正常现象,大量丢包就认为是网络出问题。
  • 可以根据网络出现问题的一些指标来衡量,网络当前的健康状态。

此时TCP为了能够更好的进行这方面的处理,就引入了 ------ 拥塞控制。

丢了大量的包,可以重传吗?答案是,不能!!

  • 接入网络的主机可不止一台,如果此时网络出问题了,所有主机的报文大量丢失了。
  • 如果遵守超时重传,那么所有的主机都进入超时重传的流程。
  • 此时一瞬间网络当中就会又诞生大量的数据,网络本来压力已经很大了,已经出现了丢包的问题,已经很拥挤了。
  • 每一台主机此时又向网络中进行赛数据,那么只会加重网络问题。
  • 所以不能立即超时重传!!

如果网络出问题的话,所有主机都能识别到,如果TCP协议规则设计成丢包无论如何都要超时重传,那么就会更加加重网络瘫痪程度,进而谁也发送不了。

正确做法:

  • 一旦出现网络拥塞的时候,这些主机最合理的做法应该是少发数据。
  • 让网络缓一缓把已经发送到网络中的数据排点出去,网络拥塞问题恢复了,再进行重新发送。
  • 此时我们要做的是减少数据发送,一次发送报文一两个,慢慢增多,这样更好一些。
  • 在特定的时间点内不能立即重传,这样可能会导致网络拥塞更加严重。

10.1 慢启动机制:

TCP引入慢启动机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。

  • 网络内的所有主机,先不要超时重传,大家都开始发送少量的数据。
  • 例如发送一个报文探探路,如果连反应都没有,那么久再等一会,过一会再发一个数据。
  • 只要发了一个数据,有响应,然后再尝试发两个数据,如果再响应,然后再尝试发四个数据。
  • 当发送数据量不断增大时,慢慢的数据量也就达到了正常发送要求了。

当网络拥塞了,其他所有的客户端主机都能识别得到,不是一个主机在进行上述过程,而是所有的主机都有共识,都在慢慢发。

网络一旦拥塞了,网络的入口流量瞬间减少,就给了网络以充足的时间来进行数据推送,数据路由,推送到指定的服务器,这样就会让网络缓一口气,网络就慢慢的缓过来了。这种做法叫做慢启动机制。


此处引入一个概念程为拥塞窗口:

  • 发送开始的时候,定义拥塞窗口大小为1。
  • 每次收到一个ACK应答,像上面这样的拥塞窗口增长速度是指数级别的。
  • 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较。
  • 取较小的值作为实际发送的窗口。

此处插播一条滑动窗口的知识点:

  • 发送方发送报文数据时,以前是没有考虑网络问题的。
  • 现在回头看,发送方既要考虑对方的接收能力,也要考虑网络的承受能力。
  • 网络特别好的时候拥塞窗口就会特别大,就以对方接收能力响应报文的16位窗口为主。
  • 网络特别差的时候就以网络为主,就以拥塞窗口的大小为数据量发送。

所有的主机都要遵守这样的规则:

  • 一次向目标主机发送数据的量 = min(对方的接受能力,拥塞窗口)

拥塞窗口没有体现在TCP报头当中,相当于TCP连接当中的属性字段。

拥塞窗口大小信息在TCP发送方的发送缓冲区中存储,通常包含于TCP协议栈内部的某个数据结构中。

这个窗口代表的含义是:这个窗口发送数据量以内不会拥塞,超过拥塞窗口可能会导致网络拥塞问题。

因为指数增长前期增长特别慢,所以叫做慢启动。但是随着重传次数增多发送的报文就会变得越来越多,势必导致下一次的网络拥塞。

难道网络是就是在不断地恢复和拥塞之中进行通信吗,那这个网络也太震荡了吧~

网络出现拥塞,最想做什么?尽快恢复!

为了不增长的那么快,因此不能使拥塞窗口单纯的加倍!!

  • 单纯的加倍也没关系,因为发送主机的发送数据量,除了受到拥塞窗口的影响,还要受到对方接收能力的影响。
  • 就算拥塞窗口特别大,对方的接收窗口也会约束发送数据。
  • 但是如果拥塞窗口特别大,甚至形同虚设,那么在全网当中所有主机向目标服务器发送那都是全量发送。
  • 所以就不能单纯的让拥塞窗口单纯的加倍,此处就引入了慢启动的阈值

拥塞控制算法:


理解:

指数增长前期慢,意味着前期都可以发送少量的数据,过了一个临界值,就会增长速度变快。我们要尽快恢复网络通信的正常速度,增长到一定程度,就让它正常的线性增长。

  • 线性增长时,再次出现网络拥塞的话,首先再执行慢启动。
  • 又恢复成了最开始只发送一个报文指数型慢增长。
  • 更重要的是,当发生过一次网络拥塞,还要再做一个工作。
  • 重新计算新的指数到线性的阈值,变成上一次拥塞窗口的一半。

如果网络趋于稳定,拥塞窗口还要增大吗?

  • 是不用的,增大到一定程度,未来要么再发生网络拥塞,再重新执行这个算法。
  • 要么增大到一定程度,网络能发送多大数据已经不用拥塞窗口来决定了。
  • 而是由对方的接受能力来决定了,窗口也就不用再增大了。

11. 延迟应答

不考虑网络拥塞的情况下,一次能够向对方发送多少数据是由滑动窗决定的。如果滑动窗口越大,那么一次向对方发送的数据量也就越大,通信效率也就越高。

本质的说对方主机接收能力越强,发送效率就越高。网络整体的发送效率的问题,最根上就变成了,上层能不能尽快取走数据的问题。

延迟应答:

  • 如果收到了数据先不着急ACK,等一会。
  • 在等上层有可能在此时将收到的数据取走,再给对方响应报文时,窗口大小就变大了。
  • 什么都没做就是等会,就可能让TCP发送数据的发送效率比之前更高了。
  • 收到一个报文不着急给对方立即进行确认,这种策略就称之为 ------ 延迟应答。

窗口越大,吞吐量就越大,传输效率就越高,保证在网络不拥堵的情况下,尽快提高效率:

当然在发送数据时也要考虑网络拥塞的问题,无论有还是没有延迟应答,都要考虑网络拥塞的问题。

只不过有了延迟应答,在一定的概率上,就可以让我们在正常通信时,一次可以让对方可以向我要么不发,要发送就发送更大块的数据。

网络也是IO:

如果一次IO就能将大块的数据传过来,那么就是可以肉眼可见的效率提升。

延迟多久时间呢?

  • 延迟时间太短了,达不到效率提高的目的。
  • 延迟时间太长了,有可能导致对方主机认为数据包丢失进而重传,反而得不偿失。
  • 所以延迟应答的时间必须要设置的合理。

那么所有的包都可以延迟应答么?肯定也不是!

延迟应答策略:

  • 数量限制:每隔N个包就应答一次。
    • 因为确认序号就代表了收到的报文的情况,这样就是变相的延迟 (一般N取2)
  • 时间限制:超过最大延迟时间就应答一次。
    • 这个时间和操作系统有关,最大的延迟时间不会超过操作系统的时间 (超时时间取200ms)

一般用的是数量的方式,更简单一些。


12. 捎带应答

为了提高效率还有一种做法 ------ 捎带应答。

  • 当向一个主机发送数据,当对方响应ACK的同时,该响应报文也可以携带其他数据。
  • 而捎带应答才是TCP通信的真相。

13. 再谈面向字节流

如果发送的字节数太长,会被拆分成多个TCP的数据包发出:

  • 这个拆分谁来做,是用户做吗,我们之前可没做过。
  • 用户拷贝下来的数据太大,发送的时候怎么去拆,拆多少,怎么发,丟包了怎么办,这些问题都是由TCP自主决定的。
  • 关于TCP拆报文的问题,要结合具体下层协议才能谈。

接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区:

  • 发送数据时,一定是用户把数据拷贝给操作系统TCP。
  • TCP再把数据通过网卡驱动,将数据发送到网卡上,然后网卡帮我们把数据发送出去的。
  • 那么接收的时候,也是先被接受方主机的网卡先收到,因为最底层一定是硬件在工作。
  • 冯·诺依曼体系结构就这么规定的,一定是主机上的外设,输入设备先收到数据。
  • 然后再通过一定的方式,让操作系统将数据拷贝到内存,或者操作系统内的接收缓冲区当中。

那网卡收到了其他主机发来的数据,网卡是怎么让操作系统知道网卡里是有数据了呢?从而让操作系统把数据从网卡硬件通过网卡驱动程序拿到操作系统的TCP接收缓冲区呢?

  • 硬件中断,当网卡收到数据时,网卡可以通过和CPU直接相连的中断,发送中断信号。
  • CPU就能识别到外设有就绪的,就可以让CPU强制执行中断对应的上下文程序调度。
  • 操作系统的拷贝程序再把数据从底层拷贝上来。

TCP面向字节流该如何理解?

  • 当发送方向发送缓冲区拷贝一个或多个字节时,可能TCP并没有发送,也可能立马发送了,应用层只管拷贝,TCP按照自己的节奏发送。
  • 可能100个字节的数据封装成了100个TCP报文发出去,也可能100个字节的数据一次封装了一个报文,直接发出去。
  • 也就是说上层调用write,调用一次或者调用100次向缓冲区里拷贝,这就叫做写入的时候与写入的格式没有关系。
  • 而在读取的时候,底层收到了若干个字节,读的时候可以一次读一个字节,一次读俩字节,一 次读十个字节等等。
  • 这也与格式毫不相关,这就叫做读取是面向字节流的。

网络通信基于TCP通信时,发送怎么发(调用write函数几次),接收怎么收(调用read函数几次),二者毫无关系。你写你的我读我的,这就叫做面向字节流。

TCP不关心数据报文和报文之间的边界,但是应用层必须关心!!


14. 粘包问题

读取报文的完整性:

  • 如果应用层读取时,字节定义一个缓冲区,直接从流式空间里去读取的话。
  • 运气好能读到一个完整的报文,运气不好读到半个报文,或者读到一个半的报文,就会把一个完整的报文给破坏了。
  • 数据包明确边界的任务,是应用层自己做的,或者规定定长报文。
  • UDP在应用层角度,不存在粘包问题。
  • TCP没有有效载荷长度的字段,只有报头的长度。
相关推荐
Peter·Pan爱编程14 分钟前
Docker在Linux中安装与使用教程
linux·docker·eureka
kunge20131 小时前
Ubuntu22.04 安装virtualbox7.1
linux·virtualbox
清溪5491 小时前
DVWA中级
linux
MUY09901 小时前
应用控制技术、内容审计技术、AAA服务器技术
运维·服务器
楠奕1 小时前
elasticsearch8.12.0安装分词
运维·jenkins
Sadsvit2 小时前
源码编译安装LAMP架构并部署WordPress(CentOS 7)
linux·运维·服务器·架构·centos
xiaok2 小时前
为什么 lsof 显示多个 nginx 都在 “使用 443”?
linux
java资料站2 小时前
Jenkins
运维·jenkins
苦学编程的谢3 小时前
Linux
linux·运维·服务器
G_H_S_3_3 小时前
【网络运维】Linux 文本处理利器:sed 命令
linux·运维·网络·操作文本