UDP パケット通信を行う

Recipe ID: net-017

アプリケーションから UDP (User Datagram Protocol) を使用して、パケットの送受信を行う方法を解説します。
TCP と同様に、Web ブラウザの標準 API には純粋な UDP ソケットを扱う機能が存在しません。
そのため、Tauri では Rust 側の Custom Command を作成し、Rust の標準ライブラリまたは tokio を使用して実装する必要があります。

UDP はコネクションレスであり、動画配信、音声通話、ゲームのリアルタイム通信、またはネットワーク内のデバイス探索(ブロードキャスト)などの用途に適しています。

実装パターン 1: 単純なデータ送信 (Fire and Forget)

応答を待たずにデータを送りっぱなしにする、最も基本的な UDP の使い方です。
ログ送信や、到達保証が不要な通知などに使えます。

Rust 側 (src-tauri/src/lib.rs)

use tauri::command;
use std::net::UdpSocket;

#[command]
fn udp_send_once(target: String, message: String) -> Result<String, String> {
    // 0.0.0.0:0 にバインドすると、OS が空いている適当なポートを割り当てます
    let socket = UdpSocket::bind("0.0.0.0:0")
        .map_err(|e| format!("Bind failed: {}", e))?;
    
    // ブロードキャストを許可する場合 (必要な場合のみ)
    // socket.set_broadcast(true).ok();

    socket.send_to(message.as_bytes(), &target)
        .map_err(|e| format!("Send failed: {}", e))?;
    
    Ok(format!("Message sent to {}", target))
}

JavaScript 側

import { invoke } from '@tauri-apps/api/core';

async function sendLog(): Promise<void> {
    await invoke('udp_send_once', {
        target: '192.168.1.10:5000',
        message: 'Info: Application started'
    });
}

---

実装パターン 2: 送信と応答待機 (タイムアウト付き)

サーバーにリクエストを送り、その応答を待つパターンです。UDP は信頼性がないため、パケットロスを考慮して必ずタイムアウトを設定する必要があります。

Rust 側 (src-tauri/src/lib.rs)

use std::time::Duration;
use std::net::UdpSocket;
use tauri::command;

#[command]
fn udp_request_sync(target: String, message: String) -> Result<String, String> {
    let socket = UdpSocket::bind("0.0.0.0:0")
        .map_err(|e| e.to_string())?;

    // タイムアウト設定 (重要)
    socket.set_read_timeout(Some(Duration::from_secs(3)))
        .map_err(|e| e.to_string())?;

    // 送信
    socket.send_to(message.as_bytes(), &target)
        .map_err(|e| e.to_string())?;

    // 受信
    let mut buf = [0; 1024];
    // recv_from は (サイズ, 送信元アドレス) を返します
    let (amt, _src) = socket.recv_from(&mut buf)
        .map_err(|e| format!("No response or error: {}", e))?;

    let response = String::from_utf8_lossy(&buf[..amt]).to_string();
    Ok(response)
}

---

実装パターン 3: 非同期 (Tokio) による実装

UI スレッドをブロックしないために、tokio を使った非同期実装が推奨されます。

依存関係 (src-tauri/Cargo.toml)

[dependencies]
tokio = { version = "1", features = ["net", "time"] }

Rust 側 (src-tauri/src/lib.rs)

use tokio::net::UdpSocket;
use tauri::command;

#[command]
async fn udp_send_async(target: String, message: String) -> Result<String, String> {
    // Tokio の UdpSocket を使用
    let socket = UdpSocket::bind("0.0.0.0:0").await
        .map_err(|e| e.to_string())?;
    
    socket.send_to(message.as_bytes(), &target).await
        .map_err(|e| e.to_string())?;

    // 応答を待つ例(省略可能)
    let mut buf = [0; 1024];
    let (len, _addr) = socket.recv_from(&mut buf).await
        .map_err(|e| e.to_string())?;

    Ok(String::from_utf8_lossy(&buf[..len]).into_owned())
}

---

実装パターン 4: ブロードキャスト送信

ネットワーク内の全デバイスに対してメッセージを一斉送信したい場合(Service Discovery など)に使用します。

Rust 側 (src-tauri/src/lib.rs)

use std::net::UdpSocket;
use tauri::command;

#[command]
fn udp_broadcast(port: u16, message: String) -> Result<String, String> {
    let socket = UdpSocket::bind("0.0.0.0:0").map_err(|e| e.to_string())?;
    
    // ブロードキャストを有効化 (必須)
    socket.set_broadcast(true).map_err(|e| e.to_string())?;

    let target = format!("255.255.255.255:{}", port);
    socket.send_to(message.as_bytes(), &target).map_err(|e| e.to_string())?;

    Ok("Broadcast sent".to_string())
}

実装パターン 5: 常時受信 (サーバーとして動作)

特定のポートで待ち受け、パケットが来るたびにフロントエンドにイベント通知を行う、サーバーのような動作をさせる場合です。

use tauri::{AppHandle, Emitter, command};

#[command]
fn start_udp_listener(app: AppHandle, port: u16) {
    tauri::async_runtime::spawn(async move {
        // 特定のポートにバインド
        let socket = match tokio::net::UdpSocket::bind(format!("0.0.0.0:{}", port)).await {
            Ok(s) => s,
            Err(e) => {
                eprintln!("Failed to bind UDP socket: {}", e);
                return;
            }
        };

        let mut buf = [0; 1024];
        loop {
            match socket.recv_from(&mut buf).await {
                Ok((len, addr)) => {
                    let msg = String::from_utf8_lossy(&buf[..len]).to_string();
                    // フロントエンドにイベント送信
                    // (必要なら addr も送るとよいでしょう)
                    let _ = app.emit("udp-message", msg); 
                }
                Err(e) => eprintln!("UDP recv error: {}", e),
            }
        }
    });
}

JavaScript 側 (Listen)

import { listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/core';

async function startUdpServer() {
    // 1. Rust 側のリスナー起動 (ポート 3000 で待機)
    // バックグラウンドで永続的に動作します
    await invoke('start_udp_listener', { port: 3000 });
    console.log('UDP Listener started on port 3000');

    // 2. イベント受信のリスナー登録
    await listen<string>('udp-message', (event) => {
        console.log('Received UDP:', event.payload);
    });
}

startUdpServer();

注意点

1. ファイアウォール: インバウンド接続(受信)を行う場合、Windows ファイアウォールなどで警告が出ることがあります。アプリの許可設定が必要です。
2. パケットサイズ: UDP の 1 パケットで送れるデータサイズには MTU (通常 1500 バイト程度) の制限があります。あまり大きなデータを送ると断片化して消失するリスクが高まります。大きなデータは TCP を使いましょう。
3. 信頼性: 送ったデータが届く保証も、順序通りに届く保証もありません。アプリケーション層でリトライなどの制御が必要になる場合があります。