0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ひとりアドベントカレンダーAdvent Calendar 2024

Day 8

【Unity】WebGLでHTTPでログを受け取るログビューワーを作る

Posted at

概要

何かしらのログを送り続けて表示してくれるようなツールが欲しいと思い作りました
わざわざUnityで作るようなものでもないと思いつつ、モデルやロジックを使い回したかったので...

実装

通信部分

HTTPサーバー部分は以下の記事を参考に実装しています

ただし、WebGLではスレッドを使えないので少しだけ改造しています
(Unity6ではマルチスレッド対応済みなので、プロジェクトのバージョンを挙げられる方は上記の記事を参考にしてください!)

通信部分コード
HttpListener.cs
using System;
using System.Collections;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using UnityEngine;
using UnityEngine.Events;

namespace HttpDataTool
{
    public class UnityHttpListener : MonoBehaviour
    {
        private HttpListener _listener;

        private const string Domain = "localhost";
        private const int Port = 8080;
        private const string ContentType = "application/json";

        private void Start()
        {
            _listener = new HttpListener();
            _listener.Prefixes.Add("http://" + Domain + ":" + Port + "/");
            _listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous;
            _listener.Start();

            // Unity6以前はWebGLでマルチスレッドが使えないため、コルーチンで非同期処理を行う
            // (メインスレッドで処理される)
            StartCoroutine(StartListenerCoroutine());
            Debug.Log("Server Started");
        }

        private void OnDestroy()
        {
            _listener.Stop();
        }

        private IEnumerator StartListenerCoroutine()
        {
            while (_listener.IsListening)
            {
                var result = _listener.GetContextAsync();
                yield return new WaitUntil(() => result.IsCompleted);
                ListenerCallback(result.Result);
            }
        }

        private void ListenerCallback(HttpListenerContext context)
        {
            var request = context.Request;
            Debug.Log($"Method: {request.HttpMethod}, Url: {request.Url}");

            try
            {
                Action<HttpListenerRequest> process = context.Request.HttpMethod switch
                {
                    var s when s == HttpMethod.Get.Method => HttpMethodProcessor.Get,
                    var s when s == HttpMethod.Post.Method => HttpMethodProcessor.Post,
                    _ => throw new Exception("Method Not Allowed")
                };
                process.Invoke(request);

                // 何か情報を返すことを想定していないので、処理成功時は固定のレスポンスを返す
                context.Response.StatusCode = (int)HttpStatusCode.OK;
                context.Response.ContentType = ContentType;
                using var writer = new StreamWriter(context.Response.OutputStream, Encoding.UTF8);
                writer.Write(JsonUtility.ToJson(new { message = "Success" }));
            }
            catch (Exception e)
            {
                // 処理中の全例外はExceptionで処理する
                ReturnInternalError(context.Response, e);
            }
        }

        private static void ReturnInternalError(HttpListenerResponse response, Exception cause)
        {
            Debug.LogError(cause);
            response.StatusCode = (int)HttpStatusCode.InternalServerError;
            response.ContentType = ContentType;
            try
            {
                using (var writer = new StreamWriter(response.OutputStream, Encoding.UTF8))
                    writer.Write(JsonUtility.ToJson(cause));

                response.Close();
            }
            catch (Exception e)
            {
                Debug.LogError(e);
                response.Abort();
            }
        }
    }
}
HttpMethodProcessor.cs
using System;
using System.Net;
using System.Text;
using UnityEngine;

namespace HttpDataTool
{
    public static class HttpMethodProcessor
    {
        public static void Get(HttpListenerRequest request)
        {
            // GETはヘルスチェックのみで使用
            if (request.Url.AbsolutePath != "/health")
                throw new Exception("Not Found");
        }

        public static void Post(HttpListenerRequest request)
        {
            switch (request.Url.AbsolutePath)
            {
                case "/api_log":
                    var body = request.RequestBody();
                    DataContainer.AddData(JsonUtility.FromJson<Data>(body));
                    break;
                default:
                    throw new Exception("Not Found");
            }
        }

