Using Web Sockets with ASP.NET Core

Brouillon

À venir

0 comment

Nowadays, web applications need more real time communications. For instance, if you create a chat application, you want the user to get messages as soon as the other users write them. So, in this kind of applications, the server must send information to the client as soon as they are available. If you have already done this a few years ago, you may have used tricky methods to simulate a full-duplex channel in a browser such as long polling, streaming, or using Flash. To replace all those hacks, W3C has defined a new standard: Web Sockets. Web sockets provide a full-duplex, bidirectional communications channel. This is now the standard way to do real time communication between the server and the client.

Can I use Web Sockets?

Yes, all major browsers support web sockets, and well-known websites such as StackOverflow use Web sockets.

Source: http://caniuse.com/#feat=websockets

How to use Web Sockets?

In this post, we'll create a simple web application that uses web sockets. The client open the web socket, then the server sends ping every seconds to the client, and of course the client replies pong.

First, we need to configure the server to accept web sockets:

  1. Create an ASP.NET Core project
  2. Add the Nuget package Microsoft.AspNetCore.WebSockets (https://github.com/aspnet/WebSockets/)
  3. Edit the Startup.cs file

public class Startup
{
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
	// ...

	// Accept web socket requests
        app.UseWebSockets();
	// handle web socket requests
        app.UseMiddleware(); 
    }
}
  1. Create the Middleware that handles the web socket requests

The code is very simple. The Invoke method get the socket if available and use it to send and receive data. We also create 2 helpers to send and receive text data using the socket. The receive method is more complex because the data send by the client can be chunked, this means we need to get all packets before building the string.

public class SampleWebSocketMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task Invoke(HttpContext context)
    {
        if (!context.WebSockets.IsWebSocketRequest)
        {
            // Not a web socket request
            await _next.Invoke(context);
            return;
        }

        var ct = context.RequestAborted;
        using (var socket = await context.WebSockets.AcceptWebSocketAsync())
        {
            for (var i = 0; i  10; i++)
            {
                await SendStringAsync(socket, "ping", ct);
                var response = await ReceiveStringAsync(socket, ct);
                if (response != "pong")
                {
                    await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Expected 'pong'", ct);
                    return;
                }

                await Task.Delay(1000, ct);
            }
        }
    }

    private static Task SendStringAsync(WebSocket socket, string data, CancellationToken ct = default(CancellationToken))
    {
        var buffer = Encoding.UTF8.GetBytes(data);
        var segment = new ArraySegment(buffer);
        return socket.SendAsync(segment, WebSocketMessageType.Text, true, ct);
    }

    private static async Task ReceiveStringAsync(WebSocket socket, CancellationToken ct = default(CancellationToken))
    {
        // Message can be sent by chunk.
        // We must read all chunks before decoding the content
        var buffer = new ArraySegment(new byte[8192]);
        using (var ms = new MemoryStream())
        {
            WebSocketReceiveResult result;
            do
            {
                ct.ThrowIfCancellationRequested();

                result = await socket.ReceiveAsync(buffer, ct);
                ms.Write(buffer.Array, buffer.Offset, result.Count);
            }
            while (!result.EndOfMessage);

            ms.Seek(0, SeekOrigin.Begin);
            if (result.MessageType != WebSocketMessageType.Text)
                throw new Exception("Unexpected message");

            // Encoding UTF8: https://tools.ietf.org/html/rfc6455#section-5.6
            using (var reader = new StreamReader(ms, Encoding.UTF8))
            {
                return await reader.ReadToEndAsync();
            }
        }
    }
}

That’s all for the server part. Now we can create the client using a few lines of JavaScript. First we create the WebSocket object. The url looks like ws://localhost:8080 or wss://localhost:8080 if you are using https (get a free certificate with let’s encrypt). Then we can send data to the server using the method send. To get the data from the server we need to use the onmessage callback. Here’s the full code:

<button id="BtnStart">Start</button>
<script>
    var btnStart = document.getElementById("BtnStart");
    btnStart.addEventListener("click", function (e) {
        e.preventDefault();

        var protocol = location.protocol === "https:" ? "wss:" : "ws:";
        var wsUri = protocol + "//" + window.location.host;
        var socket = new WebSocket(wsUri);
        socket.onopen = e => {
            console.log("socket opened", e);
        };

        socket.onclose = function (e) {
            console.log("socket closed", e);
        };

        socket.onmessage = function (e) {
            console.log(e);
            socket.send("pong");
        };

        socket.onerror = function (e) {
            console.error(e.data);
        };
    });
</script></code>

You can see the result by opening Chrome developer tools:

Azure Hosting

If you are hosting your application on Azure, don’t forget to enable the Web Sockets protocol.

Conclusion

Web sockets are now well supported by browsers and very easy to use. So, it’s time to add real time communication in your web sites.

You'll find a working example of web sockets on my web crawler project.

Gérald Barré