Click here to Skip to main content
13,295,953 members (53,019 online)
Click here to Skip to main content
Add your own
alternative version

Stats

132.6K views
7.4K downloads
178 bookmarked
Posted 13 Dec 2015

WebSocket Server in C#

, 8 Oct 2017
Rate this:
Please Sign up or sign in to vote.
Web Socket Client and Server implemented in C# for the modern version 13 of the WebSocket protocol

Set WebSocketsCmd as the startup project.

Introduction

NEW - Better logging for SSL errors

NEW - SSL support added. Secure your connections

NEW - C# client support added. Refactor for a better API

No external libraries. Completely standalone.

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 support at least version 13 of the Web Socket protocol so I'd rather not complicate things with backward compatibility support. 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 or the C# client.

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 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#:

Alternative C# implementations:

Using the Code

When you first run this app, you should get a Windows firewall warning popup message. Just accept the warning and add the automatic firewall rule. Whenever a new app listens on a port (which this app does), you will get this message and it is there to point out nefarious applications potentially sending and receiving unwanted data over your network. All the code is there for you to see so you can trust what's going on in this project.

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 is as follows:

internal class ChatWebSocketService : WebSocketService
{
    private readonly IWebSocketLogger _logger;

    public ChatWebSocketService(NetworkStream networkStream, 
                                TcpClient tcpClient, string header, IWebSocketLogger logger)
        : base(networkStream, tcpClient, header, true, logger)
    {
        _logger = logger;
    }

    protected override void OnTextFrame(string text)
    {
        string response = "ServerABC: " + text;
        base.Send(response);

        // limit the log message size
        string logMessage = response.Length > 100 ? response.Substring(0, 100) + "..." : response;
        _logger.Information(this.GetType(), logMessage);
    }
}

The factory used to create the connection is as follows:

internal class ServiceFactory : IServiceFactory
{
    public ServiceFactory(string webRoot, IWebSocketLogger logger)
    {
        _logger = logger;
        _webRoot = webRoot;
    }

    public IService 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 ChatWebSocketService(connectionDetails.NetworkStream, 
                       connectionDetails.TcpClient, connectionDetails.Header, _logger);
                }
                break;
            case ConnectionType.Http:
                // this path actually refers to the relative location of some html file or image
                return new HttpService(connectionDetails.NetworkStream, 
                                       connectionDetails.Path, _webRoot, _logger);
        }

        return new BadRequestService
                (connectionDetails.NetworkStream, connectionDetails.Header, _logger);
    }
}

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');

However, you can also write your own test client in C#. There is an example of one in the command line app. Starting the server and the test client from the command line app:

private static void Main(string[] args)
{
    IWebSocketLogger logger = new WebSocketLogger();
                
    try
    {
        string webRoot = Settings.Default.WebRoot;
        int port = Settings.Default.Port;

        // used to decide what to do with incoming connections
        ServiceFactory serviceFactory = new ServiceFactory(webRoot, logger);

        using (WebServer server = new WebServer(serviceFactory, logger))
        {
            server.Listen(port);
            Thread clientThread = new Thread(new ParameterizedThreadStart(TestClient));
            clientThread.IsBackground = false;
            clientThread.Start(logger);
            Console.ReadKey();
        }
    }
    catch (Exception ex)
    {
        logger.Error(null, ex);
        Console.ReadKey();
    }
}

The test client runs a short self test to make sure that everything is fine. Opening and closing handshakes are tested here.

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.

Server 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 = base.ComputeSocketAcceptString(secWebSocketKey);
string response = ("HTTP/1.1 101 Switching Protocols\r\n"
                    + "Connection: Upgrade\r\n"
                    + "Upgrade: websocket\r\n"
                    + "Sec-WebSocket-Accept: " + setWebSocketAccept);

HttpHelper.WriteHttpHeader(response, networkStream);

NOTE: Don't use Environment.Newline, use \r\n because the HTTP spec is looking for carriage return line feed (two specific ascii characters) and not whatever your environment deems to be equivalent.

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;
}

