SignalR is pretty high on my list of technologies that I despise. It is very difficult to work with outside of the .NET ecosystem, which is pretty absurd given that it was the defacto way to do WebSockets in .NET for quite some time. Perhaps my annoyance with SignalR will spare you a few minutes of agony. This article is going to show you how to connect to a SignalR server using nothing more than an HTTP request and a WebSocket.

This shouldn't be that hard of a thing. Yet the five minutes of Googling I did revealed little information on how to go about it. The NPM packages I found for connecting to a SignalR server were also equally useless.  One even requires jQuery. jQuery on the server!

So I will say, unlike my normal technique of digging in and doing some research, this post does not have any of that. It simply solves a problem. Specifically the problem I was trying to solve was connecting to a cryptocurrency exchange for one of our open source libraries. Complete code is here for the curious.

So here's the general idea:

  1. First you need to "negotiate" with the SignalR server via an HTTP request
  2. SignalR server will reply with some stuff and a token
  3. You then construct URL using that token
  4. You then connect to the SignalR server with a standard WebSocket
  5. You send and receive stuff

So here's what that looks like. I'm using a wrapper for https get requests which just returns a deseralized JSON result (if possible):

let data = JSON.stringify([{ name: "c3" }]);
let negotiations = await https.get(
          `https://socket-v3.bittrex.com/signalr/negotiate?connectionData=${data}&clientProtocol=1.5`
        );

The data we are passing is a list of hubs. What are hubs?  I don't really know and don't really care to investigate. But for this service we need to connect to the c3 hub. Cool.

You'll also see that we're using clientProtocol=1.5. This means that there are probably other formats and issues you would need to solve if you want to connect to a different version. Good luck!

Okay, back to negotiations. We get a reply that looks like this:

{
  "Url": "/signalr",
  "ConnectionToken": "GuRTqB3jbxJf/OuxGb0QghZEcnAWZ3OA2etpw3uXJLLJjPmqkyxY7QIUJ1DV/hkCOsXbh8i4/QfNR7ilLxU2eZvme3xByWA51fC6ryEqXmuRSIUY",
  "ConnectionId": "b16c6343-4e4b-4972-bc1e-328913e97a4b",
  "KeepAliveTimeout": 20.0,
  "DisconnectTimeout": 30.0,
  "ConnectionTimeout": 110.0,
  "TryWebSockets": true,
  "ProtocolVersion": "1.5",
  "TransportConnectTimeout": 5.0,
  "LongPollDelay": 0.0
}

I'm sure this can all be used for something, but really the only thing we care about is the ConnectionToken. We will use that when we connect to the socket server.

let token = encodeURIComponent(negotiations.ConnectionToken);
let wssPath = `wss://socket-v3.bittrex.com/signalr/connect?clientProtocol=1.5&transport=webSockets&connectionToken=${token}&connectionData=${data}&tid=10`;
let ws = new WebSocket(wssPath);

This next part constructs a URL with our connection token. It also again passes our data in.  We then connect a WebSocket using that finely tuned URL we just constructed.

Congratulations!  

Next up, we probably want to subscribe to things and listen for events. So lets see when sending subscription looks like:

ws.send(
  JSON.stringify({
    H: "c3",
    M: "Subscribe",
    A: [["market_summaries"]],
    I: ++this._messageId,
  })
);

This part actually makes some sense. We just send a JSON string, you know similar to JSON-RPC. This includes :

  • hub c3.
  • method Subscribe.
  • arguments
  • client generated identifier

We get back a response from the server with success or failure that looks like:

{
  "R": [
    { "Success": true, "ErrorCode": null }
  ],
  "I": 1
}

We then get message that looks like

{
  "C": "d-2BE602E1-B,0|BBRt,0|BBRu,3|BBRx,1|Ff,42A6",
  "M": [
    {
      "H": "C3",
      "M": "marketSummaries",
      "A": [
        // DATA IS HERE
      ]
    }
  ]
}

I have no idea what C is, but we can see that the top-level M contains various messages.

The objects under the top-level M have a hub H, message type M and then a payload of data under A. So all things considered, pretty similar to the request.

That's about all there is to it! Hope this saves you some time.