근래에 ASP.NET Core 에서 Web Socket 을 구성하느라 맨땅에 헤딩을 심하게 했었습니다. ㅋㅋㅋ

그러다 어젯밤에 CodeProject 이멜을 확인하던 중 다음 글이 올라와있더군요.


잠시 보다가 혹시나 WebSocket 을 구성하시는 분에게 도움이 될 듯하여 우리말로 바꾸어 봤습니다.


원문은 이 곳에서 확인 하실 수 있습니다.

원문과 비교하셔 보시고, 원문을 이해하는데 조금이나마 도움이 되었으면 합니다.



ASP.NET Core 에서 Web Socket 을 관리하기 위한 middleware

Startup.cs 를 깨끗하게 유지하는 WebSocket 관리 Logic 을 구성하는 방법. 

소개


ASP.NET Core SignalR 은 Web Application 의 실시간 관리를 단순화한 유용한 Library 이지만
이 글에서는 WebSocket client 와의 더 나은 유연성과 호환성을 위해 WebSockets 를 사용하려한다.
Microsoft 의 문서에서 훌륭한 WebSocket 예제를 찾았지만 한 Connection 에서 다른 Connection으로  message 를 broadcast 이 가능하도록 connection 을 관리하는 기능이 필요하다.
이 기능은 SignalR 에서는 제공하는 기능이다.
이 Logic 은 정말 복잡할걸로 생각하지만 Startup class 에서 이 복잡함을 제거하고자 한다.

Background

ASP.NET 코어에서 WebSocket 지원에 대한 내용은 이 곳에서 확인.
middleware 에 대한 내용과 작성하는 방법을 파악하기 위해서는 이 글을 확인

Code 사용하기


먼저 프로젝트에 Microsoft.AspNetCore.WebSockets package 를 추가해야한다.
그럼 이제 WebSockets 를 관리할 Extension Method 와 Class 를 추가할 수 있다.
 Hide   Shrink   Copy Code
public static class WebSocketExtensions
{
public static IApplicationBuilder UseCustomWebSocketManager(this IApplicationBuilder app)
{
return app.UseMiddleware<CustomWebSocketManager>();
}
}

public class CustomWebSocketManager
{
private readonly RequestDelegate _next;

public CustomWebSocketManager(RequestDelegate next)
{
_next = next;
}

public async Task Invoke(HttpContext context, ICustomWebSocketFactory wsFactory, ICustomWebSocketMessageHandler wsmHandler)
{
if (context.Request.Path == "/ws")
{
if (context.WebSockets.IsWebSocketRequest)
{
string username = context.Request.Query["u"];
if (!string.IsNullOrEmpty(username))
{
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
CustomWebSocket userWebSocket = new CustomWebSocket()
{
WebSocket = webSocket,
Username = username
};
wsFactory.Add(userWebSocket);
await wsmHandler.SendInitialMessages(userWebSocket);
await Listen(context, userWebSocket, wsFactory, wsmHandler);
}
}
else
{
context.Response.StatusCode = 400;
}
}
await _next(context);
}

private async Task Listen(HttpContext context, CustomWebSocket userWebSocket, ICustomWebSocketFactory wsFactory, ICustomWebSocketMessageHandler wsmHandler)
{
WebSocket webSocket = userWebSocket.WebSocket;
var buffer = new byte[1024 * 4];
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
while (!result.CloseStatus.HasValue)
{
await wsmHandler.HandleMessage(result, buffer, userWebSocket, wsFactory);
buffer = new byte[1024 * 4];
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
}
wsFactory.Remove(userWebSocket.Username);
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}
}
위 코드에서 보면 WebSocket request(요청) 은 항상 Url 에 "/ws" 를 포함하고 있다.
Query string 에 포함된 parameter 로 u 는 username 으로 WebSocket 과 관련된 login user 를 의미한다.
CustomWebSocket 는 WebSocket 과 username 을 포함한 class 다:
Hide   Copy Code
public class CustomWebSocket
{
public WebSocket WebSocket { get; set; }
public string Username { get; set; }
}
custom WebSocket Message class 도 생성한다:
Hide   Copy Code
class CustomWebSocketMessage
{
public string Text { get; set; }
public DateTime MessagDateTime { get; set; }
public string Username { get; set; }
public WSMessageType Type { get; set; }
}
Type 에는 사용할지도 모를  여러 type 들을 열거한다.
Startup class 에 다음처럼 service 들을 등록해야한다:
Hide   Copy Code
services.AddSingleton<ICustomWebSocketFactory, CustomWebSocketFactory>();
services.AddSingleton<ICustomWebSocketMessageHandler, CustomWebSocketMessageHandler>();
CustomWebSocketFactory 는 연결된 WebSocket 들의 목록을 함께 모아 관리한다:
Hide   Shrink   Copy Code
public interface ICustomWebSocketFactory
{
void Add(CustomWebSocket uws);
void Remove(string username);
List<CustomWebSocket> All();
List<CustomWebSocket> Others(CustomWebSocket client);
CustomWebSocket Client(string username);
}

