Skip to content

modulexcite/websocket-server

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WebSocket Server in c#

Set WebSockets.Cmd as the startup project

License

The MIT License (MIT)
See LICENCE.txt

Introduction

A lot of the Web Socket examples out there are for old Web Socket versions and included complicated code (and external libraries) for fall back communication. All modern browsers that anyone cares about (including safari on an iphone) support at least version 13 of the Web Socket protocol so I'd rather not complicate things. This is a bare bones implementation of the web socket protocol in C# with no external libraries involved. You can connect using standard HTML5 JavaScript.

This application serves up basic html pages as well as handling WebSocket connections. This may seem confusing but it allows you to send the client the html they need to make a web socket connection and also allows you to share the same port. However, the HttpConnection is very rudimentary. I'm sure it has some glaring security problems. It was just made to make this demo easier to run. Replace it with your own or don't use it.

Background

There is nothing magical about Web Sockets. The spec is easy to follow and there is no need to use special libraries. At one point, I was even considering somehow communicating with Node.js but that is not necessary. The spec can be a bit fiddly with bits and bytes but this was probably done to keep the overheads low. This is my first CodeProject article and I hope you will find it easy to follow. The following links offer some great advice:

Step by step guide

The official Web Socket spec

Some useful stuff in C#

Using the Code

A good place to put a breakpoint is in the WebServer class in the HandleAsyncConnection function. Note that this is a multithreaded server so you may want to freeze threads if this gets confusing. The console output prints the thread id to make things easier. If you want to skip past all the plumbing, then another good place to start is the Respond function in the WebSocketConnection class. If you are not interested in the inner workings of Web Sockets and just want to use them, then take a look at the OnTextFrame in the ChatWebSocketConnection class. See below.

Implementation of a chat web socket connection:

class ChatWebSocketConnection : WebSocketConnection
{
    public ChatWebSocketConnection(NetworkStream networkStream, TcpClient tcpClient, string header)
        : base(networkStream, header)
    {
        // send requests immediately if true 
        // (needed for small low latency packets but not a long stream). 
        // Basically, don't wait for buffer to be full
        tcpClient.NoDelay = true;
    }

    protected override void OnTextFrame(string text)
    {
        string response = "Server: " + text;
        base.Writer.WriteText(response);
        Trace.WriteLine(response);
    }
}

The factory used to create the connection is as follows:

class ConnectionFactory : IConnectionFactory
{
    public IConnection CreateInstance(ConnectionDetails connectionDetails)
    {
        switch (connectionDetails.ConnectionType)
        {
            case ConnectionType.WebSocket:
                // you can support different kinds of web socket connections using a different path
                if (connectionDetails.Path == "/chat")
                {
                    return new ChatWebSocketConnection(connectionDetails.NetworkStream, 
				connectionDetails.TcpClient, connectionDetails.Header);
                }
                break;
            case ConnectionType.Http:
                // this path actually refers to the relative location of some html file or image
                return new HttpConnection(connectionDetails.NetworkStream, connectionDetails.Path);
        }

        return new BadRequestConnection(connectionDetails.NetworkStream, connectionDetails.Header);
    }
}

HTML5 JavaScript used to connect:

// open the connection to the Web Socket server
var CONNECTION = new WebSocket('ws://localhost/chat');

// Log messages from the server
CONNECTION.onmessage = function (e) {
    console.log(e.data);
};
        
CONNECTION.send('Hellow World');

Starting the server:

static void Main(string[] args)
{
    // used to decide what to do with incoming connections
    ConnectionFactory connectionFactory = new ConnectionFactory();

    using (WebServer server = new WebServer(connectionFactory))
    {
        // listen on port 80
        server.Listen(80);
        Console.ReadKey();
    }
}

Web Socket Protocol

The first thing to realize about the protocol is that it is, in essence, a basic duplex TCP/IP socket connection. The connection starts off with the client connecting to a remote server and sending http header text to that server. The header text asks the web server to upgrade the connection to a web socket connection. This is done as a handshake where the web server responds with an appropriate http text header and from then onwards, the client and server will talk the Web Socket language.

Handshake

Regex webSocketKeyRegex = new Regex("Sec-WebSocket-Key: (.*)");
Regex webSocketVersionRegex = new Regex("Sec-WebSocket-Version: (.*)");

// check the version. Support version 13 and above
const int WebSocketVersion = 13;
int secWebSocketVersion = Convert.ToInt32(webSocketVersionRegex.Match(header).Groups[1].Value.Trim());
if (secWebSocketVersion < WebSocketVersion)
{
    throw new WebSocketVersionNotSupportedException(string.Format
    ("WebSocket Version {0} not supported. Must be {1} or above", secWebSocketVersion, WebSocketVersion));
}

string secWebSocketKey = webSocketKeyRegex.Match(header).Groups[1].Value.Trim();
string setWebSocketAccept = WebSocketHandshakeHelper.ComputeSocketAcceptString(secWebSocketKey);
string response = ("HTTP/1.1 101 Switching Protocols" + Environment.NewLine
                           + "Connection: Upgrade" + Environment.NewLine
                           + "Upgrade: websocket" + Environment.NewLine
                           + "Sec-WebSocket-Accept: " + setWebSocketAccept);

HttpHelper.WriteHttpHeader(response, networkStream);

This computes the accept string:

