应用层
协议就是一种约定
应用层:对应应用程序,是程序员打交道最多的一层,调用系统提供的网络api写出的代码都是属于应用层的。应用层有很多现成的协议,但程序员一般用的还是自定义协议
自定义协议要约定好哪些内容?
1.服务器和客户端之间要交互哪些信息(产品经理规定)
2.数据的具体格式(与产品经历无关,客户端和服务器的程序员共同敲定)
客户端和服务器之间往往要进行交互的是结构化数据;网络传输的数据其实是字符串或者二进制比特流
约定协议的过程,就是把结构化数据转成字符串/二进制比特流的过程
把结构化数据转成字符串/二进制比特流这个操作,称为序列化
把字符串/二进制比特流还原成结构化数据,这个操作叫做反序列化
序列化/反序列化具体要组织成什么样的格式,包含哪些信息,约定这两件事的过程就是自定义协议的过程
常见的协议约定格式
1.XML
请求:
<request><userId> 1000 </userId><position> [经纬度] </position>
</request>
响应:
<response><shops><shop><id> 1001 </id><name> 杨国富麻辣烫 </name><image> 图片地址 </image><rank> 4.8 </rank><description> 这是好吃的麻辣烫 </descitption></shop></shops>
</response>
< > 表示标签,标签往往是成对出现的
<userId>是开始标签
</userId>是结束标签
开始标签和结束标签之间夹着的就是标签的值,标签可以嵌套
优点:拓展性和可读性极大地提升了
如果后面要添加一些属性,就新增一个标签即可,对于已有代码影响不大
缺点:整个数据冗余信息就非常多了,标签这种描述性信息占据的空间反而比数据本身更多
尤其是网络传输的时候,带宽就会消耗很大
HTML和XML的区别
XML:里面的标签/格式/值都是自定义的,程序员自由度比较高
HTML:里面的标签/格式/值都是大佬们规定好了,程序员只能遵守已有规则
2.JSON
请求:
{userId: 1000,position: [经纬度]
}
响应:
{{id: 1001,name: "杨国福麻辣烫"},{id: 1002,name: "魏家凉皮"}
}
json是键值对结构
键和值之间用 :进行分割
键值对之间用 , 进行分割
把若干个键值对使用{ } 括起来,此时就形成了一个json对象。
还可以把多个json对象放到一起,使用“ ,”分隔开,,并且用 { } 整体括起来,可以形成一个json数组
优点:可读性和扩展性很好,相比起XML,带宽明显减少
缺点:key名字重复传输
3.protobuffer
更节省带宽的方式,效率最高的方式
在开发阶段定义出都有哪些资源,描述每个字段的含义。程序真正运行的时候,实际传输的数据是不包含这样的描述信息
这样的数据是按照二进制的方式来组织的
虽然protobuffer运行效率更高,但是使用没有比json更广泛
传输层
端口号:是一个2个字节的整数,使用端口号的时候,1~1024都是系统保留的自用的端口,一般都是把这些端口给知名的服务器使用
HTTP服务器:80
HTTPS服务器:443
UDP协议
研究这个协议就是研究UDP报文格式,也就是UDP数据报
UDP数据报 = 报头 + 载荷
UDP报头一共4个字段,每个字段2个字节,一共8个字节
协议报头中使用2个字节表示端口号,端口号取值范围就是0~65535(64KB),一个UDP数据报最大长度就是64KB
检验和/校验和
验证数据在传输过程中是否正确,如果发现错误,就把错误的数据报丢掉
检验原理:通过原始数据中的部分内容进行一系列的运算,得到一个更短的字符串,通过原始整体数据再计算一次这样的结果,再对比是否一致
前提:数据在网络传输中可能损坏(网络数据传输,本质上是光信号/电信号/电磁波,非常容易受到外界的干扰)
网络中的校验和并非是简单的按照长度/数量作为校验标准的,一定要让数据的内容能够参与
CRC算法
UDP中校验和采用比较简单的方式比如CRC算法进行校验(循环冗余校验)
//比如要产生一个两个字节的校验和
short checksum = 0;
for(遍历取出数据报中每个字节的数据){checksum+=当前字节的数据//暂时先不管溢出
}
UDP数据报发送方,在发送之前先计算一遍CRC,把算好的CRC值放到UDP数据报中(设这个CRC值为value 1)
接下来把这个数据报通过网络传输到接收端,接收端收到这个数据之后,也会按照同样的算法,再算一遍CRC的值,得到的结果是value2,再对value1和value2之间进行比较。如果是一致的就说明数据是ok的;如果不一致,说明传输过程中就发生了比特翻转了
更加精确的算法:MD5算法/sha1算法
MD5算法
特点:
1.定长。无论原始数据有多长,算出来的md5的最终值都是固定长度
2.分散。计算md5的过程中,原始数据只要变化一点点,算出来的md5值就会差异很大
网络传输中,如果出现bit反转(只有少数的bit进行翻转),但是即使只是翻转1bit,得到的md5差异会很大。这个特性也能使md5成为一个字符串哈希算法
3.不可逆。给你一个源字符串,计算md5值,可以很容易实现。但是如果给你一个md5值,让你返回源字符串,理论上无法完成
因为原始的字符串转成md5的过程中有很多信息量缺失了,所以无法直接还原
TCP协议
基本介绍
reserved保留位
UDP协议长度受到2个字节的限制,一旦改变了报头长度,就会使及其发送的UDP数据报和其他机器不兼容,无法进行通信
TCP协议就专门设定这个保留位(占位作用),后面需要拓展了,随时拿出来使用,避免TCP拓展引起的不兼容问题
6位标志位,TCP核心部分
接下来我们会讲TCP的10个特性(注意业界没有这个说法,这里笔者只是挑了10个最具代表的特性来讲述)
1.确认应答
确认应答是TCP确保可靠性的核心机制
可靠性VS安全性
安全性关注的是通信过程中,如果被黑客入侵了,黑客是否能窃取到你的关键敏感信息。一般要靠加密来保证安全性。
可靠性关注的是通信过程中数据是否能传送到对端。
发送方发送消息,接收方收到会回复一条信息
网络中的后发先至问题:我们平时发消息,可能因为网络卡顿,后面发的消息反而先被对方看到了,这种就属于网络后发先至的情况。
为了解决上面的问题,可以引入序号和确认序号对于数据进行编号,应答报文告诉发送方这次应答的是哪个数据
实际上,TCP的序号和确认序号都利用字节来进行编号
假设TCP数据报文的报头中只能存一个序号,而载荷有1000个字节,有1000个序号,由于序号是连续的,只需要在报头中保存第一个字节的序号即可。后续字节的序号都是很容易计算得到的
确认应答中,通过应答报文(也叫做ack报文)来反馈给发送方,表示当前数据正确收到了
应答报文中的确认序号,是按照发送过去的最后一个字节的序号再+1进行设定的
2.超时重传
是确认应答的补充。如果一切顺利,通过应答报文就可以告诉发送方,当前数据是不是成功收到,
但是,网络上可能存在丢包的情况。如果数据包丢了,没有到达对方,对方也没有ack报文
网络中为啥会出现丢包
网络中的路由器/交换机不仅仅给一台主机的的一次通信提供服务,还要支持千千万万的主机之间进行通信
整个网络中可能出现某个路由器/交换机,某个时刻突然负载量很高,也就是说短时间内有大量的数据包要经过这个设备转发
路由器/交换机能够处理的数据量是有限的,很可能瞬间的高负载超出了这个设备能转发的数据量的极限,此时多出来的部分就没有了
这种情况就称为丢包
这种情况就需要超时重传。发送方发了一个数据之后,要等。等的时间里收到了ack,一切正常(数据包在网络上传输需要时间),如果等待时间超过时间阈值还没等到ack,此时发送方就认为数据传输出现丢包了。当认为丢包之后会把刚才的数据报再重新传一次。
再次解读TCP的可靠性:实际上就是在对抗丢包,在丢包客观存在的因素下也尽可能把包给传过去
超时的两种可能
重新审视上面的结论,是存在不科学的地方的
ack没来有两种可能:
1.发的数据丢了,这种情况再发一次没什么问题
2.ack丢了
此时数据已经被B收到了,如果还是重新传输一次,同一份数据B就会收到两次(假设发出来的请求是扣款请求,那就麻烦大了
怎么解决?
TCP socket在内核中存在接收缓冲区(一块内存空间),发送方发来的数据先放到接收缓冲区,此时接收方会先判定一下看看当前缓冲区是否已经有这个数据了;或者这个数据在缓冲区曾经存在过,然后应用程序调用read或者scanner.next方法才能读取到缓冲区的数据。
接收方如何判定这个数据是否是重复数据?
1.数据在接收缓冲区,还没应用程序被read走。此时拿着新收到数据的序号和缓冲区中所有数据的序号进行对照,看看有没有一样的。一样的就是重复数据,把新数据丢掉
2.数据在接收缓冲区中,但已经被应用程序read走了。此时socket api中记录了上次读的最后一个字节是多少(⚠应用程序读取数据的时候是按照序号的先后顺序连续读取的,先读1~1000 1001~2000)
比如上次度的最后一个字节的序号是3000,新收到的数据包序号是1001,这个1001就一定是之前读到过的,此时同样可以把这个新的数据包当作重复数据丢掉处理
接收缓冲区除了有去重功能,还可以帮我们对收到的数据按照序号来排序,确保应用程序读到的数据和发送的数据顺序是一致的
重传策略
1.重传次数是有上限的,重传到一定程度还没有收到ack,就会尝试重新连接,如果重置也失败,就放弃连接了
2.重传的时间阈值不是固定的。随着重传次数的增加而增大(这样也会使重传频率越来越低)
假设一次网络通信中丢包的概率是10%,包顺利到达的概率是90%;
重传一次,丢包概率10% * 10% = 1%顺利到达概率变为99%;
说明随着重传次数的增加,包到达对方的概率会大大增加。假设我传了三四次还有丢包的情况,说明当前丢包概率很大,网络出现严重故障,再重传也意义不大(那就降低频率呗)
3.连接管理
建立连接:三次握手
客户端调用socket api建立连接
socket = new Socket(serverIp, serverPort);
但真正建立连接的过程是在操作系统内核中完成的
内核如何完成建立连接的呢?三次握手
连接:让通信双方都能保存对方的相关信息
握手:打招呼的意思,不包含任何业务逻辑。比如两位企业家碰面,先打招呼(就是握手)打完招呼后就可以商讨具体细节(进行业务逻辑)
step 1:客户端时主动的一方,第一次交互一定是客户端发起的
这里的syn是一个特殊的TCP数据报,特点:
1.没有载荷,不会携带应用层数据
2.位于标志位中的第5位,为1,表达的语义:我想和你建立连接(抽象)
虽然syn没有载荷,但是有IP报头(有客户端的IP)和TCP报头(有客户端的端口),这个过程就是在告诉服务器想和你建立连接的客户端是谁
接下来有两种可能性:1.服务器同意连接;2.服务器负载太高了,不同意建立连接
step 2:服务器收到syn,返回ack,表示收到
step 3:服务器返回syn,表示我愿意和客户端建立连接
syn:synchronized的简写,表示同步
与加锁的区别:
加锁的synchronized:协调多个线程之间的执行顺序
TCP的syn:进入连接状态,客户端和服务器得相互配合完成一系列工作
step 4:客户端返回ack
建立连接本质:通信双方各自给对方发起一个syn,各自给对方回应一个ack
三次握手但是流程中有四次交互?
其中服务器返回ack和syn两个步骤可以合二为一,但是这里的ack和syn可以合成一个数据包(第二位和第五位都是1),一起发给客户端
合成一个包可以减少一次封装分用的过程,整体的效率就提升了
简图:
详图(不推荐):
为何要进行握手?
1.可以先对通信路径进行投石问路,初步确认通信链路是否畅通(可靠性前提条件)
⚠三次握手的作用比较有限,关键的可靠性传输还得通过确认应答和超时重传来保证。因为三次握手只是通信一开始握了一下,后面数据开始传输就和三次握手无关了。
2.验证通信双方的发送能力和接收能力是否正常(关注点在两端)
两次握手是否可以?肯定不行,缺少了服务器的发送能力和对端接收能力的验证
3.三次握手的过程也会协商一些必要的参数
TCP是有很多参数需要进行协商,往往是以选项部分来体现(最重要的信息,TCP通信序号的起始值)
TCP一次通信过程中,序号不是从0或者1开始的,而是先选择一个比较大的数字,以这个数字为开头来继续计算。即使是同一个客户端和服务器,每次连接开始的序号都不同
防止前朝的剑,斩本朝的官:在第一次连接的过程中,传输的一个数据包在路上堵车了,迟迟无法到达对端(后发先至的情况),等终于到了对端的时候,之前的连接已经无了,现在是新的连接了(改朝换代)
这个数据包的功能特性能否被新的程序理解存在未知,所以丢弃这个数据包是一个上策
那么,怎么识别出前朝的数据包呢?通过序号来区分
正常的数据包的序号是从开始序号往后依次排的,就算偶尔数据不连续也差异不大
前朝的包,序号就会和本朝的包序号差异非常大,一眼看得出来
断开连接:四次挥手
连接本质让通信双方保存各自的信息,使用数据结构来进行保存
本质目的:把对端的信息从数据结构中删除掉/释放掉(和平分手)
如果是单方面要断开连接,就不一定要用四次挥手,可以用其他的方式强制断开连接
fin => finish 结束
四次挥手,不一定非得客户端先发fin,也可以服务器先发fin
socket.close() 触发fin数据报
四次挥手的本质:通信双方各自给对方发起fin,再各自给对方反馈ack
相当于离婚双方签署离婚协议,一方签完名字后告诉对方,对方签完后再告知回来
中间两次交互能否合二为一呢?
有时候能合并有时候不能合并。无法像3次握手一样100%会合并
三次握手:
四次挥手:
如果ack和第二个fin的时间间隔比较短还有机会合并,如果间隔比较长就无法进行合并了
连接管理过程中TCP状态转化
状态:描述某个实体现在正在干啥
TCP的服务器和客户端都有一定的数据结构来保存连接的信息,这个数据结构中就有一个属性叫做“状态”,操作系统根据状态的不同决定当前应该干什么
TCP状态转换汇总
拿三次握手的详图来举例
LISTEN状态表示服务器这边创建好serverSocket了,并且绑定端口号完成
相当于我把手机开机了,而且信号良好,随时等人(客户端)给我打电话
启动之前手写的TCP服务器(我的端口号是9090)
本地地址:本机的地址;外部地址:对端的地址
0.0.0.0:0 全0表示目前没有客户端
[: :] : 9090 是ipv6的地址
EXTABLISHED状态,已确立的,表示客户端和服务器已经连接完毕了(三次握手完毕)
相当于有人打电话给我,处于接通了的状态
运行TCP客户端
拿四次挥手的状态转换图来举例
CLOSE_WAIT表示接下来的代码中需要调用close主动发起fin,收到对方的fin之后进入这个状态
谁被断开连接谁就进入CLOSE_WAIT
谁主动断开连接谁就进入TIME_WAIT
由于代码中会比较快速地关闭socket,TCP的状态会很快的从CLOSE_WAIT转为LAST_ACK,所以观察到CLOSE_WAIT这个状态;于此同时,如果观察到大量的CLOSE_WAIT状态的话,说明这个代码有bug,忘记关闭socket等
TIME_WAIT表示本端发起FIN之后对端也给我发FIN,此时本端进入TIME_WAIT,这个状态可以防止最后一个ACK丢包
原理:在四次挥手的过程中也会涉及到确认应答和超时重传。如果没有收到ACK则认为是丢包。客户端如果出现收到服务器的FIN后还没返回ACK就提前把TCP连接释放了,就会出现异常。
而TIME_WAIT状态会给最后一个ACK的重传留有一定的时间,提高ACK重传的容错。
但是,TIME_WAIT留给ACK的重传时间并非无休止的,最多等待2MSL(MSL是一个系统内核的配置项,表示客户端到服务器之间消耗的最长时间。)
因为服务器长时间没收到客户端最后一个ACK就会重传一个FIN,客户端等待2MSL都没有收到重传的FIN,证明服务器一定是收到了客户端的ACK
4.滑动窗口
我们学习了TCP保证传输可靠性的三个性质:确认应答,超时重传,连接管理
很遗憾,保证可靠传输还是要付出代价,这个代价就是传输效率。如果仅根据上面三个性质,单位时间内可传输的数据量会很少。
确认应答机制下,每次发送方收到一个ACK才会发下一个数据,大量的时间都消耗在等待ACK上了
为了解决上面的问题,引入TCP的一个机制:滑动窗口
滑动窗口在保证可靠传输的基础上,降低损失(不是提高速度)来提高效率
机制:批量传输
先发一条数据,不等ACK,再发下一条数据。连续发了一定量的数据之后统一等ACK
这样就可以把多次请求的等待时间使用同一份时间来等待,减少了总的等待时间
为什么叫滑动窗口?
①中圈起来的四份数据是已经批量传输出去,传输出这四份数据之后就等待ack暂时先不传。不等待ack能够批量传输的数据量称为窗口大小
批量发了四个数据就会对应四个ACK,但是这四个ACK也不一定是同时到达,而是有先有后。等回来一个ACK马上往后发一组数据。此时白色圈起来的部分(窗口)就往后走一格,而窗口移动的速度很快,直观上感觉就是滑动
丢包会怎样?
1.ack丢了
接收方发的2001的ACK能到发送方的话,说明1~1000和1001到2000的数据也被接收方收到了
生活中的例子:正常人的爱情顺序:谈恋爱->结婚->生娃,如果你跟人说我已经有娃了,那就说明你已经是谈过恋爱和结过婚了
2.数据丢了
解决问题的关键:1001~2000的数据丢失了,红色圆圈的部分返回的应答报文的确认序号是100而不是3001,是告诉对端1001的数据丢失了
多次重复的确认应答之后发送方意识到1001丢包了,就对1001进行重传。重传1001之后,正好发送方意识到问题之前的数据包发送到7001,此时只需要返回7001的确认应答就行了(7000前面的数据都收到了)
上述重传是针对性的重传,哪个丢了就重传哪个。已经收到的数据是不用重复发送的。这样整体的效率没有额外损失的。这种重传称为快速重传
超时重传 VS 快速重传
1. 如果当前传输过程是按照滑动窗口(短时间传输大量数据)的,就使用快速重传。判定丢包标准为是否有连续多个ACK索要同一个数据
2. 如果当前传输过程没有传很多数据,就仍然使用超时重传。判定丢包标准为打到超时时间还没有ACK到达
5.流量控制
滑动窗口的窗口大小越大,更多的数据复用同一块时间等待,效率就更高
但是窗口大小能无限大吗?当然不能。注意,可靠传输是前提。任何提高效率的行为都不应该影响到可靠性
发送的速度太快,接收方处理不过来(接收方的接收缓冲区满了),此时会出现丢包
重传?没用!满了的东西再怎么传也没用了,反而会浪费硬件资源
这时候就需要流量控制使发送方发送速度和接收方的处理速度达到步调一致,让接收方反过来影响发送方的速度
TCP通过在这个字段给发送方反馈发送速度。在接收方返回给发送方的ack报文中,告诉发送方接下来发送的窗口设置成多少比较合适。(一般是根据接收方缓冲区大小来设置发送窗口大小)
TCP报头中的选项中还包含了一个参数:窗口扩展因子
实际上要设置的窗口大小是16位窗口大小 * 2 ^ 窗口扩展因子
6.拥塞控制
和流量控制一样都是要限制发送方的发送速率
如果当前接收方处理的速度很快,但是发送信息的中间路径出现问题了,发送的速度再快也没用
解决问题核心思路:把中间路径经过的所有设备都看成一个整体,然后通过实验找到合适的传输速率。(实验方法:如果发送方按照窗口大小发送数据之后出现丢包,就认为中间路径出现拥堵,就减小窗口大小。反之增大窗口大小)
具体如何试出窗口大小
1.慢启动。刚开始传输的数据速率比较小,就采用比较小的窗口。(此时网络拥堵状况不知)
2.如果上述传输的数据没有丢包,说明网络畅通。此时按照指数增大窗口( * 2)
3.指数增长可能太快一下导致网络拥堵。此时引入阈值,当窗口大小达到阈值之后采用线性增长
4.线性增长一段时间后引起了丢包,就把拥塞窗口重置成较小的值,重回慢启动过程。然后把刚刚引起丢包的窗口大小设置成阈值。
7.延迟应答
接收方收到数据之后不会立即返回ack,而是等一会再返回ack。等的这一会相当于给接收方的应用程序这里腾出来更多的时间消费这里的数据
核心:在允许的范围内让反馈窗口尽可能大
如果不立即返回,延迟了100ms,那么在这100ms之内,接收方应用程序就能多消费一些数据,剩余的空间就更大了,发回的ack报中告诉发送方的窗口就更大了
延迟应答还可以按照ack丢了的方式来进行处理。正常每个数据都会对应一个ack,但是此时我们隔几个数据再发一个ack(由于滑动窗口返回的ack表示其确认序号前的数据都收到了),这样就能减少ack传输的数量,也能起到节省开销的效果
8.捎带应答
基于延时应答引入的机制。捎带应答尽可能把能合并的数据包进行合并,从而起到提高效率的效果
正常的客户端和服务器的交互如上所示
有点像四次挥手,但是这里通信的数据,请求和响应是带有载荷的,具有一定的意义
而挥手类似于一种告知方式,传递的数据包是没有载荷数据的,也没什么意义
正常情况下,服务器向客户端发出的ack和响应数据报二者之间存在时间间隔,所以得用两个包进行发送。由于现在有了延迟应答,ack延迟的时间响应数据包也准备好了,二者就可以捆绑成一个数据包进行发送了
客户端和服务器之间是长连接,要进行多次交互,在捎带应答的加持下,后续每次请求响应都可能触发捎带应答,都可能把接下来要传输的业务数据与上次的ack合二为一
可能触发?得看你代码怎么写的
9.面向字节流
粘包问题
此处的包是TCP载荷中的应用数据包
TCP传输的数据到达接收方之后,接收方要根据socket api来read出数据。而read出来的结果就是应用层数据包。由于read过程非常灵活,可能会使代码中无法区分出当前的数据从哪到哪是一个完整的应用数据包
此时应用程序调用read读取数据,由于此处是字节流的,读取过程非常灵活
读取数据之后要把这里的数据转成应用层数据包,这个数据才能被正确的使用
但是这里的读数据有很多种读法,可以一次读一个a,也可以读出一个aa或者aaa或者aaab,读出来的方式千奇百怪,转成的应用层数据包混淆不清了,就称为粘包
粘包问题关注用哪种读法读出来的才是完整的应用层数据包
解决问题关键:明确包之间的边界
1.通过特殊符号作为分割符,见到分割符就认为一个包结束了
2.指出包的长度。我们可以在包开始的位置上加上一个特殊的空间来记录包的长度
当然,UDP就没有粘包问题。因为
1.UDP传输的基本单位是UDP数据报,在UDP这一层就把数据包给分开了。约定好每个UDP数据报只承载一个应用层数据包就不需要额外的手段来进行区分了。
2.UDP的缓冲区的数据结构不是队列,而是类似于链表,每个链表的一个节点就是一个UDP数据报(承载一个应用层数据包)。
10.异常情况
如果比丢包还严重的情况比如网络故障怎么办?服务器或客户端:
1)其中一方出现了进程崩溃
进程无论是正常结束还是崩溃,系统都会触发回收文件资源和关闭文件这样的操作。还能触发四次挥手,因为TCP连接的生命周期可以比进程更长一些。虽然进程已经结束了,但是TCP连接还在,仍然可以继续进行四次挥手。
2)其中一方出现关机(正常流程关机)
有一个主机出现关机,就会
1.先强制终止所有的进程(杀死进程),终止进程后就会触发四次挥手。
2.但是四次挥手不一定能在系统关闭前挥完,但至少也能把第一个fin发给对端,通知对方我这边要结束了。
3.对端收到fin就要进入删除连接的流程,返回ack并发出fin,此时发出的fin对端的对端也无法返回ack了
4.对端没收到ack就会进行重传,重传几次还没收到ack,就会单方面删除连接信息
3)其中一方出现断电(更突然地关机)
直接断电机器直接关机
a)断电的是接收方,发送方发现没有返回ack就会进行重传,重传几次还没反应。TCP就会尝试进行复位连接,相当于清除掉原来TCP地各种临时数据重新开始。此时需要应道TCP的一个复位报文段
b)断电的是发送方,接收方本来就是阻塞在等待发送方的消息,但是迟迟没来消息。此时接收方就要区分出发送方是挂了还是只是暂时没发数据。TCP中,接收方一段时间还没收到对方的信息,就会给发送方发一个心跳包(心跳包:不携带应用层数据的特殊数据包,是一种周期性的数据包。)如果对方没有心跳,就是没反应,说明对方挂了,本端就会尝试复位并且单方面释放连接了
由于TCP的心跳包机制周期比较长,所以在实际开发中会自己重新设计一个心跳包
4)网线断了
第3种情况a和b的结合,双方都用各自的解决方案(上面提到了)来处理
经典面试题:如何用UDP来进行可靠传输?
UDP本身不可靠,需要在应用层写代码,自己实现可靠传输(TCP咋弄的代码就照着写)