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.
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
.
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.
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.
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.