aptpod Advent Calendar 2020 の2日目を担当します、研究開発グループの大久保です。 弊社では、新しいプロトコルであるQUICの利用法を調査しています。そこで今回は、RustのQUIC実装の1つである Quinn を用いて、受け取ったリクエストをそのままクライアントへ返送するechoサーバを実装してみます。RustのQUIC実装には、他に quiche というものもありますが、Quinnは tokio の上に実装されているため、Rustのasync機能を活用して楽に書くことができます。 構成 quinn-echo-server と quinn-echo-client という2つのクレートを作り、それぞれの Cargo.toml に以下の依存関係を追記します。 [dependencies] anyhow = "1" clap = "3.0.0-beta.2" futures = "0.3" quinn = "0.6" tokio = { version = "0.2", features = ["full"] } QUICを使うのに quinn 、async周りの機能のために futures と tokio が必要です。また、エラー処理には、最近おなじみになった anyhow を、CLI引数のパースには clap を入れておきます。 サーバ側 quinn-echo-server は以下のようになります。 use anyhow :: * ; use clap :: Clap; use futures :: StreamExt; use quinn :: { Certificate, CertificateChain, Connecting, Endpoint, NewConnection, PrivateKey, ServerConfig, ServerConfigBuilder, TransportConfig, }; use std :: net :: {IpAddr, Ipv4Addr, SocketAddr}; use std :: path :: PathBuf; #[derive(Clap, Debug )] #[clap(version = "0.1.0" )] struct Opts { #[clap(short, long)] port: u16 , #[clap(short, long)] ca: PathBuf, #[clap(long)] privkey: PathBuf, } #[tokio::main] async fn main () -> Result < (), Error > { // コマンドライン引数のパース let opts: Opts = Opts :: parse (); // QUICの設定 let mut transport_config = TransportConfig :: default (); transport_config. stream_window_uni ( 0xFF ); let mut server_config = ServerConfig :: default (); server_config.transport = std :: sync :: Arc :: new (transport_config); let mut server_config = ServerConfigBuilder :: new (server_config); // 証明書の設定 let cert = Certificate :: from_der ( & std :: fs :: read (opts.ca)?)?; server_config. certificate ( CertificateChain :: from_certs ( vec! [cert]), PrivateKey :: from_der ( & std :: fs :: read (opts.privkey)?)?, )?; // QUICを開く let mut endpoint = Endpoint :: builder (); endpoint. listen (server_config. build ()); let addr = SocketAddr :: new ( IpAddr :: V4 ( Ipv4Addr :: new ( 0 , 0 , 0 , 0 )), opts.port); let (endpoint, mut incoming) = endpoint. bind ( & addr)?; println! ( "listeing on {}" , endpoint. local_addr ()?); // クライアントからの接続を扱う while let Some (conn) = incoming. next ().await { tokio :: spawn (async { // クライアントとの処理を行い、エラーが起きたら表示 match handle_connection (conn).await { Ok (_) => (), Err (e) => { eprintln! ( "{}" , e); } } }); } Ok (()) } // echoの処理をする関数 async fn handle_connection (conn: Connecting) -> Result < (), Error > { let NewConnection { connection, mut uni_streams, .. } = conn.await?; println! ( "connected from {}" , connection. remote_address ()); // 受信用のストリームを開く if let Some (uni_stream) = uni_streams. next ().await { let uni_stream = uni_stream?; // ストリームを読み出す let data = uni_stream. read_to_end ( 0xFF ).await?; println! ( "received \" {} \" " , String :: from_utf8_lossy ( & data)); // 送信用のストリームを開く let mut send_stream = connection. open_uni ().await?; // 返信を書き込む send_stream. write ( & data).await?; send_stream. finish ().await?; connection. close ( 0u8 . into (), & []); } else { bail! ( "cannot open uni stream" ); } println! ( "closed" ); Ok (()) } 下準備と接続を開くまで main 関数で行っています。コマンドライン引数で渡されたポート番号と、証明書ファイルのパスを基にサーバを立ち上げ、クライアントとの接続が起きたら handle_connection 関数に渡します。tokioランタイムの上で動作するので、 main 関数には #[tokio::main] 属性を追加しておきます。 handle_connection 関数では、受信用に単方向ストリームを開き、内容を全て読み出します。その後、送信用の単方向ストリームを開き、受け取った内容をそのまま書き出したら接続を終了します。 クライアント側 quinn-echo-client は以下のようになります。 use anyhow :: * ; use clap :: Clap; use futures :: StreamExt; use quinn :: {Certificate, ClientConfigBuilder, Endpoint, NewConnection}; use std :: net :: SocketAddr; use std :: path :: PathBuf; #[derive(Clap, Debug )] #[clap(version = "0.1.0" )] struct Opts { #[clap(short, long)] ipaddr: SocketAddr, #[clap(short, long)] ca: PathBuf, } #[tokio::main] async fn main () -> Result < (), Error > { // コマンドライン引数のパース let opts: Opts = Opts :: parse (); // QUICの設定 let mut client_config = ClientConfigBuilder :: default (); client_config. add_certificate_authority ( Certificate :: from_der ( & std :: fs :: read ( & opts.ca)?)?)?; let mut endpoint_builder = Endpoint :: builder (); endpoint_builder. default_client_config (client_config. build ()); let (endpoint, _incoming) = endpoint_builder. bind ( & "0.0.0.0:0" . parse (). unwrap ())?; // サーバへ接続 let NewConnection { connection, mut uni_streams, .. } = endpoint. connect ( & opts.ipaddr, "localhost" )?.await?; println! ( "connected: addr={}" , connection. remote_address ()); // メッセージの書き込み let msg = "hello" ; let mut send_stream = connection. open_uni ().await?; send_stream. write (msg. as_bytes ()).await?; send_stream. finish ().await?; println! ( "sent \" {} \" " , msg); // 返信の読み込み if let Some (uni_stream) = uni_streams. next ().await { let uni_stream = uni_stream?; let data = uni_stream. read_to_end ( 0xFF ).await?; println! ( "received \" {} \" " , String :: from_utf8_lossy ( & data)); } else { bail! ( "cannot open uni stream" ); } // 終了 endpoint. wait_idle ().await; Ok (()) } main 関数で接続、書き込み、返信の読み込みまで行います。QUICの接続を確立したら、送信用の単方向ストリームを開き、文字列 "hello" を書き込みます。その後、受信用の単方向ストリームを開き、文字列に変換して表示したら終了します。 実行 適当なDER形式の証明書ファイル cert.der と秘密鍵 priv.der を用意した場合、サーバを次のように立ち上げます。 quinn-echo-server --port 33333 --ca cert.der --privkey priv.der このサーバに接続するクライアントは、次のように実行します。 quinn-echo-client --ipaddr 127 . 0 . 0 .1:33333 --ca cert.der すると次のような出力が得られます。 サーバ側 listeing on 0.0.0.0:33333 connected from 127.0.0.1:32820 received "hello" closed クライアント側 connected: addr=127.0.0.1:33333 sent "hello" received "hello" これでechoが返ってくることを確認できました。 最後に 今回は、tokioをベースとして構築されたQuinnをつかって、簡単なQUICのechoサーバを構築してみました。別のQUIC実装であるquicheの方は、自分でイベントを扱うループを書く必要があったりしますが、Quinnの方はRustのasync/await機能の基本が分かっていれば、比較的簡単に使いこなすことができます。コネクションやストリームを開いたり、ストリームの読み書きも、とてもRustらしい書き方で行えます。今回実装したのはechoサーバですが、これでQUICの特徴の1つである ストリームの使い方がわかるので、ここから応用することもできるでしょう。 新しいプロトコルであるQUICも、Rustの知識がある程度あればQuinnで簡単に使えるので、ぜひお試し下さい。