RustでTCPクライアント/サーバーを作る【ソケット通信】

667, 2023-05-23

目次

RustでTCP通信

今回はRustでTCPなクライアント/サーバーを作ってみたいと思います。
いわゆるソケット通信というやつです。
低レイヤーな処理を行いますが、これが出来るとWebサーバーやWebクライアントも作れるようになります。
WebブラウザやFTPクライアントとかですね。

この記事ではソケット通信についての基礎的な知識から解説していきます。

ソケット通信ってなに?

ネットワーク内の他のパソコンと通信するための仕組みがソケット通信です。
ソケットとは仮想的なエンドポイントのことで、2つのパソコンにそれぞれソケットを作りそれをソフトウェア的に繋ぎます。
この繋ぐときに使われるのがアドレスとポートで、異なるソケットを同じアドレスとポートにバインドすることでそのアドレスを通じて通信できるようになります。

ソケット通信にはTCPとUDPがあり、TCPは信頼性のある通信でUDPは信頼性のない通信です。
確実に通信したいというケースではTCPが使われ、たまに通信データが途切れても問題ないという場合はUDPを使います。
ほとんどの場合、TCPを使えばいいという認識で問題ないと思います。

Rustにおけるソケット通信

Rustにはありがたいことに標準ライブラリにTCP通信用のモジュールがあります。

  • std::net::TcpListener

  • std::net::TcpStream

TcpListenerはソケットをアドレスとポートに接続するときに使います。
TcpStreamはソケットを通じてデータを読み込んだり書き込んだりするときに使います。

TCPサーバーを書く

ではTCPサーバーのサンプルコードを以下に掲載します。

//! # TCPサーバー
//!
//! アドレスとポートで待ち受けをしてクライアントからの接続を処理する。
//!
use std::io::{Read, Write};
use std::process::exit;
use std::net::TcpListener;

fn main() {
    // リステナーをアドレスとポートにバインド
    let listener = TcpListener::bind("localhost:5123");
    if let Err(e) = listener {
        eprintln!("failed to bind. {}", e);
        exit(1);
    }

    let listener = listener.unwrap();

    // サーバーの受信ループを開始
    loop {
        // クライアントからの接続を待ち受け
        println!("accept...");
        let stream = listener.accept();
        if let Err(e) = stream {
            eprintln!("failed to accept. {}", e);
            exit(1);
        }

        // クライアントからの接続に成功
        let stream = stream.unwrap();
        println!("connected to {}", stream.1);  // クライアントのアドレス表示
        let mut stream = stream.0;  // クライアントと繋がっているストリームを取得

        // クライアントからデータを読み込み
        let mut buf: [u8; 512] = [ 0; 512 ];  // 読み込みデータの保存先
        let size = stream.read(&mut buf);  // 読み込み
        if let Err(e) = size {
            eprintln!("failed to read. {}", e);
            exit(1);
        }
        let size = size.unwrap();
        if size == 0 {
            continue;  // 読み込みサイズが0ならcontinue
        }

        // 読み込んだデータを文字列にしてログとして出力
        let s = String::from_utf8(buf.to_vec()).unwrap();
        println!("{}", s);

        // クライアントにデータを書き込み
        if let Err(e) = stream.write(b"ok") {
            eprintln!("failed to write. {}", e);
        }
    }
}

ではコードを見ていきます。

    // リステナーをアドレスとポートにバインド
    let listener = TcpListener::bind("localhost:5123");
    if let Err(e) = listener {
        eprintln!("failed to bind. {}", e);
        exit(1);
    }

    let listener = listener.unwrap();

まずTcpListener::bind()でアドレスとポートにソケットをバインドします。
そしてその結果としてTcpListenerを得ます。
バインドというのは紐づけのことで、言い換えればアドレスとポートにソケットを紐づけるという意味です。
サーバーがアドレスとポートにソケットをバインドし、接続を待ち受けることで、そのアドレスとポートに接続してきたクライアントと通信できるようになります。
エラーになったら「failed to bind. ...」と出力します。

    // サーバーの受信ループを開始
    loop {
        ...
    }

サーバーの受信処理は無限ループで行います。
サーバーはクライアントからの接続を処理して、クライアントが接続してきたらそびにクライアントと接続しているソケットを作成します。
こうすることで複数のクライアントと通信できるようになります。
複数のクライアントを処理するにはマルチスレッドやマルチプロセス処理が必要ですが、ここではどちらも使っていません。

        // クライアントからの接続を待ち受け
        println!("accept...");
        let stream = listener.accept();
        if let Err(e) = stream {
            eprintln!("failed to accept. {}", e);
            exit(1);
        }

