Please star this project if you find it useful. Thank you.
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.
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.
Unsurprisingly, over the years learning and insights grew and eventually maintaining and looking back at the aging code-base became more and more painful for the ever more trained eye, hence I decided to redo most of it.
Version 7 is more or less a rewrite of 90+ % of the original code.
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.
Version 7 introduces a client ping feature, which enabling the WebSocket client to send a ping message with a constant interval.
The clientPingMessage
parameter is optional and the default value is null. The behavior for the null value 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
in this method. 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[]
.
Successfully tested with .NET 6.0.
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 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.")
};
- Fixed bug related to connecting to IPv6 endpoints.
- Updated System.Reactive to v5.0.0.
- Successfully tested with .NET 5.0.
- Updated Readme.
Updates, stability and fundamental improvements to the library. See examples below for changes in usage.
Simplifications and no longer relies on SocketLite but utilizes the cross-platform capabilities of .NET Standard 2.0+.
From hereon and forward only .NET Standard 2.0+ is supported.
For a more detailed sample of using this library please see the console example app.
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 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 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),
};
}
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 requires 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
This library have 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
need to be send 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.
The following documentation was utilized when writing this library:
- RFC 6544
- Writing WebSocket Servers
- Writing WebSocket Server in C#
- Writing WebSocket client applications
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.