Team XSockets.NET

WebSockets (RFC6455 XSockets implementation)

The XSockets implementation of the websocket protocol uses a SubProtocol since we expect a certain format to be sent into the protocol implementation from the client. If you want to use the raw websockets protocol you can take a look at the section about Native WebSockets.

Why a custom format?

WebSockets opened up great possibilities in the browser. In a world of stateless request/response a full-duplex connection between client and server was a great improvement! However, the websockets protocol is very basic by default (as it should be). It only has 4 events in the browser implementation:

  • OnOpen
  • OnClose
  • OnMessage
  • OnError

There is also some other events such as ping/pong, read all about RFC6455 here.

Of course there is also methods such as:

  • close
  • send

Since there is no specific guidelines about the format that you send to a websocket server you can pass in anything in the send method. This is nice, but it will of course mean that you have no idea about the message content on the server side. When building fairly large and complex systems this is not ideal.

This is why we implemented a custom WebSocket protocol that expects you to send in data in the IMessage format:

Usage

So now we can send structured data (IMessage) to the server, and the server will know what to do with the data.

{t: 'foo', c:'bar', d:'some message'};

Note: t = topic, c = controller, d = data

By doing so we know where to route a message server-side. The message above will end up on the controller Bar and it method Foo that expects a string.

public class Bar
{
    public async Task Foo(string m)
    {
        //Code removed for readability
    }
}

This will also enable us to do model binding so that we can send complex objects that will be serialized into the correct type server-side.

//The message
{t: 'foo', c:'bar', d:{name:'Steve', Age:42}};

// will be mapped to
public class Bar
{
    public async Task Foo(Person p)
    {
        //Code removed for readability
    }
}

SubProtocol

We mentioned that we implemented our own SubProtocol of RFC6455 to be able to handle structured data. How did we do that?

It is actually pretty simple. The websocket protocol allows you to specify a Sec-WebSocket-Protocol. Our client implementation will use the Sec-WebSocket-Protocol: XSocketsNET and on the server the handshake will match that to our implementation of RFC6455. You can of course use native websockets as well, but then you have to take care of the routing your self. You will see an example of that in the Native WebSockets section.

When connecting from Chrome using XSockets JavaScript client you can see that the sub-protocol is used.

sub-protocol

Why a SubProtocol

Since XSockets WebSockets implementation expects messages in a specific format we use a subprotocol. We could have neglected to have a subprotocol but then you would not be able to use raw websockets. As soon as you expect the messages to be of a certain format you should implement a subprotocol in XSockets. Other frameworks might do this another way, but since XSockets is modular it makes sense for us to use protocols this way.

Doing so will make it clear how each client expects to communicate. If the client use XSocketsNET as subprotocol we know that the message format will be an IMessage. If the subprotocol is specific for another server implementation we can still use the client with XSockets as long as the subprotocol lets us know the expected format.

Custom WebSocket Protocol

Since we added our own implementation of the websocket protocol so can you! All you have to do is to use your own SubProtocol. In the next section we will show you how to add a protocol for native websockets, but now we will just add a custom websocket protocol with the SubProtocol set to demoprotocol

Add a New Protocol Module

Select the protocol template from the XSockets.NET 5 templates, and let's name it DemoProtocol.

demoprotocol

The protocol template does not implement the WebSocket protocol, so lets change the class to inherit the Rfc6455Protocol. That way we get a lot of complex things taken care of. As you can see below we check use the Match method to decide if the handshake says that the client wants to use the subprotocol (our DemoProtocol)

using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using XSockets.Core.Common.Protocol;
using XSockets.Plugin.Framework;
using XSockets.Plugin.Framework.Attributes;
using XSockets.Protocol.Rfc6455;

[Export(typeof(IXSocketProtocol), Rewritable = Rewritable.No)]
public class DemoProtocol : Rfc6455Protocol
{
    /// <summary>
    /// Check if the handshake matches this protocol.
    /// We will be checking that it is a websocket handshake and that the subprotocol is 'DemoProtocol'
    /// </summary>
    /// <param name="handshake"></param>
    /// <returns></returns>
    public override async Task<bool> Match(IList<byte> handshake)
    {
        var s = Encoding.UTF8.GetString(handshake.ToArray());
        return
            Regex.Match(s, @"(^Sec-WebSocket-Version:\s13)", RegexOptions.Multiline).Success
            &&
            Regex.Match(s, @"(^Sec-WebSocket-Protocol:\sDemoProtocol)", RegexOptions.Multiline).Success;
    }

