ys memos

Blog

RustでTCP Serverを建てる


rust

2022/09/12


Rust における TCP サーバの建て方を解説する.

クライアントについてはこちら

今回は,一番簡素な実装である,シングルスレッドの Echo サーバを建てようと思う.


Rust プロジェクトを作成したら,Cargo.tomlに以下のようにtokioのパッケージを記載する.

Cargo.toml
[package]
name = "tcp_echo_server"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1.21", features = ["full"]}
main.rs
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/ AsyncWriteExtuseする必要がある点に注意!

use tokio::{
    io::{AsyncReadExt, AsyncWriteExt},
    net::TcpListener,
};

cargo newで作成される普通のmain()のままではなく,Tokio ランタイムを扱うために,#[tokio::main]を付ける.それに加えて,fnasync 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?;

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?;

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


関連タグを探す