在传输层中,有两个很重要的协议:UDP和TCP
接下来我们先来聊一聊这两个协议
目录
UDP
报文格式
UDP特点
TCP
报文格式
TCP特点
序号与确认序号
超时重传
连接管理
如何建立连接(三次握手)
如何断开连接(四次挥手)
滑动窗口
流量控制
拥塞控制
延时应答
捎带应答
粘包问题
TCP的异常处理
进程终止
机器关机
机器掉电/网线断开
如何基于UDP实现TCP
一个UDP报文包含UDP报头和UDP数据载荷两个部分
其中数据载荷就是应用层数据
UDP报头则存储着8字节的信息:分别是源端口号,目的端口号,报文长度和校验和
其中源端口号就是操作系统给客户端自动分配的端口
目的端口就是服务器的端口
报文长度在UDP中只有两个字节,也就意味着只能表示0-64k的数据,如果有很长的数据,需要在应用层分包,再通过多个UDP数据报发送(所以一般用TCP)
校验和用于验证网络传输的这个数据是否正确,校验和即可以通过数据个数判断,也可以根据数据内容判断
无连接就是指只要知道端口号和IP就进行传输,不需要建立连接
不可靠表示发送方发送数据后,不知道接受方是否收到了这些数据
面向数据报是指UDP是以数据报格式传输的,也就是发送方发送一个100字节的数据,接受方必须只能一次就接受完这个100字节的数据
全双工就是指两人通信之间,双方可以同时接受或者发送消息,就如电话。区别于单工(广播)和半双工(对讲机)
其中,TCP最重要的机制就是可靠传输,即能够知道对方是否收到消息
那TCP是如何保证可靠传输的呢?
早年的通讯没这么发达,如果我给女神发短信,在不可靠传输下,可能会有很多种情况:
第一种情况
第二种情况
为了避免这种情况,TCP引入了序号与确认序号的机制,这样就不会搞混了:
这里还要注意,TCP针对消息的序号,不是按照“消息条数”来编号的,而是按照字节来编号的:
具体过程如下:
A先给B发送了1000个字节
之后B会给A返回一个应答报文(ACK)会带有确认序号,叫做1001,也就是小于1001的数据已经被主机B收到了。(1-1000被层层封装为以太网数据帧进行传输)
接下来主机A就从1001再开始传输
上面的确认应答是网络一切正常的时候,通过ACK通知发送方表明收到了
如果出现丢包的情况,那么这个时候就是超时重传的起作用时刻
第一种情况:发送方数据丢了
第二种情况:接受方数据丢了
但是实际情况无法判断是哪种情况。
所以这里考虑最坏情况:对方没有收到
不过发送方会先有一个等待时间,如果超过了这个等待时间也没等到ACK,就会重发一次
这就是超时重传。
这里第二种情况我们还需要单独考虑一下:
如果ACK丢了,发送方的消息没丢,如果此时超时重传,会出现重复的消息
TCP的解决办法是在操作系统内核中创建一个接收缓冲区
接受方收到的数据在进入这个缓冲区前,会先判断缓冲区中是否存在过这个数据
如果存在过直接丢弃,否则就放进来。
最后注意:
但是如果网络收到了严重的波动,它也不会一直重传下去,会自动断开TCP连接。
重传的时间间隔也不是一成不变的,一般会逐渐加大,也就是重传频率会降低。
注意, 这块知识是重点
客户端和服务器之间,通过三次交互,完成了建立连接的过程。
客户端先发送一个SYN,SYN为1表示当前报文是一个“同步报文段”,开始请求建立连接。
之后服务端会发送ACK,表示收到回应,再发出SYN表示正式建立连接。
注意,这里服务器端发送的SYN和ACK是同时发送的。
最后客户端收到,发送ACK表示确认。
那么三次握手如何保证TCP连接的可靠性呢?
三次握手相当于“投石问路”,检查一下网络是否满足可靠传输的基本条件。
如果三次握手成功,就证明了通信双方网络正常,可以进行后续的传输。
就如下面的情景:
建立好连接后,操作系统内核中会用数据结构来保存信息
这个信息叫做五元组,即源IP,源端口,目的IP,目的端口,TCP
保存了信息就占据了一定内存,在断开连接后要释放这一块内存,就需要四次“挥手”
发送方(这里可能是客户端,也可能是服务器)先发送FIN,表示要结束了
接受方返回确认信息ACK,也返回FIN表示结束
接受方返回ACK确认
注意,这里中间的ACK和FIN是不能合并的,因为他们不是同时发送的(以为这里的ACK是内核发送的,FIN是用户代码负责的)
最后,这里每次都传输都会有一个状态
CLOSE_WAIT:表示四次挥手挥了两次后的状态,这个状态是在等待代码调用close方法
TIME_WAIT:表示发送方接受FIN后,不立即释放资源,会等待一段时间再释放
因为立即释放的话,如果最后一次ACK丢包了,接受方会超时重传,再发送FIN
如果接受方已经释放资源,就不会再收到这个FIN了
最多等待时间为2MSL,即网络上任意两点间传输需要的最大时间(可以配置的)
滑动窗口存在的意义就是在保证可靠性的前提下,尽量提高传输效率。
我们来看看滑动窗口是如何提高效率的。
首先没有滑动窗口的情况:
在没有滑动窗口的时候,由于确认应答机制,每次执行一次发送操作,都要等待上个ACK的到达
所以很大一部分时间都去等ACK
滑动窗口就是为了“批量发送数据” ,一次发一波,一起等ACK,如下:
这里一次就发送了4000数据,发完后统一等。
但是统一等的意思不是等4000数据的ACK都等到了才继续往下发送,而是收到一个ACK,就继续往下发送一组,如下:
但这里不可避免会出现丢包的问题,如何解决呢?
第一种情况:ACK丢了
这里1001丢了,但是先收到了2001
这里注意:2001的意思是2001之前的数据都收到了,所以1001已经无足轻重了
所以2001表示1-2000都收到了,只是丢了一个1001的ACK没有关系
第二种情况:数据丢了
数据包丢了,就会一直等待那个丢了的数据包发送。
比如1001-2000的数据丢了,后面就会一直索要1001的数据
一直索要的情况就会让主机A明白这里数据丢了,触发重传
但是这里还会有一个接受缓冲区,2000数据丢了不会一直等,让后面的数据先来到缓冲区
等到2000数据来了,整个缓冲区的数据就都发过去了
这个过程也叫做快重传。
流量控制是滑动窗口的延伸,目的是为了保证可靠性
在滑动窗口中,窗口越大,传输速率越高
但是我们还要考虑接收方能不能处理发送方一次性发送那么多的数据
如果接受方接受不过来,就会丢弃那些数据,又会引发重传。
所以流量控制就是为了衡量接受方的处理速度
此处就是直接衡量接受缓冲区的剩余空间大小来判断处理能力。
剩余空间大,就发的快点,剩余空间小,就发的慢点
那么如何知道接受缓冲区大小呢?
在ACK报文里,有一个16位窗口大小,这个就专门表示了缓冲区的大小
这里主机A每发送一次数据,B就会反馈ACK,ACK里面会有窗口大小
当窗口大小为0后,A也不会完全不发送数据,A会定期发送一个探测报文,来看看窗口大小。
拥塞控制也是滑动窗口的延申,用来限制滑动窗口发送的频率
这里注意,主机A和主机B之间通信,要经过很多转发设备
拥塞控制就是对这些设备进行衡量,最终找到一个合适的窗口范围
因为无法知道中间有多少个设备,所以采用“试一试”的方法来判断:
首先A以较小的滑动窗口发送数据,如果很快就发过去了,就加大窗口大小
如果加大到一定程度出现丢包了,就减小窗口大小
反复上面两步就能找到一个合适的窗口范围
拥塞窗口也不断变化去限制滑动窗口,实现动态平衡
下面是一个拥塞窗口大小的变化过程:
初始大小为1,一开始指数增长,之后快接近极限了,就线性增长
增长到极限后,就恢复为初始大小,然后重复这个过程找到最合适的窗口范围
图中还有一个ssthresh值,这个值表示窗口大小啥时从指数变化到线性变化
每次丢包,回到初始值后,这个阈值会减为刚刚丢包大小的一半。
这相当于流量控制的延申
本来的流量控制是为了让发送方不要发太快,延时应答是在此基础上,尽可能让滑动窗口大一点
比如A发送数据后,B会延时一会再回复
这样接受缓冲区的大小就会稍微大一些,速度也会更快一点
捎带应答又是延时应答的延申
比如主机A发送了一个数据,主机B会立刻返回一个ACK,再执行相应内容
如果触发了延时应答,ACK会迟一步再发送,这样ACK和代码内的相应内容就同时发送过去了
这个过程也叫做捎带应答
没有延时应答:
触发了延时应答:
TCP如此可靠,但是还会有粘包问题
粘包指的是在接受缓冲区中,若干个应用层数据包混在一起了
当主机B从接受缓冲区取时,由于TCP是面向字节流的,B就取得若干个字节
但是如何区分哪里到哪里是一个完整的数据报内?
所以一般会给一个标志代表结束,比如每个包末尾都加上一个分号
但是目前用一些框架,人家已经为我们考虑好了,这个问题不太需要考虑
在这个进程运行时,突然强制关闭了它,TCP连接会发生什么情况?
TCP连接是通过socket来连接的,socket本质上是一个文件
而文件存在于进程PCB里的文件描述符表里
如果强行关闭一个进程,PCB也就没了,文件也就自动关闭了,这里也会触发“四次挥手”,无异常
这个是正常关机,关机会关闭所有进程
进程关闭就会触发“四次挥手”,所以无异常
这两种情况操作系统毫无准备,所以会发生异常。
第一种情况:
主机A给主机B发送数据,此时主机B断电了,A进入超时重传
但是B已经断电了,所以A收不到ACK,进入重新连接
当然这个重连肯定是失败的,所以最终会断开连接。
第二种情况:
如果主机A断电了,B会给A发送一个探测报文,为了看看A是否返回ACK
如果A没有返回ACK,就会重连,重连失败最终断开连接。
最后来一个经典题目,其实答案很简单
本质上就是在应用层基于UDP复刻TCP的机制
谢谢你能看到这ヽ( ̄ω ̄( ̄ω ̄〃)ゝ