High performance WebSocket on .NET (both server and client - standalone or wrapper of System.Net.WebSockets.WebSocket)
$ dotnet add package VIEApps.Components.WebSocketsA concrete implementation of the System.Net.WebSockets.WebSocket abstract, that allows you to make WebSocket connections as a client or to respond to WebSocket requests as a server (or wrap existing WebSocket connections of ASP.NET / ASP.NET Core).
The class ManagedWebSocket is an implementation or a wrapper of the System.Net.WebSockets.WebSocket abstract class, that allows you send and receive messages in the same way for both side of client and server role.
async Task ReceiveAsync(ManagedWebSocket websocket)
{
var buffer = new ArraySegment<byte>(new byte[1024]);
while (true)
{
WebSocketReceiveResult result = await websocket.ReceiveAsync(buffer, CancellationToken.None).ConfigureAwait(false);
switch (result.MessageType)
{
case WebSocketMessageType.Close:
return;
case WebSocketMessageType.Text:
case WebSocketMessageType.Binary:
var value = Encoding.UTF8.GetString(buffer, result.Count);
Console.WriteLine(value);
break;
}
}
}
async Task SendAsync(ManagedWebSocket websocket)
{
var buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes("Hello World"));
await websocket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false);
}
// the identity of the connection
public Guid ID { get; }
// true if the connection was made when connect to a remote endpoint (mean client role)
public bool IsClient { get; }
// original requesting URI of the connection
public Uri RequestUri { get; }
// the time when the connection is established
public DateTime Timestamp { get; }
// the remote endpoint
public EndPoint RemoteEndPoint { get; }
// the local endpoint
public EndPoint LocalEndPoint { get; }
// Extra information
public Dictionary<string, object> Extra { get; }
// Headers information
public Dictionary<string, string> Headers { get; }
This is a centralized element for working with both side of client and server role. This class has 04 action properties (event handlers) to take care of all working cases, you just need to assign your code to cover its.
// fire when got any error
Action<ManagedWebSocket, Exception> OnError;
// fire when a connection is established
Action<ManagedWebSocket> OnConnectionEstablished;
// fire when a connection is broken
Action<ManagedWebSocket> OnConnectionBroken;
// fire when a message is received
Action<ManagedWebSocket, WebSocketReceiveResult, byte[]> OnMessageReceived;Example:
var websocket = new WebSocket
{
OnError = (webSocket, exception) =>
{
// your code to handle error
},
OnConnectionEstablished = (webSocket) =>
{
// your code to handle established connection
},
OnConnectionBroken = (webSocket) =>
{
// your code to handle broken connection
},
OnMessageReceived = (webSocket, result, data) =>
{
// your code to handle received message
}
};And this class has some methods for working on both side of client and server role:
void Connect(Uri uri, WebSocketOptions options, Action<ManagedWebSocket> onSuccess, Action<Exception> onFailure);
void StartListen(int port, X509Certificate2 certificate, Action onSuccess, Action<Exception> onFailure, Func<ManagedWebSocket, byte[]> getPingPayload, Func<ManagedWebSocket, byte[], byte[]> getPongPayload, Action<ManagedWebSocket, byte[]> onPong);
void StopListen();Use the Connect method to connect to a remote endpoint
Use the StartListen method to start the listener to listen incoming connection requests.
Use the StopListen method to stop the listener.
Enabling secure connections requires two things:
var websocket = new WebSocket
{
Certificate = new X509Certificate2("my-certificate.pfx")
// Certificate = new X509Certificate2("my-certificate.pfx", "cert-password", X509KeyStorageFlags.UserKeySet)
};
websocket.StartListen();Want to have a free SSL certificate? Take a look at Let's Encrypt.
Special: A simple tool named win-acme will help your IIS works with Let's Encrypt very well.
To enable negotiation of subprotocols, specify the supported protocols on SupportedSubProtocols property. The negotiated subprotocol will be available on the socket's SubProtocol.
If no supported subprotocols are found on the client request (Sec-WebSocket-Protocol), the listener will raises the SubProtocolNegotiationFailedException exception.
var websocket = new WebSocket
{
SupportedSubProtocols = new[] { "messenger", "chat" }
};
websocket.StartListen();The Nagle's Algorithm is disabled by default (to send a message immediately). If you want to enable the Nagle's Algorithm, set NoDelay to false
var websocket = new WebSocket
{
NoDelay = false
};
websocket.StartListen();When integrate this component with your app that hosted by ASP.NET / ASP.NET Core, you might want to use the WebSocket connections of ASP.NET / ASP.NET Core directly, then the method WrapAsync is here to help. This method will return a task that run a process for receiving messages from this WebSocket connection.
Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary<string, string> headers, Action<ManagedWebSocket> onSuccess);And might be you need an extension method to wrap an existing WebSocket connection, then take a look at some lines of code below:
ASP.NET
public static Task WrapAsync(this net.vieapps.Components.WebSockets.WebSocket websocket, AspNetWebSocketContext context)
{
var serviceProvider = (IServiceProvider)HttpContext.Current;
var httpWorker = serviceProvider?.GetService<HttpWorkerRequest>();
var remoteAddress = httpWorker == null ? context.UserHostAddress : httpWorker.GetRemoteAddress();
var remotePort = httpWorker == null ? 0 : httpWorker.GetRemotePort();
var remoteEndpoint = IPAddress.TryParse(remoteAddress, out IPAddress ipAddress)
? new IPEndPoint(ipAddress, remotePort > 0 ? remotePort : context.RequestUri.Port) as EndPoint
: new DnsEndPoint(context.UserHostName, remotePort > 0 ? remotePort : context.RequestUri.Port) as EndPoint;
var localAddress = httpWorker == null ? context.RequestUri.Host : httpWorker.GetLocalAddress();
var localPort = httpWorker == null ? 0 : httpWorker.GetLocalPort();
var localEndpoint = IPAddress.TryParse(localAddress, out ipAddress)
? new IPEndPoint(ipAddress, localPort > 0 ? localPort : context.RequestUri.Port) as EndPoint
: new DnsEndPoint(context.RequestUri.Host, localPort > 0 ? localPort : context.RequestUri.Port) as EndPoint;
return websocket.WrapAsync(context.WebSocket, context.RequestUri, remoteEndpoint, localEndpoint);
}ASP.NET Core
public static async Task WrapAsync(this net.vieapps.Components.WebSockets.WebSocket websocket, HttpContext context)
{
if (context.WebSockets.IsWebSocketRequest)
{
var webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
var requestUri = new Uri($"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.PathBase}{context.Request.QueryString}");
var remoteEndPoint = new IPEndPoint(context.Connection.RemoteIpAddress, context.Connection.RemotePort);
var localEndPoint = new IPEndPoint(context.Connection.LocalIpAddress, context.Connection.LocalPort);
await websocket.WrapAsync(webSocket, requestUri, remoteEndPoint, localEndPoint).ConfigureAwait(false);
}
}While working with ASP.NET Core, we think that you need a middle-ware to handle all request of WebSocket connections, just look like this:
public class WebSocketMiddleware
{
readonly RequestDelegate _next;
net.vieapps.Components.WebSockets.WebSocket _websocket;
public WebSocketMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
{
var logger = loggerFactory.CreateLogger<WebSocketMiddleware>();
this._websocket = new net.vieapps.Components.WebSockets.WebSocket(loggerFactory)
{
OnError = (websocket, exception) =>
{
logger.LogError(exception, $"Got an error: {websocket?.ID} @ {websocket?.RemoteEndPoint} => {exception.Message}");
},
OnConnectionEstablished = (websocket) =>
{
logger.LogDebug($"Connection is established: {websocket.ID} @ {websocket.RemoteEndPoint}");
},
OnConnectionBroken = (websocket) =>
{
logger.LogDebug($"Connection is broken: {websocket.ID} @ {websocket.RemoteEndPoint}");
},
OnMessageReceived = (websocket, result, data) =>
{
var message = result.MessageType == System.Net.WebSockets.WebSocketMessageType.Text ? data.GetString() : "(binary message)";
logger.LogDebug($"Got a message: {websocket.ID} @ {websocket.RemoteEndPoint} => {message}");
}
};
this._next = next;
}
public async Task Invoke(HttpContext context)
{
await this._websocket.WrapAsync(context).ConfigureAwait(false);
await this._next.Invoke(context).ConfigureAwait(false);
}
}And remember to tell APS.NET Core uses your middleware (at Configure method of Startup.cs)
app.UseWebSockets();
app.UseMiddleware<WebSocketMiddleware>();Messages are received automatically via parallel tasks, and you only need to assign OnMessageReceived event for handling its.
Sending messages are the same as ManagedWebSocket, with a little different: the first argument - you need to specify a WebSocket connection (by an identity) for sending your messages.
Task SendAsync(Guid id, ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken);
Task SendAsync(Guid id, string message, bool endOfMessage, CancellationToken cancellationToken);
Task SendAsync(Guid id, byte[] message, bool endOfMessage, CancellationToken cancellationToken);
Task SendAsync(Func<ManagedWebSocket, bool> predicate, ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken);
Task SendAsync(Func<ManagedWebSocket, bool> predicate, string message, bool endOfMessage, CancellationToken cancellationToken);
Task SendAsync(Func<ManagedWebSocket, bool> predicate, byte[] message, bool endOfMessage, CancellationToken cancellationToken);Take a look at some methods GetWebSocket... to work with all connections.
ManagedWebSocket GetWebSocket(Guid id);
IEnumerable<ManagedWebSocket> GetWebSockets(Func<ManagedWebSocket, bool> predicate);
bool CloseWebSocket(Guid id, WebSocketCloseStatus closeStatus, string closeStatusDescription);
bool CloseWebSocket(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus, string closeStatusDescription);Our prefers:
using net.vieapps.Components.Utility;
using net.vieapps.Components.WebSockets;While working directly with this component, performance is not your problem, but when you wrap WebSocket connections of ASP.NET or ASP.NET Core (with IIS Integration), may be you reach max 5,000 concurrent connections (because IIS allows 5,000 CCU by default).
ASP.NET and IIS scale very well, but you'll need to change a few settings to set up your server for lots of concurrent connections, as opposed to lots of requests per second.
Increase the number of concurrent requests IIS will serve at once:
Example:
appcmd.exe set config /section:system.webserver/serverRuntime /appConcurrentRequestLimit:100000
By default ASP.NET 4.0 sets the maximum concurrent connections to 5000 per CPU. If you need more concurrent connections then you need to increase the maxConcurrentRequestsPerCPU setting.
Open %windir%\Microsoft.NET\Framework\v4.0.30319\aspnet.config (Framework64 for 64 bit processes)
Copy from the sample below (ensure case is correct!)
Example:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<runtime>
<legacyUnhandledExceptionPolicy enabled="false" />
<legacyImpersonationPolicy enabled="true"/>
<alwaysFlowImpersonationPolicy enabled="false"/>
<SymbolReadingPolicy enabled="1" />
<shadowCopyVerifyByTimestamp enabled="true"/>
</runtime>
<startup useLegacyV2RuntimeActivationPolicy="true" />
<system.web>
<applicationPool maxConcurrentRequestsPerCPU="20000" />
</system.web>
</configuration>When the total amount of connections exceed the maxConcurrentRequestsPerCPU setting (i.e. maxConcurrentRequestsPerCPU * number of logical processors), ASP.NET will start throttling requests using a queue. To control the size of the queue, you can tweak the requestQueueLimit.
Example:
<processModel autoConfig="false" requestQueueLimit="250000" />The following performance counters may be useful to watch while conducting concurrency testing and adjusting the settings detailed above:
Memory
ASP.NET
CPU
TCP/IP
Web Service
Threading