本篇内容主要介绍QUIC的接收端状态机。 与发送端状态机类似,QUIC的接收端状态机有六种状态:一种初始状态、三种中间状态和两种结束状态。 如果你已经熟悉了发送端状态机的相关概念,理解接收端状态机将会很容易。
一、QUIC流接收端状态机详解
先上图,对QUIC的接收端状态机先有一个初步印象,下文将对各个状态、状态转换、各状态下的行为逐个介绍。
一个初始状态
Recv
我们回顾一下发送端状态机的初始状态(Ready),发送端状态机只在两种情况下进入初始状态:
- 本端创建了一个发送流。
- 对端创建了一个双向流。
接收端状态机的初始状态是Recv,相比于发送端状态机,接收端状态机进入初始状态的条件就比较多了:
- 本端创建了一个双向流。
- 收到了对端的STREAM帧、STREAM_DATA_BLOCKED帧或RESET_STREAM帧。
- 收到了对端的MAX_STREAM_DATA帧或STOP_SENDING帧(双向流情况下)。
- 本端创建了一个流ID数字更大的流。
前两条应该很好理解,本端创建了一个双向流当然包含接收端,此时接收端状态机进入初始状态。
对于第二条来说,收到了对端的这三种帧之一说明对端已经存在一个发送流了,并且很可能后续就要发送数据了,本端流的接收端状态机也要进入Recv的状态。
后面两个条件就不那么直观,要稍微琢磨一下: 先看第三条,MAX_STREAM_DATA帧或STOP_SENDING帧是由对端流的接收部分通知给本端流的发送部分,这两种帧的内容是用来控制本端流发送部分的数据发送的。对于对端初始化的双向流来说,我们可以把这两种帧认为是一种通知(可以这样理解:对端流的接收端已经开始工作了,本端也不能闲着,接收端也要创建起来),在此刻将本端流的接收部分一并创建是个合适的时机。
再看第四条,当本端创建了一个流ID数字更大的流时会创建流的接收部分,为什么要这样做呢?在QUIC协议中,创建流时指定的流ID是递增的,在新的流被创建时比它ID数字小的流必须被创建,这样带来的好处是在通讯的两端创建流的顺序将保持一致。
在Recv状态下,流的接收端会进行哪些动作呢。顾名思义,Recv状态下流会接收发送端传送的数据帧。流的接收端内部会有一个缓存,以应对网络的丢包与乱序,它将重组后的有序的数据交给上层应用。 另外处于Recv状态下的接收端,会向发送端发出MAX_STREAM_DATA帧,来指导对端的传送速率,MAX_STREAM_DATA控制的传输速率与接收端内部缓存的余量也是有关联的。
可以从Recv状态进入两个中间状态:
- 收到了设置FIN位的STREAM帧,此时进入Size Known状态。
- 收到了流重置的通知(RESET帧),此时进入Reset Recvd状态。
用一张图总结Recv状态:
三个中间状态
Size Known
Size Knwon状态是由Recv状态转换而来,由对端流发送部分发出的带FIN位的STREAM帧触发,表示对端数据已经发送完毕。
本端流的接收部分在此时会知晓在流上一共发送了多少的数据,并等待接收缺失的数据(缺失数据由网络丢包、延迟、乱序等造成)。 在此状态下的流接收端,不需要再生成MAX_STREAM_DATA帧。
用一张图总结Size Known状态:
Data Recvd
Data Recvd状态是由Size Known状态转变而来的,表明流的接收端已经收到了在流上传输的所有数据。 该状态仍是一个中间状态,因为此时数据只是接收到了,存储在协议层的缓存里,没有完全交付给上层应用,只有在数据完整的交付给上层应用后才能进入最终状态。
用一张图总结Data Recvd状态:
Reset Recvd
能够进入Reset Recvd的状态有很多,除了两种结束状态,其余三种状态(Recv, Size Known, Data Recvd)都可以在收到Reset后进入Reset Recv状态。 Reset Recvd表示本端流的协议层已经知晓对端流发出了异常中断,但该状态仍不是一个结束状态,原因是还没有将Reset的信息通知给上层应用。 用一张图总结Reset Recvd状态:
两个结束状态
结束状态的行为比较简单,都不再收、发消息,也不能再转换为其它的状态,此时可以释放相关资源。
Data Read
该状态由Data Recvd状态转换而来,当流上的数据都完整的交付给了上层后才会进入此状态,这是一个正常结束的状态。
Reset Read
该状态由Recv Recvd状态转换而来,协议层接收端将流被Reset的事件交付给上层后,就会进入此状态,这也是一个结束状态。
在最终状态下,除了等待释放资源就没有什么其它事情可做了,就不画图解释了。
二、两种可选的状态转换
Data Recvd和Reset Recvd两种状态之间也是可以存在状态转换的,不过该类状态转换不是强制的,在实现QUIC协议时可以自由的处理这两类情况。
第一种情况 Data Recvd后收到RESET帧
进入Data Recvd后,流上传输的数据已经全部到达接收端,在此时如果收到了RESET帧可以进入Reset Recvd状态,这就要中断数据对上层应用的交付。
这种情况下,虽然收到了完整的数据,但上层不能够完整的读取,未免太可惜了。
那么如果发生这样的情况,不将Reset事件报给上层,就当它没发生过,让上层应用完整的处理数据行不行?
答案是可以的,可以选择“抑制”reset事件,在实现QUIC协议时你将拥有这个自由。
第二种情况 收到RESET帧后收到了完整的数据
和前一种情况类似,在Size Known状态下,收到RESET帧但还没有通知给上层reset事件的间隙收到了流上所有的数据,此时也可以选择抑制reset事件,将完整的数据交付给上层应用。
三、总结与思考
QUIC流的接收端状态机设计还是非常简洁且完备的,虽然看起来要比发送端状态机复杂一些,尤其是有4个条件可以进入初始状态。
QUIC接收端状态机与发送端的状态数量是类似的,都是一个初始状态、三个中间状态和两个结束状态,这倒是有助于我们记忆。
Recv状态下可以收数据、反馈发送端收数据的ACK、控制发送端的发送速率。
Size Known状态下等待数据传送完整。
Data Recvd 和 Data Read 分别表明协议层和应用层收到了完整的数据。
Reset Recvd 和 Reset Read 分别表明协议层和应用层收到了reset事件。
我们观察接收端状态机的两个最终状态Data Read 和 Reset Read,都是保障了上层应用收到了完整数据或者事件,而不是仅仅关注协议层,对协议的使用者来说,还是非常体贴的。
此外,在实现中,在收到完整数据的情况下还允许抑制reset事件,这对某些应用场景还是很友好的(避免了重新请求)。
C++服务器开发 学习资料、教程 有需要的可以自行添加学习交流群739729163 领取
C/C++Linux服务器开发教程: https://ke.qq.com/course/417774?flowToken=1031343
作者:亦无风雨亦无晴 链接:https://juejin.cn/post/7197388716545179703