public class CustomWebSocketFactory : ICustomWebSocketFactory
{
List<CustomWebSocket> List;

public CustomWebSocketFactory()
{
List = new List<CustomWebSocket>();
}

public void Add(CustomWebSocket uws)
{
List.Add(uws);
}

//when disconnect
public void Remove(string username)
{
List.Remove(Client(username));
}

public List<CustomWebSocket> All()
{
return List;
}
public List<CustomWebSocket> Others(CustomWebSocket client)
{
return List.Where(c => c.Username != client.Username).ToList();
}
public CustomWebSocket Client(string username)
{
return List.First(c=>c.Username == username);
}
}
CustomWebSocketMessageHandler 는 message 에 관한 Logic 을 포함한다 (예를들어 , 연결시 어떤 메시지를 전송해야하는지, 수신된 message 에 어떻게 처리해야하는지에 대한 Logic이다)
Hide   Shrink   Copy Code
public interface ICustomWebSocketMessageHandler
{
Task SendInitialMessages(CustomWebSocket userWebSocket);
Task HandleMessage(WebSocketReceiveResult result, byte[] buffer, CustomWebSocket userWebSocket, ICustomWebSocketFactory wsFactory);
Task BroadcastOthers(byte[] buffer, CustomWebSocket userWebSocket, ICustomWebSocketFactory wsFactory);
Task BroadcastAll(byte[] buffer, CustomWebSocket userWebSocket, ICustomWebSocketFactory wsFactory);
}

public class CustomWebSocketMessageHandler : ICustomWebSocketMessageHandler
{
public async Task SendInitialMessages(CustomWebSocket userWebSocket)
{
WebSocket webSocket = userWebSocket.WebSocket;
var msg = new CustomWebSocketMessage
{
MessagDateTime = DateTime.Now,
Type = WSMessageType.anyType,
Text = anyText,
Username = "system"
};

string serialisedMessage = JsonConvert.SerializeObject(msg);
byte[] bytes = Encoding.ASCII.GetBytes(serialisedMessage);
await webSocket.SendAsync(new ArraySegment<byte>(bytes, 0, bytes.Length), WebSocketMessageType.Text, true, CancellationToken.None);
}

public async Task HandleMessage(WebSocketReceiveResult result, byte[] buffer, CustomWebSocket userWebSocket, ICustomWebSocketFactory wsFactory)
{
string msg = Encoding.ASCII.GetString(buffer);
try
{
var message = JsonConvert.DeserializeObject<CustomWebSocketMessage>(msg);
if (message.Type == WSMessageType.anyType)
{
await BroadcastOthers(buffer, userWebSocket, wsFactory);
}
}
catch (Exception e)
{
await userWebSocket.WebSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);
}
}

public async Task BroadcastOthers(byte[] buffer, CustomWebSocket userWebSocket, ICustomWebSocketFactory wsFactory)
{
var others = wsFactory.Others(userWebSocket);
foreach (var uws in others)
{
await uws.WebSocket.SendAsync(new ArraySegment<byte>(buffer, 0, buffer.Length), WebSocketMessageType.Text, true, CancellationToken.None);
}
}

public async Task BroadcastAll(byte[] buffer, CustomWebSocket userWebSocket, ICustomWebSocketFactory wsFactory)
{
var all = wsFactory.All();
foreach (var uws in all)
{
await uws.WebSocket.SendAsync(new ArraySegment<byte>(buffer, 0, buffer.Length), WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}
마지막으로, Startup class 의 Configure method 에 다음 code 를 추가한다:
Hide   Copy Code
var webSocketOptions = new WebSocketOptions()
{
KeepAliveInterval = TimeSpan.FromSeconds(120),
ReceiveBufferSize = 4 * 1024
};

app.UseWebSockets(webSocketOptions);
app.UseCustomWebSocketManager();
So, in this way the Starup class remains clean and the logics to manage the WebSockets can grows giving you the flexibility to organize it as you prefer.
자. 이 방법을 통해 Starup class 를 깨끗하게 만들었고 WebSocket 를 관리하는 Logic 이 계속 늘어남으로써 Logic 을 관리하는데  여러분이 원하는데로 구성할 수 있는 유연성을 제공한다.



영어가 유창하지 않아 제대로 바뀌어 지지 않았을 것입니다.

제 글을 100% 믿지 마시고 원문도 한번 확인해 보셨으면 합니다. ^^;



행복한 고수되셔요.


woojja ))*

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\



반응형

+ Recent posts