Asp. net How to use middleware to manage websocket in Core

  • 2021-10-27 07:04:48
  • OfStack

Introduction

I like this thing. NET CORE not only because of its high performance and cross-platform, but also because its design pattern is really fascinating. Before, when NET CORE didn't exist, NET had to run on windows server 2012 with websocket, but I generally wouldn't do this, and I always put websocket on the server of nodejs. In this way, trouble is definitely trouble, and js is a headache when it is complicated to write and looked at after a few years. Then, if. NET CORE is running as kestrel, then you don't need to consider the version of the server, and websocket can be used anywhere

ASP. NET Core SignalR is a useful library that simplifies the management of real-time communications in Web applications. However, I prefer to use WebSockets because I want to be more flexible and compatible with any WebSocket client.

In the documentation for Microsoft, I found a good example of WebSockets working. It still manages connections so that messages can be broadcast from one connection to the other, which is an out-of-the-box feature of SignalR. Expecting this logic to be very complicated, I want to remove it from the Startup class.

Background

To read WebSockets support in ASP. NET Core, check it out here. If you want to learn about middleware and how to write it in ASP. NET Core, read this link.

Code usage

First, you must add Microsoft.AspNetCore.WebSockets Package to your project.

Now, you can create an extension method and class to manage WebSockets:


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);
 }
}

In this case, the WebSockets request always includes "/ws" in URL. The query string contains the parameter u used to associate WebSocket with the user name of the logged-in user.

CustomWebSocket is a class that contains WebSocket and the user name:


public class CustomWebSocket
{
 public WebSocket WebSocket { get; set; }
 public string Username { get; set; }
}

I also created a custom WebSocket message:


class CustomWebSocketMessage
{
 public string Text { get; set; }
 public DateTime MessagDateTime { get; set; }
 public string Username { get; set; }
 public WSMessageType Type { get; set; }
}

Where Type is an enumeration of different types of messages that you may have.

In the Startup class, you must register the following services:


services.AddSingleton<ICustomWebSocketFactory, CustomWebSocketFactory>();
services.AddSingleton<ICustomWebSocketMessageHandler, CustomWebSocketMessageHandler>();

CustomWebSocketFactory is responsible for collecting the list of connected WebSockets:


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 contains logic about messages (that is, any messages that need to be sent at connection time and how to react to incoming messages)


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);
  }
 }
}

Finally, add the following to the Startup class of the Configure method:


var webSocketOptions = new WebSocketOptions()
{
 KeepAliveInterval = TimeSpan.FromSeconds(120),
 ReceiveBufferSize = 4 * 1024
};

app.UseWebSockets(webSocketOptions);
app.UseCustomWebSocketManager();

In this way, the Starup class is kept clean, and the logic for managing WebSockets can be extended, allowing you to organize it flexibly according to your preferences.

Summarize


Related articles: