/ kyokomi note / blog

UaalでAndroid->Unityのやりとりをlocalhostのhttpでやってみる

June 13, 2020 [Android | Unity]

Unity as a LibraryでAndroid->Unity間のやりとりで、いちいちブリッジコード書くの面倒。。。

いい感じにやれないかなと思い、localhostでhttpServer立てて動かしてみた。

Android側の実装

Android側は無難にRetrofitgson)で雑に実装した。DIとかちゃんとするのはまた今度。

// HomeFragment.kt(抜粋)

val retrofit = Retrofit.Builder()
         .baseUrl("http://localhost:8080/")
         .addConverterFactory(GsonConverterFactory.create())
         .build()
 val service: UnityBridgeService = retrofit.create(UnityBridgeService::class.java)

 val coroutineScope = CoroutineScope(context = Dispatchers.Main)

 textView.setOnClickListener {
     coroutineScope.launch {
         try {
             val res = service.ping("test")
             Log.d("HomeFragment", res.message)
             showToast(res.message)
         } catch (e: Exception) {
             Log.d("HomeFragment", e.localizedMessage)
         }
     }
 }

Unity側の実装(async/await)で動かしてみる

Unity側はSystem.NetのHttpListenerでいけそうなので、以下を参考に適当に実装した。

// HttpServer.cs(async/await版)
using System.IO;
using System.Net;
using UnityEngine;

public class HttpServer : MonoBehaviour
{
    readonly HttpListener _httpListener = new HttpListener();

    public int port = 8080;
    public string path = "/";
    public bool startOnAwake = true;

    // TODO: Listenerが死んだときのことを考えないといけない
    void Start()
    {
        _httpListener.Prefixes.Add("http://*:" + port + path);

        if (startOnAwake)
        {
            StartServer();
        }
    }

    async void StartServer()
    {
        _httpListener.Start();

        while (true)
        {
            var context = await _httpListener.GetContextAsync();

            Debug.Log($"{context.Request.HttpMethod} Request path: {context.Request.RawUrl}");

            if (context.Request.HttpMethod == "POST")
            {
                var dataText = await new StreamReader(context.Request.InputStream,
                    context.Request.ContentEncoding).ReadToEndAsync();
                Debug.Log(dataText);
            }

            var response = context.Response;

            response.StatusCode = 200;
            var buffer = System.Text.Encoding.UTF8.GetBytes("{\"message\": \"OK\"}");
            response.ContentType = "application/json";
            response.ContentLength64 = buffer.Length;
            var output = response.OutputStream;
            await output.WriteAsync(buffer, 0, buffer.Length);
            output.Close();
            context.Response.Close();
        }
    }

    void StopServer()
    {
        _httpListener.Stop();
    }

    void OnDestroy()
    {
        StopServer();
    }
}

とりあえずUnityEditorで起動して、ローカルからcurlしてみたら動いてそう。

$ curl -i -X GET http://localhost:8080/
HTTP/1.1 200 OK
Content-Type: application/json
Server: Mono-HTTPAPI/1.0
Date: Sat, 13 Jun 2020 04:19:11 GMT
Content-Length: 15
Keep-Alive: timeout=15,max=100

{"message": "OK"}⏎

LibraryExportしてAndroid端末にinstallして起動。 ローカルからAndroid端末にcurlしたらresponse帰ってきた。

$ curl -X GET http://192.168.0.10:8080/
{"message": "OK"}⏎

しかし、実機のAndroidからRetrofitでHttpRequestしてみたら、エラーになった。 どうやらUnityPlayerが存在するActivityがフォアグラウンドじゃないと動かない。 GameObjectがactiveじゃないと駄目っぽい(あんまりわかってないがそんな気がする)

Android側のエラー(単純に応答がこなくてtimeoutしてる)

