在传输层中有两个非常重要的协议,UDP和TCP,现在就来研究一下这两个协议。
UDP
报文格式
我们观察可以发现,里面UDP报文长度为2个字节,那么是多少呢?我们需要快速反应如下固定字节数据类型的取值范围:
字节大小 | 有无符号 | 取值范围 |
---|---|---|
1个字节 | 有符号 | -128-127 |
1个字节 | 无符号 | 0-255 |
2个字节 | 有符号 | -32768-32767 |
2个字节 | 无符号 | 0-65535 |
4个字节 | 有符号 | -21亿-21亿 |
4个字节 | 无符号 | 0-42亿 |
源端口:2个字节,无符号,取值0-65535.
目的端口:2个字节,无符号,取值0-65535.
UDP报文长度:2个字节,无符号,取值范围0-64k,也就是说一个UDP最多能传输64kb的数据,这在当今是一个很小的数字(现在一个文件动不动就几个G).
说明:
能否将这个长度变大呢?理论上可以,但是几乎无法做到。
理论上,我们只需要修改系统内核中udp的参数,将unsigned short改成int就可以增加长度,但是我们不能只修改自己电脑的,我们还得修改别人的,怎么说每个人都改呢???
不过我们还可以将数据拆分成多组进行传输或者使用tcp代替udp,tcp没有报文长度限制。
校验和:使用了一种简单粗暴的校验算法,把UDP数据报中的每个字节都依次进行累加。
说明:
为什么要使用校验和呢?因为网络传输本质上是光信号/电信号传输,会受到磁场,高能粒子的干扰,就有可能使传输的数据突变,即0变1,我们需要使用校验和来确保传输的数据没有变化。
一般UDP中校验和是将数据报中每个字节都累加,可能会溢出,但是不要紧,当接收方收到数据了后,再按照同样的方式进行累加,如果得到值一样,就是正确的。万一前面的字节值变小了,后面的变大了,一加一减没变,又该如何应对?这种情况是可能出现,但是概率极低,毕竟工程上有一些误差也能接受。
TCP
报文格式
序列号:给传输的数据进行编号。发送方每发送一次数据,序列号的值就累加一次该数据字节数的大小,可以用来解决乱序的问题。
乱序问题:
为了追求效率,数据会被进行分组,例如有一个2000字节的数据包,被分为两组1-1000、1001-2000,从A端发送到B端,但网络的路径非常多,这两组数据可能会走不同的路线,又因为每个路由器/交换机的繁忙程度不一样,转发的过程也就不一样,因此就不能保证先发先至了,此时我们需要搞清楚哪个数据在前,哪个数据在后,就可以根据序列号来确定。
确认应答号:用在接收方,表示期望下一次接受的数据的序列号,发送方收到这个确认应答号可以认为在这个序号前的数据都被接收了,用来解决丢包问题。
丢包问题:
由于网络的结构复杂,某一时刻某一路由器/交换机数据量非常多,就导致了设备非常繁忙,数据处理的排队时延会很大,此时就有可能采取丢弃策略,就产生了丢包现象,那为了知道丢了哪些包,需要通过确认应答号告诉对方,哪部分的数据没有收到。
首部长度:表示的是TCP报头的长度。TCP报头的前20个字节是固定的,选项部分可以有也可以没有,因此TCP报头的长度是可变的,取值范围为:0*4byte ~ 15 * 4 byte。
保留位:暂时不用,为以后升级留下空间。
标志位:
-
ACK:该位为1时,表示确认应答的为有效字段。
-
RST:该位为1时,表示TCP连接中出现异常必须强制断开连接。
-
SYN:该位为1时,表示希望建立连接,并在设置序列号字段的初始值。
-
FIN:该位为1时,表示不会有数据发送,希望断开连接。
特性
由于TCP中很多特性,在这列举比较熟知的。
一、确认应答
确认应答是保证”可靠性“最核心的机制。
确认应答就是告诉对方我收到消息了。发送方发出一个数据包,如果接收方收到了,就返回一个数据包告诉发送方我收到了。
确认应答机制往往配合着确认序号与ACK标志位使用。当ACK标志位为1时,确认序号为有效值,此时这条报文就是一个应答报文,告诉发送方我这边收到了,发送方可以根据确认序号看看有没有丢包的情况。
二、超时重传
由于网络的情况非常复杂,避免不了出现一些丢包的现象,那又该如何处理呢?此时可以大致分为两类情况,一种是发送端发送的数据包丢了,另一种是接收端的应答数据包丢了。
情况一:
如果是发送端发送的数据包丢了,此时发送端就一直接收不到ACK数据包,我们可以通过设置一个超时时间,即过了这个时间还没有收到ACK的话,就再发一次数据包。
总的来说就是没收到应答,就再发一次。
情况二:
如果是接收端已经收到了这个数据,但是应答报文丢了,此时发送端无法区分是否是第一种情况,所以还是会进行重传,接收端就需要进行去重操作。然后再次发送一个应答报文。
如何去重?
使用序列号作为判定的依据。tcp会在内核中给每个socket对象都安排一个内存空间,相当于一个队列,收到的数据就会被放到这里面,并按照序号排列好(还解决了乱序的问题),当来了一个重复的数据以后,就可以根据索引值判断是否出现过了。
丢包本质上是一个概率事件,不可避免,而且随着重传的次数,概率会大幅降低。我们需要合理的设置超时时间。
具体数值可以手动配置,我们更应该去关注里面的策略。
超时时间不是一个固定的值,会随着超时轮次增加。如果好几次都没重传成功的话,说明此时网络本身的丢包率非常高,可能遇到了非常严重的故障,需要拉长一下重传时间,给网络恢复留有一个时间。超时重传的轮次也不是无限的,达到一定次数就会尝试重置tcp连接,设置RST标志位,如果RST报文也丢了,说明此时网络严重故障,那就会放弃连接。
三、连接管理
三次握手
TCP是面向连接的协议,所以使用TCP前必须先建立连接,而连接的建立是通过三次握手来进行的。不过三次握手本质上是"四次握手",只是将其中的两步合并成了一步。
ACK是应答报文,SYN是同步报文,表示申请建立请求。
为什么要进行三次握手?
1、验证通信路径是否通畅,双方的发送和接受能力是否正常
TCP要想保证可靠传输,就得先知道有没有路径以及路径是否通畅(网络拥堵会出现历史连接原因,造成资源的浪费),然后通过三次握手,确定双方是否有发送和接受的能力。只有确定了双方都有接收和发送的能力,才能进行后续的可靠传输。
2、协商必要的参数
通信的时候会涉及到一些参数,比如序列号。由于网络是时刻变化的,会出现先发后至的现象,这时我们可以通过序列号来判断这个消息是不是合法的,即可能这个数据是上一次的连接中的。
那是否可以只进行两次握手呢?四次是否可以?
如果值进行两次握手的话,B端就无法知道A端能否接受数据,以及自己的数据有没有发过去,也就无法保证参数进行了协商,即不能保证双方的序列号是同步,并且如果出现了网络较为拥塞的时候,建立连接的消息重发了好几次,服务器在第一次握手的时候就会建立连接,即创建了一个socket对象,造成了资源的浪费。
如果是进行四次握手的话,本质上就是将三次握手的第二次握手拆开来,而三次握手就可以建立可靠的连接了,多了反而也会浪费资源。
四次挥手
在进行通信后,由于前面的连接会消耗资源,因此我们还需要进行断开连接来释放资源。由于断开连接涉及到了四次通信,因此也被称为是四次挥手。
四次握手能变成三次握手吗?理论上是可以的,当我们调用socket.close方法足够快的时候,即收到关闭请求的后续没啥业务逻辑,就可以合并。但是一般服务器后续还有很多的收尾工作要处理,这时候close方法执行的时机比较慢,就不能合并了。
丢包问题
由于网络通信的复杂,可能会出现丢包,这时候怎么办呢?
一个原则,收不到回应就重传,重传多次还收不到,那就单方面断开连接。
如果第一次挥手丢了,那么就重传,一直收不到回应就断开连接。
如果第二次挥手丢了,由于客户端无法区分,客户端会重传FIN报文,跟第一次挥手丢了同理。
如果第三次挥手丢了,重传,一直收不到回应,就断开连接。
如果第四次挥手丢了,此时站在客户端的角度,客户端收到了服务器的FIN报文并已经发出ACK报文确认了,但是客户端还不能立马释放连接,因为还不能确定服务器是否收到,因此会等待一个2*MSL的时长,在此期间没收到重传的FIN报文,就可以释放连接了。如果丢包了,服务器会重传FIN报文,客户端也就可以回应ACK报文了。
四、滑动窗口
滑动窗口机制是用来提高TCP的传输效率,让TCP在保证可靠的前提下,效率别要太低。虽然滑动窗口能提升TCP的效率,但是这也是有限的,还是不可能比UDP高。
本质就是节省了应答时间。之前是每发一次数据就需要等待一个确认报文的时间,使用滑动窗口后,可以在等待确认报文的时间内,再多发几次数据。
丢包问题
在发数据的时候,丢包情况分为两种:1)数据报了 2)确认应答(ACK)丢了
1)数据丢了
由于前面的确认应答机制,我们TCP协议中的确认序号字段会记录当前发到哪个数据了,如果前面某一个数据丢了,确认序号不会改变,依旧和上一次应答的确认序号是一样的,后面客户端连续收到了服务器索要的相同确认序号的数据时,客户端就明白了丢了哪部分数据,然后就可以进行重传。
2)确认应答丢了
确认应答丢了并不要紧,只要后续确认应答的序号比前面确认应答的序号大的话,就可以理解为,前面的数据都已经接收到了,如果所有ACK都丢了,说明网络出现了重大故障,此时也不满足网络可靠的前提条件了。
五、流量控制
流量控制是作为滑动窗口的补充,理论上滑动窗口越大,传输效率就越高,但是当窗口大小达到一定程度后,接收方可能就处理不过来了,或者说网络传输上的某条链路就处理不过来了,这样就会出现丢包,就得进行重传,结果适得其反了。因此我们需要进行流量控制,让发送方慢一点~
流量控制就是根据接收方的接收能力,来限制发送方的速度,即限制窗口大小。当接收方接收数据的时候,会先将数据存储在缓存区中,因此可以是用缓存区中的剩余空间大小来作为窗口的大小,即修改TCP的中窗口大小字段。
大致流程:
首先客户端先发送一下数据,看看窗口大小多少合适,然后根据窗口大小发送数据,当窗口大小为0的时候就不发数据,期间会发送窗口探测包,来询问服务器啥时候有空?一旦发现不是0了以后,就继续开始发数据。这样接收方可以根据窗口大小来限制发送方的传输速度了。
六、拥塞控制
上述的流量控制是针对接收方的处理能力来判断当前的窗口大小,但是由于在传输的过程中会经过许多节点(路由器/交换机),那么这些中间节点的处理能力是否能达到窗口大小呢?
由于路过哪些节点,在一开始的时候是无法确定的,因此设计TCP的大佬们选择使用"实验"的方式来测试路径节点的接收能力,然后综合分析计算得出一个值,后续再发送数据的速度就不应该超过这个值(木桶效应)。
拥塞控制具体是这样展开的:
1.慢启动:刚开始通信的时候使用一个小窗口,如果传输顺利,没有丢包就会进行扩大窗口。
2.指数增长:在传输顺利后,拥塞窗口大小就会指数增长。
3.线性增长:当指数增长到一个阈值的时候,就会从指数增长转变为线性增长。
4.拥塞窗口回归小窗口:在窗口增长的过程中,如果传输过程中出现丢包了,说明此时发送的速率接近当前网络的极限,此时会把窗口调整为最初的小窗口并将指数增长的阈值变小,然后继续重复上述的过程。
因此还可以得出一个结论:实际发送的窗口大小不光要考虑接收方的处理能力,还要考虑中间节点的处理能力。
实际发送方的窗口= min (拥塞窗口, 流量控制窗口)
七、延时应答
延时应答也是为了增大滑动窗口的大小,从而挺高传输效率而提出的。
延时应答通过在返回ACK应答报文的时候,尽量慢一点,利用拖延出来的一点点时间,让接收方多处理一些数据,这样接收方的接收缓冲区的空间就更大了,下一次就能接收更多的数据。
八、捎带应答
捎带应答是在延迟应答的基础上,引入的进一步提高效率的方式。
延迟应答是让ACK报文传输的时机更慢,我们不仅可以让接收方利用这段时间处理缓存中数据,还可以让接收方做出响应的同时再带上ACK报文(类似将四次握手合并成三次握手)。
之前所提到的四次挥手也有可能变成三次,主要是通过延时应答和捎带应答完成的。在第二挥手的时候进行延时应答,然后跟第三次挥手一起发送给对方,而数据包从两个合并成一个,效率会有明显的提升,因为每次传输数据都会进行封装分用以及传输时延都会花费不少时间。
九、面向字节流——粘包问题
由于TCP是面向字节流的,所接收到的数据包会按照一个字节一个字节的存储在缓存区中,如果我们不认为的进行约定,那么就无法区分一个数据的结尾是哪。
如上所示:
对于发送方来说,发送了3个数据报,AAA为一个应用层数据报,BBB是一个应用层数据报,CCC是一个应用层数据报,但是接收方可区分不了,三个数据报都粘在一起了,很有可能后续会理解错发送方的意思,比如将这三个数据包理解为了AAAB, BBC, CC,就出现了bug。
此时就只能在应用层面来处理这个问题了。如:
-
在应用层协议中引入分隔符区分包之间的边界。例如:以\n作为一个数据包的结束标志。
-
在应用层协议中引入包长度区分包之间的边界。
十、异常情况处理——心跳包
在实际生活中,会出现许多不可抗力的因素,比如电源被家长关了、网线被人拔了等,不过这些大差不差,可以分为四大类。
1、进程崩溃
当进程崩溃了,进程所持有的PCB中的文件描述符表也就被释放了,即相当于调用了socket.close方法,崩溃的一方在内核中就会发出FIN,就变成了四次挥手了,此时也就和进程的正常退出没啥区别了。
2、主机关机
电脑在正常关机的时候,会先结束掉所有的进程,后续就跟进程崩溃的处理一样了(如果没挥手完也没关系 ,也就演变成了丢了某一次挥手)。
3、主机掉电
如果是台式机的话,一旦拔掉电源,电脑就立马黑屏了,根本不会给操作系统留有反应的空间。此时又分为了两种情况:
a) 如果接收方突然断电了,那么发送方就无法接收到ACK,此时发送方会进行超时重传,如果一直重传失败,就会发送复位报文(RST),尝试重置连接,如果失败了,就会单方面释放连接了。
b)如果发送方突然断电了,接收方无法区分发送方是等一会发送呢,还是不发了,此处就会涉及到"心跳包",接收方就会周期性的给对方发一个不携带任何业务数据的tcp数据报,发起这个这个包的目的,就是为了触发ACK确认对方是否正常工作。
4、网线断开
如果是网线断开的话,其实也跟主机掉电基本类似。