2022/09/12
はじめに
Rust における TCP サーバの建て方を解説する.
クライアントについてはこちら.
今回は,一番簡素な実装である,シングルスレッドの Echo サーバを建てようと思う.
コード全文
Rust プロジェクトを作成したら,Cargo.toml
に以下のようにtokio
のパッケージを記載する.
[package]
name = "tcp_echo_server"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.21", features = ["full"]}
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpListener,
};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let addr = "0.0.0.0:8080";
let listener = TcpListener::bind(addr).await?;
loop {
match listener.accept().await {
Ok((mut socket, _)) => {
let mut buf = Vec::with_capacity(4096);
socket.read_buf(&mut buf).await?;
let msg = String::from_utf8(buf).expect("failed to convert str");
println!("{msg}");
socket.write(msg.as_bytes()).await?;
}
Err(err) => {
println!("{err:?}");
}
};
}
}
解説
パッケージ
今回はtokio::net::TcpListner
を使うので,それをuse
するのは当然であるが,TcpStream
に対してread
/ write
メソッドを呼び出すので,AsyncReadExt
/ AsyncWriteExt
もuse
する必要がある点に注意!
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpListener,
};
main
cargo new
で作成される普通のmain()
のままではなく,Tokio ランタイムを扱うために,#[tokio::main]
を付ける.それに加えて,fn
をasync fn
にする.
これにより,関数内で.await
を使えるように出来る.
戻り値のstd::io::Result<()>
は,関数内でResult
型の処理に?
を使いたいために指定した.
空にしてもよく,その場合は各Result
型を個別で処理する.
#[tokio::main]
async fn main() -> std::io::Result<()> {
リスナーを作成
addr
は変数にせず直接引数としてもよいが,変数にした.
アプリケーションとして実行する際には Listen するアドレスとポートなどをprintln!()
すると便利だろう.
また,?
で失敗した場合にもそのアドレスとポートを出力すると便利である.
TcpListener::bind()
により,リスナーを作成.
bind()
は非同期関数のため,.await
を付ける.
let addr = "0.0.0.0:8080";
let listener = TcpListener::bind(addr).await?;
Listen ループ
listner.accept()
により接続を確立する.こちらもawait
が必要.
ここのmatch
は,accept()
がResult
型を返し,Ok
/ Err
によって処理を分岐するためにこのようにした.
accept()
は,(TcpStream, SocketAddr)
を結果とするResult
を返す.
loop {
match listener.accept().await {
ソケットの接続に成功
Ok()
の処理であるが,タプルの各値をmut socket
/ _
で受け取る.
socket
は,ミュータブルであるためmut
を付ける.
_
はSocketAddr
であるが,ここは特に用いないため_
とした.必要であれば私の場合はaddr
などの命名にする.
Ok((mut socket, _)) => {
buf
について,Vec::with_capacity()
により初期化コストを低減しつつ定義.
mut
なのは,ソケットの読み取り結果を記録する際に値を変更するため.
let mut buf = Vec::with_capacity(4096);
初期化したbuf
をミュータブルで渡して TcpStream を読み取る.
非同期のため.await
をつける.
これを忘れるとストリームの読み取りを完了する前に次の処理に入ってしまい,データを受信していても空の受信となってしまう.
socket.read_buf(&mut buf).await?;
buf
はVec<u8>
なので,これを文字列msg
に変換する.
変換にはString::from_utf8()
が便利である.Result
を返すため,.expect()
により結果を展開する.
ここでは,エラー時の挙動を調整してもいいのかもしれない.
let msg = String::from_utf8(buf).expect("failed to convert str");
println!("{msg}");
TcpStream に値を書き込む(クライアントにデータを送信する).
Echo サーバのため,受け取ったメッセージを送り返す.
.write()
は&[u8]
を引数とするため,.as_bytes()
の結果を引数とする.
socket.write(msg.as_bytes()).await?;
}
ソケットの接続に失敗
エラーを出力.
内容によってlistner
の再生成やpanic!()
を追加してもいいのかもしれない.
Err(err) => {
println!("{err:?}");
}
カッコ閉じ
};
}
}
おわりに
Rust,スゴくいいですね! 🦀