Client Handshake

Uri uri = _uri;
WebSocketFrameReader reader = new WebSocketFrameReader();
Random rand = new Random();
byte[] keyAsBytes = new byte[16];
rand.NextBytes(keyAsBytes);
string secWebSocketKey = Convert.ToBase64String(keyAsBytes);

string handshakeHttpRequestTemplate = "GET {0} HTTP/1.1\r\n" +
                                        "Host: {1}:{2}\r\n" +
                                        "Upgrade: websocket\r\n" +
                                        "Connection: Upgrade\r\n" +
                                        "Origin: http://{1}:{2}\r\n" +
                                        "Sec-WebSocket-Key: {3}\r\n" +
                                        "Sec-WebSocket-Version: 13\r\n\r\n";

string handshakeHttpRequest = string.Format(handshakeHttpRequestTemplate, uri.PathAndQuery, uri.Host, uri.Port, secWebSocketKey);
byte[] httpRequest = Encoding.UTF8.GetBytes(handshakeHttpRequest);
networkStream.Write(httpRequest, 0, httpRequest.Length);

Reading and Writing

After the handshake as been performed, the server goes into a read loop. The following two classes convert a stream of bytes to a web socket frame and visa versa: WebSocketFrameReader and WebSocketFrameWriter.

// from WebSocketFrameReader class
public WebSocketFrame Read(Stream stream, Socket socket)
{
    byte byte1;

    try
    {
        byte1 = (byte) stream.ReadByte();
    }
    catch (IOException)
    {
        if (socket.Connected)
        {
            throw;
        }
        else
        {
            return null;
        }
    }

    // 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[] payload;

    // use the masking key to decode the data if needed
    if (isMaskBitSet)
    {
        byte[] maskKey = BinaryReaderWriter.ReadExactly(WebSocketFrameCommon.MaskKeyLength, stream);
        payload = BinaryReaderWriter.ReadExactly((int) len, stream);

        // apply the mask key to the payload (which will be mutated)
        WebSocketFrameCommon.ToggleMask(maskKey, payload);
    }
    else
    {
        payload = BinaryReaderWriter.ReadExactly((int) len, stream);
    }

    WebSocketFrame frame = new WebSocketFrame(isFinBitSet, opCode, payload, true);
    return frame;
}
// from WebSocketFrameWriter class
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, set the mask flag if we are constructing a client frame
        byte maskBitSetAsByte = _isClient ? (byte)0x80 : (byte)0x00;

        // 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)(maskBitSetAsByte | (byte) payload.Length);
            memoryStream.WriteByte(byte2);
        }
        else if (payload.Length <= ushort.MaxValue)
        {
            byte byte2 = (byte)(maskBitSetAsByte | 126);
            memoryStream.WriteByte(byte2);
            BinaryReaderWriter.WriteUShort((ushort) payload.Length, memoryStream, false);
        }
        else
        {
            byte byte2 = (byte)(maskBitSetAsByte | 127);
            memoryStream.WriteByte(byte2);
            BinaryReaderWriter.WriteULong((ulong) payload.Length, memoryStream, false);
        }

        // if we are creating a client frame then we MUST mack the payload as per the spec
        if (_isClient)
        {
            byte[] maskKey = new byte[WebSocketFrameCommon.MaskKeyLength];
            _random.NextBytes(maskKey);
            memoryStream.Write(maskKey, 0, maskKey.Length);

            // mask the payload
            WebSocketFrameCommon.ToggleMask(maskKey, payload);
        }

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

NOTE: Client frames MUST contain masked payload data. This is done to prevent primitive proxy servers from caching the data, thinking that it is static HTML. Of course, using SSL gets around the proxy issue but the authors of the protocol chose to enforce it regardless. 

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 (using an SSL certificate) if you want this to work across the wider internet especially from within a corporation.

Using SSL - Secure Web Sockets

