LiveOrDevTrying / WebsocketsSimple

WebsocketsSimple provides an easy-to-use and customizable Websocket Server and Websocket Client. The server is created using a TcpListener and upgrades a successful connection to a WebSocket. The server and client can be used for non-SSL or SSL connections and authentication (including client and server SSL certification validation) is provided for identifying the clients connected to your server. Both client and server are created in .NET Standard and use async await functionality.
Apache License 2.0
21 stars 2 forks source link

WebsocketsSimple

WebsocketsSimple provides an easy-to-use and customizable Websocket Server and Websocket Client. The server is created using a TcpListener and upgrades a successful connection to a WebSocket. The server and client can be used for non-SSL or SSL connections and authentication (including client and server SSL certification validation) is provided for identifying the clients connected to your server. All WebsocketsSimple packages referenced in this documentation are available on the NuGet package manager in 1 aggregate package - WebsocketsSimple.

Image of WebsocketsSimple Logo

Table of Contents

IPacket

IPacket is an interface contained in PHS.Networking that represents an abstract payload to be sent across Websocket. IPacket also includes a default implementation struct, Packet, which contains the following fields:


Client

A Websocket Client module is included which can be used for non-SSL or SSL connections. To get started, first install the NuGet package using the NuGet package manager:

install-package WebsocketsSimple.Client

This will add the most-recent version of the WebsocketsSimple Client package to your specified project.

IWebsocketClient

Once installed, we can create an instance of IWebsocketClient with the included implementation WebsocketClient.

OAuth Token

If you are using WebsocketClient, an optional parameter is included in the constructor for your OAuth Token - for more information, see IWebsocketClient. However, if you are creating a manual Websocket connection to an instance of WebsocketServerAuth<T>, you must append your OAuth Token to your connection Uri. This could look similar to the following:

wss://connect.websocketssimple.com/oauthtoken

Events

3 events are exposed on the IWebsocketClient interface: MessageEvent, ConnectionEvent, and ErrorEvent. These event signatures are below:

    client.MessageEvent += OMessageEvent;
    client.ConnectionEvent += OnConnectionEvent;
    client.ErrorEvent += OnErrorEvent

Connect to a Websocket Server

To connect to a Websocket Server, invoke the function ConnectAsync().

    await client.ConnectAsync());

Note: Connection parameters are provided in the constructors for WebsocketClient.

SSL

To enable SSL for WebsocketsSimple Client, set the IsSSL flag in IParamsWSClient to true. In order to connect successfully, the server must have a valid, non-expired SSL certificate where the certificate's issued hostname must match the Uri specified in IParamsWSClient. For example, the Uri in the above examples is connect.websocketssimple.com, and the SSL certificate on the server must be issued to connect.websocketssimple.com.

Please note that a self-signed certificate or one from a non-trusted Certified Authority (CA) is not considered a valid SSL certificate.

Send a Message to the Server

3 functions are exposed to send messages to the server:

An example call to send a message to the server could be:

    await client.SendToServerAsync(new Packet 
    {
        Data = JsonConvert.SerializeObject(new { header: "value" }),
        DateTime = DateTime.UtcNow
    });

More information about IPacket is available here.

Extending IPacket

IPacket can be extended with additional datatypes into a new struct / class and passed into the generic SendToServerAsync<T>(T packet) where T : IPacket function. Please note that Packet is a struct and cannot be inherited - please instead implement the interface IPacket.

    enum PacketExtendedType
    {
        PacketType1,
        PacketType2
    }

    interface IPacketExtended : IPacket 
    {
        string Username { get; set; }
        string FirstName { get; set; }
        string LastName { get; set; }
    }

    public class PacketExtended : IPacket 
    {
        string Data { get; set; }
        DateTime Timestamp { get; set; }
        PacketExtendedType PacketExtendedType {get; set; }
        string Username { get; set; }
        string FirstName { get; set; }
        string LastName { get; set; }
    }

    await SendToServerAsync(new PacketExtended 
    {
        Data = "YourDataPayload",
        DateTime = DateTime.UtcNow,
        FirstName = "FakeFirstName",
        LastName = "FakeLastName",
        PacketExtendedType = PacketExtendedType.PacketType1
    });

Receiving an Extended IPacket

