
🎬 那我掉的头发算什么 :个人主页
🔥 个人专栏 : 《javaSE》《数据结构》《数据库》《javaEE》
⛺️待到苦尽甘来日

引言
背熟了UDP和TCP的定义,面试一追问就卡壳?总把"不可靠""可靠"当标签死记,却搞不懂协议设计的底层逻辑?
这篇文章拒绝冗余!从实操角度拆解UDP的"不可靠"细节,再顺着TCP的设计思路,讲透确认应答、滑动窗口等核心机制。不管是备战面试的应届生,还是夯实基础的小白,都能快速理清两者差异、吃透底层逻辑。
接下来,从UDP的"小缺陷"切入,揭开网络传输的核心秘密!
文章目录
UDP
在前文的 UDP 基础学习中,我们已经掌握了它的四大核心特点:无连接、不可靠传输、面向数据报、全双工。
其中,无连接、面向数据报、全双工这三点都很好理解,但 "不可靠传输" 总觉得有点抽象 ------ 我们只知道它的核心逻辑是 "数据发出去就不管了",至于这种 "不可靠" 在实际代码中到底如何体现、会遇到哪些具体问题,却没有直观的感受。
OK,既然概念上的理解不够透彻,那咱们就从实操角度重新出发,彻底扒明白 UDP 的 "不可靠" 到底藏着哪些细节!
UDP协议端格式
为了防止大家忘了报头是啥,简单的复习一下:

传输层数据包=报头+应用层数据包
然后咱们就可以开始讲讲报头啦

首先源端口号目的端口号 就是咱们之前提到的五元组,16位也就是0-65535的范围。不过,一般把1024以下的端口保留,我们自己写代码一般使用1024-65535之间的端口。
至于这0-1023之间的端口号一般是要分给某些应用层协议的,比如http:80,https:443。
这里的UDP长度代表的是整个UDP的长度(报头+载荷),最大长度是64KB,在UDP产生的那个年代,这个空间已经相当的大了,但是到了现在,这个空间真的很不够用。
试想,在互联网不断发展的过程中,传输的数据越来越复杂,内存越来越大,最终会超出64kb这个范围,此时数据就会不完整,导致结果出错。
在当时,有两个解决方案:
1.在应用层进行拆包操作,将一个大的数据包分成许多小的数据包,一份数据多次传输。但是其实实现起来非常复杂,逻辑上很难处理。
2.使用TCP协议,没有数据包长度限制。
从开发的角度来看,肯定优先选择简单好实现,不容易出错的。于是UDP就这样慢慢的被抛弃了... ...
咱们刚刚学过的HTTPS中,校验和 的作用是防止黑客入侵。而UDP的校验和,与安全性无关,主要是防止传输过程中发生"比特翻转"。(0-1,1-0)
在数据发送之前,先计算一个校验和,把整个数据包的数据全部代入。将数据和校验和一起发送给对端,接收方收到后再计算一次校验和,和收到的校验和对比,如果UDP发现校验和不一致,就会把数据直接丢掉。。。
UDP的检验使用的是把所有数据都当作整数加起来的方式计算校验和,这样可能导致两个比特位同时变化,一个1-0,一个0-1,最终校验和相同。不过,比特翻转本身就是小概率事件,两个同时翻转正好抵消概率更小,因此一般忽视~
TCP
UDP作为一个诞生很早的协议,虽然目前来看似乎是一种老了不中用了的状态,一身的缺点。。但是正是因为有他在前面探路,后面的协议才能以它为鉴,修正这些缺点,就比如TCP。咱们前面说了,解决UDP承载数据有限的问题的方法之一就是使用TCP协议。那么,TCP协议到底强在哪儿呢???
TCP报文格式

TCP的报头很复杂,除了源端口目的端口以及校验和之外,其他的几个部分我们都没见过。甚至还留了一部分的空间作为选项。
首先,4位首部长度。首部长度代表的是TCP报头的长度,不过这里的1位不单单是指一个比特位,而是指四个字节,也就是说,4位首部长度代表着报头最大长度为60个字节,去除了选项部分,固定的部分字节数为20,因而选项部分最大字节数为40.
6位保留位主要是针对UDP长度不够却又无法扩展的问题,预留了一些保留位,以防未来可以用到。

