网络原理(UDP与TCP篇)


按从上到下的顺序来解析“协议”

协议

应用层

应用层,对应着应用程序,这也是程序员打交道最多的一层。
调用系统提供的 网络API 写出的代码都是属于应用层的

应用层有很多现成的协议,但是更多的,还是需要程序员根据实际场景自定义应用层协议(如:网络传输的数据要怎么使用,数据是什么样的格式,里面包含哪些内容)

自定义协议,要约定好两方面的内容:

  1. 服务器和客户端之间要交互哪些信息

  2. 数据的具体格式

客户端按照上述约定发送请求,服务器按照上述约定来解析请求;
服务器按照上述约定构造响应,客户端也按照上述约定解析响应。

客户端和服务器之间往往要进行交互的是 “结构化数据”
而网络传输的数据是 “字符串” 或是 “二进制 bit流”
约定协议的过程:就是把结构化数据转成字符串/二进制比特流的过程

结构化数据 转换成 字符串/二进制比特流,这一过程称为 “序列化
字符串/二进制比特流 转换成 结构化数据,这一过程称为 “反序列化

结构化数据:数据以 结构体 或 类 的形式表现,包含很多属性


现成的自定义协议格式

为了让程序员更方便地去约定协议格式,业界给出了几个比较好用的方案,可以直接拿过来套用:

  1. xml
    缺陷:冗余信息较多(标签占的字节反而比数据还多)

  2. json(当下一种主流的数据组织格式)

    json虽然比xml节省了带宽,但还是有重复的key值。如响应中的 id和name,如果有一百个“商家”,id和name就会出现一百次

  3. protobuffer(比json更节省带宽,效率最高的一种方式)

    只是开发阶段定义出这里都有哪些资源,描述每个字段的含义。
    程序真正运行时,实际传输的数据是不包含这样的描述信息

    缺陷:这仅仅是对程序的运行效率高,这样的数据是按照二进制的方式来组织的,并不方便程序员来阅读

    所以虽然 protobuffer 运行效率更高,但是使用并没有比 json 更广泛
    只是对于那些对性能要求非常高的场景,才会使用 protobuffer


运输层

运输层,虽然系统内核已经实现好了,但也需要重点关注(TCP/IP,HTTP),使用的 socket API 都是运输层提供的


端口号

标识一台主机上进行通信的应用程序(应用程序的 “身份证”)

端口号:2个字节的整数。1 - 1024 都属于系统保留给一些知名的服务器来使用(HTTP-80;HTTS-443)


UDP协议首部格式

UDP用户数据报 = 首部 + 数据部分

首部格式如图所示,仅有 4 个字段,每个字段占2个字节,共 8 个字节。

![[Pasted image 20240126185906.png]]

可以看见:协议报头中使用 2 个字节表示端口号,因此,端口号的取值范围就是 0 - 65535

