mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-06-08 00:16:27 +00:00
Added dedicated request websocket connection support
This commit is contained in:
parent
64ee50d98c
commit
3e5a34fb56
@ -8,6 +8,7 @@ using CryptoExchange.Net.RateLimiting.Interfaces;
|
|||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -67,6 +68,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
|
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of address to keep an alive connection to
|
||||||
|
/// </summary>
|
||||||
|
protected List<DedicatedConnectionConfig> DedicatedConnectionConfigs { get; set; } = new List<DedicatedConnectionConfig>();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public double IncomingKbps
|
public double IncomingKbps
|
||||||
{
|
{
|
||||||
@ -131,6 +137,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
|
protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Keep an open connection to this url
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url"></param>
|
||||||
|
/// <param name="auth"></param>
|
||||||
|
protected virtual void SetDedicatedConnection(string url, bool auth)
|
||||||
|
{
|
||||||
|
DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth });
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a query to periodically send on each connection
|
/// Add a query to periodically send on each connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -193,7 +209,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
// Get a new or existing socket connection
|
// Get a new or existing socket connection
|
||||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.As<UpdateSubscription>(null);
|
return socketResult.As<UpdateSubscription>(null);
|
||||||
|
|
||||||
@ -311,7 +327,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var socketResult = await GetSocketConnection(url, query.Authenticated).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.As<THandlerResponse>(default);
|
return socketResult.As<THandlerResponse>(default);
|
||||||
|
|
||||||
@ -455,19 +471,31 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="address">The address the socket is for</param>
|
/// <param name="address">The address the socket is for</param>
|
||||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||||
|
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated)
|
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection)
|
||||||
{
|
{
|
||||||
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
||||||
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||||
&& s.Value.ApiClient.GetType() == GetType()
|
&& s.Value.ApiClient.GetType() == GetType()
|
||||||
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault();
|
&& (s.Value.Authenticated == authenticated || !authenticated)
|
||||||
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
&& s.Value.Connected);
|
||||||
if (result != null)
|
|
||||||
|
SocketConnection connection;
|
||||||
|
if (!dedicatedRequestConnection)
|
||||||
{
|
{
|
||||||
if (result.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget))
|
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection).FirstOrDefault().Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connection != null)
|
||||||
|
{
|
||||||
|
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget))
|
||||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
||||||
return new CallResult<SocketConnection>(result);
|
return new CallResult<SocketConnection>(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||||
@ -484,6 +512,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
var socket = CreateSocket(connectionAddress.Data!);
|
var socket = CreateSocket(connectionAddress.Data!);
|
||||||
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
||||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||||
|
socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
|
||||||
|
|
||||||
foreach (var ptg in PeriodicTaskRegistrations)
|
foreach (var ptg in PeriodicTaskRegistrations)
|
||||||
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
|
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
|
||||||
@ -603,8 +632,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
{
|
{
|
||||||
var socketList = socketConnections.Values;
|
var socketList = socketConnections.Values;
|
||||||
foreach (var sub in socketList)
|
foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection))
|
||||||
tasks.Add(sub.CloseAsync());
|
tasks.Add(connection.CloseAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
@ -627,6 +656,23 @@ namespace CryptoExchange.Net.Clients
|
|||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual async Task<CallResult> PrepareConnectionsAsync()
|
||||||
|
{
|
||||||
|
foreach (var item in DedicatedConnectionConfigs)
|
||||||
|
{
|
||||||
|
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false);
|
||||||
|
if (!socketResult)
|
||||||
|
return socketResult.AsDataless();
|
||||||
|
|
||||||
|
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated).ConfigureAwait(false);
|
||||||
|
if (!connectResult)
|
||||||
|
return new CallResult(connectResult.Error!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CallResult(null);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Log the current state of connections and subscriptions
|
/// Log the current state of connections and subscriptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -710,11 +756,18 @@ namespace CryptoExchange.Net.Clients
|
|||||||
public override void Dispose()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
_disposing = true;
|
_disposing = true;
|
||||||
if (socketConnections.Sum(s => s.Value.UserSubscriptionCount) > 0)
|
var tasks = new List<Task>();
|
||||||
{
|
{
|
||||||
|
var socketList = socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
|
||||||
|
if (socketList.Any())
|
||||||
_logger.DisposingSocketClient();
|
_logger.DisposingSocketClient();
|
||||||
_ = UnsubscribeAllAsync();
|
|
||||||
|
foreach (var connection in socketList)
|
||||||
|
{
|
||||||
|
tasks.Add(connection.CloseAsync());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
semaphoreSlim?.Dispose();
|
semaphoreSlim?.Dispose();
|
||||||
base.Dispose();
|
base.Dispose();
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -59,5 +60,11 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <param name="subscription">The subscription to unsubscribe</param>
|
/// <param name="subscription">The subscription to unsubscribe</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task UnsubscribeAsync(UpdateSubscription subscription);
|
Task UnsubscribeAsync(UpdateSubscription subscription);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepare connections which can subsequently be used for sending websocket requests.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<CallResult> PrepareConnectionsAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
21
CryptoExchange.Net/Sockets/DedicatedConnectionConfig.cs
Normal file
21
CryptoExchange.Net/Sockets/DedicatedConnectionConfig.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Sockets
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dedicated connection configuration
|
||||||
|
/// </summary>
|
||||||
|
public class DedicatedConnectionConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Socket address
|
||||||
|
/// </summary>
|
||||||
|
public string SocketAddress { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// authenticated
|
||||||
|
/// </summary>
|
||||||
|
public bool Authenticated { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -175,6 +175,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this connection should be kept alive even when there is no subscription
|
||||||
|
/// </summary>
|
||||||
|
public bool DedicatedRequestConnection { get; internal set; }
|
||||||
|
|
||||||
private bool _pausedActivity;
|
private bool _pausedActivity;
|
||||||
private readonly object _listenersLock;
|
private readonly object _listenersLock;
|
||||||
private readonly List<IMessageProcessor> _listeners;
|
private readonly List<IMessageProcessor> _listeners;
|
||||||
@ -608,7 +613,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
bool shouldCloseConnection;
|
bool shouldCloseConnection;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
{
|
{
|
||||||
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed);
|
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection;
|
||||||
if (shouldCloseConnection)
|
if (shouldCloseConnection)
|
||||||
Status = SocketStatus.Closing;
|
Status = SocketStatus.Closing;
|
||||||
}
|
}
|
||||||
@ -811,6 +816,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
return new CallResult(new WebError("Socket not connected"));
|
return new CallResult(new WebError("Socket not connected"));
|
||||||
|
|
||||||
|
if (!DedicatedRequestConnection)
|
||||||
|
{
|
||||||
bool anySubscriptions;
|
bool anySubscriptions;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
|
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
|
||||||
@ -821,10 +828,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_ = _socket.CloseAsync();
|
_ = _socket.CloseAsync();
|
||||||
return new CallResult(null);
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool anyAuthenticated;
|
bool anyAuthenticated;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated);
|
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated) || DedicatedRequestConnection;
|
||||||
if (anyAuthenticated)
|
if (anyAuthenticated)
|
||||||
{
|
{
|
||||||
// If we reconnected a authenticated connection we need to re-authenticate
|
// If we reconnected a authenticated connection we need to re-authenticate
|
||||||
|
@ -30,9 +30,14 @@ namespace CryptoExchange.Net.Testing.Implementations
|
|||||||
public bool IsClosed => !Connected;
|
public bool IsClosed => !Connected;
|
||||||
public bool IsOpen => Connected;
|
public bool IsOpen => Connected;
|
||||||
public double IncomingKbps => 0;
|
public double IncomingKbps => 0;
|
||||||
public Uri Uri => new("wss://test.com/ws");
|
public Uri Uri { get; set; }
|
||||||
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||||
|
|
||||||
|
public TestSocket(string address)
|
||||||
|
{
|
||||||
|
Uri = new Uri(address);
|
||||||
|
}
|
||||||
|
|
||||||
public Task<CallResult> ConnectAsync()
|
public Task<CallResult> ConnectAsync()
|
||||||
{
|
{
|
||||||
Connected = CanConnect;
|
Connected = CanConnect;
|
||||||
|
@ -50,13 +50,15 @@ namespace CryptoExchange.Net.Testing
|
|||||||
/// <param name="name">Method name for looking up json test values</param>
|
/// <param name="name">Method name for looking up json test values</param>
|
||||||
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
||||||
/// <param name="ignoreProperties">Ignore certain properties</param>
|
/// <param name="ignoreProperties">Ignore certain properties</param>
|
||||||
|
/// <param name="addressPath">Path</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
/// <exception cref="Exception"></exception>
|
/// <exception cref="Exception"></exception>
|
||||||
public async Task ValidateAsync<TUpdate>(
|
public async Task ValidateAsync<TUpdate>(
|
||||||
Func<TClient, Action<DataEvent<TUpdate>>, Task<CallResult<UpdateSubscription>>> methodInvoke,
|
Func<TClient, Action<DataEvent<TUpdate>>, Task<CallResult<UpdateSubscription>>> methodInvoke,
|
||||||
string name,
|
string name,
|
||||||
string? nestedJsonProperty = null,
|
string? nestedJsonProperty = null,
|
||||||
List<string>? ignoreProperties = null)
|
List<string>? ignoreProperties = null,
|
||||||
|
string? addressPath = null)
|
||||||
{
|
{
|
||||||
var listener = new EnumValueTraceListener();
|
var listener = new EnumValueTraceListener();
|
||||||
Trace.Listeners.Add(listener);
|
Trace.Listeners.Add(listener);
|
||||||
@ -79,7 +81,7 @@ namespace CryptoExchange.Net.Testing
|
|||||||
var data = Encoding.UTF8.GetString(buffer);
|
var data = Encoding.UTF8.GetString(buffer);
|
||||||
using var reader = new StringReader(data);
|
using var reader = new StringReader(data);
|
||||||
|
|
||||||
var socket = TestHelpers.ConfigureSocketClient(_client);
|
var socket = TestHelpers.ConfigureSocketClient(_client, addressPath == null ? _baseAddress : _baseAddress.AppendPath(addressPath));
|
||||||
|
|
||||||
var waiter = new AutoResetEvent(false);
|
var waiter = new AutoResetEvent(false);
|
||||||
string? lastMessage = null;
|
string? lastMessage = null;
|
||||||
|
@ -57,9 +57,9 @@ namespace CryptoExchange.Net.Testing
|
|||||||
return self == to;
|
return self == to;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static TestSocket ConfigureSocketClient<T>(T client) where T : BaseSocketClient
|
internal static TestSocket ConfigureSocketClient<T>(T client, string address) where T : BaseSocketClient
|
||||||
{
|
{
|
||||||
var socket = new TestSocket();
|
var socket = new TestSocket(address);
|
||||||
foreach (var apiClient in client.ApiClients.OfType<SocketApiClient>())
|
foreach (var apiClient in client.ApiClients.OfType<SocketApiClient>())
|
||||||
{
|
{
|
||||||
apiClient.SocketFactory = new TestWebsocketFactory(socket);
|
apiClient.SocketFactory = new TestWebsocketFactory(socket);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user