コンテンツにスキップ

2016-10-15

注意

2016年秋ごろの調査なので最新状況は違ってます

動機

なんか色々あるけどよく分からんので crates.io をサーベイ。

最新版の日付と総ダウンロード数も合わせてリストアップ。 ~~echo サーバー作るときの典型的設計も軽く付記。~~ echo じゃなくて、単純にサーバー側で出力するだけのコード。

std::net

BSD socket API 同等。select 系は無さげ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
   let mut buf = [0u8; 1024];
   loop {
      match conn.read(&mut buf[0..]) {
         Ok(0) => {
            println!("read: closed");
            return Ok(());
         }
         Ok(n) => {
            println!("recv[{}]: {}", n, std::str::from_utf8(&buf[0..n]).unwrap());
         }
         Err(e) => {
            match e.kind() {
               std::io::ErrorKind::WouldBlock => (),
               _ => return Err(format!("read: {}", e)),
            }
         }
      }
      std::thread::sleep(sleeptime);
   }

古典的。

net2 2016-07-16(0.2.26) 236,294

std::net のラッパ。

bind とか reuse_address とかを連結して書けるだけ? listen/connect した後は net の TcpListener や TcpStream になる。

1
2
3
4
  net2::TcpBuilder::new_v4()
    .and_then(|t| {t.reuse_address(true)})
    .and_then(|t| {t.bind(addr)}
    .and_then(|t| {t.listen(backlog)})

Rust だとエラー不確定状態のモナド繋げるよりは try! で一処理ずつチェックかけるスタイルだし、 ネットワークプログラミングのキモは接続した後の処理であるから、bind/listen のとこだけスタイル変えられてもどうでもいい感じである。

mio 2016-09-02(0.6.0) 131,570

軽量のノンブロッキングI/Oライブラリ。 net2 に依存。 epoll な感じのイベントループで書く。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
   let poller = mio::Poll::new().unwrap();
   let token_id  = 0;
   let mut conn: mio::tcp::TcpStream = ...;
   poller.register(&conn, mio::Token(token_id), mio::Ready::readable(), mio::PollOpt::edge()).unwrap();

   let mut buf = [0u8; 1024];
   let mut events = mio::Events::with_capacity(1024);
   loop {
      poller.poll(&mut events, None).unwrap();
      for ev in events.iter() {
         match ev.token() {
            mio::Token(id) if id == token_id => {
               match conn.read(&mut buf[0..]) {
                  Ok(0) => {
                     println!("read: closed");
                     return Ok(());
                  }
                  Ok(n) => {
                     println!("recv[{}]: {}", n, std::str::from_utf8(&buf[0..n]).unwrap());
                  }
                  Err(e) => {
                     return Err(format!("read: {}", e));
                  }
               }
            }
            mio::Token(_) => (),
         }
      }
   }

受信イベント判定のちに read するから、wouldblock は起きない、と。 この書き方のメリットや限界は select/epoll 系そのまんまですな。

あと実際は listener も poller に突っ込むよろし。

mio 依存

rotor 2016-05-21(0.6.3) 18,705

StateMachine らしいが…どういう構造なのかさっぱり分からんw dns, redis, http なんかの実装もあるので、それなりのフレームワークにはなっているんだろうけども。 rotor-stream ってので、プロトコルが書けるみたい。

tokio-core 2016-09-10(0.1.0) 3,214

イベントループと future によるリアクティブスタイルの API future は同じ作者の future crates を使っている。この future で mio を直に使った HTTP サーバーはかなり良いパフォーマンスを出している( https://aturon.github.io/blog/2016/08/11/futures/ )

tokio はこの future を使って finagle みたいなフレームワークを作る プロジェクトで、tokio-core が mio と future を組み合わせた部分のようだ。 tokio-service には finagle の Service ぽいクラスがある。 Finagle はいいぞ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pub fn main() -> Result<(), String> {
   let addr = try!(std::net::SocketAddr::from_str("127.0.0.1:10000").map_err(|e| format!("parse: {}", e)));

   let mut core = tokio_core::reactor::Core::new().unwrap();
   let handle = core.handle();

   let listener = tokio_core::net::TcpListener::bind(&addr, &handle).unwrap();

   let r = listener.incoming().for_each(move |(conn,_addr)| {
      let buf  = vec![0u8; 1024]; // tokio_core::io::read requires AsMut
      let iter = futures::stream::iter(std::iter::repeat(()).map(Ok::<(), std::io::Error>));
      let f = iter.fold((conn,buf), |(conn,buf), _| { // use iter and fold to move ownership of buf to closure
         tokio_core::io::read(conn, buf).and_then(|(r,t,size)| {
            if size == 0 {
               println!("closed");
               Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "closed"))
            } else {
               println!("recv[{}]: {}", size, std::str::from_utf8(&t[0..size]).unwrap());
               Ok((r,t))
            }
         })
      });
      handle.spawn(f.then(|_| Ok(())));
      Ok(())
   });
   core.run(r).unwrap();
   Ok(())
}

read ループが無限イテレータの fold になるのが一寸戸惑う。 これだけならコルーチンの方が分かりやすいが、別の I/O と組み合わせるようになると future の威力が出てきそうだ。

mioco 2016-08-22(0.8.1) 3,753

コルーチンスタイルの API. コルーチンは coio 作者の context-rs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pub fn main() -> Result<(), String> {
   mioco::start(|| -> Result<(), String> {
      let listener: mioco::tcp::TcpListener = try!(bind());

      loop {
         let mut conn: mioco::tcp::TcpStream = try!(listener.accept().map_err(|e| format!("accept: {}", e)));

         mioco::spawn(move || -> Result<(), String> {
            let mut buf = [0u8; 1024];
            loop {
               match conn.read(&mut buf[0..]) {
                  Ok(0) => {
                     println!("read: closed");
                     return Ok(());
                  }
                  Ok(n) => {
                     println!("recv[{}]: {}", n, std::str::from_utf8(&buf[0..n]).unwrap());
                  }
                  Err(e) => {
                     return Err(format!("read: {}", e));
                  }
               }
            }
         });
      }
   }).unwrap()
}

mioco 使うネットワーク処理の全体を mioco::start で括る。 mioco::spawn は Rust のスレッドと同じ書き方だが、グリーンスレッドで実行されるのであろう。 接続毎コルーチンなので見通しが良い。mio のサンプルコードは接続一つだけだけど、こちらは複数対応できている。

一般に接続毎のOSスレッド方式は、

  • Pros: 接続毎にコンテキストをクリアしなくていいので、バッファ処理が楽
  • Cons: 接続数(=スレッド数)増加にともなう性能劣化

という特徴があるが、グリーンスレッドであれば欠点が無く利点だけ享受できるはず。 誰か C10K でスループット試してみて;-)

event 2015-01-05(0.2.1) 1,384

マルチスレッドのイベントループ。

aio 2015-01-05(0.0.1) 666

ノンブロッキングI/O らしい。 名前は非同期I/Oぽいが?

asio 2016-05-05(0.1.0) 240

非同期I/O

futures-mio 2016-08-01(0.1.0) 112

futures と mio 組み合わせたやつで、tokio に発展的に解消したのであろう。同じ作者だし。

coio-rs

コルーチンスタイル。 コルーチンは同作者の context-rs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pub fn main() -> Result<(), String> {
   coio::Scheduler::new().with_workers(4).run(|| {
      let listener = coio::net::TcpListener::bind("127.0.0.1:10000").unwrap();

      loop {
         let (mut conn, _addr) = try!(listener.accept().map_err(|e| format!("accept: {}", e)));

         coio::spawn(move || {
            let mut buf = [0u8; 1024];
            loop {
               match conn.read(&mut buf[0..]) {
                  Ok(0) => {
                     println!("read: closed");
                     break;
                  }
                  Ok(n) => {
                     println!("recv[{}]: {}", n, std::str::from_utf8(&buf[0..n]).unwrap());
                  }
                  Err(e) => {
                     println!("read: {}", e);
                     break;
                  }
               }
            }
         });
      }
   }).unwrap()
}

インターフェースは mioco とほとんど同じ。 コルーチンライブラリも同じ。

まとめ

std::net, net2, mio のスタックはデファクトぽい。 mio の上に aio/future/coroutine/fsm スタイルなど色々な crate がある感じかな。