        private static string RequestBody(this HttpListenerRequest request)
        {
            var body = request.InputStream;
            var bodyBytes = new byte[request.ContentLength64];
            body.Read(bodyBytes, 0, bodyBytes.Length);
            return Encoding.UTF8.GetString(bodyBytes);
        }
    }
}

データ部分

今回はAPIを叩いた時のログを受け取る前提です
DataContainerは用途に応じて RemoveAllDataとかFilterDataとかを追加で実装しましょう

データ部分コード
Data.cs
using System;

namespace HttpDataTool
{
    [Serializable]
    public class Data
    {

        public string endpoint;
        public string method;
        public int responseCode;
        public string response;
        public string request;
    }
}

DataContainer.cs
using System.Collections.Generic;
using UnityEngine.Events;

namespace HttpDataTool
{
    public static class DataContainer
    {
        public static List<Data> DataList { get; } = new();
        public static readonly UnityEvent OnDataChanged = new();

        public static void AddData(Data data)
        {
            DataList.Add(data);
            OnDataChanged.Invoke();
        }
    }
}

表示部分

EnhancedScrollerを使っていますが、持っていない場合は軽量なリストビューを別途用意してください

UIToolを使う際には、ListView用に書き換える必要があります

表示部分コード
ListView.cs
using EnhancedUI.EnhancedScroller;
using UnityEngine;

namespace HttpDataTool
{
    public class ListView : MonoBehaviour, IEnhancedScrollerDelegate
    {
        private EnhancedScroller _scroller;
        [SerializeField] private EnhancedScrollerCellView cellViewPrefab;
        [SerializeField] private DetailViewer detailViewer;

        private void Start()
        {
            _scroller = GetComponent<EnhancedScroller>();
            _scroller.Delegate = this;
            _scroller.ReloadData();

            detailViewer.SetData(null);

            DataContainer.OnDataChanged.AddListener(() => _scroller.ReloadData());
        }

        public int GetNumberOfCells(EnhancedScroller _)
            => DataContainer.DataList.Count;

        public float GetCellViewSize(EnhancedScroller _, int dataIndex)
            => 120;

        private void OnSelected(Data data)
            => detailViewer.SetData(data);

        public EnhancedScrollerCellView GetCellView(EnhancedScroller scroller, int dataIndex, int cellIndex)
        {
            var cellView = scroller.GetCellView(cellViewPrefab) as CellView;
            if (cellView == null) return cellView;

            cellView.SetData(DataContainer.DataList[dataIndex]);
            cellView.Selected = OnSelected;
            return cellView;
        }
    }
}
CellView.cs
using EnhancedUI.EnhancedScroller;
using UnityEngine;
using TMPro;

namespace HttpDataTool
{
    public class CellView : EnhancedScrollerCellView
    {
        [SerializeField] private TMP_Text methodTypeText;
        [SerializeField] private TMP_Text endpointText;

        private Data _data;
        public delegate void SelectedDelegate(Data data);
        public SelectedDelegate Selected;

        public void SetData(Data data)
        {
            methodTypeText.text = data.method;
            endpointText.text = data.endpoint;

            _data = data;
        }

        public void OnSelected()
        {
            Selected?.Invoke(_data);
        }
    }
}
DetailViewer.cs
using UnityEngine;
using TMPro;

namespace HttpDataTool
{
    public class DetailViewer : MonoBehaviour
    {
        [SerializeField] private TMP_Text endpointText;
        [SerializeField] private TMP_Text methodTypeText;
        [SerializeField] private TMP_Text responseCodeText;
        [SerializeField] private TMP_Text requestText;
        [SerializeField] private TMP_Text responseText;

        public void SetData(Data data)
        {
            if (data == null)
            {
                gameObject.SetActive(false);
                return;
            }

            gameObject.SetActive(true);

            endpointText.text = data.endpoint;
            methodTypeText.text = data.method;
            responseCodeText.text = data.responseCode.ToString();
            requestText.text = data.request;
            responseText.text = data.response;
        }
    }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?