'C# - Websocket - Sending Message Back To Client

I've been working on a C# Web Socket server for roughly 24 hours.

I've currently figured out how to complete a handshake and get the connection initialized.

Also I've figured out how to take the byte[] data and decode it into a it's original string.

But now I'm stuck and looking for help.

I can't seem to figure out how to put together a proper data structure and send it back to the client. If you send the original data you received the WebSocket on the client-side will tell you the data cannot be masked (which is why it needs to be decoded).

So basically, what I'm asking in short is how do I structure response data to send back to the WebSocket client?

I've been using https://www.rfc-editor.org/rfc/rfc6455 as a resource for my research.

Please keep in mind that I'm just using a regular socket for this.

Here is my decode code:

if (dataBuffer.Length > 0)
{
    if (dataBuffer[0] == 129)
    {
        int msg_length = dataBuffer[1] - 128;
        if (msg_length <= 125)
        {
            // Msg ready to decode.
            Log.info("Message Length: " + msg_length);


            Byte[] decoded = new Byte[dataBuffer.Length];
            Byte[] encoded = new Byte[dataBuffer.Length - 6];

            Array.Copy(dataBuffer, 6, encoded, 0, msg_length);

            Byte[] key = new Byte[4] { dataBuffer[2], dataBuffer[3], dataBuffer[4], dataBuffer[5] };
            for (int i = 0; i < encoded.Length; i++)
            {
                decoded[i] = (Byte)(encoded[i] ^ key[i % 4]);
            }

            Log.info("MSG: " + Encoding.UTF8.GetString(decoded));

            byte[] return_msg = new byte[decoded.Length + 8];

            return_msg[0] = 1;
            return_msg[1] = 0;
            return_msg[2] = 0;
            return_msg[3] = 0;
            // OP Code
            return_msg[4] = 0x1;
            return_msg[5] = 0x0;
            return_msg[6] = 0x0;
            return_msg[7] = 0x0;

            Array.Copy(decoded, 0, return_msg, 8, decoded.Length);

            socket.Send(return_msg);
        }
        else if (msg_length == 126)
        {
            // Longer Message
            msg_length = dataBuffer[2] + dataBuffer[3];

            Log.info("Message Length: " + msg_length);

            Byte[] key = new Byte[4] { dataBuffer[4], dataBuffer[5], dataBuffer[6], dataBuffer[7] };

            Byte[] decoded = new Byte[dataBuffer.Length];
            Byte[] encoded = new Byte[dataBuffer.Length - 8];

            Array.Copy(dataBuffer, 8, encoded, 0, msg_length);

            for (int i = 0; i < encoded.Length; i++)
            {
                decoded[i] = (Byte)(encoded[i] ^ key[i % 4]);
            }

            Log.info("MSG: " + Encoding.UTF8.GetString(decoded));
            byte[] return_msg = new byte[decoded.Length + 4];

            return_msg[0] = 129;
            return_msg[1] = 0;
            return_msg[2] = 0;
            return_msg[3] = 0;
                    
            Array.Copy(decoded,0,return_msg,4,decoded.Length);

            socket.Send(return_msg);
        }
        else if (msg_length == 127)
        {
            // Huge Message:
            Log.info("BIG MESSAGE");
        }
    }

}


Solution 1:[1]

Take a look to this two articles about writing C# WebSocket servers:

https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_servers

https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_server

It seems like they are both the same article, but they are not! In this part of the first link you have the explanation about how to build frames.

UPDATE

The first byte contains several information:

  • FIN :Indicates that the frame contains a complete message.
  • RSV1: Option 1
  • RSV2: Option 2
  • RSV3: Option 3
  • OptCode: Indicates the type of frame.

If you want to send a Text message smaller than 125 bytes, lets say your message has 90 bytes, in the first byte you will put the bit 0 to 1(more significant), the next 3 to 0, unless you want to enable options, and the next 4 would be 0001, indicating a Text frame. So your first bye would be 10000001, or 129.

Now in the second byte, the first bit indicates if the frame is masked. You don't mask frames from server to client, so you set a 0. The next 7 bits indicates rather the length, or the type of frame length. Because you are sending a small frame, you can indicate any value up to 125 in those 7 bits. So because the frame length is 90 bytes, the second byte of the header would be 01011010, or 90.

So when sending from server to client a Text frame of 90 byes, the first two bytes conforming the header would be 129 and 90. The rest of the message would be 90 bytes of UTF8 encoded bytes.

If the frame is longer than 125 bytes, also is the length of the header, check the spec. If the frame need to be masked (like the frames you get from the client), the first 4 bytes of the body contain the masking key. As you see there are some tidbits to cope with, so I recommend you to read the spec : https://www.rfc-editor.org/rfc/rfc6455

Solution 2:[2]