这六个称为标志位,在TCP这里的地位很重要。
16位校验和依然是检验数据是否出错。
至于剩下的几个部分,在后面讲TCP核心机制时都会涉及到。
1.确认应答
我们都知道TCP的一大特点就是可靠传输,保证可靠传输的一个重要前提就是,发送方需要知道自己发送的信息是否被对方收到 。此时需要接收方返回一个"应答报文 ",发送方收到应答报文,就可以认为对方收到了消息。

不过,互联网里存在着一个奇怪的现象:"后发先至"。后发送的数据到达对端的时间早于先发送的数据。在现在的网络通信中,咱们似乎不太能见到。但是据父母辈所说,当年他们短信交流(没QQ呢)的时候,有时会出现发了两条消息结果第二条消息比第一条消息先被对方收到。在这种情况下确实很容易闹出一些乌龙。。。不过还好,在咱们现在的社交软件中这种现象很少遇见啦,这也多亏了TCP协议做出的一些优化策略:
第一步:TCP 先给数据做 "字节级编号"
TCP 是面向字节流的协议,它不会按 "消息条" 编号,而是给每个字节都分配一个连续递增的序号(比如第 1 个字节序号是 1,第 1000 个字节序号是 1000,第 1001 个字节序号是 1001,以此类推)。
第二步:TCP 报文里的 "序号" 和 "确认序号"
每个 TCP 报文的头部会带两个关键字段:
序号:填的是当前报文载荷部分第一个字节的序号(比如一个报文携带 1-1000 字节的数据,它的 "序号" 就是 1);
确认序号:填的是自己已经收到的最后一个字节的序号 + 1(比如收到了 1-1000 字节,确认序号就是 1001)。
第三步:应对 "后发先至" 的核心逻辑
当出现 "后发的包先到"(比如先发的 1-1000 字节还没到,后发的 1001-2000 字节先到了):
1.TCP 会先根据报文的序号,把这个 "提前到的包" 暂存到对应的字节位置(比如 1001-2000 的包会存在序号 1001 开始的**接收缓存区**);
接收缓冲区:

2.此时的确认序号不会更新(因为 1-1000 字节还没收到,确认序号仍然是 1,告诉对方 "我还没收到 1 开头的包,你得重发 / 补发");
3.等先发的 1-1000 字节包到达后,TCP 会按序号的连续顺序,把 1-1000 和 1001-2000 的字节流拼接起来,再统一交给应用层;同时把确认序号更新为 2001,告诉对方 "我已经收到 2000 之前的所有数据了"。
2.超时重传

上图表示的是一个正常情况下数据的传输过程。A-B传输一个数据,B返回一个ACK表示自己受到了数据。
如果A发出数据之后,达到了等待的时间上限,仍然没有收到B的ACK回复,A就认为数据传输过程中发生了丢包
为啥会丢包?
路由器、交换机这些负责转发数据的设备,处理数据的能力是有限的 ------ 就像路口有 "最大能同时通过的车流量" 一样,这些设备也有 "最大能同时转发的数据包量"。
当短时间内要转发的数据包太多,超过了设备的 "处理 / 转发上限",设备就会处于 "忙不过来" 的状态。
这些设备会有个 "接收缓冲区"(类似 "临时停车区"),用来暂存待处理的数据包。但如果数据量太大,缓冲区被占满了------ 后面再来的新数据包就没地方放了,这时候设备只能把新到的数据包直接丢弃。
在我们确定是不是丢包时,会引入一个超时时间,这个超时时间并不是固定的,而是动态变化的。
假如A->B传输数据,丢包的超时时间阈值是T,当传输发生超时之后,就会延长这个时间阈值。此时会触发重传机制,将数据重新发送给对方。由于本身丢包的概率就很小,根据概率论的道理来说,传的次数越多,数据到达对方的概率就越大(比如丢包率是10%,两次都没到的概率是1%)。重传了还没到,说明当前网络的丢包率真的很高了,或者就是网络出现了严重故障。(事实上丢包率10%就已经达到了你玩游戏卡成PPT的程度了)此时就算继续重传也意义不大了,也就是说,如果多次延长时间多次超时到达了一定程度,就会放弃这一次传输,认为网络出现了严重故障。
不管是A-B发送数据丢失了还是B-A返回的ACK丢失了,发送方A的做法都是重传,因为A自身区分不了。
不过如果是ACK丢失,A重新发送数据,可能使得应用层最终读到了相同的两个数据,如果这个数据是类似于扣款的操作的话,就会产生重大工作失误!所以TCP会在接收方的数据缓冲区内进行去重操作:根据到来的数据的序号在缓冲区内查找,如果存在相同的就直接丢弃,没相同的就根据序号放进去。
确认应答和超时重传的两大机制,保证了TCP能够可靠传输=》网上某些资料说可靠传输的关键机制是"三次握手"的观念是错误的
至于为什么,且待我深度分析一下三次握手吧!
3.连接管理
服务器和客户端能够建立起有效的连接以及安全的断开连接,靠的是三次握手以及四次挥手 机制,对应着对端建立连接以及断开连接 。
握手操作与现实中打招呼时握手类似,没有实际的业务,只是"打个招呼"。也就是说,发送一个不携带实际业务的数据,通过这个数据和对方"打个招呼"

大致流程应该跟上图类似:其中的SYN其实对应着的是synchronized这个单词,"同步"。意味着数据上的同步,可以理解为一个SYN是把自己的关键信息发送给对方,让对方保存好,然后让对方把自己的关键信息也发来。
标志位里也有SYN:

由于返回ACK和发送SYN都是内核负责的,所以中间B-A发送信息的操作可以合并成一次信息的发送:

这个合并操作,大大提高了传输效率。咱们刚接触网络的时候就提到过分装和分用,如果两个数据分开发就要重新封装分用两次,合并到一起只需要一次,大大提高了效率!

上图是三次挥手的详细图解啦(看方框内哦),在客户端和服务器都没有建立TCP连接时二者均处于CLOSED状态 。服务器启动t后进入LISTEN 状态 ,开始"监听"是否有客户端的连接请求。客户端调用connect进入 SYN_SENT ,服务器收到后进入 SYN_RCVD ;三次握手完成后,双方进入ESTABLISHED 状态(连接已建立,像 "电话接通"),可开始传输数据。细节:SYN_SENT、SYN_RCVD 这两个状态,正常情况下肉眼很难观察到,转换速度很快。
三次握手的作用是啥?为什么要进行三次握手?
1.初步的观察网络的通信链路是否畅通(网络畅通是可靠传输的前提条件)
2.验证通信双方的发送能力和接受能力是否正常
3.三次握手可以协商一些关键信息
三次握手协商的关键信息之一是连接的初始序号是多少~每一次发送数据包,第一个数据包的序号并不是从1开始的,而是取决于对端的协商。这样做也有很多好处,比如:在AB第一次建立连接时,发送的数据包中有一个数据包迷路了,没有成功的到达B。AB断开连接之后又重新连接,在第二次连接过程中这个数据包却到达了B,此时就很尴尬,因为这个时候再连接,AB两端的环境都不是当时发数据包时的环境了,甚至可能不是同一个程序了,此时B应当拒绝处理这个数据包。那么因为我们是协商过初始序号的,假如第一次是从100000开始,第二次是从800000开始,此时出现了一个1000000-800000之间的序号,B这边可以立即判断出这个包出问题了,就可以将它舍弃了。
四次挥手


此处的FIN也是一个标志位,如果为1就代表着这个数据包发送的信息是断开连接请求。
三次挥手中SYN和ACK可以合并到一起,此处一般不可以(有时候可以)。
不可以的原因:
ACK是由内核控制返回的,而FIN是代码执行到Socket.close()这部分之后才主动释放连接。这两个行为的时机很可能不是同一个时机。(延时应答机制可能会使这个时机相同,后面解释)

TIME_WAIT&CLOSE_WAIT:谁是主动发起FIN的一方,谁就是TIME_WAIT(为了给最后一个ACK丢包的行为进行托底),谁是被动发起FIN的一方,谁就是CLOSE_WAIT(等待程序调用代码Socket.close()来释放连接,一般速度很快,代表没有BUG的话,程序会尽快释放连接)。
网络传输中,随时会出现丢包问题,三次握手四次挥手也不例外。丢包就需要出发超时重传机制,三次握手的过程中超时重传可以很好的解决这个问题。在四次挥手中,FIN和第一个ACK丢了也可以使用超时重传解决,但有一个特殊的情况:如果最后一个ACK(A->B)丢了,如果此时没有TIME_WAIT这个状态,而是A直接断开连接,后面B收不到ACK重传FIN时就无人处理了。所以此时A收到FIN后,不会立即释放连接,而是等待对方以防重传FIN,这个等待时间会很长,能确认对方确实不会重传FIN时再释放~~这个时间通常为2 倍 MSL(报文最大生存时间),MSL 是报文在网络中的最大存活时长(一般为 1-2 分钟),确保对方的重传 FIN 能被 A 接收。
重点总结(面试高频)

4.滑动窗口
刚才我们讨论了确认应答策略, 对每⼀个发送的数据段, 都要给⼀个ACK确认应答. 收到ACK后再发送下⼀个数据段. 这样做有⼀个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候

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

前几个数据包不做等待,直接往后发,发到一定的量(窗口大小)后再等待。相当于用一份等待时间等待多组ACK。并且只要来一个ACK就立即发送下一个 数据,不需要等待所有ACK都到,这样进一步增加了效率。
窗口大小(不需要等待,能够连续发送的最大数据量)
窗口大小需要有限度,不能无限增加,否则无法保证可靠性传输(我们是在可靠的前提下提升效率)。

从宏观上来看,上下两个窗口就在不停的快速"滑动"。
滑动窗口在数据交互过程中,当然也会丢包:
情况一:数据包到达,但是ACK丢了

注意:确认序号的意义是,该序号之前的序号的数据全部都收到了。
所以,上述情况下,我们不需要做处理,只要5001ACK到了,就代表着前面的ACK都到了。
情况二:数据包丢失了

若中间某分段(如 1001-2000)丢失,即使后续分段(2001-3000 等)已成功接收,接收端仍会持续向发送端反馈 "期望接收的序号(如 1001)",发送方发现了多个1001的序号ACK,就会意识到数据丢失了,触发重传。
发送端收到多次重复的期望序号后,可识别出对应分段丢包,仅重传该丢失的分段(1001-2000),已成功传输的分段无需重传,我们将这种行为称为快速重传。

丢失的分段重传成功后,接收端会补全数据的 "空缺",后续数据可从补全后的位置(如 2001)继续传输,保障数据的有序性与完整性。

数据被接收后会被放到接收缓冲区中,如果前一个位置的数据还没到,这个位置会被预留出来。
5.流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等⼀系列连锁反应.

比如说,此时接收缓冲区内部剩余的空间为N,滑动窗口的大小就不能超过这个N。一旦超过了,就很有可能造成缓冲区被填满导致丢包。

窗口大小这部分就是用来标识接收缓冲区的容量大小,在选项区域里,有一个属性为窗口扩展因子,占两个比特位。所以窗口大小最大空间为256K。

6.拥塞控制
虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送⼤量的数据. 但是如果在刚开始阶段就发送⼤量的数据, 仍然可能引发问题.
因为网络上有很多的计算机, 可能当前的网络状态就已经⽐较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的.
TCP引入慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
总体来说这个发送速度(窗口大小)是动态平衡的。丢包了我就减速,没丢包我就继续加速,一直打擦边球。

其实拥塞控制和流量控制都是在控制发送方的窗口大小,最终以两者的较小值作为最终速度(满足两种机制)。

慢启动-指数增长到阈值-线性增长到丢包-窗口缩短到一半-继续增长... ...
7.延时应答
默认情况下,接收方都是在收到数据的一瞬间立即返回ACK。但是可以通过延时返回ACK的方法来提高效率(不一定100%提高,但是大多数情况下都能提高)。