/// <summary>
/// Combines the key supplied by the client with a guid and returns the sha1 hash of the combination
/// </summary>
public static string ComputeSocketAcceptString(string secWebSocketKey)
{
    // this is a guid as per the web socket spec
    const string webSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

    string concatenated = secWebSocketKey + webSocketGuid;
    byte[] concatenatedAsBytes = Encoding.UTF8.GetBytes(concatenated);
    byte[] sha1Hash = SHA1.Create().ComputeHash(concatenatedAsBytes);
    string secWebSocketAccept = Convert.ToBase64String(sha1Hash);
    return secWebSocketAccept;
}

Reading and Writing

After the handshake as been performed, the server goes into a read loop. The following class reads Web Socket frames from the network stream:

public class WebSocketFrameReader
{
    public WebSocketFrame Read(NetworkStream stream)
    {
        // read the first byte. If the connection has been terminated abnormally 
	// then this would return an FF and we should exit
        byte byte1 = (byte)stream.ReadByte();

        // this condition will happen if the connection has terminated unexpectedly
        if (!stream.DataAvailable && byte1 == 0xFF)
        {
            return new WebSocketFrame(true, WebSocketOpCode.ConnectionClose, new byte[0], false);
        }

        // process first byte
        byte finBitFlag = 0x80;
        byte opCodeFlag = 0x0F;
        bool isFinBitSet = (byte1 & finBitFlag) == finBitFlag;
        WebSocketOpCode opCode = (WebSocketOpCode)(byte1 & opCodeFlag);

        // read and process second byte
        byte byte2 = (byte)stream.ReadByte();
        byte maskFlag = 0x80;
        bool isMaskBitSet = (byte2 & maskFlag) == maskFlag;
        uint len = ReadLength(byte2, stream);
        byte[] decodedPayload;

        // use the masking key to decode the data if needed
        if (isMaskBitSet)
        {
            const int maskKeyLen = 4;
            byte[] maskKey = BinaryReaderWriter.ReadExactly(maskKeyLen, stream);
            byte[] encodedPayload = BinaryReaderWriter.ReadExactly((int)len, stream);
            decodedPayload = new byte[len];

            // apply the mask key
            for (int i = 0; i < encodedPayload.Length; i++)
            {
                decodedPayload[i] = (Byte)(encodedPayload[i] ^ maskKey[i % maskKeyLen]);
            }
        }
        else
        {
            decodedPayload = BinaryReaderWriter.ReadExactly((int)len, stream);
        }

        WebSocketFrame frame = new WebSocketFrame(isFinBitSet, opCode, decodedPayload, true);
        return frame;
    }
    
    private static uint ReadLength(byte byte2, Stream stream)
    {
        byte payloadLenFlag = 0x7F;
        uint len = (uint)(byte2 & payloadLenFlag);

        // read a short length or a long length depending on the value of len
        if (len == 126)
        {
            byte[] lenBuffer = BinaryReaderWriter.ReadExactly(2, stream);
            len = (uint)BitConverter.ToUInt16(lenBuffer, 0);
        }
        else if (len == 127)
        {
            byte[] lenBuffer = BinaryReaderWriter.ReadExactly(8, stream);
            len = (uint)BitConverter.ToUInt64(lenBuffer, 0);
            const uint maxLen = 2147483648; // 2GB

            // protect ourselves against bad data
            if (len > maxLen || len < 0)
            {
                throw new ArgumentOutOfRangeException(string.Format
		("Payload length out of range. Min 0 max 2GB. Actual {0:#,##0} bytes.", len));
            }
        }

        return len;
    }    
}

If we want to write data to the network stream, we use the following class:

public class WebSocketFrameWriter
{
    private readonly Stream _stream;

    public WebSocketFrameWriter(Stream stream)
    {
        _stream = stream;
    }

    public void Write(WebSocketOpCode opCode, byte[] payload, bool isLastFrame)
    {
        // best to write everything to a memory stream before we push it onto the wire
        // not really necessary but I like it this way
        using (MemoryStream memoryStream = new MemoryStream())
        {
            byte finBitSetAsByte = isLastFrame ? (byte)0x80 : (byte)0x00;
            byte byte1 = (byte)(finBitSetAsByte | (byte)opCode);
            memoryStream.WriteByte(byte1);

            // NB, dont set the mask flag. No need to mask data from server to client
            // depending on the size of the length we want to write it as a byte, ushort or ulong
            if (payload.Length < 126)
            {
                byte byte2 = (byte)payload.Length;
                memoryStream.WriteByte(byte2);
            }
            else if (payload.Length <= ushort.MaxValue)
            {
                byte byte2 = 126;
                memoryStream.WriteByte(byte2);
                BinaryReaderWriter.WriteUShort((ushort)payload.Length, memoryStream);
            }
            else
            {
                byte byte2 = 127;
                memoryStream.WriteByte(byte2);
                BinaryReaderWriter.WriteULong((ulong)payload.Length, memoryStream);
            }

            memoryStream.Write(payload, 0, payload.Length);
            byte[] buffer = memoryStream.ToArray();
            _stream.Write(buffer, 0, buffer.Length);
        }            
    }

    public void Write(WebSocketOpCode opCode, byte[] payload)
    {
        Write(opCode, payload, true);
    }

    public void WriteText(string text)
    {
        byte[] responseBytes = Encoding.UTF8.GetBytes(text);
        Write(WebSocketOpCode.TextFrame, responseBytes);
    }    
}

Points of Interest

Problems with Proxy Servers:
Proxy servers which have not been configured to support Web sockets will not work well with them.
I suggest that you use transport layer security if you want this to work across the wider internet especially from within a corporation.

History

  • Version 1.0 WebSocket

About

WebSocket Server in c# - HTML5 Web Sockets

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C# 94.6%
  • HTML 5.2%
  • Batchfile 0.2%