As you may have noticed, Microsoft added WebSocket support to Internet Explorer 10 and to IIS8, both available on Windows Server 2012 and Windows 8. The usual WebSocket demo’s all assume a browser as the client but that’s not absolutely necessary: the protocol doesn’t exclude non-browser clients.

 

To support non-browser clients, .NET Framework 4.5 includes a new namespace and classes to support writing WebSocket clients: System.Net.WebSockets. I’ll demonstrate how to use these new classes by implementing a simple WebSocket chat server hosted in IIS8 that broadcasts every message it receives to all clients. The client will be a console application that uses classes from the new namespace. Because I was curious about what actually gets sent across the wire, I used Fiddler to intercept the communication between client and server.

 

Server

First of all: the server. I used an ASP.NET MVC4 ApiController for accepting WebSocket requests:

using System.Net;
using System.Net.Http;
using System.Web;
using System.Web.Http;
using Microsoft.Web.WebSockets;

namespace WebSockets.Controllers
{
  public class WebSocketsController : ApiController
  {
    public HttpResponseMessage Get(string name)
    {
      HttpContext.Current.AcceptWebSocketRequest(
            new ChatSocketHandler(name));
      return new HttpResponseMessage(
            HttpStatusCode.SwitchingProtocols);
    }

    private class ChatSocketHandler : WebSocketHandler
    {
      private static readonly WebSocketCollection Sockets =
            new WebSocketCollection();

      private readonly string _name;

      public ChatSocketHandler(string name)
      {
        _name = name;
      }

      public override void OnOpen()
      {
        Sockets.Add(this);
        Sockets.Broadcast(
              string.Format("{0} joined.", _name));
        Send(string.Format("Welcome {0}.", _name));
      }

      public override void OnMessage(string message)
      {
        Sockets.Broadcast(
              string.Format("{0} says: {1}", _name, message));
      }

      public override void OnClose()
      {
        Sockets.Remove(this);
        Sockets.Broadcast(
              string.Format("{0} left.", _name));
      }
    }
  }
}

The HttpContext class defines two overloads of AcceptWebSocketRequest that accept a Func<AspNetWebSocketContext, Task>. To make things easier, theMicrosoft.WebSockets NuGet package provides an extension method, also conveniently named AcceptWebSocketRequest and a base class WebSocketHandler that wrap the creation of the Func<AspNetWebSocketContext, Task> and provide some methods to override like OnOpen and OnMessage.

 

To enable WebSocket support on IIS8 on Windows Server 2012 you have to configure the IIS role and some associated features. The configuration I use can be seen in the two screenshots below.

 

Roles
Features

 

Client

The client is a simple console application that gives a user the opportunity to send messages to the server and simply shows all messages it receives. This code is a little longer so I’ll split it up. I’ll attach a zip file containing the entire VS2012 project. First of all: connecting to the WebSocket server:

var cts = new CancellationTokenSource();
var socket = new ClientWebSocket();
string wsUri = string.Format(
  "ws://rwwildenvs2012/WebSockets/api/websockets?name={0}", name);
await socket.ConnectAsync(new Uri(wsUri), cts.Token);

This code creates a new ClientWebSocket instance and uses the ConnectAsyncmethod to connect to the specifief URI. The ClientWebSocket does not expose any synchronous methods except Dispose and Abort. The URI has several components, described in the RFC. The name part of the query string is passed as the name parameter to the Get method of the WebSocketsController.

 

Next part is a receive loop where we receive messages from the server:

Task.Factory.StartNew(
  async () =>
  {
    var rcvBytes = new byte;
    var rcvBuffer = new ArraySegment<byte>(rcvBytes);
    while (true)
    {
      WebSocketReceiveResult rcvResult =
          await socket.ReceiveAsync(rcvBuffer, cts.Token);
      byte msgBytes = rcvBuffer
          .Skip(rcvBuffer.Offset)
          .Take(rcvResult.Count).ToArray();
      string rcvMsg = Encoding.UTF8.GetString(msgBytes);
      Console.WriteLine("Received: {0}", rcvMsg);
    }
  }, cts.Token, TaskCreationOptions.LongRunning,
     TaskScheduler.Default);

The core of this loop is the ReceiveAsync method that fills a byte buffer and gets the message from this buffer. In the real world this receive loop would be more complex. The data framing part of RFC 6455 allows for messages to be sent in multiple parts (WebSocketReceiveResult.EndOfMessage) and a message can be a closing message instead of a data message (WebSocketReceiveResult.CloseStatus).

 

Finally we’d like to send messages ourself so we build another loop that allows the user to enter messages to send:

while (true)
{
  var message = Console.ReadLine();
  if (message == "Bye")
  {
    cts.Cancel();
    return;
  }
  byte sendBytes = Encoding.UTF8.GetBytes(message);
  var sendBuffer = new ArraySegment<byte>(sendBytes);
  await socket.SendAsync(
      sendBuffer,
      WebSocketMessageType.Text,
      endOfMessage: true,
      cancellationToken: cts.Token);
}

The core part here is the SendAsync method. It accepts the message to send, themessage type (text, binary or close), whether this is the closing part of a message that was sent in multiple parts and a cancellation token. Since we only sent single-part messages in this example, the message we send is always the end of the message.

 

Result

Now, what would all of the above look like when run? Below you see a ‘conversation’ between three clients: Ronald, John and Adam.

 

Conversation

Ronald is the first to enter the conversation and the first to leave.

 

Conversation

John enters as the second participant.

 

Conversation

Adam is the third participant and the last one to leave.

 

On the wire

Every WebSocket conversation starts with a HTTP handshake that is defined in RFC 6455. When the handshake is complete, client and server have established a TCP connection that each one can use to send messages on. In Fiddler, the handshake looks like this:

 

WebSocket handshake

 

Some interesting things to note in the request:

  • The Sec-Websocket-Key HTTP header field. This is a new HTTP header field for the WebSocket protocol and its value is a base64-encoded random 16 byte nonce.
  • The Sec-Websocket-Version HTTP header field that is (for now) required to have the value 13.
  • The Upgrade HTTP header field. This is the field that informs the server that a client wishes to establish a WebSocket connection.

And in the response:

  • The 101 Switching Protocols response that is required by the protocol. If a 101 is not sent, it means that the handshake has not yet completed and that HTTP semantics still apply. For example, the server may sent a 3xx-redirect response.
  • The Sec-WebSocket-Accept HTTP header field. This is the server’s response to the client’s Sec-Websocket-Key header. It is constructed by taking the client key, appending the string “258EAFA5-E914-47DA-95CA-C5AB0DC85B11” to it, taking the SHA-1 hash of the result and base64-encoding the hash. The client must validate that it has the correct value (see section 4.2.2 of the spec, bullet 5.4).

 

Once the handshake is completed, client and server have a bidirectional communication channel that is used for data and control messages. Below is a small Fiddler trace of the previous conversation, starting from the moment John joined.

   1: 19:28:31:3477  Server->Client (14 bytes)
   2: TYPE: TEXT.
   3: MESSAGE: John joined.
   4: FLAGS: 10000001 DATA: 12 bytes.
   5: ------------------------------
   6: 19:28:31:3497 Upgrading Session #183 to websocket
   7: 19:28:31:3677  Server->Client (29 bytes)
   8: TYPE: TEXT.
   9: MESSAGE: John joined.
  10: Welcome John.
  11: FLAGS: 10000001 DATA: 12 bytes.
  12: ------------------------------
  13: 19:28:40:4195 Upgrading Session #185 to websocket
  14: 19:28:40:4215  Server->Client (14 bytes)
  15: TYPE: TEXT.
  16: MESSAGE: Adam joined.
  17: FLAGS: 10000001 DATA: 12 bytes.
  18: ------------------------------
  19: 19:28:40:4245  Server->Client (14 bytes)
  20: TYPE: TEXT.
  21: MESSAGE: Adam joined.
  22: FLAGS: 10000001 DATA: 12 bytes.
  23: ------------------------------
  24: 19:28:40:4275  Server->Client (29 bytes)
  25: TYPE: TEXT.
  26: MESSAGE: Adam joined.
  27: Welcome Adam.
  28: FLAGS: 10000001 DATA: 12 bytes.
  29: ------------------------------
  30: 19:28:46:5764  Client->Server (6 bytes)
  31: TYPE: PONG.
  32: MESSAGE:
  33: FLAGS: 10001010 DATA: 0 bytes, masked using KEY: 16-39-8F-63.
  34: ------------------------------

On lines 1 and 7, the server sends a message to its then known clients: Ronald and John. When Adam joins on line 13, three messages are sent (lines 14, 19 and 24). On line 31 you see a control message: PONG that is sent as a keep-alive mechanism. I haven’t seen any PING messages in the wild yet.

 

Conclusion

Web sockets are a really nice extension for web development and you aren’t limited to a browser client as the only way to communicate to a server that supports the WebSocket protocol. In .NET 4.5 you have a nice set of client classes that enable any application to talk to a WebSocket server so if you need this kind of functionality, you now know where to look.

The current client classes do not (yet) support the entire protocol. The protocol defines extensions that a client can request from a server via the Sec-WebSocket-Extensions HTTP header field. There is no support for that in the current implementation.

And there is still something I’d like to test with web sockets on IIS8: how many clients are supported? Every client requires some server resources so how does the server handle this. Maybe something to investigate for a next blog post.

For the attached solution to work, you have to run the NuGet package manager first because I didn’t include all packages in the zip file.

WebSockets.zip

 

ITQ

Let's talk!

Knowledge is key for our existence. This knowledge we use for disruptive innovation and changing organizations. Are you ready for change?

"*" indicates required fields

First name*
Last name*
Hidden