Websockets with Fleck

I’m going to create a .NET core console application to demonstrate using Fleck.

So create yourself a project and add the Fleck nuget package or simply add the Fleck nuget package to an existing project.

Creating a websocket server

To begin with, we simply create a websocket server, supplying the “location”, i.e.

var websocketServer = new WebSocketServer("ws://0.0.0.0:8181");

Don’t forget the using Fleck; line at the start of your code

In this example we’re using 0.0.0.0 (the non-routable meta-address) along with the port 8181 and ofcourse we prefix this with the ws protocol.

Interacting with clients

Next up we’ll want to start the server and intercept various events/messages. Fleck uses a callback function/delegate style, so we simply supply our functions for each of the connection methods that we wish to intercept, for example

websocketServer.Start(connection =>
{
  connection.OnOpen = () => 
    Console.WriteLine("OnOpen");
  connection.OnClose = () => 
    Console.WriteLine("OnClose");
  connection.OnMessage = message => 
    Console.WriteLine($"OnMessage {message}");
  connection.OnBinary = bytes => 
    Console.WriteLine($"OnBinary {Encoding.UTF8.GetString(bytes)}");
  connection.OnError = exception => 
    Console.WriteLine($"OnError {exception.Message}");
  connection.OnPing = bytes => 
    Console.WriteLine("OnPing");
  connection.OnPong = bytes => 
    Console.WriteLine("OnPong");
});

Note: if we’re handling different state for different connections to the same url, it’s our responsibility to create our own form “session state”.

In this example, we’ve listed all the OnXXX actions that we can intercept.

Obviously OnOpen occurs when a new client connects to the server (on ws://0.0.0.0:8181) and OnClose occurs if the client closes the connection.

OnMessage is called when string messages are sent over the websocket, whereas OnBinary is, ofcourse, the binary equivalent (in the example above we’re assuming the bytes represent a string, obviously change this if you’re sending raw byte data).

OnError is called with an Exception for instances where exceptions occur (as you’ll have surmised).

OnPing is used when being pinged and like wise OnPong is used when receiving a pong – ping and pong are used as ways to, in essence, check if the client or server are still running. If supported, a server might send a ping to the connected clients then mark the clients as stopped (and hence dispose of any connections) if the server does not receive a pong within a specified timeout period. Obviously one of the biggest problems for any server that is maintaining some form of state is at what point they can assume the client is no longer around. Obviously if the client closes the connection the server can handle this, but what about if they just disconnect – this is where ping and pong come into play.

Obviously we also need to be able to send data to the client, hence we use the connection’s Send method. For example let’s change the OnMessage delegate to send an “Echo” of the message back to the client

connection.OnMessage = message =>
{
  Console.WriteLine($"OnMessage {message}");
  connection.Send($"Echo: {message}");
};

Writing a client to test our server

Let’s now create a simple console app to test our server code. This will use the System.Net.WebSockets ClientWebSocket class.

We will need to actually specify a network address for the client, so we’ll use the loopback 127.0.0.1.

Below are the contents of our client console application’s Main method

var websocketClient = new ClientWebSocket();
var cancellationToken = new CancellationTokenSource();

var connection = websocketClient.ConnectAsync(
  new Uri("ws://127.0.0.1:8181"), 
  cancellationToken.Token);

connection.ContinueWith(async tsk =>
{
  // sends a string/text message causes OnMessage to be called
  await websocketClient.SendAsync(
    new ArraySegment<byte>(Encoding.UTF8.GetBytes("Hello World")),
    WebSocketMessageType.Text,
    true,
    cancellationToken.Token);

  // receives a string/text from the server
  var buffer = new byte[128];
  await websocketClient.ReceiveAsync(
    new ArraySegment<byte>(buffer), cancellationToken.Token);
  Console.WriteLine(Encoding.UTF8.GetString(buffer));

  // sends a string/text message causes OnBinary to be called
  await websocketClient.SendAsync(
    new ArraySegment<byte>(Encoding.UTF8.GetBytes("Hello World")),
    WebSocketMessageType.Binary,
    true,
    cancellationToken.Token);
  });

  Console.WriteLine("Press <enter> to exit");
  Console.Read();

  // close the connection nicely
  websocketClient.CloseAsync(
     WebSocketCloseStatus.NormalClosure, 
     String.Empty, 
     cancellationToken.Token);

  // this will cause OnError on the server if not closed first
  cancellationToken.Cancel();

Hopefully it’s fairly self explanatory what’s going on – we create a websocket client and a cancellation token (as the methods all required one). We connect to the server and when a connection is established we send and receive data (strings and then binary). Eventually we close the connection.

What and ping and pong?

At this time I don’t have any examples to implement these.

In the case of the ClientWebSocket code, if you leave the client and server running you will periodically see OnPing being called.

I almost forgot…

We can also interact with the connection’s ConnectionInfo property which gives us access to headers, cookies and whether path was specified, i.e. the client url ws://127.0.0.1:8181/somepath will result in ConnectionInfo.Path having the value /somepath.

Here’s an example of the server changes for OnOpen

connection.OnOpen = () =>
{
  Console.WriteLine("OnOpen");
  Console.WriteLine(connection.ConnectionInfo.Path);
};

From what I can tell, each connection is assigned a GUID (found in ConnectionInfo.Id), so when handling multiple different clients with different state requirements we should be able to use this Id.

References

Fleck
Writing WebSocket servers