using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net
{
///
/// Base for socket client implementations
///
public abstract class SocketClient: BaseClient, ISocketClient
{
#region fields
///
/// The factory for creating sockets. Used for unit testing
///
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
///
/// List of socket connections currently connecting/connected
///
protected internal ConcurrentDictionary sockets = new ConcurrentDictionary();
///
///
protected internal readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1);
///
public TimeSpan ReconnectInterval { get; }
///
public bool AutoReconnect { get; }
///
public TimeSpan ResponseTimeout { get; }
///
public TimeSpan SocketNoDataTimeout { get; }
///
/// The max amount of concurrent socket connections
///
public int MaxSocketConnections { get; protected set; } = 9999;
///
public int SocketCombineTarget { get; protected set; }
///
/// Handler for byte data
///
protected Func? dataInterpreterBytes;
///
/// Handler for string data
///
protected Func? dataInterpreterString;
///
/// Generic handlers
///
protected Dictionary> genericHandlers = new Dictionary>();
///
/// Periodic task
///
protected Task? periodicTask;
///
/// Periodic task event
///
protected AutoResetEvent? periodicEvent;
///
/// Is disposing
///
protected bool disposing;
///
/// If true; data which is a response to a query will also be distributed to subscriptions
/// If false; data which is a response to a query won't get forwarded to subscriptions as well
///
protected internal bool ContinueOnQueryResponse { get; protected set; }
#endregion
///
/// Create a socket client
///
/// Client options
/// Authentication provider
protected SocketClient(SocketClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeOptions, authenticationProvider)
{
if (exchangeOptions == null)
throw new ArgumentNullException(nameof(exchangeOptions));
AutoReconnect = exchangeOptions.AutoReconnect;
ReconnectInterval = exchangeOptions.ReconnectInterval;
ResponseTimeout = exchangeOptions.SocketResponseTimeout;
SocketNoDataTimeout = exchangeOptions.SocketNoDataTimeout;
SocketCombineTarget = exchangeOptions.SocketSubscriptionsCombineTarget ?? 1;
}
///
/// Set a function to interpret the data, used when the data is received as bytes instead of a string
///
/// Handler for byte data
/// Handler for string data
protected void SetDataInterpreter(Func? byteHandler, Func? stringHandler)
{
dataInterpreterBytes = byteHandler;
dataInterpreterString = stringHandler;
}
///
/// Subscribe
///
/// The expected return data
/// The request to send
/// The identifier to use
/// If the subscription should be authenticated
/// The handler of update data
///
protected virtual Task> Subscribe(object? request, string? identifier, bool authenticated, Action dataHandler)
{
return Subscribe(BaseAddress, request, identifier, authenticated, dataHandler);
}
///
/// Subscribe using a specif URL
///
/// The type of the expected data
/// The URL to connect to
/// The request to send
/// The identifier to use
/// If the subscription should be authenticated
/// The handler of update data
///
protected virtual async Task> Subscribe(string url, object? request, string? identifier, bool authenticated, Action dataHandler)
{
SocketConnection socket;
SocketSubscription handler;
var released = false;
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
socket = GetWebsocket(url, authenticated);
handler = AddHandler(request, identifier, true, socket, dataHandler);
if (SocketCombineTarget == 1)
{
// Can release early when only a single sub per connection
semaphoreSlim.Release();
released = true;
}
var connectResult = await ConnectIfNeeded(socket, authenticated).ConfigureAwait(false);
if (!connectResult)
return new CallResult(null, connectResult.Error);
}
finally
{
//When the task is ready, release the semaphore. It is vital to ALWAYS release the semaphore when we are ready, or else we will end up with a Semaphore that is forever locked.
//This is why it is important to do the Release within a try...finally clause; program execution may crash or take a different path, this way you are guaranteed execution
if(!released)
semaphoreSlim.Release();
}
if (socket.PausedActivity)
{
log.Write(LogVerbosity.Info, "Socket has been paused, can't subscribe at this moment");
return new CallResult(default, new ServerError("Socket is paused"));
}
if (request != null)
{
var subResult = await SubscribeAndWait(socket, request, handler).ConfigureAwait(false);
if (!subResult)
{
await socket.Close(handler).ConfigureAwait(false);
return new CallResult(null, subResult.Error);
}
}
else
{
handler.Confirmed = true;
}
socket.ShouldReconnect = true;
return new CallResult(new UpdateSubscription(socket, handler), null);
}
///
/// Sends the subscribe request and waits for a response to that request
///
/// The connection to send the request on
/// The request to send
/// The subscription the request is for
///
protected internal virtual async Task> SubscribeAndWait(SocketConnection socket, object request, SocketSubscription subscription)
{
CallResult