2020-06-13 14:16:47.758 9734-9734/com.unity.mynativeapp E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.unity.mynativeapp, PID: 9734
    java.net.SocketTimeoutException: timeout
        at okio.Okio$4.newTimeoutException(Okio.java:232)
        at okio.AsyncTimeout.exit(AsyncTimeout.java:286)
        at okio.AsyncTimeout$2.read(AsyncTimeout.java:241)
        at okio.RealBufferedSource.indexOf(RealBufferedSource.java:358)
        at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.java:230)
        at okhttp3.internal.http1.Http1ExchangeCodec.readHeaderLine(Http1ExchangeCodec.java:242)
        at okhttp3.internal.http1.Http1ExchangeCodec.readResponseHeaders(Http1ExchangeCodec.java:213)
        at okhttp3.internal.connection.Exchange.readResponseHeaders(Exchange.java:115)
        at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.java:94)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
        at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:43)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:117)
        at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.java:94)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:117)
        at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.java:93)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
        at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.java:88)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:117)
        at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:229)
        at okhttp3.RealCall$AsyncCall.execute(RealCall.java:172)
        at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
        at java.lang.Thread.run(Thread.java:764)
     Caused by: java.net.SocketException: Socket closed

async/awaitをやめてThreadにする

ThreadでListenerのWhileループの処理を行うようにしてみた。

// HttpServer.cs(Thread版)
using System.IO;
using System.Net;
using System.Threading;
using UnityEngine;

public class HttpServer : MonoBehaviour
{
    readonly HttpListener _httpListener = new HttpListener();
    Thread _listenerThread;

    public int port = 8080;
    public string path = "/";
    public bool startOnAwake = true;

    // TODO: Listenerが死んだときのことを考えないといけない
    void Start()
    {
        _httpListener.Prefixes.Add("http://*:" + port + path);

        if (startOnAwake)
        {
            _httpListener.Start();

            _listenerThread = new Thread(StartServer);
            _listenerThread.Start();
        }
    }

    async void StartServer()
    {
        while (true)
        {
            var context = await _httpListener.GetContextAsync();

            Debug.Log($"{context.Request.HttpMethod} Request path: {context.Request.RawUrl}");

            if (context.Request.HttpMethod == "POST")
            {
                var dataText = await new StreamReader(context.Request.InputStream,
                    context.Request.ContentEncoding).ReadToEndAsync();
                Debug.Log(dataText);
            }

            var response = context.Response;

            response.StatusCode = 200;
            var buffer = System.Text.Encoding.UTF8.GetBytes("{\"message\": \"OK\"}");
            response.ContentType = "application/json";
            response.ContentLength64 = buffer.Length;
            var output = response.OutputStream;
            await output.WriteAsync(buffer, 0, buffer.Length);
            output.Close();
            context.Response.Close();
        }
    }

    void StopServer()
    {
        _httpListener.Stop();
    }

    void OnDestroy()
    {
        StopServer();
    }
}

Threadの管理をちゃんとしないといけなくて大変そうだが、とりあえず動いた 🎉

Android側のLog

2020-06-13 14:29:29.351 10587-10587/com.unity.mynativeapp D/HomeFragment: OK

Unity側のLog

2020-06-13 14:18:16.447 9466-9663/com.unity.mynativeapp I/Unity: GET Request path: /ping/test
    HttpServer:.ctor()
    System.Threading.ContextCallback:Invoke(Object)
    System.Threading.ExecutionContext:RunInternal(ExecutionContext, ContextCallback, Object, Boolean)
    System.Runtime.CompilerServices.MoveNextRunner:Run()
    System.Action:Invoke()
    System.Threading.SendOrPostCallback:Invoke(Object)
    UnityEngine.UnitySynchronizationContext:Exec()
    UnityEngine.UnitySynchronizationContext:Exec()
     
    (Filename: ./Runtime/Export/Debug/Debug.bindings.h Line: 35)

まとめ

処理の流れイメージ図

その他メモ

last modified June 13, 2020

👋 Related posts in the uaal series...