電気ひつじ牧場

技術メモ

Rustにっき/8日目・TCPサーバ

8日目です。文法事項なぞるのに飽きてきたのでここらでTCPサーバを作ります。文法事項の解説も都度やります。

まずは必要なモジュールをインポートします。ネットワーク関連のライブラリはsrd::netにまとめられています。

use std::io::{Error, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;

メイン処理

main関数でTCP接続を待ち受けます。handlerTCP接続を受けた際に別スレッドで処理する関数です。(後ほど示します。)

fn main() {
    let listener = TcpListener::bind("0.0.0.0:33333").expect("Error. failed to bind.");
    for streams in listener.incoming() {
        match streams {
            Err(e) => { eprintln!("error: {}", e)},
            Ok(stream) => {
                thread::spawn(move || {
                    handler(stream).unwrap_or_else(|error| eprintln!("{:?}", error));
                });
            }
        }
    }
}

socketのバインド

TcpListener::bind("0.0.0.0:33333").expect("Error. failed to bind.");

0.0.0.0はホストの任意のIPv4アドレスに対してソケットをバインドさせます。今回は33333番ポートで接続を待ち受けます。

それに続くTCPListener::bind()Result<TCPListener>を返すので、そのエラー処理としてexpect(self, &str)を続けて呼び出しています。

expectについて

expectはResult<T, E>から中身を取り出し、それがOkだった場合はその値を返し、Errだった場合引数の文字列とErrの値を表示してパニックを起こします。

ストリームの処理

ソケットに接続要求が来たらそれのエラー処理をmatchで行い、スレッドを起動しています。

thread::spawn(move || {
    handler(stream).unwrap_or_else(|error| eprintln!("{:?}", error));
});

spawn()に渡しているのは引数なしのクロージャです。moveは変数streamの所有権がクロージャに移動することを意味しています。クロージャに関して言うと、result.unwrap_or_else()resultが成功した場合に成功値を返し、失敗した場合にクロージャを呼び出します。{:?}デバッグ情報を表示させます。

ハンドラ関数

fn handler(mut stream: TcpStream) -> Result<(), Error> {
    println!("Connection from {}", stream.peer_addr()?);
    let mut buffer = [0; 1024];
    loop {
        let nbytes = stream.read(&mut buffer)?;
        if nbytes == 0 {
            return Ok(());
        }
        stream.write(&buffer[..nbytes])?;
        stream.flush()?;
    }
}

Result<(), Error>

このハンドラ関数はResult型を戻り値に持ちます。成功時には特に何も返していません。またstd::ioのResultをインポートしていれば、pub type Result<T> = result::Result<T, Error>のように定義されているResultの型エイリアスを用いて、Result<()>のように簡潔に記述することもできます。先ほど出て来たTCPListener::bind()も型エイリアスが使われています。

処理内容

println!("Connection from {}", stream.peer_addr()?);
?

stream.peer_addr()?は以下のような処理と同等です。

match stream.peer_addr() {
    Ok(v) => v,
    Err(e) => return Err(e)
};

成功した場合はその結果の値を返し、エラーが生じた場合はエラーをreturnします。先ほどメイン関数で?ではなくexpect()を使いましたが、main関数の戻り値がResult型ではないからです。

書き込み

stream.write(&buffer[..nbytes])?;
stream.flush()?;

受け取ったバイト数だけソケットに投げ込んでいます。クライアントから見れば投げた値がそのまま返ってくるので、echoサーバーというやつですね。 2019/3/5追記: ソースにflush();を追加しました。write()した後はflush()しないとバッファにデータが残る可能性があり、バグの原因にもなります。

実行

サーバー側

$ cargo run

クライアント側

$ nc localhost 33333
hello
hello
foobar
foobar

続く

結構エラー処理とかも説明してしまった・・・。さっと基礎をやってから実践的なコードをサンプル見ながら書いて都度文法を参照するというやり方が効率いいですね。