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,スゴくいいですね! 🦀