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 형식으로 바꾸어 준다.
이제 테스트를 해보면 촥촥 잘 된다.
하나 해결 했으니, 다음 꺼도 해결하러..
'C#' 카테고리의 다른 글
[ASP.NET Core] Service, Repository 패턴 적용하기 (1) | 2024.12.22 |
---|---|
[C#] Lock Free 하게 써보기 (Concurrent, Interlocked 활용) (0) | 2024.12.04 |
[C#] 인터페이스와 추상 클래스 (1) | 2024.11.18 |
[C#] Csv 파일을 Json 파일로 바꾸기 (3) | 2024.11.14 |
[C#] Unity에서 ProtoBuf 사용하는 방법 (1) | 2024.11.04 |