0

I am trying to stream bitcoin prices from the Huobi USDT Swaps but my ping/pong mechanism is not working so Huobi is always closing the connection after 30 seconds.

This is what the documentation says https://huobiapi.github.io/docs/usdt_swap/v1/en/#market-heartbeat:

WebSocket API supports two-way heartbeat. Both Server and Client can send ping message, which the opposite side can return with pong message.

WebSocket Server sends heartbeat: {"ping": 18212558000}

WebSocket Client should respond:: {"pong": 18212558000}

Note: Once the WebSocket Client and WebSocket Server get connected, the server will send a heartbeat every 5 seconds (the frequency might change). The connection will get disconnected automatically if the WebSocket Client ignores the heartbeat message for 5 times. The server will remain connection if the WebSocket Client responds one “ping” value within the latest 2 heartbeat messages.

using System;
using System.IO;
using System.IO.Compression;
using System.Net.WebSockets;
using System.Text;
using System.Threading;

namespace HuobiMarketWebSocketTest
{
    class HuobiMarketWebSocketTest
    {
        public static void Main(string[] args)
        {
            int pings = 0;
            ClientWebSocket ClientWebSocket = new ClientWebSocket();
            string symbol = "btcusdt";
            ClientWebSocket.ConnectAsync(new Uri("wss://api.hbdm.com/linear-swap-ws"), CancellationToken.None).Wait();

            try
            {
                // Subscribe to BTC
                string subscribeTrade = $"{{ \"sub\": \"market.{symbol}.trade.detail\", \"id\": \"{symbol} trade\"}}";
                byte[] subscribeTradeAsBytes = Encoding.UTF8.GetBytes(subscribeTrade);
                ClientWebSocket.SendAsync(subscribeTradeAsBytes, WebSocketMessageType.Text, true, CancellationToken.None).Wait();
                Console.WriteLine($"{subscribeTrade} sent.");

                string subscribeQuote = $"{{ \"sub\": \"market.{symbol}.bbo\", \"id\": \"{symbol} bbo\"}}";
                byte[] subscribeQuoteAsBytes = Encoding.UTF8.GetBytes(subscribeQuote);
                ClientWebSocket.SendAsync(subscribeQuoteAsBytes, WebSocketMessageType.Text, true, CancellationToken.None).Wait();
                Console.WriteLine($"{subscribeQuote} sent.");

                ArraySegment<byte> bytesReceived = new ArraySegment<byte>(new byte[819200]);

                //Read from WebSocket
                while (ClientWebSocket.State == WebSocketState.Open)
                {
                    WebSocketReceiveResult receiveResult = ClientWebSocket.ReceiveAsync(bytesReceived, CancellationToken.None).Result;
                    byte[] compressedBytes = bytesReceived.Array[..receiveResult.Count];
                    byte[] decompressedBytes;

                    //Decompress json
                    using (MemoryStream compressedStream = new MemoryStream(compressedBytes))
                    {
                        using (GZipStream decompressionStream = new GZipStream(compressedStream, CompressionMode.Decompress))
                        {
                            using (MemoryStream decompressedStream = new MemoryStream())
                            {
                                decompressionStream.CopyTo(decompressedStream);
                                decompressedBytes = decompressedStream.ToArray();
                            }
                        }
                    }
                    string json = Encoding.UTF8.GetString(decompressedBytes);

                    // Handle ping
                    if (json.Contains("ping"))
                    {
                        pings++;
                        string pong = json.Replace("ping", "pong");
                        Console.WriteLine($"{json} received");
                        byte[] pongAsBytes = Encoding.UTF8.GetBytes(pong);
                        ClientWebSocket.SendAsync(pongAsBytes, WebSocketMessageType.Text, true, CancellationToken.None).Wait();
                        Console.WriteLine($"{pong} sent");
                        Console.WriteLine($"{pings} pings received");
                    }
                    else
                    {
                        // Uncomment to see that subscription worked
                        //Console.WriteLine(json);
                    }
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);

                Console.WriteLine($"Huobi closed WebSocket after {pings} pings.");
            }
        }
    }
}

Here is the output of my program:

{ "sub": "market.btcusdt.trade.detail", "id": "btcusdt trade"} sent.
{ "sub": "market.btcusdt.bbo", "id": "btcusdt bbo"} sent.
{"ping":1627090272242} received
{"pong":1627090272242} sent
1 pings received
{"ping":1627090277234} received
{"pong":1627090277234} sent
2 pings received
{"ping":1627090282238} received
{"pong":1627090282238} sent
3 pings received
{"ping":1627090287239} received
{"pong":1627090287239} sent
4 pings received
{"ping":1627090292238} received
{"pong":1627090292238} sent
5 pings received
{"ping":1627090297241} received
{"pong":1627090297241} sent
6 pings received
System.AggregateException: One or more errors occurred. (The remote party closed the WebSocket connection without completing the close handshake.)
 ---> System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake.
   at System.Net.WebSockets.ManagedWebSocket.ThrowIfEOFUnexpected(Boolean throwOnPrematureClosure)
   at System.Net.WebSockets.ManagedWebSocket.EnsureBufferContainsAsync(Int32 minimumRequiredBytes, CancellationToken cancellationToken, Boolean throwOnPrematureClosure)
   at System.Net.WebSockets.ManagedWebSocket.ReceiveAsyncPrivate[TWebSocketReceiveResultGetter,TWebSocketReceiveResult](Memory`1 payloadBuffer, CancellationToken cancellationToken, TWebSocketReceiveResultGetter resultGetter)
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at System.Threading.Tasks.Task`1.get_Result()
   at HuobiMarketWebSocketTest.HuobiMarketWebSocketTest.Main(String[] args) in C:\home\source\csharp\prog\HuobiMarketWebSocketTest - pong not recieved\HuobiMarketWebSocketTest\HuobiMarketWebSocketTest\HuobiMarketWebSocketTest.cs:line 156
Huobi closed WebSocket after 6 pings.

1 Answers1

2

The disconnect happens because the .NET ClientWebsocket uses ManagedWebSocket internally which sends an unsolicited pong frame as part of it's keep-alive handling. This is correct behaviour according to the websocket standard. However, it appears to cause an issue with Huobi's server for Futures and Swaps (but not spot). If you set the ClientwebSocket.Options.KeepAliveInterval to TimeSpan.Zero this avoids the ManagedWebSocket creating a keep alive timer and thus stops the unsolicited pong frame from being set. The downside of doing this is that the session moves to having 1 way heartbeating from 2 way heartbeating, but since there is heartbeating at the application level, there is no real downside here.

Alexis
  • 1,325
  • 1
  • 11
  • 14