ネットワーキング

Ecsonはネットワーク処理をTokioの非同期ランタイムで行い、その結果をECSのイベントとしてゲームループへ橋渡しします。開発者がネットワーク層を直接触る必要はなく、MessageReceived / SendMessage / UserDisconnected の3つのイベントを通じてすべてのやり取りができます。

プロトコルの選択

EcsonはWebSocketWebTransportに対応しています。どちらもECS側のAPIは同じなので、プラグインを差し替えるだけでプロトコルを切り替えられます。

プロトコル特徴用途
WebSocket (WS)広くサポートされており安定している汎用・チャット・コラボツール
WebSocket TLS (WSS)WS の暗号化版本番環境
WebTransport低レイテンシ・UDP系のデータグラム通信ゲーム・リアルタイム位置同期

WebSocket

開発用(WS)

TLSなしのシンプルなWebSocketサーバーです。ローカル開発に使います。

use ecson::prelude::*;

fn main() {
    EcsonApp::new()
        .add_plugins(EcsonWebSocketPlugin::new("127.0.0.1:8080"))
        .run();
}

クライアント側(ブラウザ)からは標準の WebSocket API で接続できます。

const ws = new WebSocket('ws://127.0.0.1:8080');

ws.onopen    = () => console.log('接続成功');
ws.onmessage = (e) => console.log('受信:', e.data);
ws.onclose   = () => console.log('切断');

ws.send('hello');

本番用(WSS)

PEM形式の証明書ファイルを指定してTLS付きのWSSサーバーを起動します。Let’s Encryptなど正式な認証局の証明書を使います。

use ecson::plugins::network::EcsonWebSocketTlsPlugin;

fn main() {
    EcsonApp::new()
        .add_plugins(EcsonWebSocketTlsPlugin::new(
            "0.0.0.0:8443",
            "/etc/letsencrypt/live/example.com/fullchain.pem",
            "/etc/letsencrypt/live/example.com/privkey.pem",
        ))
        .run();
}

クライアント側は wss:// スキームで接続します。

const ws = new WebSocket('wss://example.com:8443');

開発用 WSS(自己署名証明書)

証明書ファイルなしでWSSを試したい場合は EcsonWebSocketTlsDevPlugin が使えます。自己署名証明書をメモリ上に自動生成します。

use ecson::plugins::network::EcsonWebSocketTlsDevPlugin;

fn main() {
    EcsonApp::new()
        .add_plugins(EcsonWebSocketTlsDevPlugin::new("127.0.0.1:8443"))
        .run();
}

⚠️ 開発・テスト専用です。 ブラウザは自己署名証明書を信頼しないため、そのままでは接続できません。開発時は証明書の警告を無視する設定を使うか、後述のWebTransportの証明書ハッシュ方式を検討してください。

WebTransport

WebTransportはHTTP/3(QUIC)上に構築されたプロトコルです。Ecsonの実装ではデータグラム通信を使用しており、WebSocketよりも低レイテンシな通信が可能です。ゲームの位置同期など、多少のパケットロスが許容できるユースケースに適しています。

開発用(自己署名証明書を自動生成)

EcsonWebTransportDevPlugin はサーバー起動時に自己署名証明書を自動生成します。ファイルには保存されず、メモリ上にのみ存在します。

use ecson::plugins::network::EcsonWebTransportDevPlugin;

fn main() {
    EcsonApp::new()
        .add_plugins(EcsonWebTransportDevPlugin::new("127.0.0.1:4433"))
        .run();
}

起動するとコンソールに証明書ハッシュが出力されます。

Certificate Hash: [12, 34, 56, 78, ...]

この値はブラウザ側で自己署名証明書を許可するために使います。

クライアント側(ブラウザ)の接続

WebTransport API はWebSocketとは異なります。また、自己署名証明書を使う場合は serverCertificateHashes にサーバーから出力されたハッシュを渡す必要があります。

// サーバーが出力した "[12, 34, 56, ...]" 形式の文字列を Uint8Array に変換
function parseHashArray(hashStr) {
    const nums = hashStr.replace(/\[|\]/g, '').split(',').map(s => parseInt(s.trim(), 10));
    return new Uint8Array(nums);
}

const hashBytes = parseHashArray('[12, 34, 56, 78, ...]'); // サーバーの出力をここに貼る

const transport = new WebTransport('https://127.0.0.1:4433', {
    serverCertificateHashes: [{ algorithm: 'sha-256', value: hashBytes }],
});

await transport.ready;
console.log('WebTransport 接続成功');

// 送信
async function send(text) {
    const writer = transport.datagrams.writable.getWriter();
    await writer.write(new TextEncoder().encode(text));
    writer.releaseLock();
}

// 受信ループ
async function startReceiving() {
    const reader = transport.datagrams.readable.getReader();
    while (true) {
        const { value, done } = await reader.read();
        if (done) break;
        console.log('受信:', new TextDecoder().decode(value));
    }
}

startReceiving();

Note: WebTransport はブラウザのサポート状況が限られています(2026年現在、Chrome / Edge で対応)。Firefoxなど非対応ブラウザを考慮する場合はWebSocketを選んでください。

NetworkPayload

受信・送信のどちらでも、データの本体は NetworkPayload として扱われます。

pub enum NetworkPayload {
    Text(String),       // テキストデータ(JSONなど)
    Binary(Vec<u8>),    // バイナリデータ
}

テキストを受け取る

受信した MessageReceived のペイロードをパターンマッチで取り出します。

fn my_system(mut ev: MessageReader<MessageReceived>) {
    for msg in ev.read() {
        match &msg.payload {
            NetworkPayload::Text(text) => {
                println!("テキスト受信: {}", text);
            }
            NetworkPayload::Binary(bytes) => {
                println!("バイナリ受信: {} bytes", bytes.len());
            }
        }
    }
}

テキストだけ処理する場合はよりシンプルに書けます。

fn my_system(mut ev: MessageReader<MessageReceived>) {
    for msg in ev.read() {
        let NetworkPayload::Text(text) = &msg.payload else { continue };
        println!("{}", text);
    }
}

テキストを送る

ev_send.write(SendMessage {
    target: msg.entity,
    payload: NetworkPayload::Text("hello".to_string()),
});

JSONを扱う

Ecsonはシリアライズライブラリを内包していませんが、serde_json などと組み合わせることができます。

// Cargo.toml に serde_json を追加してから

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct GameEvent {
    kind: String,
    data: String,
}

fn send_json_system(mut ev_send: MessageWriter<SendMessage>, target: Entity) {
    let event = GameEvent {
        kind: "spawn".to_string(),
        data: "player_1".to_string(),
    };
    let json = serde_json::to_string(&event).unwrap();

    ev_send.write(SendMessage {
        target,
        payload: NetworkPayload::Text(json),
    });
}

バッファのチューニング

EcsonWebSocketPlugin および EcsonWebTransportDevPlugin にはバッファサイズを調整するオプションがあります。

EcsonWebSocketPlugin::new("0.0.0.0:8080")
    .ecs_buffer(4096)    // 全クライアントからの受信キュー。接続数・メッセージ頻度が高い場合は増やす(デフォルト: 1024)
    .client_buffer(200)  // 1クライアントへの送信キュー。送信が詰まる場合は増やす(デフォルト: 100)

バッファが溢れるとメッセージが欠落するため、高負荷が想定される場合は余裕を持たせてください。

Built with ❤ in Rust · MIT License · © 2024 Ecson Contributors