Help us understand the problem. What is going on with this article?

UnityでマルチスレッドなHTTPサーバー

Unity と Webアプリ間で データ連携したかったので、Unity で HTTPサーバーを作ってみました。後々、Unityで簡易な WebAPI を作りたいので、再利用しやすい方法をまとめています。

やりたいこと

・Unityで HTTPサーバーを作成
・利用するライブラリは System.Net.HttpListener
・マルチスレッド処理で描画負荷の影響を抑える
・ Get / Post リクエストを処理
・再利用したいので、サーバー処理とリクエスト処理のコンポーネントを分離
・UnityEvent を使って、インスペクタでイベントを管理
・通信テストは、Postman で行う

System.Net.HttpListenerとは

HTTP 要求に応答する単純な HTTP プロトコルリスナーを作成できます。
.NET 標準クラスなので、Unityでも標準で使うことができます。
MS-DOC HttpListener クラス概要

マルチスレッド処理の準備

Unity は描画負荷が高いので、安定させるために System.Net.HttpListener を別スレッドで実行します。

ただし、リクエスト内容によってはメインスレッドで描画を行う必要があります。Unity ではスレッドをまたいだ関数の実行はできないので、UnityMainThreadDispatcher を使って、メインスレッドのアクションを呼び出せるようにします。

下記からダウンロードし、Assetsに配置します。
UnityMainThreadDispatcher - GitHub

HTTPサーバーのコンポーネントを作る

以下のようなスクリプトを書きます。

HTTPサーバーのコード

UnityHttpListener.cs
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using UnityEngine;
using UnityEngine.Events;

public class UnityHttpListener : MonoBehaviour
{
    private HttpListener listener;
    private Thread listenerThread;

    public string domain = "localhost";
    public int port = 8080;

    [System.Serializable]
    public class OnGetRequestEvent : UnityEvent<HttpListenerContext> { }
    public OnGetRequestEvent OnGetRequest;

    [System.Serializable]
    public class OnPostRequestEvent : UnityEvent<HttpListenerContext> { }
    public OnPostRequestEvent OnPostRequest;

    void Start()
    {
        listener = new HttpListener();
        listener.Prefixes.Add("http://" + domain + ":" + port + "/");
        listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous;
        listener.Start();

        listenerThread = new Thread(startListener);
        listenerThread.Start();
        Debug.Log("Server Started");
    }

    private void OnDestroy()
    {
        listener.Stop();
        listenerThread.Join();
    }

    private void startListener()
    {
        while (listener.IsListening)
        {
            var result = listener.BeginGetContext(ListenerCallback, listener);
            result.AsyncWaitHandle.WaitOne();
        }
    }

    private void ListenerCallback(IAsyncResult result)
    {
        if (!listener.IsListening) return;
        HttpListenerContext context = listener.EndGetContext(result);
        Debug.Log("Method: " + context.Request.HttpMethod);
        Debug.Log("LocalUrl: " + context.Request.Url.LocalPath);

        try
        {
            if (ProcessGetRequest(context)) return;
            if (ProcessPostRequest(context)) return;
        }
        catch (Exception e)
        {
            ReturnInternalError(context.Response, e);
        }
    }

    private bool CanAccept(HttpMethod expected, string requested)
    {
        return string.Equals(expected.Method, requested, StringComparison.CurrentCultureIgnoreCase);
    }

    private bool ProcessGetRequest(HttpListenerContext context)
    {
        if (!CanAccept(HttpMethod.Get, context.Request.HttpMethod) || context.Request.IsWebSocketRequest)
            return false;
        //メインスレッドでGetリクエストイベントを呼び出し
        UnityMainThreadDispatcher.Instance().Enqueue(() => OnGetRequest.Invoke(context));
        return true;
    }

    private bool ProcessPostRequest(HttpListenerContext context)
    {
        if (!CanAccept(HttpMethod.Post, context.Request.HttpMethod))
            return false;
        //メインスレッドでPostリクエストイベントを呼び出し
        UnityMainThreadDispatcher.Instance().Enqueue(() => OnPostRequest.Invoke(context));
        return true;
    }

    private void ReturnInternalError(HttpListenerResponse response, Exception cause)
    {
        Console.Error.WriteLine(cause);
        response.StatusCode = (int) HttpStatusCode.InternalServerError;
        response.ContentType = "text/plain";
        try
        {
            using(var writer = new StreamWriter(response.OutputStream, Encoding.UTF8))
            writer.Write(cause.ToString());
            response.Close();
        }
        catch (Exception e)
        {
            Console.Error.WriteLine(e);
            response.Abort();
        }
    }
}