@vtortola Thank you for posting the links and explanation - I spent a bunch of time studying that and an open source code base (to write my own essentially) and I distilled it down to this for sending a message from the server to the client.

The key, for me, was realizing a couple of things:

First, understand the header. GetHeader() takes care of whether or not its the final frame and whether or not the opcode is set to text of continuation frame. The link @vtortola posted explains it, but I had to really stare at it before I saw the bits:

This post actually explains it decently, but you have to take time and study it - take note of how the FIN and opcode bits match GetHeader()'s job:

  • Client: FIN=1, opcode=0x1, msg="hello" <-- this is a single message, length <= 125
  • Server: (process complete message immediately) Hi.
  • Client: FIN=0, opcode=0x1, msg="and a" <-- begin multi-frame message, length > 125
  • Server: (listening, new message containing text started)
  • Client: FIN=0, opcode=0x0, msg="happy new" <-- continue multi-frame message
  • Server: (listening, payload concatenated to previous message)
  • Client: FIN=1, opcode=0x0, msg="year!" <-- end multi-frame message
  • Server: (process complete message) Happy new year to you too!

Next, understand what you're sending when you call stream.Write() - bytes[], index, LENGTH OF BYTES YOU'RE SENDING ;)


NOTE: My intent was to send JSON formatted strings to and from the web client, so my opcode is set for text (and my example here is based on the assumption you're wanting to send string data), but you can send other types as well.

SendMessageToClient() basically splits the message into 125 chunks and creates a que to be pulled from. Based on where we're at in the que, the header is created with the proper flags for FIN and opcode. Finally, with the header ready, backfill the rest of the header with the actual length of the chunk of string (<= 125). Then write the header to the stream after converting it to a byte array.

At this point, your header is created properly (FIN, rsv1,2,3 set properly, opcode and mask as well as the size of the payload). Now send it.

public void SendMessageToClient(TcpClient client, string msg)
{
    NetworkStream stream = client.GetStream();
    Queue<string> que = new Queue<string>(msg.SplitInGroups(125));
    int len = que.Count;

    while (que.Count > 0)
    {
        var header = GetHeader(
            que.Count > 1 ? false : true,
            que.Count == len ? false : true
        );

        byte[] list = Encoding.UTF8.GetBytes(que.Dequeue());
        header = (header << 7) + list.Length;
        stream.Write(IntToByteArray((ushort)header), 0, 2);
        stream.Write(list, 0, list.Length);
    }            
}


protected int GetHeader(bool finalFrame, bool contFrame)
{
    int header = finalFrame ? 1 : 0;//fin: 0 = more frames, 1 = final frame
    header = (header << 1) + 0;//rsv1
    header = (header << 1) + 0;//rsv2
    header = (header << 1) + 0;//rsv3
    header = (header << 4) + (contFrame ? 0 : 1);//opcode : 0 = continuation frame, 1 = text
    header = (header << 1) + 0;//mask: server -> client = no mask

    return header;
}


protected byte[] IntToByteArray(ushort value)
{
    var ary = BitConverter.GetBytes(value);
    if (BitConverter.IsLittleEndian)
    {
        Array.Reverse(ary);
    }

    return ary;
}

/// ================= [ extension class ]==============>

public static class XLExtensions
{
    public static IEnumerable<string> SplitInGroups(this string original, int size)
    {
        var p = 0;
        var l = original.Length;
        while (l - p > size)
        {
            yield return original.Substring(p, size);
            p += size;
        }
        yield return original.Substring(p);
    }
}

Along with the post above, I studied websocket-sharp's code base. I literally wanted to learn how to do this and write my own server/client and was able to do so studying that code base as well as a great starting point here for creating a basic c# WebSocket server.

Finally, I thank God for the patience to read through all of this ;)

Solution 3:[3]

Here is how to encode the message for length < 125 AND for 126 and less then 65535, Just pass the string to the method, and it will return array of byte[]:

    private byte[] encodeMessage(string msg)
    {
        int msgLength = msg.Length;
        byte[] msgEncoded = new byte[msgLength + 5];
        int indexAt = 0;

        msgEncoded[0] = 0x81;
        indexAt++;

        if (msgLength <= 125)
        {
            msgEncoded[1] = (byte)msgLength;
            indexAt++;
        } else if (msgLength >= 126 && msgLength <= 65535)
        {
            msgEncoded[1] = 126;
            msgEncoded[2] = (byte)((byte)msgLength >> 8);
            msgEncoded[3] = (byte)msgLength;
            indexAt += 3;
        }

        for (int i = 0; i < msgLength; i++)
        {
            msgEncoded[indexAt + i] = (byte)msg[i];
        }
        return msgEncoded;
    }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Community
Solution 2 neoRiley
Solution 3 obeid_s