Skip to content

1iveowl/WebsocketClientLite.PCL

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WebSocket Client Lite (Rx)

NuGet Badge

.NET Standard .NET Standard System.Reactive

CI/CD

Please star this project if you find it useful. Thank you.

A Lightweight Cross Platform WebSocket Client

This library is a ground-up implementation of the WebSocket specification (RFC 6544) - i.e., this implementation does not rely on the build-in WebSocket libraries in .NET.

The library allows developers additional flexibility, including the ability to establish secure wss websocket connections to websocket servers that have self-signing certificates, expired certificates etc. This capability should be used with care for obvious reasons, however it is useful for testing environments, closed local networks, local IoT set-ups etc.

Furthermore, this library utilize ReactiveX (aka Rx or Reactive Extensions). Although taking this dependency introduces an added learning curve, it is a learning curve worthwhile investing in, as it IMHO makes using and creating a library like this much more elegant compared to using traditional call-back or events based patterns etc.

New in version 7.0

At writing time, this library has been around for more than 6 years. The work represented in this repo was mainly initiated on a desire to learn and play around with the technologies involved.

Over the years new learning and new insights grew and eventually, looking back at the aging code-base, it became more and more obvious that a more fundamental clean-up was overdue. Hence, I decided to redo most of what was, give birth to version 7 which more or less became a rewrite of the original code base.

The version 7 NuGet package includes both a .NET Standard 2.0 package and a .NET Standard 2.1, with e .NET Standard 2.1 package having a few less dependencies.

Introduced with version 7.3

Version 7.3 introduces a new feature for managing TCP socket. Specifically, in the case were an existing TCP socket can now be supplied and passed in using the constructor.

It is now also possible to transfer ownership of the life-cycle of this TCP socket to the library. When transferring the life-cycle ownership of the socket connection, the TCP Socket client will be closed and disposed of, when the observable WebSocket is being disposed.

To transfer ownership of the life-cycle of the TCP Socket Client set the hasTranferTcoSocketLifeCycleOwnership to true:

new MessageWebsocketRx(tcpClient, hasTransferTcpSocketLifeCycleOwnership: true)

Client Ping

Version 7.0 introduced the client ping feature, which enabling the WebSocket client to send a ping message with a predefined constant interval.

It is also possible to include a message as part of the client ping. The clientPingMessage parameter is optional and the default value is null. When the value is null (default) the behavior is to not include any message as part of the ping.

var websocketConnectionObservable = 
    client.WebsocketConnectWithStatusObservable(
        uri: WebsocketServerUri, 
        hasClientPing: true, // default is false. 
        clientPingInterval: TimeSpan.FromSeconds(20), // default is 30 seconds.
        clientPingMessage: "my ping message"); // default no message when set to null.

It is only possible to use a string as a message. For more advanced scenarios, the ISender has a SendPing method that can be used for full control when sending client pings as string or as byte[].

New in version 6.4

Previously the library only accepted the ws and wss scheme. Now http and https is also supported.

To further extend supported schemes override the IsSecureConnectionScheme method of the MessageWebSocketRx class.

The default virtual method looks like this:

public virtual bool IsSecureConnectionScheme(Uri uri) => 
    uri.Scheme switch
    {
        "ws" or "http" => false,
        "https" or "wss"=> true,
        _ => throw new ArgumentException("Unknown Uri type.")
    };

New in version 6.3

  • Fixed bug related to connecting to IPv6 endpoints.
  • Updated System.Reactive to v5.0.0.
  • Successfully tested with .NET 5.0.
  • Updated Readme.

New in version 6.1.

Updates, stability and fundamental improvements to the library. See examples below for changes in usage.

New in version 6.0.

Simplifications and no longer relies on SocketLite but utilizes the cross-platform capabilities of .NET Standard 2.0+.

New in version 5.0.

From hereon and forward only .NET Standard 2.0+ is supported.

Usage

For a more detailed sample of using this library please see the console example app.

To instantiate WebSocket lite class

To use the WebSocket client create an instance of the class MessageWebsocketRx:

var websocketClient = new MessageWebsocketRx()
{
    IgnoreServerCertificateErrors = false,
    Headers = new Dictionary<string, string> {{ "Pragma", "no-cache" }, { "Cache-Control", "no-cache" }}
};

