Putty
The PuttyProtocol
is used in examples through this book. As you already know it is a really stupid and simple protocol. The idea with the PuttyProtocol
is to show how easy it is to extend XSockets to allow other client and protocols to communicate.
This section will take a look at the PuttyProtocol
implementation. Knowing how this was done will make it easier for you to understand the CustomProtocol
section. It is also fundamental to know this to be able to add you own protocol.
Protocol Format
First of all we had to decide the format of our PuttyProtocol
. We decided to go with controller|topic|data
. So basically the protocol expects us to separate information with a pipe |
.
So this protocol is clearly based on delimiters and not length-prefix. Since the protocol is for the Putty client it makes sense to use delimiters :)
Handshake
To be able to identify the CustomProtocol
we need to have a unique handshake. In real protocols this can be complex things with bitwise operations etc. However, we just use the string PuttyProtocol
as the handshake. So when a client connects and send in the PuttyProtocol
as handshake the server will identify our custom PuttyProtocol
as the one to use.
Reading a Frame
Before we can parse a frame we have to be sure that we have received a complete frame. This is done in the ReceiveData
method.
In there we just add data read from the Socket into a readstate
. Since Putty
will add CRLF
at the end of every message (when we press enter) we can use that to know if the frame is complete.
Parsing
Now that we know that each incoming frame will be controller|topic|data
we know how to parse the message. All we need to do is to split the payload on the position for |
. With that information we can easily create an IMessage
to pass further into the server.
Outgoing frames (IMessage
) will be parsed back into the format that the Putty client sent int. So that will be controller|topic|data once again.
All the parsing is done in a ProtocolProxy (shown at the end).
Code PuttyProtocol
This is the complete source code of the PuttyProtocol
. The actual parsing is done in the protocol proxy pasted in after the protocol.
So this simple protocol will now allow Putty to communicate with any other clients regardless of the protocol that the other clients is using.
namespace XSockets.Protocol.Putty
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Core.Common.Protocol;
using Core.Common.Socket.Event.Arguments;
using Core.Common.Socket.Event.Interface;
using Plugin.Framework;
using Plugin.Framework.Attributes;
/// <summary>
/// A really simple/stupid demo-protocol for Putty.
/// </summary>
[Export(typeof(IXSocketProtocol), Rewritable = Rewritable.No)]
public class PuttyProtocol : XSocketProtocol
{
public PuttyProtocol()
{
this.ProtocolProxy = new PuttyProtocolProxy();
}
/// <summary>
/// Is this the protocol matching the handshake?
/// </summary>
/// <param name="handshake"></param>
/// <returns></returns>
public override Task<bool> Match(IList<byte> handshake)
{
//This will actually check if the handshake matches the class name of this protocol. So PuttyProtocol will be valid
return base.Match(handshake);
}
/// <summary>
/// The string to return after handshake
/// </summary>
public override string HostResponse
{
get { return "Welcome to PuttyProtocol"; }
}
/// <summary>
/// Returns the host response of the putty protocol and adds a CRLF
/// </summary>
/// <returns></returns>
public override async Task<byte[]> GetHostResponse()
{
return Encoding.UTF8.GetBytes(string.Format("{0}\r\n", HostResponse));
}
/// <summary>
/// Set to true if your clients connected to this protocol will return pong on ping.
/// </summary>
/// <returns></returns>
public override bool CanDoHeartbeat()
{
//Putty does not support heartbeats
return false;
}
/// <summary>
/// We get data from the transport, decide if the frame is completed.
/// If so, call the OnFrameReady method
/// </summary>
/// <param name="data"></param>
/// <param name="readState"></param>
/// <param name="processFrame"></param>
public override async Task ReceiveData(ArraySegment<byte> data)
{
for (int i = 0; i < data.Count; i++)
{
this.ReadState.Data.Add(data.Array[i]);
//if the frame is completed we will find \r\n at the end since Putty adds that when we press enter
if (this.ReadState.Data.Count >= 2 && this.ReadState.Data[this.ReadState.Data.Count - 1] == 10 &&
this.ReadState.Data[this.ReadState.Data.Count - 2] == 13)
{
await this.OnFrameReady(this.ReadState.Data.Take(this.ReadState.Data.Count - 2), FrameType.Text);
this.ReadState.Clear();
}
}
}
/// <summary>
/// Provide the ProtocolHandshakeHandler with an instance of this protocol
/// </summary>
/// <returns></returns>
public override IXSocketProtocol NewInstance()
{
return new PuttyProtocol();
}
/// <summary>
/// Converts the incomming data from putty into a IMessage
/// The data is expected to be in the format "controller|topic|data"
/// </summary>
/// <param name="payload"></param>
/// <param name="messageType"></param>
/// <returns></returns>
public override IMessage OnIncomingFrame(IEnumerable<byte> payload, MessageType messageType)
{
return this.ProtocolProxy.In(payload, messageType);
}
/// <summary>
/// Converts a IMessage into a string to send to putty.
/// Putty will receive the data in the format "controller|topic|data"
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public override byte[] OnOutgoingFrame(IMessage message)
{
return this.ProtocolProxy.Out(message);
}
}
}
Code PuttyProtocolProxy
The protocol proxy is just for converting incoming and outgoing data for protocols. It is in a separate module since several protocols might use the same protocol proxy (not likely in the putty case though).
Worth mentioning that the PuttyProtocol allows you to use topics as subscribe
and unsubscribe
in clear text
namespace XSockets.Protocol.Putty
{
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Core.Common.Globals;
using Core.Common.Protocol;
using Core.Common.Socket.Event.Arguments;
using Core.Common.Socket.Event.Interface;
using Core.Common.Utility.Serialization;
using Core.XSocket.Model;
using Plugin.Framework;
public class PuttyProtocolProxy : IProtocolProxy
{
private IXSocketJsonSerializer JsonSerializer { get; set; }
public PuttyProtocolProxy()
{
JsonSerializer = Composable.GetExport<IXSocketJsonSerializer>();
}
/// <summary>
/// Make sure that that incoming payload (controller|topic|data) becomes an IMessage
/// </summary>
/// <returns></returns>
public IMessage In(IEnumerable<byte> payload, MessageType messageType)
{
var data = Encoding.UTF8.GetString(payload.ToArray());
if (data.Length == 0) return null;
var d = data.Split('|');
switch (d[1])
{
case "subscribe":
case Constants.Events.PubSub.Subscribe:
return new Message(new XSubscription { Topic = d[2] }, Constants.Events.PubSub.Subscribe, d[0], JsonSerializer);
case "unsubscribe":
case Constants.Events.PubSub.Unsubscribe:
return new Message(new XSubscription { Topic = d[2] }, Constants.Events.PubSub.Unsubscribe, d[0], JsonSerializer);
case Constants.Events.Storage.Set:
var kv = d[2].Split(',');
return new Message(new XStorage { Key = kv[0], Value = kv[1] }, Constants.Events.Storage.Set, d[0], JsonSerializer);
case Constants.Events.Storage.Get:
return new Message(new XStorage { Key = d[2] }, Constants.Events.Storage.Get, d[0], JsonSerializer);
case Constants.Events.Storage.Remove:
return new Message(new XStorage { Key = d[2] }, Constants.Events.Storage.Remove, d[0], JsonSerializer);
default:
return new Message(d[2], d[1], d[0], JsonSerializer);
}
}
/// <summary>
/// Make sure that that IMessage (outgoing) is converted to the Putty format of controller|topic|data
/// </summary>
/// <returns></returns>
public byte[] Out(IMessage message)
{
if (message.Topic == Constants.Events.Controller.Opened)
{
var c = this.JsonSerializer.DeserializeFromString<ClientInfo>(message.Data);
var d = string.Format("PI:{0},CI:{1}",c.PersistentId, c.ConnectionId);
message = new Message(d, message.Topic, message.Controller);
}
return Encoding.UTF8.GetBytes(string.Format("{0}|{1}|{2}\r\n", message.Controller, message.Topic, message.Data));
}
}
}