To enable ssl in the demo, you need to do these things:

  1. Get a valid signed certificate (usually a .pfx file)
  2. Fill in the CertificateFile and CertificatePassword settings in the application (or better still, modify the GetCertificate function to get your certificate more securely)
  3. Change the port to 443
  4. (for JavaScript client) Change the client.html file to use "wss"instead of "ws" in the web socket URL.
  5. (for command line client) Change the client URL to "wss" instead of "ws".

I suggest that you get the demo chat to work before you attempt to use a JavaScript client since there are many things that can go wrong and the demo exposes more logging information. If you are getting certificate errors (like name mismatch or expiries), then you can always disable the check by making the WebSocketClient.ValidateServerCertificate function always return true.

If you are having trouble creating a certificate, I strongly recommend using LetsEncrypt to get yourself a free certificate that is signed by a proper root authority. You can use this certificate on your localhost (but your browser will give you certificate warnings).

History

  • Version 1.01 - WebSocket
  • Version 1.02 - Fixed endianness bug with length field
  • Version 1.03 - Major refactor and added support for C# Client
  • Version 1.04 - SSL support added. You can now secure your websocket connections with an SSL certificate
  • Version 1.05 - Bug fixes

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Dave Haig
Software Developer (Senior)
United Kingdom United Kingdom
Works as a senior software developer in investment banking

You may also be interested in...

Pro

