rust之mio

随着 tokio 0.1 的发布,rust 语言的异步 io 库逐渐成熟, 打算用几篇博客来描述一下 tokio 框架相关的基础 crate(当然,也是为了学习一下):

本篇主要讲解 mio

mio(Metal IO) 是 rust 语言中的异步 io crate,tokio 框架就是基于这个 crate 的。 从源码上看 mio 基本就是对 linux epoll(bsd kqueue) 的简单包装,只是披了一层 rust 外衣, 性能与原生相同,接口调用方式与 linux epoll 极为相似,mio 本身实现了可以监控以下几种事件:

  • tcp io
  • udp io
  • timer
  • channel(mpsc)

同时,mio 也提供了一种扩展机制,可以很轻松的实现以下类似事件:

  • uds(unix domain socket)
  • signal
  • pipe
  • thread
  • subprocess

当然在看这篇文章之前,你需要了解 linux epoll 的知识, 可以查看博文如何使用epoll系统调用,本文示例与 epoll 示例功能实现完全相同。

我们可以通过以下示例来看一下 mio 的用法:

extern crate mio;
extern crate slab;

use std::net::SocketAddr;
use std::io::{Read, Write, ErrorKind};
use mio::*;
use mio::tcp::TcpListener;

type Slab<T> = slab::Slab<T, Token>;

const SERVER_TOKEN: Token = Token(::std::usize::MAX-1);


fn main() {
    let mut args = ::std::env::args();
    let cmd = args.next().unwrap();
    let port = args.next().expect(&format!("Usage: {} [port]", cmd));
    let addr: SocketAddr = format!("127.0.0.1:{}", port).parse().expect("argument format error: port");
    let server = TcpListener::bind(&addr).expect("socket binding error");
    let poll = Poll::new().expect("poll create error");
    let mut conns = Slab::with_capacity(1024);
    let mut events = Events::with_capacity(1024);
    let mut buf: [u8; 1024] = [0; 1024];
    let stdout = ::std::io::stdout();

    poll.register(&server, SERVER_TOKEN, Ready::readable(), PollOpt::edge())
        .expect("poll register error");
    // the event loop
    loop {
        poll.poll(&mut events, None).expect("poll error");
        for event in events.iter() {
            let (token, kind) = (event.token(), event.kind());

            if kind.is_error() || kind.is_hup() || !kind.is_readable() {
                println!("kind error");
                if token == SERVER_TOKEN {
                    ::std::process::exit(1);
                }
                conns.remove(token);
            } else if token == SERVER_TOKEN {
                loop {
                    let sock = match server.accept() {
                        Ok((sock, addr)) => {
                            println!("Accepted connection: {}", addr);
                            sock
                        }
                        Err(_) => break
                    };
                    let new_token = conns.insert(sock).expect("add connection error");
                    poll.register(&conns[new_token], new_token, Ready::readable(), PollOpt::edge())
                        .expect("poll register error");

                }
            } else {
                let mut need_to_close = false;
                {
                    let ref mut client = conns[token];
                    loop {
                        match client.read(&mut buf) {
                            Ok(n) => {
                                if n == 0 {
                                    need_to_close = true;
                                    break;
                                } else {
                                    let mut handle = stdout.lock();
                                    handle.write(&buf[..n]).expect("write error");
                                    handle.flush().expect("flush error");
                                }
                            },
                            Err(e) => {
                                if e.kind() != ErrorKind::WouldBlock {
                                    need_to_close = true;
                                }
                                break;
                            }
                        }
                    }
                }
                if need_to_close {
                    println!("Closing connection on token={:?}", token);
                    conns.remove(token);
                }

            }
        }
    }
}

我们从源码上面看,代码结构与 linux epoll 大致一样, 有一个显著的不同就是 mio 用了 token(usize 类型) 来绑定 tcp 链接, 这个应该主要是因为在 linux 系统中,每一个 socket 都是一个文件,对应一个文件描述符(usize 类型), 而 rust 语言标准库里并没有跨平台的文件描述符类型, 而且也为了扩展其他事件类型, 引入 token 机制,统一了事件的索引类型。

示例代码中也引入了 slab crate 用来保存 token 和链接的一一对应关系, 当然,你也可以用 hashmap 来建立索引,不过这个不是很高效。

示例代码 repo 可以到mio-example下载。