본문 바로가기
Unity3D

[Unity3D] Unity에서 멀티쓰레드와 HTTP 서버

by 따봉이 2020. 12. 18.
728x90
반응형

 

Unity Web어플리케이션 사이에서 데이터를 연동하고 싶어서 Unity에서 HTTP서버를 만들어 보았다. Unity에서 용이한 WebAPI 만들어 보고 싶어서 사용에 편리한 방법을 정리해 보았다.

 

할일

Unity에서 HTTP서버를 생성

사용한 라이브러리 - System.Net.HttpListener
멀티스레드 처리에서 부하를 줄인다.

Get / Post 리퀘스트를 처리
재사용을 위한서버처리와 리퀘스트처리의 콤포넌트를 분리

UnityEvent 사용하여 Inspecter 에서 이벤트를 관리
통신 테스트는 Postman 사용

 

System.Net.HttpListener?

HTTP요청에 응답하는 간단한 HTTP프로토콜 리스너를 만들 있다.

.NET 표준 클래스이므로 Unity에도 표준으로 사용할 있다.

멀티스레드 처리의 표준

Unity 부하고 크기 때문에 안정적인 동작을 위해 System.Net.HttpListener 별도 스레드로 실행한다.

, 리퀘스트 내용에따라 메인스레드에서 처리할 필요가 있다.

Unity에서 스레드를 넘은 함수의 실행은 없기 때문에

UnityMainThreadDispatcher 사용하여 메인 스레드의 작업을 호출 수있습니다.

 

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;

 

UnityMainThreadDispatcher.Instance().Enqueue(() => OnGetRequest.Invoke(context));

        return true;

    }

 

    private bool ProcessPostRequest(HttpListenerContext context)

    {

        if (!CanAccept(HttpMethod.Post, context.Request.HttpMethod))

            return false;

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;

                }

            }

        }

 

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;

                }

            }

        }

       

        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 모두 같은 내용이다.

728x90
반응형

댓글