Comments and Discussions

 
GeneralMy vote of 5 Pin
Igor Ladnik18-Nov-17 5:09
professionalIgor Ladnik18-Nov-17 5:09 
QuestionMultiple connections? Pin
pjosip15-Nov-17 10:22
memberpjosip15-Nov-17 10:22 
AnswerRe: Multiple connections? Pin
Dave Haig12-Dec-17 13:13
professionalDave Haig12-Dec-17 13:13 
QuestionCrash after days running in EndAccept() Pin
Lars [Large] Werner3-Nov-17 0:05
memberLars [Large] Werner3-Nov-17 0:05 
AnswerRe: Crash after days running in EndAccept() Pin
Lars [Large] Werner5-Nov-17 13:39
professionalLars [Large] Werner5-Nov-17 13:39 
GeneralRe: Crash after days running in EndAccept() Pin
Dave Haig12-Dec-17 13:37
professionalDave Haig12-Dec-17 13:37 
AnswerRe: Crash after days running in EndAccept() Pin
Dave Haig12-Dec-17 13:33
professionalDave Haig12-Dec-17 13:33 
BugCrashing when sending a big text file Pin
visualbruno7-Oct-17 23:05
membervisualbruno7-Oct-17 23:05 
GeneralRe: Crashing when sending a big text file Pin
Dave Haig8-Oct-17 3:48
professionalDave Haig8-Oct-17 3:48 
QuestionGreat article Pin
pmcgoohan8-Sep-17 3:07
memberpmcgoohan8-Sep-17 3:07 
AnswerRe: Great article Pin
Dave Haig8-Oct-17 3:49
professionalDave Haig8-Oct-17 3:49 
QuestionSending messages to all connected clients Pin
Ragnar Wässman25-Jan-17 12:30
memberRagnar Wässman25-Jan-17 12:30 
AnswerRe: Sending messages to all connected clients Pin
Tigris de Suisse21-Sep-17 1:54
memberTigris de Suisse21-Sep-17 1:54 
GeneralRe: Sending messages to all connected clients Pin
Dave Haig3-Oct-17 16:02
professionalDave Haig3-Oct-17 16:02 
AnswerRe: Sending messages to all connected clients Pin
Lars [Large] Werner2-Nov-17 23:49
memberLars [Large] Werner2-Nov-17 23:49 
BugCert file code Pin
Member 968910030-Dec-16 1:25
memberMember 968910030-Dec-16 1:25 
GeneralRe: Cert file code Pin
Dave Haig8-Oct-17 4:29
professionalDave Haig8-Oct-17 4:29 
QuestionImplementing basic authentication as client app Pin
Tom Corbett Space Cadet29-Aug-16 8:31
professionalTom Corbett Space Cadet29-Aug-16 8:31 
AnswerRe: Implementing basic authentication as client app Pin
Dave Haig31-Aug-16 0:13
professionalDave Haig31-Aug-16 0:13 
QuestionWould this work in an MVC app on IIS? Pin
2374126-Aug-16 12:17
member2374126-Aug-16 12:17 
AnswerRe: Would this work in an MVC app on IIS? Pin
Dave Haig31-Aug-16 0:01
professionalDave Haig31-Aug-16 0:01 
QuestionSSL Issue Pin
Member 1268995217-Aug-16 11:11
memberMember 1268995217-Aug-16 11:11 
AnswerRe: SSL Issue Pin
Member 1269791922-Aug-16 13:22
memberMember 1269791922-Aug-16 13:22 
GeneralRe: SSL Issue Pin
Member 1269329625-Aug-16 1:51
memberMember 1269329625-Aug-16 1:51 
GeneralRe: SSL Issue Pin
Dave Haig25-Aug-16 15:53
professionalDave Haig25-Aug-16 15:53 
GeneralRe: SSL Issue Pin
Dave Haig25-Aug-16 15:51
professionalDave Haig25-Aug-16 15:51 
AnswerRe: SSL Issue Pin
Dave Haig25-Aug-16 15:48
professionalDave Haig25-Aug-16 15:48 
QuestionRelay web request Pin
MAW308-Aug-16 20:13
memberMAW308-Aug-16 20:13 
AnswerRe: Relay web request Pin
Dave Haig25-Aug-16 15:43
professionalDave Haig25-Aug-16 15:43 
QuestionHello Dave ( please explain ) Pin
clip135611-Jun-16 1:40
memberclip135611-Jun-16 1:40 
AnswerRe: Hello Dave ( please explain ) Pin
Dave Haig29-Jun-16 14:16
professionalDave Haig29-Jun-16 14:16 
QuestionC++ client Pin
Member 1113146728-May-16 2:37
memberMember 1113146728-May-16 2:37 
AnswerRe: C++ client Pin
Dave Haig6-Jun-16 5:19
professionalDave Haig6-Jun-16 5:19 
QuestionSend TextMessages longer then 126 characters to the WebSocketServer Pin
Wassink7-Jan-16 14:56
memberWassink7-Jan-16 14:56 
AnswerRe: Send TextMessages longer then 126 characters to the WebSocketServer Pin
Garth J Lancaster7-Jan-16 17:58
professionalGarth J Lancaster7-Jan-16 17:58 
GeneralRe: Send TextMessages longer then 126 characters to the WebSocketServer Pin
Dave Haig9-Jan-16 8:01
professionalDave Haig9-Jan-16 8:01 
AnswerRe: Send TextMessages longer then 126 characters to the WebSocketServer Pin
Dave Haig8-Jan-16 9:07
professionalDave Haig8-Jan-16 9:07 
PraiseRe: Send TextMessages longer then 126 characters to the WebSocketServer Pin
Wassink9-Jan-16 10:45
memberWassink9-Jan-16 10:45 
GeneralMy vote of 5 Pin
Jean-Pierre Bachmann14-Dec-15 4:23
professionalJean-Pierre Bachmann14-Dec-15 4:23 
GeneralRe: My vote of 5 Pin
Dave Haig19-Dec-15 12:11
professionalDave Haig19-Dec-15 12:11 
QuestionThe download link is not working. Pin
descartes13-Dec-15 8:29
memberdescartes13-Dec-15 8:29 
AnswerRe: The download link is not working. Pin
DaveSimple13-Dec-15 8:36
professionalDaveSimple13-Dec-15 8:36 
GeneralRe: The download link is not working. Pin
descartes13-Dec-15 8:44
memberdescartes13-Dec-15 8:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.171207.1 | Last Updated 8 Oct 2017
Article Copyright 2015 by Dave Haig
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid