Writing a WebSocket server in C#

Introduction

If you would like to use the WebSocket API, it is useful if you have a server. In this article I will show you how to write one in C#. You can do it in any server-side language, but to keep things simple and more understandable, I chose Microsoft's language.

This server conforms to RFC 6455 so it will only handle connections from Chrome version 16, Firefox 11, IE 10 and over.

Handshaking

When a client connects to a server, it sends a GET request to upgrade the connection to a WebSocket from a simple HTTP request. This is known as handshaking.

This sample code can detect a GET from the client. Note that this will block until the first 3 bytes of a message are available. Alternative solutions should be investigated for production environments.

The response is easy to build, but might be a little bit difficult to understand. The full explanation of the Server handshake can be found in RFC 6455, section 4.2.2. For our purposes, we'll just build a simple response.

You must:

Obtain the value of the "Sec-WebSocket-Key" request header without any leading or trailing whitespace

Concatenate it with "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" (a special GUID specified by RFC 6455)

Compute SHA-1 and Base64 hash of the new value

Write the hash back as the value of "Sec-WebSocket-Accept" response header in an HTTP response

Decoding messages

After a successful handshake, the client will send encoded messages to the server.

If we send "MDN", we get these bytes:

129

131

61

84

35

6

112

16

109

Let's take a look at what these bytes mean.

The first byte, which currently has a value of 129, is a bitfield that breaks down as such:

FIN (Bit 0)

RSV1 (Bit 1)

RSV2 (Bit 2)

RSV3 (Bit 3)

Opcode (Bit 4:7)

1

0

0

0

0x1=0001

FIN bit: This bit indicates whether the full message has been sent from the client. Messages may be sent in frames, but for now we will keep things simple.

RSV1, RSV2, RSV3: These bits must be 0 unless an extension is negotiated which supplies a nonzero value to them.

Opcode: These bits describe the type of message received. Opcode 0x1 means this is a text message. Full list of Opcodes

The second byte, which currently has a value of 131, is another bitfield that breaks down as such:

MASK (Bit 0)

Payload Length (Bit 1:7)

1

0x83=0000011

MASK bit: Defines whether the "Payload data" is masked. If set to 1, a masking key is present in Masking-Key, and this is used to unmask the "Payload data". All messages from the client to the server have this bit set.

Payload Length: If this value is between 0 and 125, then it is the length of message. If it is 126, the following 2 bytes (16-bit unsigned integer) are the length. If it is 127, the following 8 bytes (64-bit unsigned integer) are the length.

Because the first bit is always 1 for client-to-server messages, you can subtract 128 from this byte to get rid of the MASK bit.

Note that the MASK bit is set in our message. This means that the next four bytes (61, 84, 35, and 6) are the mask bytes used to decode the message. These bytes change with every message.

The remaining bytes are the encoded message payload.

Decoding algorithm

Di = Ei XOR M(i mod 4)

where D is the decoded message array, E is the encoded message array, M is the mask byte array, and i is the index of the message byte to decode.