执行器
rust 中有很多异步执行环境。但是异步执行环境有一个问题。它有"传染性",如果一个函数是异步的,那么它的所有调用者都必须是异步的。在前面的流量处理模型并不是异步的。所以为了不至于把协议解析部分的异步影响到流量处理模型,我们需要一个自己的执行器。这个执行器把异步操作封装起来,然后在流量处理模型中使用。尽量不影响使用者的习惯和模型,是一个库的修养。
rust 的执行器简单来说类似于前文所讲的c语言的状态机驱动函数。如果一个future返回了 Ready ,说明future已经完成了,需要执行下一步。如果一个future返回了 Pending ,说明future还没有完成,需要等待下一次调用。因为我们不需要复杂的异步运行时,所以这里的执行器只需要控制好future的返回值即可。代码如下:
rust
let waker = dummy_waker();
let mut context = Context::from_waker(&waker);
match Pin::as_mut(parser).poll(&mut context) {
Poll::Ready(Ok(())) => {
self.c2s_state = TaskState::End;
Some(Ok(()))
}
Poll::Ready(Err(())) => {
self.c2s_state = TaskState::Error;
Some(Err(()))
}
Poll::Pending => None,
}
这样,内部是异步,外部是同步。每到来一个包,执行器就执行一次future,也就是解码器。外部不需要异步操作。也就是在同步代码中可以调用执行器:
rust
loop {
pkt = get_packet();
task.run(pkt);
}
当然,要获取解码结果。需要提前设置回调函数。解码过程中就会触发回调函数。
解码器
所有的这些工作,都是为了解码器。因为需要同时处理大量的链接,解码过程中是不能等待后续数据包的。所以解码器是一个future,也就是异步函数。当后续数据还没有到来时,解码器返回Pending,暂时挂起。执行器得到Pending后,不会认为这个链接的解码已经结束,而是继续处理下一个链接。
但这种异步调用的复杂性都被rust的future机制封装起来了,解码器的实现关注解码器的实现即可。完全可以按照同步的模式来写,例如SMTP协议的解码器的部分代码:
rust
loop {
let (line, seq) = stm.readline_str().await?;
if line == "\r\n" {
return Ok((boundary, te));
}
}
这就是读取重组后的头部数据的过程。可以看到,它并没有关心数据没有到来的情况。如果当前的数据包并没有完整的数据,解码器中也不需要对此进行判断,和退出。它完全按照同步的过程在执行解码。直到读取到头结束为止。唯一的区别就是它的读取数据函数式一个异步函数。末尾加了await关键字。
每一个await都会触发一次future的调用。如果当前的数据包没有完整的数据,那么await会返回Pending,解码器会被挂起。执行器会继续处理下一个数据包。当数据包到来时,解码器会被唤醒,继续执行。直到读取到头结束为止。
这样,我们就可以把协议解析的工作交给执行器来处理。执行器会自动处理异步的情况。使用者只需要关注解码器的实现即可。而且解码器的过程变得简单容易理解。
从最开始的c语言中的大循环中记录状态,到c语言中用状态机驱动,再到rust的future机制,再到现在的解码器,过程越来越简单,最终我们以同步的流水账式的代码来实现了异步的解码过程。