    public override IXSocketProtocol NewInstance()
    {
        return new DemoProtocol();
    }    
}

This is all we need. We can now start the server and use our DemoProtocol as subprotocol over WebSockets. It is easy to test by just opening up the console in chrome and write some javascript.

var conn = new WebSocket('ws://127.0.0.1:4502','DemoProtocol');

That is all we need, we can now communicate over our protocol, but there is some requirements. Since we inherit the XSockets implementation of Rfc6455 we need to pass in data in the IMessage format. If we ignore that there will be an error and the connection will be closed.

So right now for us the send a message and get it back we need to do something like.

//open a connection using the subprotocol
var conn = new WebSocket('ws://127.0.0.1:4502','DemoProtocol');
//when the onmessage occurs we just print out the data
conn.onmessage = function(d){console.log('CustomProtocol Message', d.data)}
//construct a message the way XSockets Rfc6455 expects it
var json = {t:'foo',c:'generic',d:'hi'}
var m = JSON.stringify(json)
//send the message
conn.send(m)

This would return a open message from the generic controller as well as the message that we sent in. See the snippet from chromes console below.

custom protocol message

Adding a custom format

We will take a look at using native WebSockets in the next section. Now we will add a custom message format to our WebSocket subprotocol named DemoProtocol.

In our DemoProtocol we will assume that all communication will be using a default controller, our DemoController. So all we need to pass in is the method name and the parameters. Since the client will be a browser we might as well use JSON.

The expected format of our DemoProtocol will be

{
    M:'MethodName',
    J:'parameters in JSON format'
}

To be able to do this we will overwrite the methods for incoming and outgoing data.

//Make sure to parse the incoming data into a IMessage
public override IMessage OnIncomingFrame(IEnumerable<byte> payload, MessageType messageType)
{
    //we expect the client to send the method name and the parameter data
    var data = this.JsonSerializer.DeserializeFromString<DemoFormat>(Encoding.UTF8.GetString(payload.ToArray()));
    return new Message(data.J, data.M,"demo");
}

//Parse outgoing IMessage back to the format {M:'method', J:'paraneters in JSON format'}
public override byte[] OnOutgoingFrame(IMessage message)
{
    //We will send it back in the same format "{M:'',J:''}"
    var rnd = new Random().Next(0, 34298);
    var frame = new Rfc6455DataFrame
    {
        FrameType = FrameType.Text,
        IsFinal = true,
        MaskKey = rnd,
        Payload = Encoding.UTF8.GetBytes(this.JsonSerializer.SerializeToString(new DemoFormat { J = message.Data, M = message.Topic }))
    };
    return frame.ToBytes();
}

Demo Controller

Since our custom protocol does not expect the controller to be included in the message we have to assign a controller in the method OnIncomingFrame. As you can see above we set the controller to Demo. So we have to add that controller.

public class Demo : XSocketController
{
    /// <summary>
    /// By overriding the OnMessage method we can catch all messages that enters the controller wihtout a matching method.
    /// So if the method is someting else than Foo this method will fire.
    /// </summary>
    /// <param name="message"></param>
    /// <returns></returns>
    public override async Task OnMessage(IMessage message)
    {
        await this.InvokeToAll(message);
    }

    public async Task Foo(string s, int i)
    {
        await this.InvokeToAll(new { stringValue = s, intValue = i }, "foo");
    }
}

DemoFormat

We also use a class called DemoFormat and deserialize incoming data into that format.

public class DemoFormat
{
    public string M { get; set; }
    public string J { get; set; }
}

Testing the Custom Format in the Custom Protocol

Now we have a custom websocket protocol that expects a specific format. The protocol will convert incoming data into the internal IMessage format. The protocol will also convert outgoing data into the format of the custom protocol.

To test this we can write some javascript in the console the same way we did above.

var conn = new WebSocket('ws://127.0.0.1:4502','DemoProtocol');
conn.onmessage = function(d){console.log('CustomProtocol Message', d.data)}(d){console.log('CustomProtocol Message', d.data)}
var json = {M:'foo',J:'{s:\'bar\',i:42}'}
var m = JSON.stringify(json)
conn.send(m)

The sample code above will call the Foo method and pass in the parameters since the data matches the the parameter values.

See the result below.

DemoProtocol custom format

Summary

In this section we explained why XSockets has a custom implementation of the websocket protocol. We also took a look at how you can implement your own custom websocket protocol. Now that you know about this it will be easier to create protocols and clients with more complexity.

results matching ""

    No results matching ""