... or use the alternative constructor to pass your own TcpClient for more control of the configuration and the management of your TCP socket connection.

MessageWebSocketRx(TcpClient tcpClient)

Note: If the TcpClient is not connected the library will connect it. Also, the TcpClient will not be disposed automatically when passed in using the constructor, as it will in the case when no TcpClient is supplied.

To connect client to WebSocket server

To connect and observe websocket connection use WebsocketConnectionObservable:

var websocketConnectionObservable = 
    client.WebsocketConnectObservable(
        new Uri(WebsocketTestServerUrl)
);

... or use WebsocketConnectionWithStatusObservable to also observe connection status :

var websocketConnectionWithStatusObservable = 
    client.WebsocketConnectWithStatusObservable(
        new Uri(WebsocketTestServerUrl)
);

To control TLS/SSL certificate validation behavior

To control TLS/SSL Server certificate behavior, either use the IgnoreServerCertificateErrors parameter to ignore any issues with the certificate or override the ValidateServerCertificate method to your liking.

The existing virtual method implementation looks like this:

public virtual bool ValidateServerCertificate(
    object senderObject,
    X509Certificate certificate,
    X509Chain chain,
    SslPolicyErrors tlsPolicyErrors)
{
    if (IgnoreServerCertificateErrors) return true;

    return tlsPolicyErrors switch
    {
        SslPolicyErrors.None => true,
        SslPolicyErrors.RemoteCertificateChainErrors => 
            throw new Exception($"SSL/TLS error: {SslPolicyErrors.RemoteCertificateChainErrors}"),
        SslPolicyErrors.RemoteCertificateNameMismatch => 
            throw new Exception($"SSL/TLS error: {SslPolicyErrors.RemoteCertificateNameMismatch}"),
        SslPolicyErrors.RemoteCertificateNotAvailable => 
            throw new Exception($"SSL/TLS error: {SslPolicyErrors.RemoteCertificateNotAvailable}"),
        _ => throw new ArgumentOutOfRangeException(nameof(tlsPolicyErrors), tlsPolicyErrors, null),
    };
}

Working With Slack (And maybe also other WebSocket server implementations)

The RFC 6455 section defining how ping/pong works seems to be ambiguous on the question whether or not a pong must include the byte defining the length of data-frame, in the special case when there is no data and the length of the data is zero.

When testing against for instance the Postman WebSocket test server Postman WebSocket Server the data-frame byte is expected and should have the value 0 (zero), when there's no data in the data-frame.

However, when used with the slack.rtm API the byte should not be there at all in the case of no data in the data-frame, and if it is, the slack WebSocket server will disconnect.

To manage this length byte-issue the following property can be set to true, in which case the byte with the zero value will NOT be added to the pong. For instance like this:

var websocketClient = new MessageWebSocketRx
{
    ExcludeZeroApplicationDataInPong = true
}

To further complicate matters the slack.rtm api seems to require a ping at the Slack application layer too.

A simplified implementation of this could look like this, which obviously would need to be repeated in some interval to keep the slack connection going:

await _webSocket.SendText("{\"id\": 1234, // ID, see \"sending messages\" above\"type\": \"ping\",...}");

For details read the Ping and Pong section of the slack.rtm API documentation

Working with socket.io

This library has also been tested with socket.io.

A typical connection will look like this:

var websocketConnectionObservable = 
    client.WebsocketConnectWithStatusObservable(
        new Uri($"http://{url}:{port}/socket.io/?EIO=4&transport=websocket"));

This will connect on the WebSocket layer with socket.io server.

To further connect on socket.io level see documentation. For instance, typically a text message with the content 40 needs to be sent right after the connection have been established. Also, some socket.io server implementations seem to be very sensitive to the encoding of the messages that are being send, and will disconnect immediately if receiving a data-frame with a text message that does not comply with the expected socket.io encoding protocol.

For more see here: WebSocket client not connecting to the socket.io server.

References:

The following documentation was utilized when writing this library:

Thank you !

Thank you to all the developers who've been using this library through the years, many of which that have reported issues or bugs, or made contributions and pull requests to make the library better and/or more capable. It is this interaction with all of you that makes sharing and learning fun.