If you want to extend IPacket to include additional fields, you will need to extend and override the WebsocketClient implementation to support the extended type(s). First define a new class that inherits WebsocketClient, override the protected method MessageReceived(string message), and deserialize into the extended IPacket of your choice. An example of this logic is below:

    public class WebsocketClientExtended : WebsocketClient
    {
        public WebsocketClientExtended(IParamsWSClient parameters, string oauthToken = "")
        {
        }

        protected override void MessageReceived(string message)
        {
            IPacket packet;

            try
            {
                packet = JsonConvert.DeserializeObject<PacketExtended>(message);

                if (string.IsNullOrWhiteSpace(packet.Data))
                {
                    packet = new PacketExtended
                    {
                        Data = message,
                        Timestamp = DateTime.UtcNow,
                        FirstName = "FakeFirstName",
                        LastName = "FakeLastName",
                        PacketExtendedType = PacketExtendedType.PacketType1
                    };
                }
            }
            catch
            {
                packet = new PacketExtended
                {
                    Data = message,
                    Timestamp = DateTime.UtcNow,
                    FirstName = "FakeFirstName",
                    LastName = "FakeLastName",
                    PacketExtendedType = PacketExtendedType.PacketType1
                };
            }

            FireEvent(this, new WSMessageClientEventArgs
            {
                MessageEventType = MessageEventType.Receive,
                Message = packet.Data,
                Packet = packet,
                Connection = _connection
            });
        }
    }

    enum PacketExtendedType
    {
        PacketType1,
        PacketType2
    }

    interface IPacketExtended : IPacket 
    {
        string Username { get; set; }
        string FirstName { get; set; }
        string LastName { get; set; }
    }

    public class PacketExtended : IPacket 
    {
        string Data { get; set; }
        DateTime Timestamp { get; set; }
        PacketExtendedType PacketExtendedType {get; set; }
        string Username { get; set; }
        string FirstName { get; set; }
        string LastName { get; set; }
    }

If you are sending polymorphic objects, first deserialize the initial message into a class or struct that contains “common” fields, such as PacketExtended with a PacketExtendedType enum field. Then use the value of PacketExtendedType and deserialize a second time into the type the enum represents. Repeat until the your polymorphic object is completely deserialized.

Ping

A WebsocketServer will send a raw message containing ping to every client every 120 seconds to verify which connections are still alive. If a client fails to respond with a raw message containing pong, during the the next ping cycle, the connection will be severed and disposed. However, if you are using WebsocketClient, the ping / pong messages are digested and handled before reaching MessageReceived(string message). This means you do not need to worry about ping and pong messages if you are using WebsocketClient. If you are creating your own Websocket connection, you should incorporate logic to listen for raw messages containing ping, and if received, immediately respond with a raw message containing pong.

Note: Failure to implement this logic will result in a connection being disconnected and disposed in up to approximately 240 seconds.

Disconnect from the Server

To disconnect from the server, invoke the function DisconnectAsync().

    await client.DisconnectAsync();

Disposal

At the end of usage, be sure to call Dispose() on the IWebsocketClient to free all allocated memory and resources.

    client.Dispose();

Server

A Websocket Server module is included which can be used for non-SSL or SSL connections. To get started, create a new console application and install the NuGet package using the NuGet package manager:

install-package WebsocketsSimple.Server

This will add the most-recent version of the WebsocketsSimple Server package to your specified project.

Once installed, we can create 2 different classes of Websocket Servers.