accept()でクライアントからの接続を待ち受けします。
クライアントが接続してくるとストリーム(ソケット)が生成されますので、これを使ってクライアントと通信を行います。
accept()を実行するとブロッキングしてループが止まります。接続を確立するとループが進むようになります。

        // クライアントからの接続に成功
        let stream = stream.unwrap();
        println!("connected to {}", stream.1);  // クライアントのアドレス表示
        let mut stream = stream.0;  // クライアントと繋がっているストリームを取得

上記のようにaccept()が成功したらクライアントと接続しているストリームとアドレスを得ます。

        // クライアントからデータを読み込み
        let mut buf: [u8; 512] = [ 0; 512 ];  // 読み込みデータの保存先
        let size = stream.read(&mut buf);  // 読み込み
        if let Err(e) = size {
            eprintln!("failed to read. {}", e);
            exit(1);
        }
        let size = size.unwrap();
        if size == 0 {
            continue;  // 読み込みサイズが0ならcontinue
        }

上記のようにクライアントからデータを読み込みます。
bufread()に渡してデータを読み込みますが、ここはかなり低レイヤーな処理ですね。
C言語を彷彿とさせるようなコードですが低レイヤーな処理はたいていこんなもんです。
読み込んだサイズが0だったらループをcontinueするようにします。

        // 読み込んだデータを文字列にしてログとして出力
        let s = String::from_utf8(buf.to_vec()).unwrap();
        println!("{}", s);

読み込んだバイト列をStringに変換して画面に出すようにします。
クライアントから「hello」と書き込まれるとここでサーバー側に「hello」と表示されます。

        // クライアントにデータを書き込み
        if let Err(e) = stream.write(b"ok") {
            eprintln!("failed to write. {}", e);
        }

上記のようにwrite()でクライアントにデータを書き込み(送り)ます。
こうすることでクライアント側に「ok」と出力されます。
言い忘れましたが今回のサンプルではサーバー側が最初に読み込んでいます。そのあとに書き込みです。
この順番はけっこう重要です。サーバー側が読み込み、書き込みという順番ならクライアント側は書き込み、読み込みという順番になるからですね。

TCPクライアントを書く

次いでTCPクライアントを書きます。

//! # TCPクライアント
//!
//! サーバーに接続して書き込みと読み込みを行う。
//!
use std::net::TcpStream;
use std::io::{Read, Write};
use std::process::exit;

fn main() {
    // アドレスとポートに接続しストリームを得る
    let stream = TcpStream::connect("localhost:5123");
    if let Err(e) = stream {
        eprintln!("failed to connect. {}", e);
        exit(1);
    }
    let mut stream = stream.unwrap();

    // 接続したストリームに書き込み
    if let Err(e) = stream.write_all(b"hello") {
        eprintln!("failed to write_all. {}", e);
        exit(1);
    }

    // 接続したストリームから読み込み
    let mut buf: [u8; 512] = [0; 512];
    if let Err(e) = stream.read(&mut buf) {
        eprintln!("failed to read. {}", e);
        exit(1);
    }

    // 読み込んだバイト列を文字列にして出力
    let s = String::from_utf8(buf.to_vec()).unwrap();
    println!("{}", s);
}

このプログラムはサーバーに「hello」と書き込んでその返信を出力するだけのプログラムです。
コードを見ていきましょう。

    // アドレスとポートに接続しストリームを得る
    let stream = TcpStream::connect("localhost:5123");
    if let Err(e) = stream {
        eprintln!("failed to connect. {}", e);
        exit(1);
    }
    let mut stream = stream.unwrap();

まずTcpStream::connect()でアドレスとポートに接続します。
サーバーがbind()だったのに比較してクライアントではこのようにconnect()を使います。

    // 接続したストリームに書き込み
    if let Err(e) = stream.write_all(b"hello") {
        eprintln!("failed to write_all. {}", e);
        exit(1);
    }

接続したストリームに上記のようにバイト列を書き込みます。
こうするとサーバー側に「hello」と書き込まれます。

    // 接続したストリームから読み込み
    let mut buf: [u8; 512] = [0; 512];
    if let Err(e) = stream.read(&mut buf) {
        eprintln!("failed to read. {}", e);
        exit(1);
    }

ストリームからデータを読み込みます。
このデータはサーバー側から送られてきますがサーバーは「ok」と書き込んできますので、bufにデータを保存します。

    // 読み込んだバイト列を文字列にして出力
    let s = String::from_utf8(buf.to_vec()).unwrap();
    println!("{}", s);

読み込んだデータをStringに変換して画面に出力します。
バイト列で「ok」と送られてきますので文字列にすると画面に「ok」と出力されます。

おわりに

今回はRustでTCPクライアント/サーバーを書いてみました。
Rustでは非常に簡単にソケット通信ができます。
ザ・モダン言語という感じですね。
なにか参考になれば幸いです。



この記事のアンケートを送信する