Websockets and Kestrel

In my post Websockets with Fleck we looked at using Fleck to create a websocket based server, let’s not turn our attention to integrating websockets with an ASP.NET core application using Kestrel.

This is NOT meant to implement anything near as complete as the Fleck library, but is just an example of how we might implement websockets in a Kestrel application and we’re going to try to emulate the code we had for that Fleck example.

  • Create an ASP.NET Core Web Application
  • Select the Empty template

Let’s clean out the Properties | launchSettings.json by remove the iisExpression and IIS Express profile, so mine looks like this

{
  "profiles": {
    "YOUR_APP_NAME": {
      "commandName": "Project",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Obviously keep your application name in the YOUR_APP_NAME string.

Now in Program.cs we’ll add code to allow us to use the 8181 port, so the CreateHostBuilder method should now look like this

public static IHostBuilder CreateHostBuilder(string[] args) =>
  Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder =>
    {
      webBuilder.UseStartup<Startup>();
      webBuilder.UseUrls("http://*:8181");
    });

Delete everything within the Startup.cs’s Configure method and replace with

app.UseWebSockets();

this adds the websocket middleware.

We’re actually going to then create our own middleware to handle web socket requests, so
let’s create the file WebSocketManagerMiddleware.cs. Here’s the code…

public class WebSocketManagerMiddleware
{
  private readonly RequestDelegate _next;
  private readonly WebSocketConnection _connection;

  public WebSocketManagerMiddleware(
    RequestDelegate next, 
    WebSocketConnection connection)
  {
    _next = next;
    _connection = connection;
  }

  public async Task InvokeAsync(HttpContext context)
  {
    if (context.WebSockets.IsWebSocketRequest)
    {
      var socket = await context.WebSockets.AcceptWebSocketAsync();

      _connection.OnOpen(socket);

      await Receive(socket, (result, buffer) =>
      {
        switch (result.MessageType)
        {
          case WebSocketMessageType.Text:
            var s = Encoding.UTF8.GetString(buffer);
            _connection.OnMessage(socket, s.Substring(0, Math.Max(0, s.IndexOf('\0'))));
            break;
          case WebSocketMessageType.Binary:
            _connection.OnBinary(socket, buffer);
            break;
          case WebSocketMessageType.Close:
            _connection.OnClose(socket);
            break;
        }
      });
    }
    await _next(context);
  }

  private async Task Receive(
    WebSocket socket, 
    Action<WebSocketReceiveResult, 
    byte[]> handler)
  {
    var buffer = new byte[1024];

    while (socket.State == WebSocketState.Open)
    {
      var result = await socket.ReceiveAsync(buffer: 
        new ArraySegment<byte>(buffer),
        cancellationToken: CancellationToken.None);

      handler(result, buffer);
    }
  }
}

Middleware expects an Invoke or InvokeAsync method that returns a Task. In our example, we firstly ensure this is a websocket request before accepting the request. In this example we pass in a WebSocketConnection instance (we’ll have a look at that next), but basically this middleware intercepts the websockets and then calls the WebSocketConnection class in a manner similar to the way our Fleck server was implemented, i.e. using OnOpen, OnClose, OnMessage and OnBinary calls.

At the end of the code we pass the context through to the next piece of middleware in the pipeline.

The reason we have a WebSocketConnection class is to just give us an abstraction for creating our actual application websocket code.

Add the file WebScocketConnection.cs, this is going to expose OnOpen, OnClose etc. extension points as well as a SendAsync method for sending data to the connected client, here’s the code

public class WebSocketConnection
{
  public void Start(Action<WebSocketConnection> connection)
  {
    connection(this);
  }

  public Action<WebSocket> OnOpen { get; set; } = 
    webSocket => { };
  public Action<WebSocket> OnClose { get; set; } = 
    webSocket => { };
  public Action<WebSocket, string> OnMessage { get; set; } = 
    (webSocket, message) => { };
  public Action<WebSocket, byte[]> OnBinary { get; set; } = 
    (webSocket, bytes) => { };

  public async Task SendAsync(WebSocket socket, string message)
  {
    if (socket.State == WebSocketState.Open)
    {
      await socket.SendAsync(
        new ArraySegment<byte>(Encoding.ASCII.GetBytes(message),
          0,
          message.Length),
        WebSocketMessageType.Text,
        true,
        CancellationToken.None);
      }
    }
  }
}

Finally let’s return to Startup.cs and the Configure method, here’s the full code

var websocketServer = new WebSocketConnection();
websocketServer.Start(connection =>
{
  connection.OnOpen = socket => Console.WriteLine("OnOpen");
  connection.OnClose = socket => Console.WriteLine("OnClose");
  connection.OnMessage = async (socket, message) =>
  {
    Console.WriteLine($"OnMessage {message}");
    await connection.SendAsync(socket, $"Echo: {message}");
  };
  connection.OnBinary = (socket, bytes) => 
    Console.WriteLine($"OnBinary {Encoding.UTF8.GetString(bytes)}");
});

app.UseWebSockets();
app.UseMiddleware<WebSocketManagerMiddleware>(websocketServer);

References

WebSockets support in ASP.NET Core
Write custom ASP.NET Core middleware