Web sockets were invented to push data from web server to clients. So, one would expect that sending data to a web socket would look something like this:
void OnNewDataReady(string data) { webSocket.SendAsync(data); // does not work as expected }
Unfortunately, this does not work, because in .NET and .NET Core, web sockets only support one outstanding write operation at a time. Or, as the documentation puts it, “exactly one send and one receive is supported on each WebSocket object in parallel“.
To achieve simple data pushing, one needs an intermediate object that would queue incoming data items and push them to the socket one by one in the background.
WebSocketDemo Project
WebSocketDemo is a sample that implements pushing data to a web socket in ASP.NET and ASP.NET Core, and reading it in the browser via JQuery client and Angular 8 client.
I created this sample, because in the Echo sample found in Microsoft documentation the communication is client-driven. The server is not pushing data to the client as it becomes available, but listens to the incoming client messages and echoes them back. This kind of functionality can be accomplished by standard HTTP.
SignalR Stock Ticker Sample does push data to the web socket, but it requires SignalR. I wanted to create a sample that pushes unsolicited data to the client without SignalR.
WebSocketSender Class
WebSocketSender class implements easy data pushing to the web socket. Data items are scheduled for sending via by QueueSend()
method, and then WebSocketSender
writes them to the web socket in order, waking up either when the web socket finishes the write, or when the web socket is idle and more data becomes available.
It also monitors the web socket state and allows either the client or the server to close the socket gracefully.
dataSource.DataReady += OnDataReady; var sender = new WebSocketSender(webSocket); await sender.HandleCommunicationAsync(); // this task finishes when the socket is closed ... void OnDataReady(string data) { sender.QueueSend(data); }
Helper class AsyncQueue implements task-based queue, where adding data is synchronous, and retrieving data is an awaitable task.
The communication diagram on the server side looks as follows:
Using WebSocketSender in ASP.NET Core
Upon accepting web socket request, create an instance of WebSocketSender
, sign up to the data source and await the task returned by HandleCommunicationAsync()
method.
public class TimeController : ControllerBase { ... public async Task Index() { var context = ControllerContext.HttpContext; if (context.WebSockets.IsWebSocketRequest) { await ProcessRequest(context.WebSockets); return null; // by this time the socket is closed, it does not matter w } ... // handle normal HTTP response } private async Task ProcessRequest(WebSocketManager wsManager) { using (var ws = await wsManager.AcceptWebSocketAsync()) { var sender = new WebSocketSender(ws); void OnDataReady(string data) { sender.QueueSend(data); if (TimeToClose()) sender.CloseAsync(); } _externalDataSource.DataReady += OnDataReady; try { await sender.HandleCommunicationAsync(); } finally { _externalDataSource.DataReady -= OnDataReady; } } } }
Using WebSocketSender in ASP.NET
This is similar to ASP.NET Core, the main difference is in how one obtains the web socket object.
public class TimeController : ApiController { ... [HttpGet] public HttpResponseMessage GetResponse() { var context = HttpContext.Current; if (context.IsWebSocketRequest) { context.AcceptWebSocketRequest(ProcessRequest); return new HttpResponseMessage(HttpStatusCode.SwitchingProtocols); } ... // handle normal HTTP response } private static async Task ProcessRequest(AspNetWebSocketContext context) { var ws = context.WebSocket; var sender = new WebSocketSender(ws); void OnDataReady(string data) { sender.QueueSend(data); if (TimeToClose()) sender.CloseAsync(); } _externalDataSource.DataReady += OnDataReady; try { await sender.HandleCommunicationAsync(); } finally { _externalDataSource.DataReady -= OnDataReady; } } }
Running the sample
ASP.NET Server
Open server\aspnet\WebSocketsTest.sln
in Visual Studio and hit “Run”. This automatically opens a JQuery client implemented in index.html
. HTTP request to api/time
returns current time as HTTP response. Web socket request to api/time
pushes current time every second via web socket. Optional parameter ticks
specifies how many messages to send, e.g. api/time?ticks=3
will send three messages and then close the socket. If ticks
is not specified, the server will send the messages indefinitely.
ASP.NET Core Server
Open server\aspnetcore\WebSocketsSrv.sln
in Visual Studio and hit “Run”. .NET Core SDK 2.1 must be installed. The JQUery client is the same as for ASP.NET version.
Angular 8 Client
Open client\angular\web-socket-app
in Visual Studio code. Perform npm install
and then ng serve
. Specify the URL to connect to in the text boes. This client can connect to either ASP.NET or ASP.NET Core server, the communication protocols are identical. It is functionally equivalent to the JQuery client.
Closing the Web Socket
WebSocketSender does not handle any data received from the client, but it still needs to call _webSocket.ReceiveAsync()
, or the state of the web socket object on the server does not change when the client closes it.
To close the socket from the server side, one needs to call _webSocket.CloseOutputAsync()
. Trying to close the socket completely via CloseAsync()
causes an InvalidOperationException
“a receiver operation is already in progress”
CORS
In order to enable Angular client served by ng serve
, I could either proxy it, so it would be on the same host/port as the ASP.NET [Core] server, or I could enable CORS. I chose the latter, since it seemed more interesting. Note, that web sockets by default are not protected by CORS and can be opened by any client. This, of course, may lead to interesting cross-scripting attacks and must be carefully watched to prevent security breaches.
In Conclusion
Web sockets are great technology, but in order to achieve the main use case they were allegedly designed for, I had to write significant amount of non-trivial boiler plate code. This was unexpected and dissapointing. Nothing like that is required on the client side in the browser: one just reads and writes data without any concern to the number of pending operations. WebSocketSender is just a sample, and it does not handle reads, but hopefully it can serve as a better starting point then the “echo” sample from the documentation. Enjoy!
Andriy Kravets is writer and experience .NET developer and like .NET for regular development. He likes to build cross-platform libraries/software with .NET.