반응형

json 형식으로는 제대로 작동하고 있는데, protobuf로 통신하려고 하니, 제대로 작동이 되지 않았어서

 

하루 종일 붙잡고 찾았는데, 마지막에 너무나도 허무하긴 했다.

 

디버깅 하면서 에러 확인하고 try{} catch{} 문을 도배하면서 찾아갔다.

 

stack overflow도 열심히 찾고.. 하면서 그냥 json 형식해도 되지 않을까 라는 생각도 있었지만,

 

욕심상 무조건 protobuf로 작성하고 싶었다.

 

처음엔 request가 문제였다. 

 

분명, unity에서 login, account 을 보냈는데, 서버에서 아무런 응답도 없고 에러 코드만 발생했다.


Content-Type

 

Content-Type이 json 형식일 때는 application/json 이지만, protobuf로 사용하려면 application/x-protobuf 였다.

uwr.SetRequestHeader("Content-Type", "application/x-protobuf");

 

Accept

 

Response를 받을 때도 계속해서 문제가 발생해서 response 된 데이터를 뜯어보았지만..

 

데이터 문제는 없었다. 그러면 무엇이 문제일까? 열심히 찾아봤는데, 응답 받을 때, 어떤 Content-Type으로 받아야하는지도 지정해주어야했다.

uwr.SetRequestHeader("Accept", "application/x-protobuf");

 

Client 에서는 이 2가지 문제로 꽤 오랫동안 찾았는데.. 너무나도 간단한 문제였다.

 

private IEnumerator CoSendWebRequest<TRequest, TResponse>(string url, string method, TRequest req, Action<TResponse> res, Action<string> onError)
where TRequest : IMessage
where TResponse : IMessage<TResponse>, new()
{
    string sendUrl = $"{BaseUrl}/{url}";

    using (var uwr = new UnityWebRequest(sendUrl, method))
    {
        uwr.uploadHandler = new UploadHandlerRaw(req.ToByteArray());
        uwr.downloadHandler = new DownloadHandlerBuffer();
        uwr.SetRequestHeader("Content-Type", "application/x-protobuf");
        uwr.SetRequestHeader("Accept", "application/x-protobuf");

        yield return uwr.SendWebRequest();

        if (uwr.result == UnityWebRequest.Result.ConnectionError || uwr.result == UnityWebRequest.Result.ProtocolError)
        {
            Debug.LogError($"[WebManager] Error: {uwr.error}, HTTP Status: {uwr.responseCode}");
            onError?.Invoke($"Error: {uwr.error}, HTTP Status: {uwr.responseCode}");
        }
        else
        {
            try
            {
                if (uwr.downloadHandler.data == null || uwr.downloadHandler.data.Length == 0)
                {
                    Debug.LogError("[WebManager] Response data is empty or null.");
                    onError?.Invoke("Response data is empty or null.");
                    yield break;
                }

                Debug.Log($"Response Raw Data (as Bytes): {BitConverter.ToString(uwr.downloadHandler.data)}");

                TResponse responseData = new TResponse();
                responseData.MergeFrom(uwr.downloadHandler.data);
                res?.Invoke(responseData);
            }
            catch (Exception e)
            {
                Debug.LogError($"[WebManager] Failed to parse response: {e.Message}");
                onError?.Invoke($"Failed to parse response: {e.Message}");
            }
        }
    }
}

결과적으로 try catch 문과 Log로 가득찬 코드가 되어 버렸다..

 

responseData.MergeFrom(uwr.downloadHandler.data);

이 부분에서 계속 Exception이 발생하여 오랜 시간을 보냈는데.. Accept 설정 후, 바로 해결!!


* Content-Type : 간단히 말해 보내는 자원의 형식을 명시하기 위해 헤더에 실리는 정보


InputFormatters

HTTP 요청 본문을 요청하는 데 사용 되는 부분인데, 어떠한 데이터를 요청 받았을 때, 어떤 형식으로 읽을 것인지를 정해준다.

기본적으로 json 형식으로 받게 되어 있었는데, 이 부분에 대한 추가가 필요했다.

즉, HTTP 요청 본문을 객체로 변환 하는 작업에 사용된다.이 때, 필요한 것이 Content-Type인데, 이것에 맞춰서 디시리얼라이징을 처리한다.

 

OutputFormatters

서버에서 응답 본문을 처리하는 데 사용된다. 즉, 어떤 형식으로 클라이언트에게 보낼지 결정하게 된다.

이것 역시 cotent-type이 사용되는데, 객체를 특정 형식으로 변환하여 응답 본문을 작성해주는 부분이다.

즉, 직렬화 하는 부분

 

builder.Services
    .AddControllers(options =>
    {
        options.InputFormatters.Add(new ProtobufInputFormatter());
        options.OutputFormatters.Add(new ProtobufOutputFormatter());
    })

이런 식으로 Controller option에서 Protobuf 형식에 맞게 디시리얼라이징과 직렬화하는 클래스를 만들어 추가해주었고,

 

이러한 부분들을 뭔가 라이브러리가 존재하지 않을까 찾아보다가 실패하고 직접 만들게 되었다..

public class ProtobufInputFormatter : InputFormatter
{
    public ProtobufInputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/x-protobuf"));
    }

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
    {
        var httpContext = context.HttpContext;
        var type = context.ModelType;

        try
        {
            using var stream = new MemoryStream();
            await httpContext.Request.Body.CopyToAsync(stream);
            stream.Seek(0, SeekOrigin.Begin);

            var message = (IMessage)Activator.CreateInstance(type)!;
            message.MergeFrom(stream);

            return await InputFormatterResult.SuccessAsync(message);
        }
        catch (Exception e)
        {
            Log.Error($"Failed to read request body. error : {e.Message}");
            return await InputFormatterResult.FailureAsync();
        }
    }

    protected override bool CanReadType(Type type)
    {
        return type != null && typeof(IMessage).IsAssignableFrom(type);
    }
}

public class ProtobufOutputFormatter : OutputFormatter
{
    public ProtobufOutputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/x-protobuf"));
    }

    public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
    {
        var httpContext = context.HttpContext;

        if (context.Object is IMessage message)
        {
            using var stream = new MemoryStream();
            message.WriteTo(stream);
            stream.Seek(0, SeekOrigin.Begin);
            await stream.CopyToAsync(httpContext.Response.Body);
        }
    }

    protected override bool CanWriteType(Type type)
    {
        return type != null && typeof(IMessage).IsAssignableFrom(type);
    }
}

이런 식으로 request 가 들어 올때, ProtobufInputFormatter.ReadRequestBodyAsync() 매서드가 호출되어 Protobuf 데이터를 받아와주고,

 

response를 할 때, ProtobufOutputFormatter.WriteResponseBodyAsync() 매서드가 호출되어 Protobuf 형식으로 바꾸어 준다.

 

이제 테스트를 해보면 촥촥 잘 된다.

하나 해결 했으니, 다음 꺼도 해결하러..

 

반응형

+ Recent posts