如果这篇文章有帮到你,能给我一个 star 吗🥰 👉 github.com/night-cruis…
在前面的章节中,我们讲到过异步运行时负责调度执行使用者创建的 Future,那么异步运行时到底是如何工作的呢?在本章中,我们将会实现一个简单的单线程异步运行时,提供异步的网络IO读写操作,以探讨运行时的具体工作机制。
在正式开始之前,我们首先明确一下即将实现的运行时的工作原理:
- 用户使用
async fn或者async {}的方式创建Non-Leaf Future,然后使用spawn方法创建一个异步task,并将这个task发送到executor的任务队列中。 executor从task_queue中取出task,调用task的poll方法,驱动Non-Leaf Future开始执行(如果已经开始执行了,则从上次的await断点处继续执行),就这样一直执行Future中的代码,直到遇到Leaf Future.await。- 调用
Leaf Future的poll方法,如果Leaf Future对应的IO事件已经就绪,则直接返回Poll::Ready(data);如果对应的IO事件没有就绪,则调用Reactor的register方法注册等待的IO事件和waker,然后Poll::Pending(Non-Leaf Future将会被挂起),executor可以继续执行其他的task。 Reactor会把注册的文件描述符fd、waker保存在BTreeMap<fd, waker>中,然后调用Epoll提供的方法注册在fd上想要等待的event到Epoll系统中。Reactor调用Epoll提供的wait方法获取所有就绪的文件描述符fds,然后遍历fds,通过fd匹配之前在BTreeMap中存储的waker,然后调用waker的wake方法把task发送到executor的执行队列中,这样之前挂起的Non-Leaf Future就能够继续执行了。
通过上面的原理讲解我们可以知道,异步代码之所以高效的原因就是避免了IO对线程的阻塞:
- 当执行一个
task时,如果遇到了没有就绪的 IO 操作,就注册waker到Reactor中,然后挂起这个task,executor就可以继续执行其他的task。 - 当
task等待的 IO 事件就绪时,Reactor就会通过waker唤醒关联的task,然后就可以执行之前挂起的task了。
epoll
就像 Epoll servere example 一节中那样,为了方便地调用 libc 提供的 api,我们先创建一个 syscall 宏:
macro_rules! syscall {
($fn: ident ( $($arg: expr),* $(,)* ) ) => {{
let res = unsafe { libc::$fn($($arg, )*) };
if res == -1 {
Err(io::Error::last_os_error())
} else {
Ok(res)
}
}};
}
Epoll
抽象出 Epoll 和 EpollEventType 类型:
pub(crate) struct Epoll {
fd: RawFd,
}
pub(crate) enum EpollEventType {
// Only event types used in this example
In,
Out,
}
RawFd 表示原始文件描述符。
方法实现
new
创建一个 Epoll 实例:
pub(crate) fn new() -> io::Result<Self> {
let fd = syscall!(epoll_create1(libc::EPOLL_CLOEXEC))?;
Ok(Epoll { fd })
}
添加事件/修改事件
fn run_ctl(&self, epoll_ctl: libc::c_int, fd: RawFd, op: EpollEventType) -> io::Result<()> {
let mut event: libc::epoll_event = unsafe { mem::zeroed() };
event.u64 = fd as u64;
event.events = match op {
EpollEventType::In => libc::EPOLLIN as u32,
EpollEventType::Out => libc::EPOLLOUT as u32,
};
let event_p: *mut _ = &mut event as *mut _;
syscall!(epoll_ctl(self.fd, epoll_ctl, fd, event_p))?;
Ok(())
}
pub(crate) fn add_event(&self, fd: RawFd, op: EpollEventType) -> io::Result<()> {
self.run_ctl(libc::EPOLL_CTL_ADD, fd, op)
}
#[allow(dead_code)]
pub(crate) fn mod_event(&self, fd: RawFd, op: EpollEventType) -> io::Result<()> {
self.run_ctl(libc::EPOLL_CTL_MOD, fd, op)
}
add_event 和 mod_event 都是通过调用run_ctl 方法实现的。在 run_ctl 方法中根据 op 类型设置要注册/修改的事件类型,然后调用 epoll_ctl 方法来注册/修改事件。
删除事件
pub(crate) fn del_event(&self, fd: RawFd) -> io::Result<()> {
syscall!(epoll_ctl(
self.fd,
libc::EPOLL_CTL_DEL,
fd,
std::ptr::null_mut() as *mut libc::epoll_event
))?;
Ok(())
}
删除在 Epoll 实例中注册描述符 fd。
等待就绪事件
pub(crate) fn wait(&self, events: &mut [libc::epoll_event]) -> io::Result<usize> {
let nfd = syscall!(epoll_wait(
self.fd,
events.as_mut_ptr(),
events.len() as i32,
-1
))?;
Ok(nfd as usize)
}
调用 epoll_wait 函数获取所有就绪的文件描述符,并将就绪的描述符存放到 events 中,最后返回就绪的描述符数量。
关闭Epoll
为 Epoll 实现 Drop trait,在清理 Epoll 时关闭 Epoll 的文件描述符:
impl Drop for Epoll {
fn drop(&mut self) {
syscall!(close(self.fd)).ok();
}
}
reactor
Reactor
pub(crate) struct Reactor {
pub epoll: Epoll,
pub wakers: Mutex<BTreeMap<RawFd, Waker>>,
}
字段 epoll 存储创建的 Epoll 实例,wakers 存储等待的IO事件的文件描述符和对应的 waker。
我们稍后将会创建 Epoll 的静态变量,为了内部可变性,就把 BTreeMap<RawFd, Waker> 包在 Mutex 中。
添加事件
impl Reactor {
pub(crate) fn add_event(&self, fd: RawFd, op: EpollEventType, waker: Waker) -> io::Result<()> {
info!("(Reactor) add event: {}", fd);
self.epoll.add_event(fd, op)?;
self.wakers.lock().unwrap().insert(fd, waker);
Ok(())
}
}
在 Reactor 的添加事件的方法中,首先调用 epoll 的 add_event 方法注册文件描述符和监听的事件,然后把描述符和对应的 waker 存储在 BTreeMap<RawFd, Waker> 中。
reactor 循环
fn reactor_main_loop() -> io::Result<()> {
info!("Start reactor main loop");
let max_event = 32;
let event: libc::epoll_event = unsafe { mem::zeroed() };
let mut events = vec![event; max_event];
let reactor = &REACTOR;
loop {
let nfd = reactor.epoll.wait(&mut events)?;
info!("(Reactor) wake up. nfd = {}", nfd);
#[allow(clippy::needless_range_loop)]
for i in 0..nfd {
let fd = events[i].u64 as RawFd;
if let Some(waker) = reactor.wakers.lock().unwrap().remove(&fd) {
info!("(Reactor) delete event: {}", fd);
reactor.epoll.del_event(fd)?;
waker.wake();
}
}
}
}
在 reactor_main_loop 函数中,我们使用一个 loop 循环,在循环中调用 epoll 的 wait 方法获取所有就绪的 IO 事件的文件描述符,如果没有事件就绪,wait 方法就会阻塞 reactor 线程,避免 CPU 空转。
然后遍历就绪的描述符,从 wakers 中获取描述符对应的 waker,之后调用 epoll 的 delete_event 方法删除描述符,表示这个事件已经处理完毕。
最后,调用 waker 的 wake 方法,把因为等待IO事件而挂起的 task 发送到 executor 的执行队列中。
REACTOR 静态变量
lazy_static! {
pub(crate) static ref REACTOR: Reactor = {
// Start reactor main loop
std::thread::spawn(move || {
reactor_main_loop()
});
Reactor {
epoll: Epoll::new().expect("failed to create epoll"),
wakers: Mutex::new(BTreeMap::new())
}
};
}
Executor 在主线程运行,负责调度执行 task,而 reactor_main_loop 内部使用一个无线循环不断地获取就绪的 fd,并唤醒挂起的 task。为了避免 reactor_main_loop 阻塞 Executor,我们就开一个线程去执行 reactor_main_loop。
async_io
在 async_io 模块中,我们将会创建 Leaf Future,异步化网络IO的监听和读写操作。
Ipv4Addr
pub struct Ipv4Addr(libc::in_addr);
impl Ipv4Addr {
pub fn new(a: u8, b: u8, c: u8, d: u8) -> Self {
Ipv4Addr(libc::in_addr {
s_addr: ((u32::from(a) << 24)
| (u32::from(b) << 16)
| (u32::from(c) << 8)
| u32::from(d))
.to_be(),
})
}
}
Ipv4Addr 就是 IPv4 地址,new 方法负责创建一个 Ipv4Addr 类型。
TcpListener
pub struct TcpListener(RawFd);
impl TcpListener {
// NOTE: bind() may be block. So this should be an async function in reality.
pub fn bind(addr: Ipv4Addr, port: u16) -> io::Result<TcpListener> {
let backlog = 128;
let sock = syscall!(socket(
libc::PF_INET,
libc::SOCK_STREAM | libc::SOCK_CLOEXEC,
0
))?;
let opt: i32 = 1;
syscall!(setsockopt(
sock,
libc::SOL_SOCKET,
libc::SO_REUSEADDR,
&opt as *const _ as *const libc::c_void,
std::mem::size_of_val(&opt) as u32
))?;
let sin: libc::sockaddr_in = libc::sockaddr_in {
sin_family: libc::AF_INET as libc::sa_family_t,
sin_port: port.to_be(),
sin_addr: addr.0,
..unsafe { mem::zeroed() }
};
let addr_p: *const libc::sockaddr = &sin as *const _ as *const _;
let len = mem::size_of_val(&sin) as libc::socklen_t;
syscall!(bind(sock, addr_p, len))?;
syscall!(listen(sock, backlog))?;
info!("(TcpListener) listen: {}", sock);
let listener = TcpListener(sock);
listener.nonblocking()?;
Ok(listener)
}
pub(crate) fn accept(&self) -> io::Result<TcpStream> {
let mut sin_client: libc::sockaddr_in = unsafe { mem::zeroed() };
let addr_p: *mut libc::sockaddr = &mut sin_client as *mut _ as *mut _;
let mut len: libc::socklen_t = unsafe { mem::zeroed() };
let len_p: *mut _ = &mut len as *mut _;
let sock_client = syscall!(accept(self.0, addr_p, len_p))?;
info!("(TcpStream) accept: {}", sock_client);
Ok(TcpStream(sock_client))
}
pub fn incoming(&self) -> Incoming<'_> {
Incoming(self)
}
fn nonblocking(&self) -> io::Result<()> {
let flag = syscall!(fcntl(self.0, libc::F_GETFL, 0))?;
syscall!(fcntl(self.0, libc::F_SETFL, flag | libc::O_NONBLOCK))?;
Ok(())
}
}
impl Drop for TcpListener {
fn drop(&mut self) {
info!("(TcpListener) close : {}", self.0);
syscall!(close(self.0)).ok();
}
}
pub struct Incoming<'a>(&'a TcpListener);
impl<'a> Incoming<'a> {
pub fn next(&self) -> AcceptFuture<'a> {
AcceptFuture(self.0)
}
}
bind 方法负责绑定传入的的 IpV4 地址和端口号,创建一个 TcpListener 实例,需要注意的是要把 TcpListener 设置为非阻塞:listener.nonblocking(),这样在调用 accept 方法接收客户端连接时才不会阻塞。
accept 方法负责接收到来的客户端连接,然后创建 TcpStream,如果没有连接到来就返回一个 io error。
nonblocking 方法调用 libc::fcntl 函数把 TcpListener 设置为非阻塞。
incoming 方法把 TcpListener 的引用包在 Incoming 中,然后返回一个 Incoming 的实例。
Incoming 表示 TcpListener 接收连接的流式处理,每当我们想要接收一个新的连接时,就调用 next 方法返回一个 AcceptFuture(后面会讲这个)。
TcpStream
pub struct TcpStream(RawFd);
impl TcpStream {
fn nonblocking(&self) -> io::Result<()> {
let flag = syscall!(fcntl(self.0, libc::F_GETFL, 0))?;
syscall!(fcntl(self.0, libc::F_SETFL, flag | libc::O_NONBLOCK))?;
Ok(())
}
pub fn read<'a>(&'a self, buf: &'a mut [u8]) -> ReadFuture<'a> {
ReadFuture(self, buf)
}
pub fn write<'a>(&'a self, buf: &'a [u8]) -> WriteFuture<'a> {
WriteFuture(self, buf)
}
pub fn raw_fd(&self) -> RawFd {
self.0
}
}
impl Drop for TcpStream {
fn drop(&mut self) {
info!("(TcpStream) close : {}", self.0);
syscall!(close(self.0)).ok();
}
}
nonblocking 方法调用 libc::fcntl 函数把 TcpStream 设置为非阻塞。
read/write 方法分别返回 RreadFture/WriteFuture,和上面的 AcceptFuture 一样,我们将会在下面讲解这些 Future 的定义和作用。
Leaf Future
pub struct AcceptFuture<'a>(&'a TcpListener);
pub struct ReadFuture<'a>(&'a TcpStream, &'a mut [u8]);
pub struct WriteFuture<'a>(&'a TcpStream, &'a [u8]);
impl<'a> Future for AcceptFuture<'a> {
type Output = Option<io::Result<TcpStream>>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.0.accept() {
Ok(stream) => {
stream.nonblocking()?;
Poll::Ready(Some(Ok(stream)))
}
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
REACTOR.add_event((self.0).0, EpollEventType::In, cx.waker().clone())?;
Poll::Pending
}
Err(e) => Poll::Ready(Some(Err(e))),
}
}
}
impl<'a> Future for ReadFuture<'a> {
type Output = io::Result<usize>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let res = syscall!(read(
(self.0).0,
self.1.as_mut_ptr() as *mut libc::c_void,
self.1.len()
));
match res {
Ok(n) => Poll::Ready(Ok(n as usize)),
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
REACTOR.add_event((self.0).0, EpollEventType::In, cx.waker().clone())?;
Poll::Pending
}
Err(e) => Poll::Ready(Err(e)),
}
}
}
impl<'a> Future for WriteFuture<'a> {
type Output = io::Result<usize>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let res = syscall!(write(
(self.0).0,
self.1.as_ptr() as *mut libc::c_void,
self.1.len()
));
match res {
Ok(n) => Poll::Ready(Ok(n as usize)),
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
REACTOR.add_event((self.0).0, EpollEventType::Out, cx.waker().clone())?;
Poll::Pending
}
Err(e) => Poll::Ready(Err(e)),
}
}
}
在同步的处理方式中,监听 TcpListener 和读写 TcpStream 是阻塞式的,即会阻塞线程直到相应的 IO 事件发生;
而在异步的处理方式中,监听 TcpListener 和读写 TcpStream 不会阻塞掉线程,而是会返回一个对应的 Leaf Future:
Incoming的next方法会返回一个AcceptFuture。TcpStream的read/write方法分别返回ReadFuture/WriteFuture。
为 AcceptFuture/ReadFuture/WriteFuture 实现 Future trait,这样它们就有了 poll 方法。上述三个 Future 的 poll 方法的执行流程时类似的,因此下面我们只讲解 ReadFutrue 的执行流程。
当调用 ReadFutrue.await 时,会调用 ReadFutrue 的 poll 方法,在 poll 方法内部:
-
调用
libc::read函数从TcpStream中读取数据,返回res。 -
匹配
res的值:- 如果是
Ok(n),则读取到了数据,此时直接返回Poll::Ready(OK(n)),调用方继续执行ReadFutrue.await下面的代码。 - 如果是
Err(e),并且e.kind() == io::ErrorKind::WouldBlock,则说明TcpStream中没有数据可读,这时就调用REACTOR的add_event方法注册文件描述符(关联读事件)和waker。最后返回Poll::Pending,调用方接收到Poll::Pending后就会调用yield表达式挂起当前的执行流(Task)。 - 如果是其他的
Err(e),则说明读取数据发生了其他错误,此时返回Poll::Ready(Err(e))表示读取失败,调用方继续执行ReadFutrue.await下面的代码。
- 如果是
当注册到 REACTOR 中的事件就绪时,REACTOR 就会使用注册的 waker 唤醒挂起的 Task,继续调用 ReadFutrue 的 poll 方法,重复上述的执行流程。
task
Task 是对 async fn 或者 async {} 创建的 Non-Leaf Future 的抽象,一个 task 就代表一个异步执行的任务:
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub(crate) struct TaskId(u64);
impl TaskId {
pub(crate) fn new() -> Self {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
TaskId(NEXT_ID.fetch_add(1, Ordering::Relaxed))
}
}
pub(crate) struct Task {
id: TaskId,
future: Mutex<Pin<Box<dyn Future<Output = ()> + 'static + Send>>>,
task_sender: Sender<Arc<Task>>,
}
Task 中有三个字段:
id:每个task都有一个唯一的TaskId,TaskId是有可能在不同的线程中创建的,因此使用原子类型AtomicU64来创建TaskId的实例,保证唯一性。future:对用户创建的Non-Leaf Future的包装,使用Pin的目的是为了安全地使用自引用结构(Future生成的状态机中可能存在自引用结构),使用Mutex的目的稍后讲解。task_sender:一个channel的发送端,发送的 item 是Arc<Task>,之所以使用Arc<Task>一方面是想要减小克隆Task的开销,另一方面与Waker的实现机制有关(稍后讲解)。
方法实现
impl Task {
pub(crate) fn new(
future: impl Future<Output = ()> + 'static + Send,
task_sender: Sender<Arc<Task>>,
) -> Self {
Task {
id: TaskId::new(),
future: Mutex::new(Box::pin(future)),
task_sender,
}
}
pub(crate) fn task_id(&self) -> TaskId {
self.id
}
pub(crate) fn poll(&self, context: &mut Context) -> Poll<()> {
self.future
.lock()
.expect("get lock failed")
.as_mut()
.poll(context)
}
}
new 方法中传入参数 future 和 task_sender 后创建一个 Task 实例:
- 参数
Future要求满足'static生命周期是因为task的存在时间可能是任意长的,因此需要Future具有静态生命周期。 - 要求
Future满足Send是因为Task需要跨线程发送。 - 由于
Future最终使用Mutex包了起来,因此future字段最终同时满足Send + Sync + 'static。 Task的其他两个字段也满足Send + Sync + 'static,因此Task满足Send + Sync + 'static。
由于 task_sender 发送的 item 是 Arc<Task>,executor 的执行队列中收到的也是 Arc<Task>,因此 poll 方法的定义中只能使用 &self 不变引用。
又因为 Future 的 poll 方法调用需要可变引用,为了实现内部可变性,我们就用 Mutex 把 Pin<Box<Future>> 包了起来,这就是使用 Mutex 的原因。
在 poll 方法中,首先调用 self.future.lock() 获取锁,然后将调用 .as_mut() 方法获取 Pin<&mut dyn Future>,最后再调用 Future 中的 poll 方法执行 Future。
实现 Wake trait
为 Task 实现 Wake trait,这样就可以通过 Task 来构建一个 Waker:
impl Wake for Task {
fn wake(self: Arc<Self>) {
self.task_sender
.send(self.clone())
.expect("send task failed");
}
fn wake_by_ref(self: &Arc<Self>) {
self.task_sender
.send(self.clone())
.expect("send task failed");
}
}
Wake 中的 wake/wake_by_ref 方法实现就是具体的唤醒 task 的机制,在这个实现中,我们把想要唤醒的 task 通过 task_sender 发送到 executor 的执行队列中,这样 executor 就可以执行这个 task 了,这也是在 Task 定义中,需要 task_sender 字段的原因。
此外,wake/wake_by_ref 方法中都需要 Arc<Task>,这是 task_sender 的 item 类型为 Arc<Task> 的原因之一。
构造 Waker
对于实现了 Wake trait 的 Task,可以使用 std::task::Waker 的 from 方法构造一个 Waker:
impl<W: Wake + Send + Sync + 'static> From<Arc<W>> for Waker {
fn from(waker: Arc<W>) -> Waker {}
}
通过前面的分析,我们知道 Task 已经同时满足 Wake + Send + Sync + 'static,因此可以安全地使用 from 方法构造一个 Waker。
executor
Executor
pub struct Executor {
task_queue: Receiver<Arc<Task>>,
waker_cache: BTreeMap<TaskId, Waker>,
}
task_queue 是一个 channel 的接收端,当 spawn 或者 wake 一个 task 时,就会发送 Arc<Task> 到 task_queue 中。
waker_cache 使用 BTreeMap 缓存可能会重复使用的 Waker,这是为了减小构造 Waker 的开销。
实际上,
Executor中的task_queue只是一个管道的接收端,并不是队列,只是我个人更习惯称之为队列。
方法实现
impl Executor {
fn new(task_queue: Receiver<Arc<Task>>) -> Self {
Self {
task_queue,
waker_cache: BTreeMap::new(),
}
}
fn run_ready_task(&mut self) {
while let Ok(task) = self.task_queue.recv() {
let waker = self
.waker_cache
.entry(task.task_id())
.or_insert_with(|| Waker::from(task.clone()));
let mut context = Context::from_waker(waker);
match task.poll(&mut context) {
Poll::Ready(_) => {
self.waker_cache.remove(&task.task_id());
}
Poll::Pending => {}
}
}
}
pub fn run(&mut self) {
self.run_ready_task();
}
}
new 方法接收 Receiver<Arc<Task>> 参数,然后创建一个执行器实例。
run_ready_task 方法中:
-
从
task_queue中接收task: Arc<Task>,然后从waker_cache中查找是否存在对应的waker,如果没有则构造一个Waker。 -
使用
Context的from_waker方法通过waker的引用创建context。 -
调用
task的poll方法,传入&mut context参数,开始执行task。- 如果返回的是
Poll::Ready,说明task执行完毕,从waker_cache中删除缓存的waker。 - 如果返回的是
Poll::Pending,则什么都不做(最终执行的Leaf-Future中会注册等待的事件和waker)。
- 如果返回的是
Spawner
在初始状态下,executor 的执行队列中是空的,我们需要一种机制能够让用户手动地创建 task 并将 task 发送到 executor 的执行队列中,最后开启 executor 的执行。Spawner 抽象便提供了这种机制:
#[derive(Clone)]
pub struct Spawner {
task_sender: Sender<Arc<Task>>,
}
Spawner 中的 task_sender 和 Task 的 task_sender 一样,都是为了把 task 发送到 executor 的执行队列中。
方法实现
impl Spawner {
fn new(task_sender: Sender<Arc<Task>>) -> Self {
Self { task_sender }
}
pub fn spawn(&self, future: impl Future<Output = ()> + 'static + Send) {
let task = Task::new(future, self.task_sender.clone());
self.task_sender
.send(Arc::new(task))
.expect("send task failed");
}
}
new 方法接收Sender<Arc<Task>> 参数,然后创建一个 Spawner 实例。
spawn 方法中,使用传入的 future 参数创建一个 Task 实例,然后把这个 task 发送到 executor 的执行队列中。当 executor 开始执行的时候就可以从队列中接收 task,驱动 task 的执行了。
创建 Spawner & Executor
定义一个公开的函数,创建 Spawner 和 Executor 实例:
pub fn spawner_and_executor() -> (Spawner, Executor) {
let (task_sender, task_queue) = bounded(10000);
let spawner = Spawner::new(task_sender);
let executor = Executor::new(task_queue);
(spawner, executor)
}
在 spawner_and_executor 函数中,我们使用 crossbeam-channel 提供的 unbounded 函数创建一个容量为 10000 的管道,分别返回管道的发送端和接收端,然后创建 Spawner 和 Executor 实例并返回。
源代码仓库地址:github.com/night-cruis…
example
在这一节中,我们将使用之前实现的异步运行时创建一个 tcp echo server。需要导入的模块如下所示:
use std::env;
use std::io::Write;
use log::info;
use async_runtime::async_io::{Ipv4Addr, TcpListener, TcpStream};
use async_runtime::executor::{spawner_and_executor, Spawner};
日志打印
fn init_log() {
// format = [file:line] msg
env::set_var("RUST_LOG", "info");
env_logger::Builder::from_default_env()
.format(|buf, record| {
writeln!(
buf,
"[{}:{:>3}] {}",
record.file().unwrap_or("unknown"),
record.line().unwrap_or(0),
record.args(),
)
})
.init();
}
init_log 函数是为了设置日志打印的消息格式,这跟异步运行时的使用没啥关系,这里就不再赘述了。
handle_client
async fn handle_client(stream: TcpStream) {
let mut buf = [0u8; 1024];
info!("(handle client) {}", stream.raw_fd());
loop {
let n = stream.read(&mut buf).await.unwrap();
if n == 0 {
break;
}
stream.write(&buf[..n]).await.unwrap();
}
}
在 handle_client 函数中,我们首先创建一个 buf 数组,然后打印一个 handle client 的日志消息,接着开启一个无限循环:
-
调用
stream.read(&mut buf)方法后会返回一个ReadFuture,在ReadFuture上调用await方法:ReadFuture.await会展开成一个无限循环,在循环内部会调用ReadFuture的poll方法。- 如果返回
Poll::Pending,则使用Yield表达式挂起当前的task; - 如果返回
poll::Ready则中断循环并返回结果。
-
如果
n== 0,则说明客户端已经断开了连接,则退出循环。 -
调用
stream.write(&mut buf[.n])会返回一个WriteFuture,在WriteFuture上调用await方法后执行流程与ReadFuture一致。
server_loop
async fn server_loop(spawner: Spawner) {
let addr = Ipv4Addr::new(127, 0, 0, 1);
let port = 8080;
let listener = TcpListener::bind(addr, port).unwrap();
let incoming = listener.incoming();
while let Some(stream) = incoming.next().await {
let stream = stream.unwrap();
spawner.spawn(handle_client(stream));
}
}
在 server_loop 函数中,我们调用 TcpListener 的 bind 方法创建一个 TcpListener 实例,然后调用 incoming 方法创建一个 Incoming 实例。
接着,在 While let 循环中,调用 incoming 的 next 方法返回一个 AcceptFuture 实例,在 AcceptFuture 上调用 await 方法后,如果返回的 Poll::Pending,则挂起当前的 task。
当有客户端连接到来时,AcceptFuture 等待的 IO 事件就绪,会返回 io::Readult<TcpStream> 的实例并绑定到 stream 变量上,接着使用 spawner 调用 spawn 方法创建一个 task 处理与客户端的交互。
最后,又进入循环的开始位置,继续等待新的连接到来。
main 函数
fn main() {
init_log();
let (spawner, mut executor) = spawner_and_executor();
spawner.spawn(server_loop(spawner.clone()));
executor.run();
}
在 main 函数中,我们首先调用 init_log 函数设置日志消息格式,接着使用 spawner_and_executor 函数创建 Spawner 和 Executor 的实例。
然后调用 spawner.spawn 方法创建一个 task 用于执行 server_loop。
最后调用 executor.run() 方法开启 executor 的运行,开始调度执行各个 task。
运行示例
开启运行后的 echo server 会监听地址:127.0.0.1:8080:
cargo run --example echo_server
Finished dev [unoptimized + debuginfo] target(s) in 0.69s
Running `target/debug/examples/echo_server`
[src/async_io.rs: 56] (TcpListener) listen: 3
[src/reactor.rs: 27] (Reactor) add event: 3
[src/reactor.rs: 35] Start reactor main loop
使用 Python 写一个小脚本模拟 TCP 客户端:
import socket
import threading
HOST = '127.0.0.1'
PORT = 8080
def send_request():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
for i in range(1, 1025):
s.send(f"HELLO WORLD[{i}]".encode())
data = s.recv(1024).decode()
print(f"RECEIVE DATA: '{data}' in THREAD[{threading.currentThread().name}]")
s.close()
def main():
t_lst = []
for _ in range(10):
t = threading.Thread(target=send_request)
t_lst.append(t)
t.start()
for t in t_lst:
t.join()
if __name__ == '__main__':
main()
运行脚本,服务端会输出以下内容:
.....
.....
.....
[src/reactor.rs: 43] (Reactor) wake up. nfd = 2
[src/reactor.rs: 49] (Reactor) delete event: 6
[src/reactor.rs: 49] (Reactor) delete event: 7
[src/reactor.rs: 43] (Reactor) wake up. nfd = 3
[src/reactor.rs: 49] (Reactor) delete event: 9
[src/reactor.rs: 27] (Reactor) add event: 6
[src/reactor.rs: 49] (Reactor) delete event: 10
[src/reactor.rs: 49] (Reactor) delete event: 11
[src/reactor.rs: 43] (Reactor) wake up. nfd = 2
客户端的输出内容如下所示:
.....
.....
.....
RECEIVE DATA: 'HELLO WORLD[1022]' in THREAD[Thread-3]
RECEIVE DATA: 'HELLO WORLD[1015]' in THREAD[Thread-7]
RECEIVE DATA: 'HELLO WORLD[1013]' in THREAD[Thread-6]
RECEIVE DATA: 'HELLO WORLD[1021]' in THREAD[Thread-1]
RECEIVE DATA: 'HELLO WORLD[1023]' in THREAD[Thread-3]
RECEIVE DATA: 'HELLO WORLD[1008]' in THREAD[Thread-10]
RECEIVE DATA: 'HELLO WORLD[1014]' in THREAD[Thread-6]
RECEIVE DATA: 'HELLO WORLD[1016]' in THREAD[Thread-7]
可以看出,我们的 echo server 正确地返回了响应,wake up. nfd = 3 表示有3个事件同时就绪,这说明 server 确实在并发地处理多个请求!
本书上一个章节:Epoll