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サーバーのコード
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を呼び出しています。
リクエスト処理のコード
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をアタッチします。
次にイベントをアタッチしていきます。
インスペクタの OnGetRequest
と OnPostRequest
下部の +
ボタンからイベントを追加し、それぞれにRequest Handler (Script)
コンポーネントを貼り付けます。
プルダウンメニューから OnGetRequest
と OnPostRequest
それぞれの呼び出す関数を設定します。
通信テストのために、Unity を再生しておきます。
通信テストしてみる
Postman を使って、通信テストをします。
Postman Download
インストールの手順や使い方はこちら
Postmanを使ったAPIテストのやり方 - IT業務で使えるプログラミングテクニック
Postman で Get テスト
メソッドをGet
にして、http://localhost:8080/?GetData=
を送ってみます。
Unity で作った MyData が Json で返ってきてました。
次は、http://localhost:8080/?SetData=true
を送ってみます。
ちゃんと success
が true
に変わりました。
メソッドを Post にすれば、Postのテストが出来ます。
まとめ
Unityだけで Post/Get リクエストを処理出来るのは、とても楽ですね。プロトタイプでは充分使えます。
今回はローカルでテストしましたが、ngrok やlocaltunnel を使って外部公開すれば、Unity でも 簡易な WebAPI が作れそうです。
参考にしたサイト
UnityでHTTPリクエストを処理してみる -Qiita
C#でHTTPSサーバ(Ver. HttpListener) -Qiita