The WebsocketsSimple Server does not specify a listening Uri / host. Instead, the server is configured to automatically listen on all available interfaces (including 127.0.0.1, localhost, and the server's exposed IPs).

Parameters

Events

4 events are exposed on the IWebsocketServer interface: MessageEvent, ConnectionEvent, ErrorEvent, and ServerEvent. These event signatures are below:

    server.MessageEvent += OMessageEvent;
    server.ConnectionEvent += OnConnectionEvent;
    server.ErrorEvent += OnErrorEvent;
    server.ServerEvent += OnServerEvent;

Starting the Websocket Server

To start the IWebsocketServer, call the Start() method to instruct the server to begin listening for messages. Likewise, you can stop the server by calling the Stop() method.

    server.Start();
    ...
    server.Stop();
    server.Dispose();

SSL

To enable SSL for WebsocketsSimple Server, use one of the two provided SSL server constructors and manually specify your exported SSL certificate with private key as a byte[] and your certificate's private key as parameters.

The SSL Certificate MUST match the domain where the Websocket Server is hosted / can be accessed or clients will not able to connect to the Websocket Server.

In order to allow successful SSL connections, you must have a valid, non-expired SSL certificate. There are many sources for SSL certificates and some of them are open-source - we recommend Let's Encrypt.

Note: A self-signed certificate or one from a non-trusted CA is not considered a valid SSL certificate.

Send a Message to a Connection

3 functions are exposed to send messages to connections:

More information about IPacket is available here.

IConnectionWSServer represents a connected client to the server. These are exposed in ConnectionEvent or can be retrieved from Connections inside of IWebsocketServer.

An example call to send a message to a connection could be:

    IConnectionWSServer[] connections = server.Connections;

    await server.SendToConnectionAsync(new Packet 
    {
        Data = "YourDataPayload",
        DateTime = DateTime.UtcNow
    }, connections[0]);

Receiving an Extended IPacket

If you want to extend IPacket to include additional fields, you will need to add the optional parameter WebsocketHandler that can be included with each constructor. The default WebsocketHandler has logic which is specific to deserialize messages of type Packet, but to receive your own extended IPacket, we will need to inherit / extend WebsocketHandler with our own class. Once WebsocketHandler has been extended, override the protected method MessageReceived(string message, IConnectionWSServer connection) and deserialize the message into an extended IPacket of your choice. An example of this logic is below:

    public class WebsocketHandlerExtended : WebsocketHandler
    {
        public WebsocketHandlerExtended(IParamsWSServer parameters) : base(parameters)
        {
        }

        public WebsocketHandlerExtended(IParamsWSServer parameters, byte[] certificate, string certificatePassword) : base(parameters, certificate, certificatePassword)
        {
        }

        protected override void MessageReceived(string message, IConnectionWSServer connection)
        {
            IPacket packet;

            try
            {
                packet = JsonConvert.DeserializeObject<PacketExtended>(message);

                if (string.IsNullOrWhiteSpace(packet.Data))
                {
                    packet = new PacketExtended
                    {
                        Data = message,
                        Timestamp = DateTime.UtcNow,
                        PacketExtendedType = PacketExtendedType.PacketType1,
                        FirstName = "FakeFirstName",
                        LastName = "FakeLastName",
                    };
                }
            }
            catch
            {
                packet = new PacketExtended
                {
                    Data = message,
                    Timestamp = DateTime.UtcNow,
                    PacketExtendedType = PacketExtendedType.PacketType1,
                    FirstName = "FakeFirstName",
                    LastName = "FakeLastName"
                };
            }

            FireEvent(this, new WSMessageServerEventArgs
            {
                MessageEventType = MessageEventType.Receive,
                Message = packet.Data,
                Packet = packet,
                Connection = connection
            });
        }
    }

    enum PacketExtendedType
    {
        PacketType1,
        PacketType2
    }

    interface IPacketExtended : IPacket 
    {
        string Username { get; set; }
        string FirstName { get; set; }
        string LastName { get; set; }
    }

    public class PacketExtended : IPacket 
    {
        string Data { get; set; }
        DateTime Timestamp { get; set; }
        PacketExtendedType PacketExtendedType {get; set; }
        string Username { get; set; }
        string FirstName { get; set; }
        string LastName { get; set; }
    }

If you are sending polymorphic objects, first deserialize the initial message into a class or struct that contains “common” fields, such as PacketExtended with a PacketExtendedType enum field. Then use the value of PacketExtendedType and deserialize a second time into the type the enum represents. Repeat until the your polymorphic object is completely deserialized.

Finally, when constructing your WebsocketServer, pass in your new WebsocketHandlerExtended extended class you created. An example is below:

    IParamsWSServer parameters = new ParamsWSServer 
    {
        ConnectionSuccessString = "Connected Successfully",
        Port = 5345
    };

    IWebsocketServer server = new WebsocketServer(parameters, handler: new WebsocketHandlerExtended(parameters));

Ping

A raw message containing ping is sent automatically every 120 seconds to each client connected to a WebsocketServer. Each client is expected to immediately return a raw message containing pong. If a raw message containing pong is not received by the server before the next ping interval, the connection will be severed, disconnected, and removed from the WebsocketServer. This interval time is hard-coded to 120 seconds.

Disconnect a Connection

To disconnect a connection from the server, invoke the function DisconnectConnectionAsync(IConnectionWSServer connection).

    await DisconnectConnectionAsync(connection);

IConnectionWServer represents a connected client to the server. These are exposed in ConnectionEvent or can be retrieved from Connections inside of IWebsocketServer.

Stop the Server and Disposal

To stop the server, call the Stop() method. If you are not going to start the server again, call the Dispose() method to free all allocated memory and resources.

    server.Stop();
    server.Dispose();

IWebsocketServerAuth<T>

The second Websocket Server includes authentication for identifying your connections / users. We will create an instance of IWebsocketServerAuth<T> with the included implementation WebsocketServerAuth<T>. This object includes a generic, T, which represents the datatype of your user unique Id. For example, T could be an int, a string, a long, or a guid - this depends on the datatype of the unique Id you have set for your user. This generic allows the IWebsocketServerAuth<T> implementation to allow authentication and identification of users within many different user systems. The included implementation includes the following constructors (for SSL or non-SSL servers):

    public class MockUserService : IUserService<long> 
    { }

    IWebsocketServerAuth<long> server = new WebsocketServerAuth<long>(new ParamsWSServerAuth 
    {
        ConnectionSuccessString = "Connected Successfully",
        ConnectionUnauthorizedString = "Connection not authorized",
        Port = 5555
    }, new MockUserService());
    public class MockUserService : IUserService<long> 
    { }

    byte[] certificate = File.ReadAllBytes("yourCert.pfx");
    string certificatePassword = "yourCertificatePassword";

    IWebsocketServerAuth<long> server = new WebsocketServerAuth<long>(new ParamsWSServerAuth 
    {
        ConnectionSuccessString = "Connected Successfully",
        ConnectionUnauthorizedString = "Connection not authorized",
        Port = 5555
    }, new MockUserService(), certificate, certificatePassword);

The WebsocketsSimple Authentication Server does not specify a listening Uri / host. Instead, the server is configured to automatically listen on all available interfaces (including 127.0.0.1, localhost, and the server's exposed IPs).

Parameters

IUserService<T>

This is an interface contained in PHS.Networking.Server. When creating a WebsocketServerAuth<T>, the interface IUserService<T> will need to be implemented into a concrete class.

A default implementation is not included with WebsocketsSimple. You will need to implement this interface and add logic here.

An example implementation using Entity Framework is shown below:

    public class UserServiceWS : IUserService<long>
    {
        protected readonly ApplicationDbContext _ctx;

        public UserServiceWS(ApplicationDbContext ctx)
        {
            _ctx = ctx;
        }

        public virtual async Task<long> GetIdAsync(string token)
        {
            // Obfuscate the token in the database
            token = Convert.ToBase64String(Encoding.UTF8.GetBytes(token));
            var user = await _ctx.Users.FirstOrDefaultAsync(s => s.OAuthToken == token);
            return user != null ? user.Id : (default);
        }

        public void Dispose()
        {
        }
    }

Because you are responsible for creating the logic in GetIdAsync(string oauthToken), the data could reside in many stores including (but not limited to) in memory, a database, or an identity server. In our implementation, we are checking the OAuth Token using Entity Framework and validating it against a quick User table in SQL Server. If the OAuth Token is found, then the appropriate UserId will be returned as type T, and if not, the default of type T will be returned (e.g. 0, "", Guid.Empty).

Events

4 events are exposed on the IWebsocketServerAuth<T> interface: MessageEvent, ConnectionEvent, ErrorEvent, and ServerEvent. These event signatures are below:

    server.MessageEvent += OMessageEvent;
    server.ConnectionEvent += OnConnectionEvent;
    server.ErrorEvent += OnErrorEvent;
    server.ServerEvent += OnServerEvent;

Starting the Websocket Authentication Server

To start the WebsocketsSimple Authentication Server, call the Start() method to instruct the server to begin listening for messages. Likewise, you can stop the server by calling the Stop() method.

    server.Start();
    ...
    server.Stop();
    server.Dispose();

SSL

To enable SSL for WebsocketsSimple Server, use one of the two provided SSL server constructors and manually specify your exported SSL certificate with private key as a byte[] and your certificate's private key as parameters.

The SSL Certificate MUST match the domain where the Websocket Server is hosted / can be accessed or clients will not able to connect to the Websocket Server.

In order to allow successful SSL connections, you must have a valid, non-expired SSL certificate. There are many sources for SSL certificates and some of them are open-source - we recommend Let's Encrypt.

Note: A self-signed certificate or one from a non-trusted CA is not considered a valid SSL certificate.

Send a Message to a Connection

To send messages to connections, 11 functions are exposed:

More information about IPacket is available here.

IConnectionWSServer represents a connected client to the server. These are exposed in the ConnectionEvent or can be retrieved from Connections or Identities inside of IWebsocketServerAuth<T>.

An example call to send a message to a connection could be:

    IIdentityWS<Guid>[] identities = server.Identities;

    await server.SendToConnectionAsync(new Packet 
    {
        Data = "YourDataPayload",
        DateTime = DateTime.UtcNow
    }, identities[0].Connections[0]);

Receiving an Extended IPacket

If you want to extend IPacket to include additional fields, you will need to add the optional parameter WebsocketHandlerAuth that can be included with each constructor. The included WebsocketHandlerAuth has logic which is specific to deserialize messages of type Packet, but to receive your own extended IPacket, we will need to inherit / extend WebsocketHandlerAuth. Once WebsocketHandlerAuth has been extended, override the protected method MessageReceived(string message, IConnectionWSServer connection) and deserialize into the extended IPacket of your choice. An example of this logic is below:

    public class WebsocketHandlerAuthExtended : WebsocketHandlerAuth
    {
        public WebsocketHandlerAuthExtended(IParamsWSServerAuth parameters) : base(parameters)
        {
        }

        public WebsocketHandlerAuthExtended(IParamsWSServerAuth parameters, byte[] certificate, string certificatePassword) : base(parameters, certificate, certificatePassword)
        {
        }

        protected override void MessageReceived(string message, IConnectionWSServer connection)
        {
            IPacket packet;

            try
            {
                packet = JsonConvert.DeserializeObject<PacketExtended>(message);

                if (string.IsNullOrWhiteSpace(packet.Data))
                {
                    packet = new PacketExtended
                    {
                        Data = message,
                        Timestamp = DateTime.UtcNow,
                        PacketExtendedType = PacketExtendedType.PacketType1,
                        FirstName = "FakeFirstName",
                        LastName = "FakeLastName"
                    };
                }
            }
            catch
            {
                packet = new PacketExtended
                {
                    Data = message,
                    Timestamp = DateTime.UtcNow,
                    PacketExtendedType = PacketExtendedType.PacketType1,
                    FirstName = "FakeFirstName",
                    LastName = "FakeLastName"
                };
            }

            FireEvent(this, new WSMessageServerEventArgs
            {
                MessageEventType = MessageEventType.Receive,
                Message = packet.Data,
                Packet = packet,
                Connection = connection
            });
        }
    }

    enum PacketExtendedType
    {
        PacketType1,
        PacketType2
    }

    interface IPacketExtended : IPacket 
    {
        string Username { get; set; }
        string FirstName { get; set; }
        string LastName { get; set; }
    }

    public class PacketExtended : IPacket 
    {
        string Data { get; set; }
        DateTime Timestamp { get; set; }
        PacketExtendedType PacketExtendedType {get; set; }
        string Username { get; set; }
        string FirstName { get; set; }
        string LastName { get; set; }
    }

If you are sending polymorphic objects, first deserialize the initial message into a class or struct that contains “common” fields, such as PacketExtended with a PacketExtendedType enum field. Then use the value of PacketExtendedType and deserialize a second time into the type the enum represents. Repeat until the your polymorphic object is completely deserialized.

Finally, when constructing WebsocketServerAuth<T>, pass in your new WebsocketHandlerAuthExtended class you created. An example is as follows:

    IParamsWSServerAuth parameters = new ParamsTWSServerAuth 
    {
        ConnectionSuccessString = "Connected Successfully",
        ConnectionUnauthorizedString = "Connection Not Authorized",
        Port = 5555
    };

    IWebsocketServerAuth<long> server = new WebsocketServerAuth<long>(parameters, new MockUserService(), handler: new WebsocketHandlerAuthExtended(parameters));

Ping

A raw message containing ping is sent automatically every 120 seconds to each client connected to a WebsocketServerAuth<T>. Each client is expected to return a raw message containing a pong. If a pong is not received before the next ping interval, the connection will be severed, disconnected, and removed from the WebsocketServerAuth<T>. This interval time is hard-coded to 120 seconds. If you are using the provided WebsocketClient, Ping / Pong logic is already handled for you.

Disconnect a Connection

To disconnect a connection from the server, invoke the function DisconnectConnectionAsync(IConnectionWSServer connection).

    await DisconnectConnectionAsync(connection);

IConnectionWSServer represents a connected client to the server. These are exposed in the ConnectionEvent or can be retrieved from Connections or Identities inside of IWebsocketServerAuth<T>. If a logged-in user disconnects from all connections, that user is automatically removed from Identities.

Stop the Server and Disposal

To stop the server, call the Stop() method. If you are not going to start the server again, call the Dispose() method to free all allocated memory and resources.

    server.Stop();
    server.Dispose();

Additional Information

WebsocketsSimple was created by LiveOrDevTrying and is maintained by Pixel Horror Studios. WebsocketsSimple is currently implemented in (but not limited to) the following projects: Allie.Chat and The Monitaur.
Pixel Horror Studios Logo