Considerations for a WebSocket Library

Tags: websocket

I recently release version 2.0 of the WebSocket# library which changes the API significantly from the original websocket library (WebSocket#) which I cloned.

So what is the difference? First of all that all handling of incoming messages is asynchronous. This is not a big thing these days. The bigger change is that the API has changed from raising an event when a message arrives (or a socket is opened/closed/errors) to invoking a delegate. Why this change? In order to avoid having multiple subscribers to the same incoming message.

Why is this important? It's about how best to support streaming in the library. In my previous post about adding streaming support I quote the RFC which is quite explicit about the need to handle infinite streaming. If you consider an infinite stream (or a 2^63 bytes long stream, which is roughly 9 exabytes) then you have to consider memory usage. Having multiple consumers means that you cannot let them jointly read the incoming message fragments off the incoming connection, since that would corrupt the message. This means that you will have to read it and copy it to all subscribers which adds overhead. By changing the handling of incoming messages to actually handling incoming fragments, you can allow the single subscriber to read the sequence of incoming fragments as a single stream. If the subscriber intends to copy the stream, then let them do so, but make sure that it's intentional.

Side note: If you are buffering the complete websocket message off the network and passing that to subscribers you are doing it wrong! The RFC is very clear about not requiring buffering of incoming messages.

The change to the API means that the WebSocket class now takes delegates (Func<T, Task>) to handle incoming messages (and for consistency open/closed/errors events). The OnMessage delegate gets a MessageEventArgs which exposes both a Stream (for reading incoming binary data) and a StreamReader (for reading incoming text). In this way the consumer can safely read from an encapsulation of the underlying stream while preserving the stream nature of the websocket.

The following code shows how to connect a websocket client.

using System;
using WebSocketSharp;

namespace Example
{
	public class Program
	{
		public static void Main (string[] args)
		{
			using (var ws = new WebSocket("ws://echo.websocket.org") { OnError = PrintMessage })
			{
				ws.Connect ();
				ws.Send("Something");
				Console.ReadKey (true);
			}
		}
		
		private Task PrintMessage(MessageEventArgs e)
		{
			e.RawData.CopyToAsync(Console.OpenStandardInput())
		}
	}
}

Another thing to consider when dealing with streams is back pressure. This occurs when input arrives quicker than it can be processed. Allowing only a single consumer means that the risk of back pressure can be reduced because subscribers don't have to wait for each other to process the stream. As I mention above buffering is a no go, so the option of letting a slow subscriber fall behind is not an option.

As usual, if you want to raise issues with the library, or contribute, please do so at the project site.

Latest Tweets