长度:就是表示数据载荷部分有多长(经过以下详解,UDP数据报能发送的数据大小也就接近64KB

一旦数据报的长度超过64KB,那么就可能会导致数据出现 “截断” (截断:数据本来是完整的,但一截断后,后面部分的数据就丢掉了)

对于 UDP 数据报本身来说,Length 字段为 16 位,理论限制为 65535 字节(2^16 - 1),那么能传输的数据为 65535 - IPHeader(20) - UDPHeader(8) = 65507字节。(接近于64KB)

其次,对于网络层,以太网规定 MTU 上限为 1500 字节,如果按照 MTU = 1500 计算,那么 UDP 能传输的数据报上限为 MTU(1500) - IPHeader(20) - UDPHeader(8) = 1472 字节。

  • 如果 UDP 数据报小于等于 1472 字节,则正常发送不用分片
  • 如果 UDP 数据报超过1472字节,那么移交网络层进行分片并在接收方进行重组

检验和:验证数据在传输过程中是否正确(即差错检测)。

UDP 中使用的就是CRC循环冗余校验码,当然还有一些更高精度的校验和算法:md5算法/sha1算法。


TCP报文段的首部格式
首部格式

TCP报文段由 首部数据载荷 两部分组成

下图为TCP报文段首部格式
![[Pasted image 20240127170834.png]]


源端口号 和 目的端口号

源端口号(16位)目的端口号(16位) 和 UDP 是一样的


序号

序号(32位):占32位bit,取值范围 [0,2^32 - 1],序号增加到最后一个后,下一个序号就又回到0。指出本TCP报文段数据载荷的第一个字节的序号

如图:这是一个TCP报文段,它由首部和数据载荷两部分构成。
数据载荷中的每个字节数据都有 “序号”,如图所示。但要注意:它们是 字节数据的 “序号”,而不是数据本身的内容!
对于本例来说,首部中 “序号” 字段应填入的十进制值为 “166”,用来指出数据载荷的第一个字节的序号为166

![[Pasted image 20240128093058.png]]


确认号ack 和 ACK

确认号ack(32位):占32位bit,取值范围 [0,2^32 - 1],确认号同序号,也是增加到最后一个,下一个确认号就又回到0。确认号字段的值用来指出期望收到对方下一个TCP报文段的数据载荷的第一个字节的序号,同时也是对之前收到的所有数据的确认

ACK:标明该报文段是否是应答报文(1表示是,0表示否)

注:(小写ack是确认号字段,大写ACK是六位标志位其中之一)

比如:若 “确认号” 为 n,则表明到 “序号” n - 1 为止的所有数据都已正确接收,期望接收序号为 n 的数据
只有当 “确认标志位ACK(如图)” 取值为 1 时,确认号字段才 “有效”;取值为 0 时确认号字段 “无效”

TCP规定:在连接建立后,所有传输的TCP报文段都必须把ACK置为1


数据偏移(首部长度

数据偏移(4位):占4bit,并以 4 字节为单位。用来指出TCP报文段的数据载荷部分的起始处 距离 TCP报文段的起始处有多远

该字段实际上是指出了 TCP报文段的 首部长度,所以也可以将这个字段称为 “首部长度”

首部固定长度为 20 字节,因此数据偏移字段的二进制最小值为 0101

加上最大扩展首部 40 字节,首部固定长度位 60 字节,因此数据偏移字段的二进制最小值为 1111

举例:
假设该TCP报文段首部的 ”首部长度“ 取值为二进制的 0101,那么首部长度就是20字节

因为二进制0101的十进制值为5,而 “首部长度” 字段以 4字节 为单位,因此 5 乘以 4字节 等于 20 字节

如果TCP报文段首部的 “首部长度” 取值为二进制的 1111,那么首部长度就是 60字节

因为二进制1111的十进制值为15,而 “首部长度” 字段以 4字节 为单位,因此 15 乘以 4字节 等于 60字节

保留

保留(6位):保留字段占 6bit,保留为今后使用,目前应置为0


窗口

窗口(16位):窗口字段占 16bit,以 字节 为单位。指出发送本报文段的一方的接收窗口。 窗口值作为接收方让发送方设置其发送窗口的依据,这是以接收方的接收能力,来控制发送方的发送能力,称为 “流量控制

注:发送窗口的大小,还取决于拥塞窗口的大小(即在接收窗口拥塞窗口取较小者


校验和

校验和(16位):校验和字段占 16bit,检查范围包括TCP报文段的首部和数据载荷两部分。用来检查整个TCP报文段在传输过程中是否出现了误码

与UDP类似,在计算校验和时,要在TCP报文段的前面加上 12字节 的 “” 首部


SYN

同步标志位SYN:在TCP连接建立时用来同步序号
syn就是 synchronized ,但这里的同步和线程那边的同步不一样,这里的 SYN 表达的语义就是:我想和你建立连接

如图所示,这是TCP通过 “三报文握手” 建立连接的过程

TCP客户进程发送的 TCP 连接请求报文段,首部中的同步标志位SYN被置1,表明这是一个TCP连接请求报文段。
TCP服务器进程发送的TCP连接请求 “确认” 报文段,首部中的同步标志位SYN被置1,确认位ACK也被置1,表明这是一个TCP连接请求确认报文段

![[Pasted image 20240128115030.png]]


FIN

终止标志位FIN:用来释放TCP连接

如图为TCP通过 “四报文挥手” 释放连接的过程

不管是TCP客户进程还是TCP服务器进程,它们所发送的TCP连接 “释放报文段”,首部中的 终止标志位FIN 都被置1,表明这是TCP连接释放报文段

![[Pasted image 20240128115543.png]]


RST

复位标志位RST:用来复位TCP连接

RST = 1 时,表明TCP连接出现了异常,必须释放连接,然后再重新建立连接

注:RST 置 1 还可以用来拒绝一个非法的报文段拒绝打开一个TCP连接


PSH

推送标志位PSH:接收方的TCP收到该标志位为 1 的报文段会立即将缓冲区中的所有数据推送给应用程序,而不必等到接收缓存区都填满后再向上交付


URG 和 紧急指针

URG紧急指针字段:用来实现紧急操作。URG取值为 1 时紧急指针字段有效;取值为 0 时紧急指针字段无效

紧急指针(16位):紧急指针字段占 16bit,以字节为单位,用来指明紧急数据的长度

当发送方有紧急数据时,就可将紧急数据 “插队” 到发送缓存的最前面,并立刻封装到一个TCP报文段中进行发送。
紧急指针会指出本报文段数据载荷部分包含了多长的紧急数据,紧急数据之后是普通数据。
接收方收到紧急标志URG为1的报文段,就会按照紧急指针字段的值,从报文段数据载荷部分取出紧急数据(也就是说这个报文段有紧急数据也有普通数据,先把紧急数据取出来)并直接上交应用程序,而不必在接收缓存区中排队


扩展首部

TCP报文段首部除了 20字节的固定部分,还有最大40字节的选项部分。增加选项可以增加TCP的功能

  • 目前有以下功能:
    1. 最大报文段长度MSS选项:TCP报文段数据载荷部分的最大长度

    2. 窗口扩大选项:为了扩大窗口(提高吞吐率)

    3. 时间戳选项:有以下两个功能

      • 用来计算往返时间RTT
      • 用来处理序号 超范围 的情况,又称为 “防止序号绕回PAWS
    4. 选择确认选项:用来实现选择确认功能


填充

填充:由于选项的长度可变,因此使用 “填充” 来确保报文段首部能被4整除(因为数据偏移字段,也就是表示首部长度的字段,是以4字节为单位的)


TCP的可靠传输

TCP的初心:就是为了解决 “可靠传输” 的问题。

当然,网络通信的过程是非常复杂的,没有谁能够保证发送方发送的数据能 100% 地到达接收方。

这里所说的 “可靠性” 指:发送方能够知道接收方是否收到。

而可靠传输的最关键部分,就是TCP的确认应答机制


TCP的超时重传

超时重传,可以认为是 确认应答补充

在编程中,TCP Socket 在内核中有一个 接收缓冲区(一块内存空间),发送方发来的数据,是要先放到接收缓冲区中,然后应用程序调用 read() / scanner.next() 才能读到数据。这里的 “读” 操作其实就是在读取接收缓冲区

当数据到达接收缓冲区后,接收方首先会判断这个数据是否已经在之前出现过。而这个判断的依据,就是数据的 “序号

接下来介绍一个超时重传的机制,它就用到了用序号来分辨重复与不重复的数据 —— 停止-等待协议SWStop and Wait)

机制原理:

发送方每发送完一个TCP报文段后停止 发送下一个报文段,必须等待来自接收方的 确认报文ACK否认报文NAK。收到这两个报文,发送方会做不同的应对:

若收到确认报文ACK,则继续发送下一个报文段
若收到否认报文NAK,则立刻重传该报文段

  • 这一过程像两个人通信,当一方给另一方说话时,对方一定要回复一下说”我收到了你的消息“

但这个机制会有以下三个问题:

  1. 发送方给接收方发送报文段时,该报文段在传输过程中丢失了

    解决方案:添加一个超时计时器,如果超时,则重传该报文段

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

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

  1. ACK 或 NAK 在传输过程中丢失

    ACK 或者 NAK 在传输过程中丢失,势必会造成 超时重传,那么发送方就会发送重复的数据,接收方如何判断数据是否是重复的呢?

    解决方案:给发送方的报文段加上序号,由于停止-等待协议的特性,只要保证每发送一个新的报文段的发送序号与上一个不同即可。因此采用一个比特来编号就足够了

    在实际编程场景中,并不是按照一个比特位来编号的。

    应用程序读取数据的时候,是按照序号的先后顺序连续读取的,一定是先读序号小的数据,再读序号大的数据(可以把接收缓冲区的这个队列想象成带有优先级的阻塞队列),此时 socket API 中就可以记录上次读的最后一个字节的序号是多少。

    比如:上次读到的最后一个字节的序号是 3000,而新收到的数据包的序号是1001,那么说明这个 1001 数据包一定是已经读取过的了,可以直接将这个新的数据包判定为 “重复的包” 直接丢弃

    接收方丢弃重复的数据包,再次给发送方发送ACK,以免再次超时重传(“我再发送一个ACK过去告诉你我已经收到这个数据包了,你别再发了,赶紧地发下一个”)


  1. ACK 没有丢失,仅仅是 “迟到”,导致超时重传,然后发送方收到两份ACK

    1. ACK因为某些原因 “迟到” 了,导致发送方对 0号数据包 的超时重传(0号数据包发送了两次)。

    2. 在重传 0号数据包 的过程中,发送方收到了 “迟来的ACK,于是发送方认为对端接收到了 0号数据包,然后发送 1号数据包。

    3. 接收方收到 重传的0号数据包,将该重复的数据丢弃,并又发送了一份ACK,告诉发送方我已经收到了该数据,不要再发了

    4. 问题就来了:第二份ACK,本意是告诉发送方,你的0号数据包重复了,不要再发了。但是这一过程中,1号数据包已经发送出去了,发送方就会认为:这第二份ACK,是1号数据包的确认报文ACK

解决方案:给ACK或NAK 编号


总结:对数据包进行编号这件事,是很有意义的!

可以理解为:TCP在内核中的接收缓存区有两个功能:
接收缓冲区可以认为是一个 “优先级队列” ,以序号作为优先级的参考依据

  1. 能对数据包去重(不再接收重复的数据)

  2. 可以对收到的数据进行排序,按照序号来排序。确保应用程序 读到的数据 和 发送的数据 顺序是一致的!

按序号来决定数据的进入顺序这件事情,有点像我在驾照考试,我的序号是37,我第一个到考场,没用~ 必须要等前面的36个人到,等他们按照序号一次进入候考室,我才能进去。

这件事跟数据交付给应用程序一样的,如果后来的数据能够先进入应用程序,那么就可能会对程序处理逻辑造成影响

注1:上述谈到的 ack、重传、保证顺序、自动去重等等功能,都是TCP内置的,我们使用TCP的API时,像调用 outputStream.write() 这么一个表面上很简单的代码,上述我们谈到的功能就全部自动生效了

注2:如果使用UDP,UDP的API并没有内置这些功能,所以在写的时候就需要程序员自行考虑了


TCP的连接管理

如图所示,TCP连接有以下三个阶段:

在这里插入图片描述

在我们写程序时,比如在客户端执行 socket = new Socket(serverIP, serverPort) 这一语句,就是在建立连接。 这一操作,只是调用 Socket 的API,真正连接建立的过程,是在 操作系统内核 里完成的。


滑动窗口

确认应答、超时重传、连接管理可靠传输 提供了一个良好的条件

但是凡事有利必有弊,可靠传输的代价就是:传输效率比较低

在确认应答机制下,每次发送一个数据必须得收到 ACK 后才能发送下一个数据,这就导致有很大一部分的时间是花在了等待 ACK 上了

而滑动窗口的提出,就是为了解决上述问题的:滑动窗口就可以在保证可靠传输的基础上,提高效率(这里的提高效率,其实是降低损失,而不是增加速度)

虽然通过这个机制提高了效率,但这效率也不可能高于UDP

如图:滑动窗口的核心操作就是:批量传输,把多次请求的等待时间,使用同一份时间来等,即减少了总的等待时间

![[Pasted image 20240130103134.png]]

丢包会分为两种情况:

  1. 数据包都到达了,ACK丢了
    这种情况并不要紧,可以通过后续的ACK来确认

  2. 数据包丢了

    当某一段报文段丢失后,服务器就会一直发送 ack = 1001 的ACK,告诉客户端:”你丫的,我只收到了 1001 之前的数据,我要的是 1001 之后的数据,你在干神魔?“

    如果发送端主机连续三次收到同一个 “1001” 的 ACK,就会将对应的数据重新发送

    这里的重传机制,不是按照 “超时” 来重传的,而是按照 “次数”,做到了 “针对性” 的重传,哪个丢了就重传哪个,整体的效率没有额外损失,这种重传称为 “快速重传"

    这个时候接收方收到 1001 之后,再次返回的ACK就是 ack = 7001 了(因为 2001 - 7000 的数据接收方已经收到,被放到了接收方操作系统内核的接收缓冲区了)


流量控制(流控)

虽然说滑动窗口能让更多的数据等待同一块时间,但窗口大小可以无限大吗?显然是不能的,要知道,TCP安身立命的本钱就是可靠传输,任何提升效率的行为都不应该影响到可靠性

接收方的接收缓冲区满了,如果继续发送数据,接受方只能丢弃这些数据。这和生产者消费者模型理念是一致的。

这也是为什么接收方发送的ACK会带有 rwnd 的值(rwnd代表接收方能接收的窗口大小)

当然,还会出现下述情况:接收方的接收缓冲区满了,发送的ACK中 rwnd 置为 0,此时发送方不能再发送数据。过了一会儿后,接收方接受缓冲区又有了一些存储空间,然后发送 ACK,将 rwnd 置为 300,但这个ACK在中途丢失了,这就会造成 ”A一直在等待B发送非0窗口的通知“ 而 ”B也在一直等待A发送的数据“ 的局面,导致死锁

当然,这也有解决办法:TCP为每一个连接设有一个持续计时器,只要TCP连接的一方收到对方的 0窗口 通知,就启动持续计时器,若超时,就发送一个 0窗口探测报文,仅携带 1字节 的数据。(TCP规定:即使接收窗口为0,也必须接收0窗口探测报文段、确认报文段以及携带有紧急数据的报文段


拥塞控制

上述的流量控制是站在**接收方的角度来约束发送方发送速率的。**

输入负载:代表单位时间内输入给网络的分组数量
吞吐量:代表单位时间内从网络输出的分组数量

理想状态下的拥塞控制网络,在吞吐量达到饱和(由于硬件的限制,吞吐量是有上限的)之前,网络吞吐量是应该等于所输入的负载

小于理想曲线,说明网络中出现了堵塞,数据在中间某一网络结点需要排队等待发送

![[Pasted image 20240130153500.png]]

所以总的原则是:发送窗口的大小,还取决于拥塞窗口的大小。即在接收窗口拥塞窗口取较小者

拥塞窗口是如何试出来的呢?具体有以下4种算法:

在介绍算法之前,先介绍执行算法的前提:

  1. 发送方维护一个叫做拥塞窗口cwnd的变量,其值取决于网络的拥塞程度,是动态变化的(靠这个变量来维持窗口大小的合理动态平衡)
  • 如何维护cwnd?
    只要网络没有出现拥塞拥塞窗口就大一些;但只要出现拥塞拥塞窗口就小一些(网络通畅,我这窗口就大一些,让能发送的数据更多一些;出现拥塞了,我发再多数据,接收方也可能收不到啊(因为拥塞导致丢包),所以窗口就适当小一些)

  • 那么如何判断网络是否出现拥塞呢
    只要数据包发生超时重传,就判断网络出现了拥塞

  1. 发送方将拥塞窗口作为发送窗口swnd,即 swnd = cwnd
  1. 维护一个阈值(ssthresh)

当 cwnd < ssthresh 时,使用慢开始算法

当 cwnd > ssthresh 时,使用拥塞避免算法

当 cwnd = ssthresh 时,使用慢开始也可以,拥塞避免也可以

  1. 慢开始
    滑动窗口所能包含的数据报文段,以指数级增长,直到达到阈值 ssthresh 的值

  1. 拥塞避免
    当窗口内包含的报文段数量达到阈值,窗口新增进来的报文段,以线性+1的规律增长

注:

当传输过程中发生了超时重传,即判断网络很可能出现了拥塞,随后进行以下工作:

  1. 将 ssthresh 值更新为发生拥塞时,cwnd值的一半
  2. 将 cwnd 值降为 1,然后重新执行慢开始算法

上述的注,也存在这样一种情况:个别报文段只是在网络中丢失,实际上网络并未发生拥塞,那么按照上述规则,就会降低传输效率,于是又提出了两个新的算法,在新算法中,就不会让cwnd值降为1并重新执行慢开始算法

  1. 快重传
    接收方给发送方发送连续3个重复确认,发送方一旦收到,就立即将相应的报文段立即重传,而不是等超时计时器(这一算法目的是为了让发送方尽快重传,如果确认报文段能在一定时间内顺利到达发送方,也从侧面证明网络实际上并没有拥塞)

  2. 快恢复
    发送方一旦收到3个重复确认,就知道网络中并没有拥塞,于是不启动慢开始算法,而是执行快恢复算法,快恢复算法方案:

    • 第一种方案:发送方将 ssthresh值拥塞窗口cwnd值 调整为当前窗口的一半;开始执行拥塞避免算法
    • 第二种方案:把快恢复开始时的拥塞窗口cwnd值 = 新的 ssthresh + 3(既然发送方收到3个重复确认,就表明有3个数据报文段已经离开了网络,这3个报文段不再消耗网络资源,而是停留在接收方的接收缓存区中,可见网络中不是堆积了报文段,而是减少了3个报文段。因此可以适当把拥塞窗口扩大些)

![[Pasted image 20240201193522.png]]


延时应答

延时应答的作用:基于滑动窗口,尽可能再提高一些效率

延时应答:结合滑动窗口以及流量控制,能够通过延时应答ACK的方式,让窗口值尽可能的大一些,即接收方收到数据之后,不会立即返回ACK,而是等一会再返回ACK。

因为接收方需要时间去消化数据,等一会,就是在给接收方提供更多的时间将数据上交给上层应用程序,这样接收缓冲区的空余空间就会相对比较大,一次性能够接收的数据也就更多,窗口值也就能更大

当然也不可能给所有的包都执行延时应答策略:

  1. 每隔N个包就应答一次
  2. 超过最大延时时间就应答一次

捎带应答

捎带应答:基于延时应答,提高传输效率

延时应答本质上是修改窗口值的大小

而捎带应答,走的就是另一条方向:尽可能地把能合并的数据包合并起来

也就是说,执行捎带应答策略,后续每次传输请求和响应,都可能触发捎带应答,把接下来要传输的业务数据 和 对上次数据的ACK 合二为一

注:是有可能触发捎带应答,要看两个数据包发送的时机。(延时应答+捎带应答,四次挥手就可能合并成三次挥手)


面向字节流

创建一个TCP的 socket,同一时间会在内核中创建一个 发送缓冲区 和一个 接收缓冲区

  • 调用 write() 时,数据会先写入 发送缓冲区 中;
  • 如果发送的字节数太长,会被拆分成多个TCP的数据包发送;
  • 如果发送的字节数太少,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者到了合适的时机,才会发送出去;
  • 接收数据时,数据也是从网卡驱动程序到达内核的接收缓冲区,然后应用程序调用 read() 从接收缓冲区中拿数据
  • 另一方面,TCP的一个连接,一方既有发送缓冲区,也有接收缓冲区,则说明该端既可以写数据,也可以读数据,这就叫 “全双工

由于缓冲区的存在,TCP程序的 “读” 和 “写” 也就不需要一一匹配,如:

  • 写100个字节数据时,可以调用1次 wirte() 写100个字节,也可以调用 100 次 write(),每次写 1 个字节
  • 读100个字节数据时,也是同理

粘包问题

多个应用层数据混淆这一情况,就称为 “粘包”。粘包问题不是TCP独有,而是只要是面向字节流的,就存在这个问题。

解决 “粘包问题” 的方案:

  1. 通过特殊符号作为分隔符,见到分隔符,就视为一个数据包结束。(在TCP写的回显服务器,就是使用这一方法)

    • 使用任意字符作为分隔符都可以,这是程序员可以约定的

    就像有些网站取名不让你使用一些特殊的字符,可能就是出于这个原因

  2. 指定一个包的长度。 在包头位置,加上一个特殊的空间来表示一个完整数据包的长度即可

注:UDP协议并没有这个问题,因为UDP传输的的基本单位就是一个UDP数据报,在UDP这一层就将数据分开了。


异常情况

上述我们谈到的,数据在传输过程中基本上都很顺利,最多不过是 “滞留” “丢包” 两种问题,并且重传之后就能够收到,所以问题不大。

但如果一直丢包一直重传,但就是收不到,怎么办?甚至说网络直接出现了故障,这又怎么办?


  1. 其中有一方出现进程崩溃
    进程无论是正常结束,还是异常崩溃,都会触发 “回收文件资源”、”关闭文件“ 这样的效果(因为这些是由系统内核来完成的),然后会正常进行 ”四次挥手“ 释放连接

  1. 其中一方关机(正常关机)
    正常关机,系统也会先强制终止进程(kill进程),所以本质上和第1种情况是一样的。当然,关机之后可能 “挥不完四次”:
    • 如果挥得快,那么是能够顺利删掉本端和对端的连接信息
    • 如果挥得不快,但至少也能发送第一个FIN 给对端,至少可以告诉对方我这边要删除连接信息了。而对端收到FIN后,返回ACK,并且也发送FIN,但这个FIN不再会有ACK,这样势必会导致重传重传几次还没有ACK,那么就会单方面释放连接信息。

  1. 其中一方出现了断电(更突然性的关机)
    这种情况,机器瞬间关机,肯定是来不及发送FIN的。这又分为两种情况:
    1. 断电是接收方。发送方就会突然发现没有收到ACK了,这就会重传数据,重传几次发现还是收不到ACK,发送方就会尝试 “复位连接” (相当于清除原来TCP中的各种临时数据,重新开始连接。)
    2. 断电的是发送方。这在 [[6.2 TCP释放连接过程]] 最后一章中提到过,接收方会发送 “心跳包” 来检测发送方是否正常,如果发送方 “没心跳” 了,接收方也会尝试复位,不成功则单方面释放连接

  1. 中间的网线出现故障
    这就是 3. 中 两个情况的结合了,各自按照各自的逻辑进行判断。
  • 28
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Prectie.RTE

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值