土鍋で雑多煮

UnityでXR・ゲーム開発をしています。学んだことや備忘録、趣味の記録などを書いていきます。

MENU

Unity・Python間でTCP通信をする【Quest・PC間通信】

はじめに

どうも、土鍋です。

ハッカソンでPCで用意した機械学習サーバーにMeta Quest3からアクセスし通信を行う必要があったので、その際のことをメモ的に記事にしました。

今回はPython側でサーバーを立て、Unity側がクライアントとしてアクセスするTCP通信です。

TCP通信

PythonサーバーのPCでifconfig(ipconfig)でIPアドレスを調べ、Quest3側のUnityで設定する必要があります。

ローカルネットワーク内でTCPを行いたいので、同じWifiにいる必要があります。普通に同じWifiに設定でもいいですし、Windowsではモバイルホットスポットがあるのでそれを利用すると簡単に同じネットワーク内になれます。

Unityのクライアント

UniTaskのRunOnThreadPool();で受信処理をマルチスレッド化しています。

using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;

public class NetworkManager : MonoBehaviour
{
    public static NetworkManager instance;
    public string serverIP = "127.0.0.1"; // サーバーのIPアドレス
    public int port = 49152; // サーバーのポート番号
    
    private TcpClient client;
    private NetworkStream stream;
    private bool isConnected = false;

    private CancellationTokenSource tokenSource;

    // シングルトン
    void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }

    private async void Start()
    {
        // CancellationTokenSourceの生成  
        tokenSource = new CancellationTokenSource();  
        var token = tokenSource.Token;

        ConnectToServer(token);
        await UniTask.WaitForSeconds(3);
        SendMessageToServer("Test"); // 3秒後にサーバー側にメッセージ送る
    }

    void OnDestroy()
    {
        DisconnectFromServer();
    }

    // 接続の確立
    public async void ConnectToServer(CancellationToken token = default)
    {
        try
        {
            client = new TcpClient();
            await client.ConnectAsync(serverIP, port);
            stream = client.GetStream();
            isConnected = true;

            // 非同期で受信するスレッドを開始
            await UniTask.RunOnThreadPool(ReceiveData, cancellationToken: token);

            Debug.Log("サーバーに接続しました。");
        }
        catch (Exception e)
        {
            Debug.LogError("サーバーへの接続に失敗しました: " + e.Message);
        }
    }

    // サーバーサイドへメッセージの送信
    public void SendMessageToServer(string message)
    {
        if (isConnected && stream != null)
        {
            try
            {
                byte[] data = Encoding.UTF8.GetBytes(message);
                stream.Write(data, 0, data.Length);
                Debug.Log("メッセージを送信しました: " + message);
            }
            catch (Exception e)
            {
                Debug.LogError("メッセージ送信に失敗しました: " + e.Message);
            }
        }
    }

    // サーバーサイドからのメッセージの受信(非同期)
    private void ReceiveData()
    {
        while (isConnected)
        {
            try
            {
                if (stream != null && stream.DataAvailable)
                {
                    byte[] buffer = new byte[1024];
                    int bytesRead = stream.Read(buffer, 0, buffer.Length);
                    if (bytesRead > 0)
                    {
                        string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
                        Debug.Log("サーバーからのメッセージ: " + message);
                    }
                }
            }
            catch (Exception e)
            {
                Debug.LogError("メッセージ受信に失敗しました: " + e.Message);
                isConnected = false;
            }
        }
    }

    // 接続の切断
    public void DisconnectFromServer()
    {
        isConnected = false;

        if (stream != null)
        {
            tokenSource.Cancel();
            stream.Close();
            stream = null;
        }

        if (client != null)
        {
            tokenSource.Cancel();
            client.Close();
            client = null;
        }

        Debug.Log("サーバーとの接続を切断しました。");
    }
}

※11/12 追記: マルチスレッド化できていなかったので修正
※11/13 追記: CancellationTokenによるキャンセル処理追加

※11/27 追記: マルチスレッドなのでちょっと注意が必要です。以下の記事も参考にしてください。

donabenabe.hatenablog.com

Pythonサーバー

テスト用コードなので雑ですが…(ChatGPT)

import socket

# サーバーのホストとポートを設定
HOST = socket.gethostbyname(socket.gethostname()) # ローカルホスト
PORT = 12345        # 任意のポート番号

def start_server():
    # TCPソケットを作成
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # ソケットをホストとポートにバインド
    server_socket.bind((HOST, PORT))
    
    # クライアントからの接続待ち状態にする
    server_socket.listen(1)
    print(f"サーバーが起動しました。接続を待っています... ({HOST}:{PORT})")
    
    # クライアントからの接続を待機
    client_socket, client_address = server_socket.accept()
    print(f"クライアントが接続しました: {client_address}")
    
    # クライアントに最初のメッセージを送信
    welcome_message = "Hello, client! You've connected to the server."
    client_socket.sendall(welcome_message.encode('utf-8'))
    print("クライアントに最初のメッセージを送信しました。")

    # クライアントからのメッセージを受信する
    try:
        while True:
            # 1024バイトまで受信
            data = client_socket.recv(1024)
            if not data:
                # データが空の場合は接続が終了したとみなす
                print("クライアントが接続を終了しました。")
                break
            
            # クライアントからのメッセージをデコードして表示
            received_message = data.decode('utf-8')
            print(f"クライアントからのメッセージ: {received_message}")

            # 応答メッセージをクライアントに送信
            response_message = "Message received: " + received_message
            client_socket.sendall(response_message.encode('utf-8'))
            print("応答メッセージをクライアントに送信しました。")

    except Exception as e:
        print(f"エラーが発生しました: {e}")
    
    # 接続を閉じる
    client_socket.close()
    server_socket.close()
    print("サーバーを終了しました。")

if __name__ == "__main__":
    start_server()

参考

note.com

blog.applibot.co.jp

qiita.com

qiita.com