🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀Java EE(95平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
目录
1. 应用层
关于应用层协议,一个比较重要的协议就是后续我们要介绍的http协议.这是现成的应用层协议.当然,我们也可以自定义应用层协议.但是想要定义出应用层协议,我们就需要对传输数据的格式进行一系列的约定,具体包含以下两点:
- 约定传输的数据有哪些信息.
- 传输的数据要遵守什么样的格式.
关于格式这个事情,又会涉及到网络传输中字符串的序列化问题,序列化的方式有好多种,常见的有以下几种:
- 基于行文本方式传输
- 基于json
- 基于xml
- 基于yml
- 基于portobuffer
我们下面来举一个例子:比如一个外卖平台.在下单的时候,客户端请求有"配送位置"(即经纬度)和用户id两个数据.
- 基于文本行:
id,位置\n
1234,20E40N\n
- 基于json:(使用花括号作为边界,花括号中是键值对,键和值之间用:隔开)
{
userId:1234
position:"120E40N"
}
- 基于xml:(和html的方式非常相似,是以标签的形式展现的)
<request>
<userId> 1234</userId>
<position> "180E40N"</position>
</request>
- 基于yml(采用缩进的方式,和python的语法类似)
request:
userId:1234
position:"120E40N"
- 最后一种portobuffer的序列化方式,是一种二进制的格式,我们后面细说.
2. 传输层
2.1 再谈端口号
在传输层,我们有一个非常重要的概念,叫做端口号,是一个两个字节的无符号整数. 范围是0到65535,其中,0到1023为知名端口号,之后的端口号在客户端启动的时候,操作系统会随机分配一个.
在端口号这里,还有两个非常重要的问题:
- 一个进程是否可以有多个端口号? 可以,比如我们在一个程序中,可能会有"管理端口"和"业务端口",两个端口,可以利用 多个端口对一个程序进行更加精细的控制.只要这个端口号不和其他进程冲突即可.
- 一个端口号是否可以有多个进程? 不可以,除非有一种情况,同时使用一个端口号的多个进程使用的通信协议不同,这时候可以一个端口号绑定多个进程.正所谓"一山不能容二虎,除非一公和一母".
上面两个点的关系就像一个名字可以指代多个人,但是一个人不可以同时拥有多个名字.
2.2 UDP协议
2.2.1 UDP协议格式
下面我们对上面这个图进行解释:
- 16位源端口号:这个数据报从哪里来
- 16位目的端口号:这个数据报要到哪去
- 16位UDP长度:表示整个数据报(包括UDP报头+UDP载荷)的最大长度.最大是64KB.
- 16位校验和:在网络传输的整个过程中,可能会出现错误.这时候就需要校验和出马了.那么校验和是如何检验数据报的正确性的呢? OS会拿着一部分数据进行计算,得到结果,如果数据部分发生了改变,此时得到的结果就会不一样,此时便可以判定该数据报出现了错误. UDP具体在主机中是怎么进行计算的呢?它在使用一种叫做CRC的算法作为校验: 设定2个字节的变量.把数据中的每一个字节取出,往这个变量上的每个字节上进行累加.如果结果溢出,超过了两个字节,溢出的部分就会舍弃.
拓展:md5算法
处理crc算法,还有一个非常常用的算法,md5算法.是一个字符串的Hash算法,就是把String映射到Map中,这个算法有以下特点:
- 定长:无论输入的内容有多长,得到的结果,一定是固定长度.
- 分散:输入的内容,哪怕只有一点点的改变,计算结果也会有很大的差异.就是因为非常的分散,所以就适合用作字符串的Hash算法,这样可以有效减少Hash冲突.
- 不可逆:原数据转为md5数据格式非常容易,成本很低,但是md5数据格式想要还原为原来的数据,成本非常高,甚至说不可能.就是由于这种特性,所以md5算法经常用于加密领域.
2.2.2 UDP协议的特点
- 无连接 什么是无连接:就是双方不会保存对方的信息,就可以进行通信.比如我们发微信,只要发送的一方点击发送按钮的时候,信息就会发送,对方只能接受,不能拒绝.
- 不可靠传输 传输数据的时候,不关心对方是否收到数据,也不管对方收到的数据是否完整,只要把数据发送出去就算完事.
- 面向数据报 面相数据报流,就是传输数据的时候,使用的是整个数据报为单位进行传输,每次传输的时候,只能传输一个完整的数据报,不可以传输半个,或者1.5个.
- 全双工 通俗一点来讲,就是一条通行链路上,可以进行双向通信,既可以接收,也可以发送. 与全双工相对的是半双工.半双工就是一条通信链路上,只可以进行单线的通信,要不只能接收,要不只能发送.
2.3 TCP协议
TCP全称为"传输控制协议(Transmission Control Protocol").⼈如其名,要对数据的传输进⾏⼀个详细的控制 .
2.3.1 TCP协议格式
由于部分的知识限制,我们目前有一部分的字段还无法解释,所以这里我们先解释一部分.
- 16位源端口号:数据从哪里来.
- 16位目的端口号:数据到哪里去
- 4位首部长度:这里与UDP协议中的长度有一定的区别,TCP这里是数据报头的长度,而UDP中是指报头+载荷的总长度. 这里的4位报头长度可以变化.值得注意的是,这里的每一位是16进制的,即每一位表示0~0xf(0到15).根据下面选项字段的变化,首部长度也在发生变化.选项这一项可有可无,如果选项这一字段完全没有,这时候,TCP报头是固定字段的20字节,如果拉满,就是60(4*15)个字节,去掉20个固定字段的字节数,选项最长是40个字节.
- 紧急指针:和URG配合使用,一般用于带外数据传输.
2.3.2 TCP协议核心特点(必考面试题)
2.3.2.1 确认应答
确认应答是保证TCP传输可靠性的一个核心机制.所谓可靠传输,我们在之前也提到过,就是尽可能地把数据都发送给接收方,但是这里不可以100%让接收方收到,只是尽可能.
- 所谓确认应答,就是在发送方给接收方发送数据之后,接收方要给发送方返回一个确认应答的数据报,这样发送方就可以确认接收方已经收到这个消息了.我们称确认应答这样的数据报为应答报文,应答报文报头中的6个标志位中,其中ACK那一位为1,所以我们也称应答报文为ACK报文. 但是如果说上图中第二次ACK比第一次先到达主机A的话,那么主机是不是就无法把发送的数据和ACK报文对应起来了呢?不是.
- TCP将每个字节的数据都进行了编号,用来解决由于发送数据的延迟问题所造成的"不对应"的问题.这个编号就是所谓的序列号. 其中序列号又分为32位序号和32为确认序号,分别对应的是TCP报头结构图中的第2,3两行.- 32位序号就是发送方规定传输数据的字节范围,这其中,每个字节都有编号,且一次递增,比如从1
1000位字节是要传输的数据.- 32位确认序号就是接收方发送ACK报文确认应答的时候,用来表示发送方某个范围的数据已经收 到的序号,确认序号一般是发送方数据范围的最后一个字节+1,比如用1001表示11000的数据已经收到了.
确认序号有两层含义:比如发送方的序号是1~1000
- 1001表示1~1000的数据已经收到了.
- 1001表示接接收方要开始向发送方索要1001开始的数据报.
举例说明:接亲
结婚的时候,总会有男方去女方家接亲的常场面,接亲会有车队,如果后面的车走在的头车(接新娘的车)的前面,先到达娘家的就是后面的车,不可以让新娘坐在后面的车上.这时候如果告诉新娘头车的车牌号,新娘会更具车牌号来辨认头车,新娘也就不会做错车了.上面的序列号和他是同样的道理,数据报中都存在编号,就不会出现发送数据与确认应答数据对不上的情况.
2.3.2.2 超时重传
超时重传也是保证TCP可靠传输的机制.如果上述的确认应答的过程没有那么的顺利,出现了丢包的情况,那么超时重传便会起到一定的作用.
所谓丢包,就是在数据传输的过程中,数据报被丢弃,无法到达指定的发送端.这件事情客观存在,是随机的事件.原因可能是交换机或者是路由器在通信过程中繁忙.导致了多出来的部分直接被丢弃.
- 主机A发送数据给B之后,由于丢包的原因,无法使得数据到达主机B. 如果主机A在⼀个特定时间间隔内没有收到B发来的确认应答,就会进行重发.
- 如果主机A未收到B发来的ACK报文,即确认应答的报文,说明可能是因为ACK也丢失了.此时发送方就会再次对数据进行重传. 但是这种情况下会有一个问题,B会收到两份相同的数据,这一点不必担心,因为接收方有去重机制.可以利用前面的序列号达到去重的目的.
- 去重机制 接收方存在一个类似于BlockingQueue的数据结构,我们称这种数据结构为接收缓冲区. B收到数据之后,经过层层地分用,最后到达传输层TCP协议,这时候会有一个接收缓冲区,把收到的数据先放入接收缓冲区中,放入的过程中,接收缓冲区会根据序号在队列中进行判断,这个数据在队列中是否存在或存在过.
此处的这种接收缓冲区的模型其实也是一个生产者消费者模型,发送方是生产者,接收方是消费者,接收缓冲区是用来支撑上述机制的核心数据结构.
- 超时重传周期 如果说超时重传的数据报也一直在丢包,就会随着重传轮次的增加,间隔时间越来越长. 比如第一次传输的时候等待50ms,没收到ACK,重传 第二次等待100ms,没收到ACK,重传,等待150ms… 如果重传次数过多,就会尝试"重置连接",会触发"复位报文",(RST报文).通信双方清空TCP传输过程中的中间状态,重新开始传输,如果还不成功,就会断开连接.
2.3.2.3 连接管理(三次握手,四次挥手)
在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接.
- 三次握手 两个机器进行交互之前,先要建立连接,也就是一个打招呼的过程,在这个过程中,没有任何实质性的数据交互.我们前面提到过,建立连接,就是双方各自保存对方的信息.具体完成上述过程,就是经过三次握手来完成的. [注意] 三次握手,一定是客户端优先发起的.
举例说明:相亲
在相亲之前,不是先问对方有没有车,有没有房,有没有存款,而是先嘘寒问暖,问问外面冷不冷?最近怎么样?
- SYN 这里的SYN表示的是数据报不携带任何数据,即载荷是空的,只有报头.其中TCP报头中6个标志位中的SYN一位为1.这样的数据报也被称为同步数据报.
SYN是synchronize的缩写,翻译成英文,就是"同步"的意思.这里的同步就是希望服务器与客户端达成某种有关联的状态.
- 发送数据 在建立连接之后,双方就可以通过网络进通信了.也就是我们上面确认应答和超时重传的过程.
建立连接的意义:
- 投石问路,查看通信路径是否畅通.比如地铁在早上拉客之前,要先空拉一圈.查看轨道是否存在异常.
- 确认通信双方的接收能力与发送能力正常.比如我们在打游戏开开麦的时候,要先确认双发的耳机和麦克风都是正常的.
- 协商参数,通信双方共同确认一些通信中的必备参数的数值. 上面我们所展示的序号与确认序号就是通过三次握手协商出来的.第一次连接与第二次连接以及后续的连接都会协商出不同的序列号.这样做的目的就是,防止出现"前朝的剑斩后朝的官",或者是防止不同的连接相互干扰. 在一次通信的时候,如果数据报在网络中共迷路,或者是绕远路,等到这个数据报到达服务器,发现与之前协商好的序列号不对应,说明之前的连接可能已经断开了,现在是一个新的连接,那么这个数据报就应该丢弃,不应该按照程序正常处理.
举例:办酒席
办酒席这件事情,需要婆家与娘家共同商量一共有多少人,需要办几桌.
- 四次挥手 四次挥手与三次握手的效果正好相反, 是两个主机之间断开连接的一种机制.此时也没有任何实质性的数据交互,断开连接就是各自都要删除对方的信息. [注意] 与三次握手不同,四次挥手的发起者不确定,可能是客户端,可能是服务器.
举例:恋爱
在想要交往的时候,往往是男生(客户端)比较主动,而分手,可能是男生先提出,也可能是女生先提出,这个不一定.
- FIN 这里的FIN和SYN一样,不携带任何数据,只是用于两个主机之间断开连接,其中6个标志位的FIN位为1,这样的数据报也叫结束报文.
- 问题:四次挥手中间的FIN与ACK是否可以像三次握手那样合并到一起? 三次握手,服务器收到SYN之后,系统内核立即返回SYN和ACK,由于它们两个是同时返回的,所以可以合并.但是四次挥手,收到FIN之后,系统内核立即返回ACK,但是下一个FIN是由应用程序代码控制的.
close()
方法被调用之后,才会触发FIN操作,两个操作相隔一定的时间.很难合并到一起. - 比如:我们前面提到的TCP协议的Socket套接字. https://blog.csdn.net/2301_80050796/article/details/139204881 我们结合我们前面提到过的TCP协议套接字,我们来理解一下四次挥手: 在客户端close()退出之后,客户端便会给服务器发送一个FIN报文,当FIN到达服务器之后,hasNext()感受到了没有下一个字节流过来了也就是返回了ACK,break之后,才调用close()进行了关闭,也就是客户端返回了FIN报文.
但是这样的情况也存在一些例外,比如服务器FIN出现了延时应答,这时候就可能会出现ACK与FIN合并的情况.
- 服务器与客户端的状态 我们以上图为例来说明: - LISTEN:服务器绑定端口号之后,进入到LISTEN状态.类似与开机之后,信号良好,可以进行通信的状态.- ESTABLISHED:连接建立完成,可以正式发送数据.- TIME_WAIT: - 这个状态是为了在四次挥手的过程中,防止最后一个服务器发送过来的ACK发生丢包. 在客户端收到服务器的FIN之后,不会立即断开TCP协议连接,而是会进入TIME_WAIT状态,如果说ACK发生了丢包,服务器对客户端再次发送FIN报文的时候,客户端就无法接收到服务器的FIN报文.因此,我们便会有TIME_WAIT这样一个状态,以便等待可能到达的FIN重传数据.- 但是这个状态也存在一定的生存时间. 一般是2MSL.大约是1min.这样就可以保证两个传输方向上尚未被接受的数据或者迟到的数据都已经消失.同时也在理论上保证了最后一个报文可以到达.在一定时间之内,没有FIN过来,TCP连接就会被服务器释放.- CLOSE_WAIT : 被动一方收到主动方FIN之后进入的状态.等待被动一方调用close(),close越及时,这个状态越不容易看到.一旦被动一方调用close之后,就会给主动一方发送FIN报文.
面试题:
- 如果发现服务器出现了大量的TIME_WAIT状态,如何处理?说明服务器出现了大量主动断开TCP连接的操作.但是这样的情况并不科学,一般是在客户端中出现.
- 对于服务器上出现了大量的CLOSE_WAIT状态,原因就是服务器没有正确的关闭socket,导致四次挥手没有正常完成,可以查看服务器的close操作是否正确.
2.3.2.4 滑动窗口
TCP协议在可靠传输之外,也希望高效完成数据传输.
刚才我们讨论了确认应答策略,对每⼀个发送的数据段,都要给⼀个ACK确认应答.收到ACK后再发送下⼀个数据段.这样做有⼀个比较⼤的缺点,就是性能较差.尤其是数据往返的时间较长的时候.
既然这样⼀发⼀收的方式性能较低,那么我们⼀次发送多条数据,就可以大大的提高性能(其实是将多个段的等待时间重叠在⼀起了).一次性发送到少数据,这个称为窗口大小.
- 特点 • 窗口大小指的是无需等待确认应答可以继续发送数据的最大值.上图的窗口大小就是4000个字节 (四个段). • 发送前四个段的时候,不需要等待任何ACK,直接发送; • 收到第⼀个ACK后,滑动窗口向后移动,继续发送第五个段的数据;依次类推; • 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答;只 有确认应答过的数据,才能从缓冲区删掉; • 窗口越大,则网络的吞吐率就越高;
[注意]
- 滑动窗口,说是提升效率的机制,其实就是"亡羊补牢"的机制,TCP为了保证可靠性,牺牲了很多效率,引入滑动窗口会让效率牺牲的少一些,但是仍然存在牺牲,没办法和UDP协议的速度比.
- 在窗口头部数据接收到ACK之后,窗口就向后滑动一位,并不是等待4个数据全部收到ACK之后再向后滑动窗口.
- 丢包情况 - 情况一: 数据报抵达,ACK丢失 这种情况下,并不要紧,因为我们可以通过后续的ACK进行确认.比如上图: 1001ACK丢包,2001会告诉发送方,1
2000的数据的数据都已经收到了,涵盖了1001的效果.比如:一个人被问到,你上大几了,那个人回答道,我娃都上小学了,是同样的道理.- 情况二: 数据报丢包 数据报丢失之后,就会触发滑动窗口的一种重传机制:快速重传.也叫快重传. • 当某⼀段报⽂段丢失之后,发送端会⼀直收到1001这样的ACK,就像是在提醒发送端"我想要的是"1001"⼀样; • 如果发送端主机连续三次收到了同样⼀个"1001"这样的应答,这时候发送方就会反应到,原来是10012000的这个数据报丢了,就会将对应的数据1001-2000重新发送; • 这个时候接收端收到了1001之后,就会把接收缓冲区的空白补上(若空白区域一直填补不上,滑动窗口就不会向后移动),再次返回的ACK就是7001了,因为2001-7000接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中;
2.3.2.5 流量控制
接收端处理数据的速度是有限的.如果发送端发的太快,导致接收端的缓冲区被打满,也就是接收方处理不过来的情况下,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等⼀系列连锁反应.这时候我们就需要通过控制数据流量的方式来防止这种情况的出现.TCP会根据接收端的处理能⼒,来决定发送端的发送速度.
- 流量控制原理 通过窗口大小来控制,窗口大小可变,所以可通过窗口大小,来控制发送速度.窗口大小字段越大,说明网络的吞吐量越大.
- 流量控制原则 接收方根据自己的处理能力,反向制约发送方速度,使得双方达成"平衡". - 接收端⼀旦发现自己的缓冲区快满了,就会将窗口大小设置成⼀个更小的值通知给发送端;- 发送端接受到这个窗口之后,就会减慢自己的发送速度;- 如果接收端缓冲区满了,就会将窗口置为0;这时发送方不再发送数据,但是需要定期发送⼀个窗口探测数据段,使接收端把窗口大小告诉发送端.这个数据会通过ACK报文中的"16位窗口"字段来指定.(一般会以接收缓冲区空闲空间的大小作为发送数据的窗口大小).- 这其中"窗口探测报文"是周期性发送的,他和前面的SYN报文和FIN报文一样,都是不带载荷的.
2.3.2.6 拥塞控制
因为⽹络上有很多的计算机(路由器,交换机,集线器等网络设备),可能当前的网络状态就已经比较拥堵.在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的.
- 所以TCP引入了"慢启动"的机制:先发送少量的数据,摸清当前的网络状态.如果网络没有拥堵,再去增加窗口大小,直到出现丢包的情况,减小窗口大小,直到不丢包,周期循环,直到找到合适的值,使得数据传输达到"动态平衡"的状态.总结一句话:在丢包的边缘疯狂试探.
- 下面的一张图是一张非常经典的图,他可以形象地描述出TCP拥塞控制的慢启动机制.
- 首先先慢启动,试探通信网络的畅通性.
- 之后确认没有问题之后,开始指数式增长.
- 增长到一定阈值之后,开始线性增长.
- 丢包之后,降低传输速度.
- 降低之后,再次线性增长.
上面这张图就好像是恋爱的感觉.
[注意]
拥塞控制窗口和流量控制窗口共同控制数据传输速度大小.这里遵循的是木桶效应.哪个窗口小,就按哪个的速度来.
2.3.2.7 延迟应答
延迟应答的核心目的,为了提升TCP传输数据的效率.
- 延时应答如何提高效率? 如果接收数据的主机立刻返回ACK,这时候返回的窗口大小可能比较小.如果说延迟应答的话,就会让应用程序消耗一部分的数据,这时候接收方的缓冲区就会增大空闲空间,从而使得返回的ACK报文中,窗口大小增大.窗口大小增大之后,自然会增大数据传输的速度.
- 延时的策略1. 按照一定的时间延时2. 按照收到的数据量延时.两者结合使用
2.3.2.8 捎带应答
由于延时应答的存在,ACK不一定立即返回,在ACK稍等一会的时候,正好响应的数据也准备返回,就会在响应数据中,ACK标志位设置为1,确认序号和窗口大小都设置上.可以提高传输的效率,两次传输合并为了一次.
2.3.2.9 粘包问题
我们拿前面的翻译服务器来说明:
当客户端给服务器发送了一个请求,服务器拿到这个请求之后.由于TCP协议面向字节流的特点,读的时候咋读都行,此时,服务器就无法区分从哪里到哪里是一个完整的单词,即应用程序看到了这么⼀连串的字节数据,就不知道从哪个部分开始到哪个部分,是⼀个完整的应用层数据包,这种问题叫做粘包问题.
那么如何避免粘包问题?归根结底就是明确两个包之间的边界.
- 对于定长的包,保证每次按照固定的大小读取.
- 对于变长的包,可以在包头的位置约定一个包的总长度字段.从而达到分割效果.
- 也可以使用分隔符的方式分割数据.比如我们在前面Socket套接字的时候,使用’\n’作为分隔符.
2.3.2.10 异常情况
- 进程终止:进程终止会释放文件描述符,仍然可以发送FIN.和正常关闭没有什么区别(调用close,4次挥手).
- 机器重启:和进程终止情况相同.
- 机器掉电/网线断开: 这样的异常分为两种情况. - 情况一: B是数据方 B接下来发送的数据,都不会有ACK,B会超时重传,重传几次之后,发送复位报文(RST),仍然没有响应,B就会单方面删除A的信息.- 情况二: B是接收方 A沉默了之后,B不知道A是暂停发送数据,还是直接挂了.B在一定时间之内没有收到A的数据之后,会触发"心跳包",和SYN,FIN,RST一样,都是没有载荷的数据报.发送之后,如果A有正常的ACK回应,就没有挂,否则就挂了.
面试题:如何使用UDP完成可靠的数据传输
可以引入TCP协议的10条特性,确认应答,超时重传,连接管理,滑动窗口,流量控制,拥塞控制,延时应答,捎带应答,粘包问题,异常情况.
版权归原作者 LileSily 所有, 如有侵权,请联系我们删除。