Work with WebSocket by using .Net Core
Work with WebSocket by using .Net Core
WebSocket
First of all, that's talk about what is WebSocket.
WebSocket is a computer communications protocol, providing full-duplex communication channels over a single TCP connection. The WebSocket protocol was standardized by the IETF as RFC 6455 in 2011, and the WebSocket API in Web IDL is being standardized by the W3C.
WebSocket is distinct from HTTP. Both protocols are located at layer 7 in the OSI model and depend on TCP at layer 4. Although they are different, RFC 6455 states that WebSocket "is designed to work over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries," thus making it compatible with the HTTP protocol. To achieve compatibility, the WebSocket handshake uses the HTTP Upgrade header to change from the HTTP protocol to the WebSocket protocol.
The WebSocket protocol enables interaction between a web browser (or other client application) and a web server with lower overhead than half-duplex alternatives such as HTTP polling, facilitating real-time data transfer from and to the server. This is made possible by providing a standardized way for the server to send content to the client without being first requested by the client, and allowing messages to be passed back and forth while keeping the connection open. In this way, a two-way ongoing conversation can take place between the client and the server. The communications are done over TCP port number 80 (or 443 in the case of TLS-encrypted connections), which is of benefit for those environments which block non-web Internet connections using a firewall. Similar two-way browser-server communications have been achieved in non-standardized ways using stopgap technologies such as Comet.
Most browsers support the protocol, including Google Chrome, Microsoft Edge, Internet Explorer, Firefox, Safari and Opera. We could check easily from here.
People who new to WebSockets, they might ask: Why we need that, sine we have HTTP protocal already ? What benifits could it bring to us ? It's quite easy. Because of HTTP has a flaw: the communication must be initiated by client, only.
For a common example, how to know the weather today ? We just sends a request to server, and wait for the server to return the result. It means HTTP protocol does not allow server to push information to clients actively. It's frustrated.
The characteristics of one-way request are doomed to be very troublesome for the client to know if the server has continuous state changes. We need to use "long polling": every a bit time, we send a query to find out if there is any new information. The most typical scene is chat room application.
Here is a simple demonstration about chat system by using WebSocket. It's my recently working projects (sorry about masked many details on it). When a new message coming in, it will emit a WebSocket event, and display new one on specific chat window.
Login Polling is inefficient and wastes resources. Because you need to keep connecting to server, or let HTTP connection is always opened.
As we can see, WebSocket (features) are included:
- Based on TCP protocol, the implementation on the server side is pretty easy.
- It has good compatibility with HTTP protocol, default port is also 80 and 443. The handshake phase goes through by HTTP protocol, so it is not easy to mask when shaking hands and can pass various HTTP proxy servers.
- The data format is relatively lightweight, the performance overhead is very small, and the communication is efficient.
- Very low latency: there is limited HTTP overhead (like headers, cookies, etc.) making the speed at which data transfers happen much faster.
- You can send plaintext or binary data (such as a file, or image).
- There is no same-origin resistace, and client can communicate with any server.
- The protocol identifier is ws (wss if encrypted) and the server URL is the ws (wss) URL.
WebSocket Server Side Impletemention using .Net Core
There are two ways to arrive. The simple way is using a SignalR project, the end, case close. The difficult way is using an ASP.Net MVC project. And this would make us learn about the basic of basic of WebSocket programming (c#). That's try it, and code for fun.
GETTING STARTED WITH WEBSOCKETS IN ASP .NET CORE
There are some great references we should research, before we beginning to do it. Let’s start exploring how to use WebSockets in ASP.NET Core.
- WebSockets [Archived] repository on GitHub, it contains all resources we need. It did help us to understand how's WebSockets working.
- WebSockets support in ASP.NET Core from Microsoft
- Using WebSockets in ASP.NET Core from dotnetthoughts
ASP.NET CORE MIDDLEWARE
Let's start programming.
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
// using Microsoft.AspNetCore.Hosting; // core 2.2
using Microsoft.Extensions.Hosting; // core 3.0
using System;
using WebSocketManager;
namespace ChatApplication
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddWebSocketService();
}
//public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider) // core 2.2
public void Configure(IApplicationBuilder app, IHostEnvironment env, IServiceProvider serviceProvider) // core 3.0
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseWebSockets();
app.MapWebSocketManager("/channel", serviceProvider.GetService<ChatMessageHandler>());
app.UseStaticFiles();
}
}
}
1. add a reference to Microsoft.AspNetCore.WebSockets
2. add app.UseWebSockets() in the Configure method inside the Startup class
Middleware are software components that are assembled into an application pipeline to handle requests and responses. Each component chooses whether to pass the request on to the next component in the pipeline, and can perform certain actions before and after the next component is invoked in the pipeline. Request delegates are used to build the request pipeline. The request delegates handle each HTTP request.
The new pipeline consists of a series of RequestDelegate objects being called one after the next, and each component can perform operations before and after the next delegate, or can short-circuit the pipeline, handle the request itself and not pass the context further.
Before writing the middleware itself, we need a few components(classes) that deal with connections and handling messages.
The selected projects are components to deal with connections and handling messages. I separated those components into divided projects to focus on each class easily, but we could twisted them into one project.
WEBSOCKET CONNECTION MANAGER
The first thing we noticed when using the WebSocket package is that everything is low-level: we deal with individual connections, buffers and cancellation tokens. There is no built-in way of storing sockets, nor are they identified in any way. So we will build a class that keeps all active sockets in a thread-safe collection and assigns each a unique ID, while also maintaining the collection (getting, adding and removing sockets).
WebSocketConnectionManager.cs
public class WebSocketConnectionManager
{
private readonly ConcurrentDictionary<string, WebSocket> _sockets = new ConcurrentDictionary<string, WebSocket>();
public WebSocket GetSocketById(string socketId)
{
return this._sockets.FirstOrDefault(x => x.Key == socketId).Value;
}
public ConcurrentDictionary<string, WebSocket> GetAllSockets()
{
return this._sockets;
}
public string GetSocketId(WebSocket socket)
{
return this._sockets.FirstOrDefault(x => x.Value == socket).Key;
}
public void AddSocket(WebSocket socket)
{
this._sockets.TryAdd(this.GenerateConnectionId(), socket);
}
private string GenerateConnectionId()
{
return Guid.NewGuid().ToString("N");
}
public async Task RemoveSocket(string socketId)
{
this._sockets.TryRemove(socketId, out var socket);
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Connection closed.", CancellationToken.None);
}
}
Know that, it holds the sockets and the socket IDs in a concurrent dictionary (it's a thread safe dictionary) and deals with getting, adding and removing sockets.
WEBSOCKET HANDLER
Now that we have a way of keeping track of the connected clients, we want a class that handles connection and disconnection events and manages sending and receiving messages from the socket. Let’s see how a class like this might look like:
WebSocketHandler.cs
public abstract class WebSocketHandler
{
protected WebSocketConnectionManager WebSocketConnectionManager { get; set; }
public virtual async Task OnConnected(WebSocket socket)
{
this.WebSocketConnectionManager.AddSocket(socket);
}
public virtual async Task OnDisconnected(WebSocket socket)
{
var id = this.WebSocketConnectionManager.GetSocketId(socket);
await this.WebSocketConnectionManager.RemoveSocket(id);
}
public async Task SendMessageAsync(WebSocket socket, string message)
{
Debug.Print(message);
if (socket.State != WebSocketState.Open) { return; }
var bytes = Encoding.UTF8.GetBytes(message);
var buffer = new ArraySegment<byte>(bytes, 0, bytes.Length);
await socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
}
public abstract Task ReceiveAsync(WebSocket socket, WebSocketReceiveResult result, byte[] buffer);
}
It's an abstract class. That means you need to inherit it and provide actual implementation for the ReceiveAsync method, as well as you can override the methods marked as virtual.
There’s the OnConnected and OnDisconnected methods that are executed whenever a new socket connects or an existing one sends a Close message. These methods are virtual, meaning that you can provide your own functionality for connecting and disconnecting events.
There is also the SendMessageAsync which sends a message to a specific socketId and SendMessageToAllAsync, which sends a message to all connected clients.
THE MIDDLEWARE ITSELF
So far we built classes that help us maintaining a record of connected sockets and handled sending and receiving messages to and from those sockets. Now it’s time to create the middleware.
As any middleware, it needs to receive a RequestDelegate for the next component in the pipeline, while executing operations on the HttpContext before and after invoking the next component, and it needs an async Task Invoke method. It doesn’t have to inherit or implement anything, just to have the Invoke method.
WebSocketManagerMiddleware.cs
public class WebSocketManagerMiddleware
{
......
public async Task Invoke(HttpContext context)
{
// If the request is not a WebSocket request, it just exits the middleware.
if (!context.WebSockets.IsWebSocketRequest) { return; }
// If it is a WebSockets request,
// then it accepts the connection and passes the socket to the OnConnected method from the WebSocketHandler.
var socket = await context.WebSockets.AcceptWebSocketAsync();
// while the socket is in the Open state, it awaits for the receival of new data.
await this._webSocketHandler.OnConnected(socket);
// When it receives the data, it decides wether to pass the context to the ReceiveAsync method of WebSocketHandler
// (that's why you need to pass an actual implementation of the abstract WebSocketHandler class)
// or to the OnDisconnected method (if the message type is Close).
await Receive(socket, async (result, buffer) =>
{
var str = System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count);
Debug.Print(str);
if (result.MessageType == WebSocketMessageType.Text)
{
await this._webSocketHandler.ReceiveAsync(socket, result, buffer);
return;
}
else if (result.MessageType == WebSocketMessageType.Binary)
{
// at this time, we won't deal this part
}
else if (result.MessageType == WebSocketMessageType.Close) {
await this._webSocketHandler.OnDisconnected(socket);
return;
}
});
}
private async Task Receive(WebSocket socket, Action<WebSocketReceiveResult, byte[]> handleMessage)
{
const int BUFFER_LENGTG = 4096; // 4 * 1024;
var buffer = new byte[BUFFER_LENGTG];
while(socket.State == WebSocketState.Open)
{
var result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
handleMessage(result, buffer);
}
}
}
The middleware is passed an implementation of WebSocketHandler and a RequestDelegate. If the request is not a WebSocket request, it just exits the middleware. If it is a WebSockets request, then it accepts the connection and passes the socket to the OnConnected method from the WebSocketHandler.
Then, while the socket is in the Open state, it awaits for the receival of new data. When it receives the data, it decides wether to pass the context to the ReceiveAsync method of WebSocketHandler (notice why you need to pass an actual implementation of the abstract WebSocketHandler class) or to the OnDisconnected method (if the message type is Close).
This is a simple middleware, basically.
THE MIDDLEWARE EXTENSION METHODS
Most likely in modern applications you want to send notifications and messages only to clients connected to a specific part of the application (think SignalR Hubs).
With this middleware, you can map different paths of your application with specific implementations of WebSocketHandler, so you would get completely isolated environments (and different instances of WebSocketConnectionManager, but more on this later).
So mapping the middleware is done using the following extension method on IApplicationBuilder:
app.MapWebSocketManager("/channel", serviceProvider.GetService<ChatMessageHandler>());
WebSocketManagerExtensions.cs
public static class WebSocketManagerExtensions
{
public static IServiceCollection AddWebSocketService(this IServiceCollection services)
{
// Besides from adding the WebSocketConnectionManager service,
services.AddTransient<WebSocketConnectionManager>();
// it also searches the executing assembly for types that inherit WebSocketHandler
// and it registers them as singleton using reflection.
// so that every request gets the same instance of the message handler, it's important!
foreach (var type in Assembly.GetEntryAssembly().ExportedTypes)
{
if (type.GetTypeInfo().BaseType == typeof(WebSocketHandler))
{
services.AddSingleton(type);
}
}
return services;
}
// It receives a path and it maps that path using with the WebSocketManagerMiddleware which is passed the specific implementation
// of WebSocketHandler you provided as argument for the MapWebSocketManager extension method.
public static IApplicationBuilder MapWebSocketManager(this IApplicationBuilder app, PathString path, WebSocketHandler handler)
{
return app.Map(path, (_app) => {
_app.UseMiddleware<WebSocketManagerMiddleware>(handler);
});
}
}
It receives a path and it maps that path using with the WebSocketManagerMiddleware which is passed the specific implementation of WebSocketHandler you provided as argument for the MapWebSocketManager extension method.
We will also need to add some services in order to use them, and this is done in another extension method on IServiceCollection:
WebSocketManagerExtensions.cs Line: 13
services.AddTransient<WebSocketConnectionManager>();
Besides from adding the WebSocketConnectionManager service, it also searches the executing assembly for types that inherit WebSocketHandler and it registers them as singleton using reflection, so that every request gets the same instance of the message handler, it's important!
WebSocketManagerExtensions.cs Line: 18
foreach (var type in Assembly.GetEntryAssembly().ExportedTypes)
{
if (type.GetTypeInfo().BaseType == typeof(WebSocketHandler))
{
services.AddSingleton(type);
}
}
For now, the middleware we built did not work for us. We need to add MapWebSocketManager to previous ASP.Net Core MVC project we created, for starting up a WebSocket server.
Startup.cs
app.MapWebSocketManager("/channel", serviceProvider.GetService<ChatMessageHandler>());
Okey, the WebSocket server is now working for us. Thus, we need to create a client. There are two forms client we could create.
First, is a simple Html with javascript, it stands on our WebSocket server. It just a easy way for demonstraion. Yes, as we can see, all actions run via javascript.
The WebSocket API provided by the browser is remarkably small and simple. Once again, all the low-level details of connection management and message processing are taken care of by the browser. To initiate a new connection, we need the URL of a WebSocket resource and a few application callbacks.
app.js
(function connect() {
socket = new WebSocket(uri);
socket.onopen = (event) => {
console.log(`opened connection to ` + uri);
};
socket.onclose = (event) => {
console.log(`closed connection from ` + uri);
};
socket.onmessage = (event) => {
appendItem(list, event.data);
console.log(event.data);
};
socket.onerror = (event) => {
console.log(`error: ` + event.data);
};
})();
However, we could use "Echo Test" to taste it easily from here.
Second, we could also build a Console Application, sending and receiving messages.
We still need to use System.Net.WebSockets namespace.
private static async Task WebSocketRunner()
{
var client = new ClientWebSocket();
// leave it to configurations will be a great idea.
var webSocketUri = "ws://localhost:5000/channel";
// var webSocketUri = "wss://localhost:5001/channel";
await client.ConnectAsync(new Uri(webSocketUri), CancellationToken.None);
Console.WriteLine("Connected!");
var sending = Task.Run(async () =>
{
string line;
while ((line = Console.ReadLine()) != null && line != string.Empty)
{
var bytes = Encoding.UTF8.GetBytes(line);
var b64 = Convert.ToBase64String(bytes);
var b64bytes = Encoding.UTF8.GetBytes(b64);
await client.SendAsync(new ArraySegment<byte>(b64bytes), WebSocketMessageType.Text, true, CancellationToken.None);
}
await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
});
var receiving = Receiving(client);
await Task.WhenAll(sending, receiving);
}
private static async Task Receiving(ClientWebSocket client)
{
const int BUFFER_LENGTH = 4096; // 4 * 1024
var buffer = new byte[BUFFER_LENGTH];
while (true)
{
var result = await client.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
var str = Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine(str);
}
else if (result.MessageType == WebSocketMessageType.Binary)
{
// do nothing
}
else if (result.MessageType == WebSocketMessageType.Close)
{
await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
break;
}
}
}
The complete solution uploaded on GitHub repository, here.
CONCLUSION
WebSockets bring our modern applications into a new stage. It presents a simple solution to huge problems. Easy integration, easy implementation, lightweight coding are tempting. Try it, and make our applications' user experience more batter than before.
REFERENCES
https://tools.ietf.org/html/rfc6455
https://www.w3.org/TR/websockets/
https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
https://docs.microsoft.com/zh-tw/aspnet/core/fundamentals/websockets?view=aspnetcore-3.0
https://docs.microsoft.com/zh-tw/azure/application-gateway/application-gateway-websocket