如果应用程序读取数据速度很快,缓冲区在延时一会儿之后可以被消耗掉很大的空间,剩余空间变多,我们就可以增大窗口大小,一次性多发送一些数据。但是如果这个时间内接收方收到了新的数据,有可能反而剩余空间变小。所以不能保证100%提高效率。
延时应答也是有限制的,如果发送的数据量很多,建议每隔N个包发送一次应答,如果数据量稀疏,建议每隔一段时间发送一次。返回的ACK数量其实不重要,毕竟因为确认序号的存在,只要返回了后面的ACK,我们就能认为之前的数据都被收到了,所以不会对程序有影响。

8.捎带应答
捎带应答是基于 TCP "延迟应答" 的扩展机制 ------ 延迟应答本身是让 ACK(确认包)延迟一段时间发送,而捎带应答进一步优化了 ACK 的发送时机。当接收方需要返回业务响应时,将原本需单独发送的 ACK(包含确认序号、接收窗口大小等信息),嵌入到业务响应数据包中一并发送;这些 ACK 相关信息封装在 TCP 报头中,不会影响业务响应的数据内容。(毕竟ACK本身也没有什么实体数据,只是标志位ACK为1,给出确认序号)
本质上是"懒汉"思想的体现,将多个数据包放在一起发送,只用封装分用一次,提高了效率。
比如我们的四次挥手:第二次FIN和被动断开连接方的ACK就可以一起发送回去。四次挥手此时可以看成三次挥手啦~~~
不过三次握手因为本身就是一起的,不能称之为捎带。
9.面向字节流&粘包问题
在UDP协议中,以数据包为单位发送数据,没有一个数据包都是一个完整的数据。但是TCP采用字节为单位发送数据,我们会混淆包与包之间的边界,无法确定从哪儿到哪儿是一个完整的应用层数据包。(毕竟是散着发过来的)

比如上述情况下,我们在read数据时,不知道要读几个字节了。。。我是读A还是AA还是AAAB???
这个问题在TCP层面上没有任何解决方案,是无解命题。但是我们可以在应用层通过协议来规定包与包之间的边界。
1.约定好分隔符来划分包
2.约定包的长度(比如约定每个包开头四个字节表示的是数据包一共有多长)

HTTP中使用的传输层协议就是TCP,这两种处理粘包问题的方案都用到了:
1.GET请求,没有BODY时,使用空行来划分包
2.POST请求,使用Content_length来表示BODY的长度
10.异常处理
TCP在通信过程中会遇到一些特殊情况:
1.某个进程崩溃了
其实进程崩溃和主动退出没区别,进程崩溃后回收文件描述符,资源也会被释放,自动调用Socket.close()然后触发四次挥手(进程没了,但是TCP连接信息还存在,还是可以正常四次挥手)
2.主机关机了
主机关机后本质上还是会杀死进程,释放资源进入四次挥手。并且关机过程会有一些时间,这段时间内可以把四次挥手进行完。但是如果没挥完手... ...

3.主机掉电了
直接把电源拔掉------
接收方掉电:

多次重传依然没有ACK,此时B会触发"重置TCP连接",发送一个复位报文(标志位RST为1)

如果RST还是没有ACK,B就会单方面释放连接。
发送方掉电:

B突然发现A没反应了,但是自己没法判定他是离线了还是歇一会儿再发。B就会等待
B 在等待一段时间后,向 A 发送无业务载荷的 "心跳包",核心目的是触发 A 的 ACK 响应。心跳包具有周期性;若未收到对端的心跳响应,则判定对方已离线。此时先尝试发送 RST 报文(重置连接);若仍无反馈,则单方面释放当前连接。
4.网络断开
与断电处理机制一样。
TCP&UDP的对比
TCP主要用于可靠传输的场景(大部分情况下都使用TCP)
UDP主要用于性能要求高的场景
经典面试题:使用UDP实现可靠传输
答案就是往TCP上面靠,例如:
引⼊序列号, 保证数据顺序;
引⼊确认应答, 确保对端收到了数据;
引⼊超时重传, 如果隔⼀段时间没有应答, 就重发数据;
以上就是本篇博客全部内容啦!!!