Get / Post のリクエストがあれば、UnityMainThreadDispatcher を使って、メインスレッドでそれぞれのUnityEventを呼び出しています。

リクエスト処理のコード

RequestHandler.cs
using System;
using System.Net;
using UnityEngine;

public class RequestHandler : MonoBehaviour
{
    public MyData data;

    private void Start()
    {
        data = new MyData();
    }

    public void OnGetRequest(HttpListenerContext context)
    {
        var request = context.Request;
        var response = context.Response;
        response.StatusCode = (int)HttpStatusCode.OK;
        response.ContentType = "application/json";

        string message = "";
        if (request.QueryString.AllKeys.Length > 0)
        {
            foreach (var key in request.QueryString.AllKeys)
            {
                object value = request.QueryString.GetValues(key)[0];
                Debug.Log("key: " + key + " , value: " + value);
                switch (key)
                {
                    case "GetData":
                        message = JsonUtility.ToJson(data);
                        break;
                    case "SetData":
                        data.success = Convert.ToBoolean(value);
                        message = JsonUtility.ToJson(data);
                        break;
                }
            }
        }
        // message の内容をバイト配列に変換してレスポンスを返す
        var bytes = System.Text.Encoding.UTF8.GetBytes(message);
        response.Close(bytes, false);
    }

    public void OnPostRequest(HttpListenerContext context)
    {
        var request = context.Request;
        var response = context.Response;
        response.StatusCode = (int)HttpStatusCode.OK;
        response.ContentType = "application/json";

        string message = "";
        if (request.QueryString.AllKeys.Length > 0)
        {
            foreach (var key in request.QueryString.AllKeys)
            {
                object value = request.QueryString.GetValues(key)[0];
                Debug.Log("key: " + key + " , value: " + value);
                switch (key)
                {
                    case "GetData":
                        message = JsonUtility.ToJson(data);
                        break;
                    case "SetData":
                        data.success = Convert.ToBoolean(value);
                        message = JsonUtility.ToJson(data);
                        break;
                }
            }
        }
        // message の内容をバイト配列に変換してレスポンスを返す
        var bytes = System.Text.Encoding.UTF8.GetBytes(message);
        response.Close(bytes, false);
    }
}

[System.Serializable]
public class MyData
{
    public bool success = false;
}

ここでは、UnityEvent を受けて、リクエストに対応したレスポンスを返しています。

  • リクエストキー が GetData の場合: Unity 側のデータを返信
  • リクエストキー が SetData の場合: Unity 側のデータをリクエスト値に変更 -> 変更後のデータを返信

レスポンスの ContentType はひとまず Json にしました。
データを保持する MyData クラスを用意して、JsonUtilityでJsonに変換しています。
通信テストができればいいので、 Get / Post どちらも同じ内容です。

コンポーネントをアタッチする

空の GameObject を作り、準備していた UnityMainThreadDispatcher と 先ほど作った UnityHttpListener、RequestHandlerをアタッチします。
スクリーンショット 2020-01-24 17.33.23.png

次にイベントをアタッチしていきます。
インスペクタの OnGetRequestOnPostRequest 下部の + ボタンからイベントを追加し、それぞれにRequest Handler (Script)コンポーネントを貼り付けます。
スクリーンショット 2020-01-24 17.57.26.png

プルダウンメニューから OnGetRequestOnPostRequest それぞれの呼び出す関数を設定します。
スクリーンショット 2020-01-24 17.57.59.png

こんな感じになれば、完成
スクリーンショット 2020-01-24 18.03.43.png

通信テストのために、Unity を再生しておきます。

通信テストしてみる

Postman を使って、通信テストをします。
Postman Download

インストールの手順や使い方はこちら
Postmanを使ったAPIテストのやり方 - IT業務で使えるプログラミングテクニック

Postman で Get テスト

メソッドをGetにして、http://localhost:8080/?GetData=を送ってみます。
スクリーンショット 2020-01-24 18.47.20.png

Unity で作った MyData が Json で返ってきてました。
スクリーンショット 2020-01-24 18.45.14.png

次は、http://localhost:8080/?SetData=trueを送ってみます。
スクリーンショット 2020-01-24 18.47.41.png

ちゃんと successtrue に変わりました。

メソッドを Post にすれば、Postのテストが出来ます。

まとめ

Unityだけで Post/Get リクエストを処理出来るのは、とても楽ですね。プロトタイプでは充分使えます。

今回はローカルでテストしましたが、ngroklocaltunnel を使って外部公開すれば、Unity でも 簡易な WebAPI が作れそうです。

参考にしたサイト

UnityでHTTPリクエストを処理してみる -Qiita
C#でHTTPSサーバ(Ver. HttpListener) -Qiita

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした