mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 277be7ab9b | |||
| 45f3459f59 | |||
| 98dad4a8ed | |||
| 1e5f19271b | |||
| 8abeeb4cf0 | |||
| cae0cd9ead | |||
| 811574ae01 | |||
| 0ddecf7f8d | |||
| 5bcf50fb4d | |||
| 9f0654815d | |||
| 465e9f04f4 | |||
| 7c8cbfa4e2 | |||
| 4c79d13ff9 | |||
| c815fad135 | |||
| 41f17d0378 | |||
| 50715ff2f7 | |||
| ea9375d582 | |||
| 2cf3c93e5e | |||
| ca888d8e41 | |||
| 2040b1c175 | |||
| d451c18821 | |||
| c13dfa4461 | |||
| c2080ef75f | |||
| 6b252e8024 | |||
| d06bd5f176 | |||
| d55fc8da65 | |||
| 01184f2c5d | |||
| cadc93c2f0 | |||
| 2600a51461 | |||
| 9e6a86ba8b | |||
| c4430d63fa | |||
| f3e1cfef33 | |||
| cc3053719c | |||
| cd6907e601 | |||
| 8fe00693bd | |||
| fb90d1e015 | |||
| 4b44861e43 | |||
| e42ca4ab5a | |||
| 5b97f6dd67 | |||
| a9813ecb0a | |||
| c7069a4049 | |||
| 5683ae0b3c | |||
| 1c8cf5ac98 |
@@ -6,10 +6,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<packagereference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02"></packagereference>
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02"></PackageReference>
|
||||||
<PackageReference Include="Moq" Version="4.16.1" />
|
<PackageReference Include="Moq" Version="4.16.1" />
|
||||||
<packagereference Include="NUnit" Version="3.13.2"></packagereference>
|
<PackageReference Include="NUnit" Version="3.13.2"></PackageReference>
|
||||||
<packagereference Include="NUnit3TestAdapter" Version="4.2.0"></packagereference>
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0"></PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
socket.CanConnect = canConnect;
|
socket.CanConnect = canConnect;
|
||||||
|
|
||||||
//act
|
//act
|
||||||
var connectResult = client.ConnectSocketSub(new SocketConnection(client, null, socket));
|
var connectResult = client.ConnectSocketSub(new SocketConnection(client, null, socket, null));
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
Assert.IsTrue(connectResult.Success == canConnect);
|
Assert.IsTrue(connectResult.Success == canConnect);
|
||||||
@@ -57,10 +57,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
socket.ShouldReconnect = true;
|
socket.ShouldReconnect = true;
|
||||||
socket.CanConnect = true;
|
socket.CanConnect = true;
|
||||||
socket.DisconnectTime = DateTime.UtcNow;
|
socket.DisconnectTime = DateTime.UtcNow;
|
||||||
var sub = new SocketConnection(client, null, socket);
|
var sub = new SocketConnection(client, null, socket, null);
|
||||||
var rstEvent = new ManualResetEvent(false);
|
var rstEvent = new ManualResetEvent(false);
|
||||||
JToken result = null;
|
JToken result = null;
|
||||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
|
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
|
||||||
{
|
{
|
||||||
result = messageEvent.JsonData;
|
result = messageEvent.JsonData;
|
||||||
rstEvent.Set();
|
rstEvent.Set();
|
||||||
@@ -85,10 +85,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
socket.ShouldReconnect = true;
|
socket.ShouldReconnect = true;
|
||||||
socket.CanConnect = true;
|
socket.CanConnect = true;
|
||||||
socket.DisconnectTime = DateTime.UtcNow;
|
socket.DisconnectTime = DateTime.UtcNow;
|
||||||
var sub = new SocketConnection(client, null, socket);
|
var sub = new SocketConnection(client, null, socket, null);
|
||||||
var rstEvent = new ManualResetEvent(false);
|
var rstEvent = new ManualResetEvent(false);
|
||||||
string original = null;
|
string original = null;
|
||||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
|
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
|
||||||
{
|
{
|
||||||
original = messageEvent.OriginalData;
|
original = messageEvent.OriginalData;
|
||||||
rstEvent.Set();
|
rstEvent.Set();
|
||||||
@@ -103,34 +103,6 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
Assert.IsTrue(original == (enabled ? "{\"property\": 123}" : null));
|
Assert.IsTrue(original == (enabled ? "{\"property\": 123}" : null));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
|
||||||
public void DisconnectedSocket_Should_Reconnect()
|
|
||||||
{
|
|
||||||
// arrange
|
|
||||||
bool reconnected = false;
|
|
||||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
|
||||||
var socket = client.CreateSocket();
|
|
||||||
socket.ShouldReconnect = true;
|
|
||||||
socket.CanConnect = true;
|
|
||||||
socket.DisconnectTime = DateTime.UtcNow;
|
|
||||||
var sub = new SocketConnection(client, null, socket);
|
|
||||||
sub.ShouldReconnect = true;
|
|
||||||
client.ConnectSocketSub(sub);
|
|
||||||
var rstEvent = new ManualResetEvent(false);
|
|
||||||
sub.ConnectionRestored += (a) =>
|
|
||||||
{
|
|
||||||
reconnected = true;
|
|
||||||
rstEvent.Set();
|
|
||||||
};
|
|
||||||
|
|
||||||
// act
|
|
||||||
socket.InvokeClose();
|
|
||||||
rstEvent.WaitOne(1000);
|
|
||||||
|
|
||||||
// assert
|
|
||||||
Assert.IsTrue(reconnected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase()]
|
[TestCase()]
|
||||||
public void UnsubscribingStream_Should_CloseTheSocket()
|
public void UnsubscribingStream_Should_CloseTheSocket()
|
||||||
{
|
{
|
||||||
@@ -138,9 +110,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
socket.CanConnect = true;
|
||||||
var sub = new SocketConnection(client, null, socket);
|
var sub = new SocketConnection(client, null, socket, null);
|
||||||
client.ConnectSocketSub(sub);
|
client.ConnectSocketSub(sub);
|
||||||
var ups = new UpdateSubscription(sub, SocketSubscription.CreateForIdentifier(10, "Test", true, (e) => {}));
|
var us = SocketSubscription.CreateForIdentifier(10, "Test", true, false, (e) => { });
|
||||||
|
var ups = new UpdateSubscription(sub, us);
|
||||||
|
sub.AddSubscription(us);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
client.UnsubscribeAsync(ups).Wait();
|
client.UnsubscribeAsync(ups).Wait();
|
||||||
@@ -158,8 +132,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var socket2 = client.CreateSocket();
|
var socket2 = client.CreateSocket();
|
||||||
socket1.CanConnect = true;
|
socket1.CanConnect = true;
|
||||||
socket2.CanConnect = true;
|
socket2.CanConnect = true;
|
||||||
var sub1 = new SocketConnection(client, null, socket1);
|
var sub1 = new SocketConnection(client, null, socket1, null);
|
||||||
var sub2 = new SocketConnection(client, null, socket2);
|
var sub2 = new SocketConnection(client, null, socket2, null);
|
||||||
client.ConnectSocketSub(sub1);
|
client.ConnectSocketSub(sub1);
|
||||||
client.ConnectSocketSub(sub2);
|
client.ConnectSocketSub(sub2);
|
||||||
|
|
||||||
@@ -178,7 +152,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
socket.CanConnect = false;
|
socket.CanConnect = false;
|
||||||
var sub = new SocketConnection(client, null, socket);
|
var sub = new SocketConnection(client, null, socket, null);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
var connectResult = client.ConnectSocketSub(sub);
|
var connectResult = client.ConnectSocketSub(sub);
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override TimeSyncInfo GetTimeSyncInfo()
|
public override TimeSyncInfo GetTimeSyncInfo()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -161,7 +161,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override TimeSyncInfo GetTimeSyncInfo()
|
public override TimeSyncInfo GetTimeSyncInfo()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,15 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
public bool Connected { get; set; }
|
public bool Connected { get; set; }
|
||||||
|
|
||||||
public event Action OnClose;
|
public event Action OnClose;
|
||||||
|
|
||||||
|
#pragma warning disable 0067
|
||||||
|
public event Action OnReconnected;
|
||||||
|
public event Action OnReconnecting;
|
||||||
|
#pragma warning restore 0067
|
||||||
public event Action<string> OnMessage;
|
public event Action<string> OnMessage;
|
||||||
public event Action<Exception> OnError;
|
public event Action<Exception> OnError;
|
||||||
public event Action OnOpen;
|
public event Action OnOpen;
|
||||||
|
public Func<Task<Uri>> GetReconnectionUrl { get; set; }
|
||||||
|
|
||||||
public int Id { get; }
|
public int Id { get; }
|
||||||
public bool ShouldReconnect { get; set; }
|
public bool ShouldReconnect { get; set; }
|
||||||
@@ -40,6 +46,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
public Uri Uri => new Uri("");
|
public Uri Uri => new Uri("");
|
||||||
|
|
||||||
|
public TimeSpan KeepAliveInterval { get; set; }
|
||||||
|
|
||||||
public static int lastId = 0;
|
public static int lastId = 0;
|
||||||
public static object lastIdLock = new object();
|
public static object lastIdLock = new object();
|
||||||
|
|
||||||
@@ -91,6 +99,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
{
|
{
|
||||||
Connected = false;
|
Connected = false;
|
||||||
DisconnectTime = DateTime.UtcNow;
|
DisconnectTime = DateTime.UtcNow;
|
||||||
|
Reconnecting = true;
|
||||||
OnClose?.Invoke();
|
OnClose?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,11 +122,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
{
|
{
|
||||||
OnError?.Invoke(error);
|
OnError?.Invoke(error);
|
||||||
}
|
}
|
||||||
|
public Task ReconnectAsync() => Task.CompletedTask;
|
||||||
public async Task ProcessAsync()
|
|
||||||
{
|
|
||||||
while (Connected)
|
|
||||||
await Task.Delay(50);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
{
|
{
|
||||||
SubClient = new TestSubSocketClient(exchangeOptions, exchangeOptions.SubOptions);
|
SubClient = new TestSubSocketClient(exchangeOptions, exchangeOptions.SubOptions);
|
||||||
SocketFactory = new Mock<IWebsocketFactory>().Object;
|
SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||||
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
|
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestSocket CreateSocket()
|
public TestSocket CreateSocket()
|
||||||
{
|
{
|
||||||
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
|
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||||
return (TestSocket)CreateSocket("123");
|
return (TestSocket)CreateSocket("https://localhost:123/");
|
||||||
}
|
}
|
||||||
|
|
||||||
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
|
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ namespace CryptoExchange.Net
|
|||||||
log = new Log(name);
|
log = new Log(name);
|
||||||
log.UpdateWriters(options.LogWriters);
|
log.UpdateWriters(options.LogWriters);
|
||||||
log.Level = options.LogLevel;
|
log.Level = options.LogLevel;
|
||||||
|
options.OnLoggingChanged += HandleLogConfigChange;
|
||||||
|
|
||||||
ClientOptions = options;
|
ClientOptions = options;
|
||||||
|
|
||||||
@@ -282,12 +283,22 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle a change in the client options log config
|
||||||
|
/// </summary>
|
||||||
|
private void HandleLogConfigChange()
|
||||||
|
{
|
||||||
|
log.UpdateWriters(ClientOptions.LogWriters);
|
||||||
|
log.Level = ClientOptions.LogLevel;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose
|
/// Dispose
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Dispose()
|
public virtual void Dispose()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Debug, "Disposing client");
|
log.Write(LogLevel.Debug, "Disposing client");
|
||||||
|
ClientOptions.OnLoggingChanged -= HandleLogConfigChange;
|
||||||
foreach (var client in ApiClients)
|
foreach (var client in ApiClients)
|
||||||
client.Dispose();
|
client.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,11 +172,17 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
if (signed)
|
if (signed)
|
||||||
{
|
{
|
||||||
var syncTimeResult = await apiClient.SyncTimeAsync().ConfigureAwait(false);
|
var syncTask = apiClient.SyncTimeAsync();
|
||||||
if (!syncTimeResult)
|
var timeSyncInfo = apiClient.GetTimeSyncInfo();
|
||||||
|
if (timeSyncInfo.TimeSyncState.LastSyncTime == default)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
|
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
|
||||||
return syncTimeResult.As<IRequest>(default);
|
var syncTimeResult = await syncTask.ConfigureAwait(false);
|
||||||
|
if (!syncTimeResult)
|
||||||
|
{
|
||||||
|
log.Write(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
|
||||||
|
return syncTimeResult.As<IRequest>(default);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,7 +320,7 @@ namespace CryptoExchange.Net
|
|||||||
responseStream.Close();
|
responseStream.Close();
|
||||||
response.Close();
|
response.Close();
|
||||||
var parseResult = ValidateJson(data);
|
var parseResult = ValidateJson(data);
|
||||||
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : parseResult.Error!;
|
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : new ServerError(data)!;
|
||||||
if(error.Code == null || error.Code == 0)
|
if(error.Code == null || error.Code == 0)
|
||||||
error.Code = (int)response.StatusCode;
|
error.Code = (int)response.StatusCode;
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using System.Collections.Concurrent;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.WebSockets;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
@@ -11,7 +11,6 @@ using CryptoExchange.Net.Interfaces;
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
@@ -36,9 +35,9 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal readonly SemaphoreSlim semaphoreSlim = new(1);
|
protected internal readonly SemaphoreSlim semaphoreSlim = new(1);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The max amount of concurrent socket connections
|
/// Keep alive interval for websocket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected int MaxSocketConnections { get; set; } = 9999;
|
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
|
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -92,6 +91,20 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int CurrentConnections => socketConnections.Count;
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int CurrentSubscriptions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!socketConnections.Any())
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return socketConnections.Sum(s => s.Value.SubscriptionCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Client options
|
/// Client options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -106,10 +119,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="options">The options for this client</param>
|
/// <param name="options">The options for this client</param>
|
||||||
protected BaseSocketClient(string name, BaseSocketClientOptions options) : base(name, options)
|
protected BaseSocketClient(string name, BaseSocketClientOptions options) : base(name, options)
|
||||||
{
|
{
|
||||||
if (options == null)
|
ClientOptions = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
throw new ArgumentNullException(nameof(options));
|
|
||||||
|
|
||||||
ClientOptions = options;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -164,7 +174,7 @@ namespace CryptoExchange.Net
|
|||||||
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
||||||
|
|
||||||
SocketConnection socketConnection;
|
SocketConnection socketConnection;
|
||||||
SocketSubscription subscription;
|
SocketSubscription? subscription;
|
||||||
var released = false;
|
var released = false;
|
||||||
// Wait for a semaphore here, so we only connect 1 socket at a time.
|
// Wait for a semaphore here, so we only connect 1 socket at a time.
|
||||||
// This is necessary for being able to see if connections can be combined
|
// This is necessary for being able to see if connections can be combined
|
||||||
@@ -179,23 +189,38 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get a new or existing socket connection
|
while (true)
|
||||||
socketConnection = GetSocketConnection(apiClient, url, authenticated);
|
|
||||||
|
|
||||||
// Add a subscription on the socket connection
|
|
||||||
subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler);
|
|
||||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
|
||||||
{
|
{
|
||||||
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
|
// Get a new or existing socket connection
|
||||||
semaphoreSlim.Release();
|
var socketResult = await GetSocketConnection(apiClient, url, authenticated).ConfigureAwait(false);
|
||||||
released = true;
|
if(!socketResult)
|
||||||
|
return socketResult.As<UpdateSubscription>(null);
|
||||||
|
|
||||||
|
socketConnection = socketResult.Data;
|
||||||
|
|
||||||
|
// Add a subscription on the socket connection
|
||||||
|
subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler, authenticated);
|
||||||
|
if (subscription == null)
|
||||||
|
{
|
||||||
|
log.Write(LogLevel.Trace, $"Socket {socketConnection.SocketId} failed to add subscription, retrying on different connection");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||||
|
{
|
||||||
|
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
|
||||||
|
semaphoreSlim.Release();
|
||||||
|
released = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var needsConnecting = !socketConnection.Connected;
|
||||||
|
|
||||||
|
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
|
||||||
|
if (!connectResult)
|
||||||
|
return new CallResult<UpdateSubscription>(connectResult.Error!);
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var needsConnecting = !socketConnection.Connected;
|
|
||||||
|
|
||||||
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
|
|
||||||
if (!connectResult)
|
|
||||||
return new CallResult<UpdateSubscription>(connectResult.Error!);
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -215,6 +240,7 @@ namespace CryptoExchange.Net
|
|||||||
var subResult = await SubscribeAndWaitAsync(socketConnection, request, subscription).ConfigureAwait(false);
|
var subResult = await SubscribeAndWaitAsync(socketConnection, request, subscription).ConfigureAwait(false);
|
||||||
if (!subResult)
|
if (!subResult)
|
||||||
{
|
{
|
||||||
|
log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} failed to subscribe: {subResult.Error}");
|
||||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||||
}
|
}
|
||||||
@@ -225,7 +251,6 @@ namespace CryptoExchange.Net
|
|||||||
subscription.Confirmed = true;
|
subscription.Confirmed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
socketConnection.ShouldReconnect = true;
|
|
||||||
if (ct != default)
|
if (ct != default)
|
||||||
{
|
{
|
||||||
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
||||||
@@ -235,7 +260,7 @@ namespace CryptoExchange.Net
|
|||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Write(LogLevel.Information, $"Socket {socketConnection.SocketId} subscription completed");
|
log.Write(LogLevel.Information, $"Socket {socketConnection.SocketId} subscription {subscription.Id} completed successfully");
|
||||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +320,12 @@ namespace CryptoExchange.Net
|
|||||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
socketConnection = GetSocketConnection(apiClient, url, authenticated);
|
var socketResult = await GetSocketConnection(apiClient, url, authenticated).ConfigureAwait(false);
|
||||||
|
if (!socketResult)
|
||||||
|
return socketResult.As<T>(default);
|
||||||
|
|
||||||
|
socketConnection = socketResult.Data;
|
||||||
|
|
||||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||||
{
|
{
|
||||||
// Can release early when only a single sub per connection
|
// Can release early when only a single sub per connection
|
||||||
@@ -309,8 +339,6 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
finally
|
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)
|
if (!released)
|
||||||
semaphoreSlim.Release();
|
semaphoreSlim.Release();
|
||||||
}
|
}
|
||||||
@@ -364,11 +392,14 @@ namespace CryptoExchange.Net
|
|||||||
if (!authenticated || socket.Authenticated)
|
if (!authenticated || socket.Authenticated)
|
||||||
return new CallResult<bool>(true);
|
return new CallResult<bool>(true);
|
||||||
|
|
||||||
|
log.Write(LogLevel.Debug, $"Attempting to authenticate {socket.SocketId}");
|
||||||
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
||||||
if (!result)
|
if (!result)
|
||||||
{
|
{
|
||||||
await socket.CloseAsync().ConfigureAwait(false);
|
|
||||||
log.Write(LogLevel.Warning, $"Socket {socket.SocketId} authentication failed");
|
log.Write(LogLevel.Warning, $"Socket {socket.SocketId} authentication failed");
|
||||||
|
if(socket.Connected)
|
||||||
|
await socket.CloseAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
||||||
return new CallResult<bool>(result.Error);
|
return new CallResult<bool>(result.Error);
|
||||||
}
|
}
|
||||||
@@ -443,9 +474,6 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual JToken ProcessTokenData(JToken message)
|
protected internal virtual JToken ProcessTokenData(JToken message)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
@@ -459,8 +487,9 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="userSubscription">Whether or not this is a user subscription (counts towards the max amount of handlers on a socket)</param>
|
/// <param name="userSubscription">Whether or not this is a user subscription (counts towards the max amount of handlers on a socket)</param>
|
||||||
/// <param name="connection">The socket connection the handler is on</param>
|
/// <param name="connection">The socket connection the handler is on</param>
|
||||||
/// <param name="dataHandler">The handler of the data received</param>
|
/// <param name="dataHandler">The handler of the data received</param>
|
||||||
|
/// <param name="authenticated">Whether the subscription needs authentication</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual SocketSubscription AddSubscription<T>(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action<DataEvent<T>> dataHandler)
|
protected virtual SocketSubscription? AddSubscription<T>(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action<DataEvent<T>> dataHandler, bool authenticated)
|
||||||
{
|
{
|
||||||
void InternalHandler(MessageEvent messageEvent)
|
void InternalHandler(MessageEvent messageEvent)
|
||||||
{
|
{
|
||||||
@@ -482,9 +511,10 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
|
|
||||||
var subscription = request == null
|
var subscription = request == null
|
||||||
? SocketSubscription.CreateForIdentifier(NextId(), identifier!, userSubscription, InternalHandler)
|
? SocketSubscription.CreateForIdentifier(NextId(), identifier!, userSubscription, authenticated, InternalHandler)
|
||||||
: SocketSubscription.CreateForRequest(NextId(), request, userSubscription, InternalHandler);
|
: SocketSubscription.CreateForRequest(NextId(), request, userSubscription, authenticated, InternalHandler);
|
||||||
connection.AddSubscription(subscription);
|
if (!connection.AddSubscription(subscription))
|
||||||
|
return null;
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,11 +526,34 @@ namespace CryptoExchange.Net
|
|||||||
protected void AddGenericHandler(string identifier, Action<MessageEvent> action)
|
protected void AddGenericHandler(string identifier, Action<MessageEvent> action)
|
||||||
{
|
{
|
||||||
genericHandlers.Add(identifier, action);
|
genericHandlers.Add(identifier, action);
|
||||||
var subscription = SocketSubscription.CreateForIdentifier(NextId(), identifier, false, action);
|
var subscription = SocketSubscription.CreateForIdentifier(NextId(), identifier, false, false, action);
|
||||||
foreach (var connection in socketConnections.Values)
|
foreach (var connection in socketConnections.Values)
|
||||||
connection.AddSubscription(subscription);
|
connection.AddSubscription(subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the url to connect to (defaults to BaseAddress form the client options)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiClient"></param>
|
||||||
|
/// <param name="address"></param>
|
||||||
|
/// <param name="authentication"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual Task<CallResult<string?>> GetConnectionUrlAsync(SocketApiClient apiClient, string address, bool authentication)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new CallResult<string?>(address));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the url to reconnect to after losing a connection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiClient"></param>
|
||||||
|
/// <param name="connection"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual Task<Uri?> GetReconnectUriAsync(SocketApiClient apiClient, SocketConnection connection)
|
||||||
|
{
|
||||||
|
return Task.FromResult<Uri?>(connection.ConnectionUri);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -508,32 +561,43 @@ namespace CryptoExchange.Net
|
|||||||
/// <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>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual SocketConnection GetSocketConnection(SocketApiClient apiClient, string address, bool authenticated)
|
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(SocketApiClient apiClient, string address, bool authenticated)
|
||||||
{
|
{
|
||||||
var socketResult = socketConnections.Where(s => s.Value.Uri.ToString().TrimEnd('/') == address.TrimEnd('/')
|
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
||||||
|
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||||
&& (s.Value.ApiClient.GetType() == apiClient.GetType())
|
&& (s.Value.ApiClient.GetType() == apiClient.GetType())
|
||||||
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.SubscriptionCount).FirstOrDefault();
|
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.SubscriptionCount).FirstOrDefault();
|
||||||
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
||||||
if (result != null)
|
if (result != null)
|
||||||
{
|
{
|
||||||
if (result.SubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= MaxSocketConnections && socketConnections.All(s => s.Value.SubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
|
if (result.SubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= ClientOptions.MaxSocketConnections && socketConnections.All(s => s.Value.SubscriptionCount >= 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 result;
|
return new CallResult<SocketConnection>(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var connectionAddress = await GetConnectionUrlAsync(apiClient, address, authenticated).ConfigureAwait(false);
|
||||||
|
if (!connectionAddress)
|
||||||
|
{
|
||||||
|
log.Write(LogLevel.Warning, $"Failed to determine connection url: " + connectionAddress.Error);
|
||||||
|
return connectionAddress.As<SocketConnection>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectionAddress.Data != address)
|
||||||
|
log.Write(LogLevel.Debug, $"Connection address set to " + connectionAddress.Data);
|
||||||
|
|
||||||
// Create new socket
|
// Create new socket
|
||||||
var socket = CreateSocket(address);
|
var socket = CreateSocket(connectionAddress.Data!);
|
||||||
var socketConnection = new SocketConnection(this, apiClient, socket);
|
var socketConnection = new SocketConnection(this, apiClient, socket, address);
|
||||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||||
foreach (var kvp in genericHandlers)
|
foreach (var kvp in genericHandlers)
|
||||||
{
|
{
|
||||||
var handler = SocketSubscription.CreateForIdentifier(NextId(), kvp.Key, false, kvp.Value);
|
var handler = SocketSubscription.CreateForIdentifier(NextId(), kvp.Key, false, false, kvp.Value);
|
||||||
socketConnection.AddSubscription(handler);
|
socketConnection.AddSubscription(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
return socketConnection;
|
return new CallResult<SocketConnection>(socketConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -561,6 +625,23 @@ namespace CryptoExchange.Net
|
|||||||
return new CallResult<bool>(new CantConnectError());
|
return new CallResult<bool>(new CantConnectError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get parameters for the websocket connection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">The address to connect to</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual WebSocketParameters GetWebSocketParameters(string address)
|
||||||
|
=> new (new Uri(address), ClientOptions.AutoReconnect)
|
||||||
|
{
|
||||||
|
DataInterpreterBytes = dataInterpreterBytes,
|
||||||
|
DataInterpreterString = dataInterpreterString,
|
||||||
|
KeepAliveInterval = KeepAliveInterval,
|
||||||
|
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||||
|
RatelimitPerSecond = RateLimitPerSocketPerSecond,
|
||||||
|
Proxy = ClientOptions.Proxy,
|
||||||
|
Timeout = ClientOptions.SocketNoDataTimeout
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a socket for an address
|
/// Create a socket for an address
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -568,23 +649,8 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual IWebsocket CreateSocket(string address)
|
protected virtual IWebsocket CreateSocket(string address)
|
||||||
{
|
{
|
||||||
var socket = SocketFactory.CreateWebsocket(log, address);
|
var socket = SocketFactory.CreateWebsocket(log, GetWebSocketParameters(address));
|
||||||
log.Write(LogLevel.Debug, $"Socket {socket.Id} new socket created for " + address);
|
log.Write(LogLevel.Debug, $"Socket {socket.Id} new socket created for " + address);
|
||||||
|
|
||||||
if (ClientOptions.Proxy != null)
|
|
||||||
socket.SetProxy(ClientOptions.Proxy);
|
|
||||||
|
|
||||||
socket.Timeout = ClientOptions.SocketNoDataTimeout;
|
|
||||||
socket.DataInterpreterBytes = dataInterpreterBytes;
|
|
||||||
socket.DataInterpreterString = dataInterpreterString;
|
|
||||||
socket.RatelimitPerSecond = RateLimitPerSocketPerSecond;
|
|
||||||
socket.OnError += e =>
|
|
||||||
{
|
|
||||||
if(e is WebSocketException wse)
|
|
||||||
log.Write(LogLevel.Warning, $"Socket {socket.Id} error: Websocket error code {wse.WebSocketErrorCode}, details: " + e.ToLogString());
|
|
||||||
else
|
|
||||||
log.Write(LogLevel.Warning, $"Socket {socket.Id} error: " + e.ToLogString());
|
|
||||||
};
|
|
||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,7 +708,6 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
||||||
{
|
{
|
||||||
|
|
||||||
SocketSubscription? subscription = null;
|
SocketSubscription? subscription = null;
|
||||||
SocketConnection? connection = null;
|
SocketConnection? connection = null;
|
||||||
foreach(var socket in socketConnections.Values.ToList())
|
foreach(var socket in socketConnections.Values.ToList())
|
||||||
@@ -658,7 +723,7 @@ namespace CryptoExchange.Net
|
|||||||
if (subscription == null || connection == null)
|
if (subscription == null || connection == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
log.Write(LogLevel.Information, "Closing subscription " + subscriptionId);
|
log.Write(LogLevel.Information, $"Socket {connection.SocketId} Unsubscribing subscription " + subscriptionId);
|
||||||
await connection.CloseAsync(subscription).ConfigureAwait(false);
|
await connection.CloseAsync(subscription).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,7 +737,7 @@ namespace CryptoExchange.Net
|
|||||||
if (subscription == null)
|
if (subscription == null)
|
||||||
throw new ArgumentNullException(nameof(subscription));
|
throw new ArgumentNullException(nameof(subscription));
|
||||||
|
|
||||||
log.Write(LogLevel.Information, "Closing subscription " + subscription.Id);
|
log.Write(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
|
||||||
await subscription.CloseAsync().ConfigureAwait(false);
|
await subscription.CloseAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,7 +747,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task UnsubscribeAllAsync()
|
public virtual async Task UnsubscribeAllAsync()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Information, $"Closing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
log.Write(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
{
|
{
|
||||||
var socketList = socketConnections.Values;
|
var socketList = socketConnections.Values;
|
||||||
@@ -693,6 +758,39 @@ namespace CryptoExchange.Net
|
|||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect all connections
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual async Task ReconnectAsync()
|
||||||
|
{
|
||||||
|
log.Write(LogLevel.Information, $"Reconnecting all {socketConnections.Count} connections");
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
{
|
||||||
|
var socketList = socketConnections.Values;
|
||||||
|
foreach (var sub in socketList)
|
||||||
|
tasks.Add(sub.TriggerReconnectAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Log the current state of connections and subscriptions
|
||||||
|
/// </summary>
|
||||||
|
public string GetSubscriptionsState()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"{socketConnections.Count} connections, {CurrentSubscriptions} subscriptions, kbps: {IncomingKbps}");
|
||||||
|
foreach(var connection in socketConnections)
|
||||||
|
{
|
||||||
|
sb.AppendLine($" Connection {connection.Key}: {connection.Value.SubscriptionCount} subscriptions, status: {connection.Value.Status}, authenticated: {connection.Value.Authenticated}, kbps: {connection.Value.IncomingKbps}");
|
||||||
|
foreach (var subscription in connection.Value.Subscriptions)
|
||||||
|
sb.AppendLine($" Subscription {subscription.Id}, authenticated: {subscription.Authenticated}, confirmed: {subscription.Confirmed}");
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose the client
|
/// Dispose the client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -701,8 +799,11 @@ namespace CryptoExchange.Net
|
|||||||
disposing = true;
|
disposing = true;
|
||||||
periodicEvent?.Set();
|
periodicEvent?.Set();
|
||||||
periodicEvent?.Dispose();
|
periodicEvent?.Dispose();
|
||||||
log.Write(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
|
if (socketConnections.Sum(s => s.Value.SubscriptionCount) > 0)
|
||||||
_ = UnsubscribeAllAsync();
|
{
|
||||||
|
log.Write(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
|
||||||
|
_ = UnsubscribeAllAsync();
|
||||||
|
}
|
||||||
semaphoreSlim?.Dispose();
|
semaphoreSlim?.Dispose();
|
||||||
base.Dispose();
|
base.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace CryptoExchange.Net
|
|||||||
/// Get time sync info for an API client
|
/// Get time sync info for an API client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected abstract TimeSyncInfo GetTimeSyncInfo();
|
public abstract TimeSyncInfo GetTimeSyncInfo();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get time offset for an API client
|
/// Get time offset for an API client
|
||||||
@@ -92,7 +92,7 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate time offset between local and server
|
// Calculate time offset between local and server
|
||||||
var offset = result.Data - localTime;
|
var offset = result.Data - (localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2));
|
||||||
timeSyncParams.UpdateTimeOffset(offset);
|
timeSyncParams.UpdateTimeOffset(offset);
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ namespace CryptoExchange.Net.Converters
|
|||||||
if(reader.TokenType is JsonToken.Integer)
|
if(reader.TokenType is JsonToken.Integer)
|
||||||
{
|
{
|
||||||
var longValue = (long)reader.Value;
|
var longValue = (long)reader.Value;
|
||||||
if (longValue == 0)
|
if (longValue == 0 || longValue == -1)
|
||||||
return objectType == typeof(DateTime) ? default(DateTime): null;
|
return objectType == typeof(DateTime) ? default(DateTime): null;
|
||||||
if (longValue < 19999999999)
|
if (longValue < 19999999999)
|
||||||
return ConvertFromSeconds(longValue);
|
return ConvertFromSeconds(longValue);
|
||||||
@@ -45,6 +45,9 @@ namespace CryptoExchange.Net.Converters
|
|||||||
else if (reader.TokenType is JsonToken.Float)
|
else if (reader.TokenType is JsonToken.Float)
|
||||||
{
|
{
|
||||||
var doubleValue = (double)reader.Value;
|
var doubleValue = (double)reader.Value;
|
||||||
|
if (doubleValue == 0 || doubleValue == -1)
|
||||||
|
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
||||||
|
|
||||||
if (doubleValue < 19999999999)
|
if (doubleValue < 19999999999)
|
||||||
return ConvertFromSeconds(doubleValue);
|
return ConvertFromSeconds(doubleValue);
|
||||||
|
|
||||||
@@ -56,6 +59,9 @@ namespace CryptoExchange.Net.Converters
|
|||||||
if (string.IsNullOrWhiteSpace(stringValue))
|
if (string.IsNullOrWhiteSpace(stringValue))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(stringValue) || stringValue == "0" || stringValue == "-1")
|
||||||
|
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
||||||
|
|
||||||
if (stringValue.Length == 8)
|
if (stringValue.Length == 8)
|
||||||
{
|
{
|
||||||
// Parse 20211103 format
|
// Parse 20211103 format
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ namespace CryptoExchange.Net.Converters
|
|||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
var stringValue = GetString(value);
|
var stringValue = GetString(value);
|
||||||
writer.WriteRawValue(stringValue);
|
writer.WriteValue(stringValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,16 @@
|
|||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>A base package for implementing cryptocurrency API's</Description>
|
<Description>A base package for implementing cryptocurrency API's</Description>
|
||||||
<PackageVersion>5.1.8</PackageVersion>
|
<PackageVersion>5.2.4</PackageVersion>
|
||||||
<AssemblyVersion>5.1.8</AssemblyVersion>
|
<AssemblyVersion>5.2.4</AssemblyVersion>
|
||||||
<FileVersion>5.1.8</FileVersion>
|
<FileVersion>5.2.4</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||||
<NeutralLanguage>en</NeutralLanguage>
|
<NeutralLanguage>en</NeutralLanguage>
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
<PackageReleaseNotes>5.1.8 - Cleanup socket code, fixed an issue which could cause connections to never reconnect when connection was lost, Added support for sending requests which expect an empty response, Fixed issue with the DateTimeConverter date interpretation</PackageReleaseNotes>
|
<PackageReleaseNotes>5.2.4 - Added handling of PlatformNotSupportedException when trying to use websocket from WebAssembly, Changed DataEvent to have a public constructor for testing purposes, Fixed EnumConverter serializing values without proper quotes, Fixed websocket connection reconnecting too quickly when resubscribing/reauthenticating fails</PackageReleaseNotes>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<LangVersion>9.0</LangVersion>
|
<LangVersion>9.0</LangVersion>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
|||||||
@@ -426,6 +426,7 @@ namespace CryptoExchange.Net
|
|||||||
var uriBuilder = new UriBuilder();
|
var uriBuilder = new UriBuilder();
|
||||||
uriBuilder.Scheme = baseUri.Scheme;
|
uriBuilder.Scheme = baseUri.Scheme;
|
||||||
uriBuilder.Host = baseUri.Host;
|
uriBuilder.Host = baseUri.Host;
|
||||||
|
uriBuilder.Port = baseUri.Port;
|
||||||
uriBuilder.Path = baseUri.AbsolutePath;
|
uriBuilder.Path = baseUri.AbsolutePath;
|
||||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||||
foreach (var parameter in parameters)
|
foreach (var parameter in parameters)
|
||||||
@@ -454,6 +455,7 @@ namespace CryptoExchange.Net
|
|||||||
var uriBuilder = new UriBuilder();
|
var uriBuilder = new UriBuilder();
|
||||||
uriBuilder.Scheme = baseUri.Scheme;
|
uriBuilder.Scheme = baseUri.Scheme;
|
||||||
uriBuilder.Host = baseUri.Host;
|
uriBuilder.Host = baseUri.Host;
|
||||||
|
uriBuilder.Port = baseUri.Port;
|
||||||
uriBuilder.Path = baseUri.AbsolutePath;
|
uriBuilder.Path = baseUri.AbsolutePath;
|
||||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||||
foreach (var parameter in parameters)
|
foreach (var parameter in parameters)
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double IncomingKbps { get; }
|
public double IncomingKbps { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current amount of connections to the API from this client. A connection can have multiple subscriptions.
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentConnections { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current amount of subscriptions running from the client
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentSubscriptions { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unsubscribe from a stream using the subscription id received when starting the subscription
|
/// Unsubscribe from a stream using the subscription id received when starting the subscription
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Sockets;
|
||||||
using System;
|
using System;
|
||||||
using System.Security.Authentication;
|
using System.Security.Authentication;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -27,36 +28,28 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// Websocket opened event
|
/// Websocket opened event
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event Action OnOpen;
|
event Action OnOpen;
|
||||||
|
/// <summary>
|
||||||
|
/// Websocket has lost connection to the server and is attempting to reconnect
|
||||||
|
/// </summary>
|
||||||
|
event Action OnReconnecting;
|
||||||
|
/// <summary>
|
||||||
|
/// Websocket has reconnected to the server
|
||||||
|
/// </summary>
|
||||||
|
event Action OnReconnected;
|
||||||
|
/// <summary>
|
||||||
|
/// Get reconntion url
|
||||||
|
/// </summary>
|
||||||
|
Func<Task<Uri?>> GetReconnectionUrl { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unique id for this socket
|
/// Unique id for this socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
int Id { get; }
|
int Id { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Origin header
|
|
||||||
/// </summary>
|
|
||||||
string? Origin { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Encoding to use for sending/receiving string data
|
|
||||||
/// </summary>
|
|
||||||
Encoding? Encoding { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The max amount of outgoing messages per second
|
|
||||||
/// </summary>
|
|
||||||
int? RatelimitPerSecond { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The current kilobytes per second of data being received, averaged over the last 3 seconds
|
/// The current kilobytes per second of data being received, averaged over the last 3 seconds
|
||||||
/// </summary>
|
/// </summary>
|
||||||
double IncomingKbps { get; }
|
double IncomingKbps { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for byte data
|
|
||||||
/// </summary>
|
|
||||||
Func<byte[], string>? DataInterpreterBytes { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Handler for string data
|
|
||||||
/// </summary>
|
|
||||||
Func<string, string>? DataInterpreterString { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The uri the socket connects to
|
/// The uri the socket connects to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Uri Uri { get; }
|
Uri Uri { get; }
|
||||||
@@ -69,37 +62,20 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsOpen { get; }
|
bool IsOpen { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Supported ssl protocols
|
|
||||||
/// </summary>
|
|
||||||
SslProtocols SSLProtocols { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The max time for no data being received before the connection is considered lost
|
|
||||||
/// </summary>
|
|
||||||
TimeSpan Timeout { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Set a proxy to use when connecting
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="proxy"></param>
|
|
||||||
void SetProxy(ApiProxy proxy);
|
|
||||||
/// <summary>
|
|
||||||
/// Connect the socket
|
/// Connect the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<bool> ConnectAsync();
|
Task<bool> ConnectAsync();
|
||||||
/// <summary>
|
|
||||||
/// Receive and send messages over the connection. Resulting task should complete when closing the socket.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
Task ProcessAsync();
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send data
|
/// Send data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
void Send(string data);
|
void Send(string data);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reset socket when a connection is lost to prepare for a new connection
|
/// Reconnect the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void Reset();
|
/// <returns></returns>
|
||||||
|
Task ReconnectAsync();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Close the connection
|
/// Close the connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using CryptoExchange.Net.Logging;
|
||||||
using CryptoExchange.Net.Logging;
|
using CryptoExchange.Net.Sockets;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
namespace CryptoExchange.Net.Interfaces
|
||||||
{
|
{
|
||||||
@@ -12,17 +12,8 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// Create a websocket for an url
|
/// Create a websocket for an url
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="log">The logger</param>
|
/// <param name="log">The logger</param>
|
||||||
/// <param name="url">The url the socket is fo</param>
|
/// <param name="parameters">The parameters to use for the connection</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
IWebsocket CreateWebsocket(Log log, string url);
|
IWebsocket CreateWebsocket(Log log, WebSocketParameters parameters);
|
||||||
/// <summary>
|
|
||||||
/// Create a websocket for an url
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="log">The logger</param>
|
|
||||||
/// <param name="url">The url the socket is fo</param>
|
|
||||||
/// <param name="cookies">Cookies to be send in the initial request</param>
|
|
||||||
/// <param name="headers">Headers to be send in the initial request</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
IWebsocket CreateWebsocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ namespace CryptoExchange.Net.Logging
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string ClientName { get; set; }
|
public string ClientName { get; set; }
|
||||||
|
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -42,7 +44,8 @@ namespace CryptoExchange.Net.Logging
|
|||||||
/// <param name="textWriters"></param>
|
/// <param name="textWriters"></param>
|
||||||
public void UpdateWriters(List<ILogger> textWriters)
|
public void UpdateWriters(List<ILogger> textWriters)
|
||||||
{
|
{
|
||||||
writers = textWriters;
|
lock (_lock)
|
||||||
|
writers = textWriters;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -56,16 +59,19 @@ namespace CryptoExchange.Net.Logging
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var logMessage = $"{ClientName,-10} | {message}";
|
var logMessage = $"{ClientName,-10} | {message}";
|
||||||
foreach (var writer in writers.ToList())
|
lock (_lock)
|
||||||
{
|
{
|
||||||
try
|
foreach (var writer in writers)
|
||||||
{
|
{
|
||||||
writer.Log(logLevel, logMessage);
|
try
|
||||||
}
|
{
|
||||||
catch (Exception e)
|
writer.Log(logLevel, logMessage);
|
||||||
{
|
}
|
||||||
// Can't write to the logging so where else to output..
|
catch (Exception e)
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Failed to write log to writer {writer.GetType()}: " + e.ToLogString());
|
{
|
||||||
|
// Can't write to the logging so where else to output..
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Failed to write log to writer {writer.GetType()}: " + e.ToLogString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,15 +14,35 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class BaseOptions
|
public class BaseOptions
|
||||||
{
|
{
|
||||||
|
internal event Action? OnLoggingChanged;
|
||||||
|
|
||||||
|
private LogLevel _logLevel = LogLevel.Information;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The minimum log level to output
|
/// The minimum log level to output
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public LogLevel LogLevel { get; set; } = LogLevel.Information;
|
public LogLevel LogLevel
|
||||||
|
{
|
||||||
|
get => _logLevel;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_logLevel = value;
|
||||||
|
OnLoggingChanged?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ILogger> _logWriters = new List<ILogger> { new DebugLogger() };
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The log writers
|
/// The log writers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<ILogger> LogWriters { get; set; } = new List<ILogger> { new DebugLogger() };
|
public List<ILogger> LogWriters
|
||||||
|
{
|
||||||
|
get => _logWriters;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_logWriters = value;
|
||||||
|
OnLoggingChanged?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
||||||
@@ -156,16 +176,6 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The maximum number of times to try to reconnect, default null will retry indefinitely
|
|
||||||
/// </summary>
|
|
||||||
public int? MaxReconnectTries { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The maximum number of times to try to resubscribe after reconnecting
|
|
||||||
/// </summary>
|
|
||||||
public int? MaxResubscribeTries { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -189,6 +199,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? SocketSubscriptionsCombineTarget { get; set; }
|
public int? SocketSubscriptionsCombineTarget { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxSocketConnections { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -207,18 +222,17 @@ namespace CryptoExchange.Net.Objects
|
|||||||
|
|
||||||
AutoReconnect = baseOptions.AutoReconnect;
|
AutoReconnect = baseOptions.AutoReconnect;
|
||||||
ReconnectInterval = baseOptions.ReconnectInterval;
|
ReconnectInterval = baseOptions.ReconnectInterval;
|
||||||
MaxReconnectTries = baseOptions.MaxReconnectTries;
|
|
||||||
MaxResubscribeTries = baseOptions.MaxResubscribeTries;
|
|
||||||
MaxConcurrentResubscriptionsPerSocket = baseOptions.MaxConcurrentResubscriptionsPerSocket;
|
MaxConcurrentResubscriptionsPerSocket = baseOptions.MaxConcurrentResubscriptionsPerSocket;
|
||||||
SocketResponseTimeout = baseOptions.SocketResponseTimeout;
|
SocketResponseTimeout = baseOptions.SocketResponseTimeout;
|
||||||
SocketNoDataTimeout = baseOptions.SocketNoDataTimeout;
|
SocketNoDataTimeout = baseOptions.SocketNoDataTimeout;
|
||||||
SocketSubscriptionsCombineTarget = baseOptions.SocketSubscriptionsCombineTarget;
|
SocketSubscriptionsCombineTarget = baseOptions.SocketSubscriptionsCombineTarget;
|
||||||
|
MaxSocketConnections = baseOptions.MaxSocketConnections;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, MaxReconnectTries: {MaxReconnectTries}, MaxResubscribeTries: {MaxResubscribeTries}, MaxConcurrentResubscriptionsPerSocket: {MaxConcurrentResubscriptionsPerSocket}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketNoDataTimeout: {SocketNoDataTimeout}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}";
|
return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, MaxConcurrentResubscriptionsPerSocket: {MaxConcurrentResubscriptionsPerSocket}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketNoDataTimeout: {SocketNoDataTimeout}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}, MaxSocketConnections: {MaxSocketConnections}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -258,24 +258,32 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
}
|
}
|
||||||
|
|
||||||
_subscription = startResult.Data;
|
_subscription = startResult.Data;
|
||||||
_subscription.ConnectionLost += () =>
|
_subscription.ConnectionLost += HandleConnectionLost;
|
||||||
{
|
_subscription.ConnectionClosed += HandleConnectionClosed;
|
||||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
|
_subscription.ConnectionRestored += HandleConnectionRestored;
|
||||||
Status = OrderBookStatus.Reconnecting;
|
|
||||||
Reset();
|
|
||||||
};
|
|
||||||
_subscription.ConnectionClosed += () =>
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
|
|
||||||
Status = OrderBookStatus.Disconnected;
|
|
||||||
_ = StopAsync();
|
|
||||||
};
|
|
||||||
|
|
||||||
_subscription.ConnectionRestored += async time => await ResyncAsync().ConfigureAwait(false);
|
|
||||||
Status = OrderBookStatus.Synced;
|
Status = OrderBookStatus.Synced;
|
||||||
return new CallResult<bool>(true);
|
return new CallResult<bool>(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleConnectionLost() {
|
||||||
|
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
|
||||||
|
if (Status != OrderBookStatus.Disposed) {
|
||||||
|
Status = OrderBookStatus.Reconnecting;
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleConnectionClosed() {
|
||||||
|
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
|
||||||
|
Status = OrderBookStatus.Disconnected;
|
||||||
|
_ = StopAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void HandleConnectionRestored(TimeSpan _) {
|
||||||
|
await ResyncAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task StopAsync()
|
public async Task StopAsync()
|
||||||
{
|
{
|
||||||
@@ -286,8 +294,12 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
if (_processTask != null)
|
if (_processTask != null)
|
||||||
await _processTask.ConfigureAwait(false);
|
await _processTask.ConfigureAwait(false);
|
||||||
|
|
||||||
if (_subscription != null)
|
if (_subscription != null) {
|
||||||
await _subscription.CloseAsync().ConfigureAwait(false);
|
await _subscription.CloseAsync().ConfigureAwait(false);
|
||||||
|
_subscription.ConnectionLost -= HandleConnectionLost;
|
||||||
|
_subscription.ConnectionClosed -= HandleConnectionClosed;
|
||||||
|
_subscription.ConnectionRestored -= HandleConnectionRestored;
|
||||||
|
}
|
||||||
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} stopped");
|
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,13 +613,13 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
|
|
||||||
private async Task ProcessQueue()
|
private async Task ProcessQueue()
|
||||||
{
|
{
|
||||||
while (Status != OrderBookStatus.Disconnected)
|
while (Status != OrderBookStatus.Disconnected && Status != OrderBookStatus.Disposed)
|
||||||
{
|
{
|
||||||
await _queueEvent.WaitAsync().ConfigureAwait(false);
|
await _queueEvent.WaitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
while (_processQueue.TryDequeue(out var item))
|
while (_processQueue.TryDequeue(out var item))
|
||||||
{
|
{
|
||||||
if (Status == OrderBookStatus.Disconnected)
|
if (Status == OrderBookStatus.Disconnected || Status == OrderBookStatus.Disposed)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
if (_stopProcessing)
|
if (_stopProcessing)
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ using Microsoft.Extensions.Logging;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Security.Authentication;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -22,18 +20,32 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CryptoExchangeWebSocketClient : IWebsocket
|
public class CryptoExchangeWebSocketClient : IWebsocket
|
||||||
{
|
{
|
||||||
|
enum ProcessState
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
Processing,
|
||||||
|
WaitingForClose,
|
||||||
|
Reconnecting
|
||||||
|
}
|
||||||
|
|
||||||
internal static int lastStreamId;
|
internal static int lastStreamId;
|
||||||
private static readonly object streamIdLock = new();
|
private static readonly object streamIdLock = new();
|
||||||
|
|
||||||
private ClientWebSocket _socket;
|
|
||||||
private readonly AsyncResetEvent _sendEvent;
|
private readonly AsyncResetEvent _sendEvent;
|
||||||
private readonly ConcurrentQueue<byte[]> _sendBuffer;
|
private readonly ConcurrentQueue<byte[]> _sendBuffer;
|
||||||
private readonly IDictionary<string, string> cookies;
|
private readonly SemaphoreSlim _closeSem;
|
||||||
private readonly IDictionary<string, string> headers;
|
|
||||||
private CancellationTokenSource _ctsSource;
|
|
||||||
|
|
||||||
private readonly List<DateTime> _outgoingMessages;
|
private readonly List<DateTime> _outgoingMessages;
|
||||||
|
|
||||||
|
private ClientWebSocket _socket;
|
||||||
|
private CancellationTokenSource _ctsSource;
|
||||||
private DateTime _lastReceivedMessagesUpdate;
|
private DateTime _lastReceivedMessagesUpdate;
|
||||||
|
private Task? _processTask;
|
||||||
|
private Task? _closeTask;
|
||||||
|
private bool _stopRequested;
|
||||||
|
private bool _disposed;
|
||||||
|
private ProcessState _processState;
|
||||||
|
private DateTime _lastReconnectTime;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Received messages, the size and the timstamp
|
/// Received messages, the size and the timstamp
|
||||||
@@ -48,48 +60,21 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Log
|
/// Log
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected Log log;
|
protected Log _log;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handlers for when an error happens on the socket
|
|
||||||
/// </summary>
|
|
||||||
protected readonly List<Action<Exception>> errorHandlers = new();
|
|
||||||
/// <summary>
|
|
||||||
/// Handlers for when the socket connection is opened
|
|
||||||
/// </summary>
|
|
||||||
protected readonly List<Action> openHandlers = new();
|
|
||||||
/// <summary>
|
|
||||||
/// Handlers for when the connection is closed
|
|
||||||
/// </summary>
|
|
||||||
protected readonly List<Action> closeHandlers = new();
|
|
||||||
/// <summary>
|
|
||||||
/// Handlers for when a message is received
|
|
||||||
/// </summary>
|
|
||||||
protected readonly List<Action<string>> messageHandlers = new();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int Id { get; }
|
public int Id { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string? Origin { get; set; }
|
public WebSocketParameters Parameters { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The timestamp this socket has been active for the last time
|
/// The timestamp this socket has been active for the last time
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime LastActionTime { get; private set; }
|
public DateTime LastActionTime { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
|
|
||||||
/// </summary>
|
|
||||||
public Func<byte[], string>? DataInterpreterBytes { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Delegate used for processing string data received from socket connections before it is processed by handlers
|
|
||||||
/// </summary>
|
|
||||||
public Func<string, string>? DataInterpreterString { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Uri Uri { get; }
|
public Uri Uri => Parameters.Uri;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsClosed => _socket.State == WebSocketState.Closed;
|
public bool IsClosed => _socket.State == WebSocketState.Closed;
|
||||||
@@ -97,31 +82,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsOpen => _socket.State == WebSocketState.Open && !_ctsSource.IsCancellationRequested;
|
public bool IsOpen => _socket.State == WebSocketState.Open && !_ctsSource.IsCancellationRequested;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ssl protocols supported. NOT USED BY THIS IMPLEMENTATION
|
|
||||||
/// </summary>
|
|
||||||
public SslProtocols SSLProtocols { get; set; }
|
|
||||||
|
|
||||||
private Encoding _encoding = Encoding.UTF8;
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Encoding? Encoding
|
|
||||||
{
|
|
||||||
get => _encoding;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if(value != null)
|
|
||||||
_encoding = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The max amount of outgoing messages per second
|
|
||||||
/// </summary>
|
|
||||||
public int? RatelimitPerSecond { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public TimeSpan Timeout { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public double IncomingKbps
|
public double IncomingKbps
|
||||||
{
|
{
|
||||||
@@ -140,57 +100,31 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Action OnClose
|
public event Action? OnClose;
|
||||||
{
|
|
||||||
add => closeHandlers.Add(value);
|
|
||||||
remove => closeHandlers.Remove(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Action<string> OnMessage
|
public event Action<string>? OnMessage;
|
||||||
{
|
|
||||||
add => messageHandlers.Add(value);
|
|
||||||
remove => messageHandlers.Remove(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Action<Exception> OnError
|
public event Action<Exception>? OnError;
|
||||||
{
|
|
||||||
add => errorHandlers.Add(value);
|
|
||||||
remove => errorHandlers.Remove(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Action OnOpen
|
public event Action? OnOpen;
|
||||||
{
|
/// <inheritdoc />
|
||||||
add => openHandlers.Add(value);
|
public event Action? OnReconnecting;
|
||||||
remove => openHandlers.Remove(value);
|
/// <inheritdoc />
|
||||||
}
|
public event Action? OnReconnected;
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="log">The log object to use</param>
|
/// <param name="log">The log object to use</param>
|
||||||
/// <param name="uri">The uri the socket should connect to</param>
|
/// <param name="websocketParameters">The parameters for this socket</param>
|
||||||
public CryptoExchangeWebSocketClient(Log log, Uri uri) : this(log, uri, new Dictionary<string, string>(), new Dictionary<string, string>())
|
public CryptoExchangeWebSocketClient(Log log, WebSocketParameters websocketParameters)
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="log">The log object to use</param>
|
|
||||||
/// <param name="uri">The uri the socket should connect to</param>
|
|
||||||
/// <param name="cookies">Cookies to sent in the socket connection request</param>
|
|
||||||
/// <param name="headers">Headers to sent in the socket connection request</param>
|
|
||||||
public CryptoExchangeWebSocketClient(Log log, Uri uri, IDictionary<string, string> cookies, IDictionary<string, string> headers)
|
|
||||||
{
|
{
|
||||||
Id = NextStreamId();
|
Id = NextStreamId();
|
||||||
this.log = log;
|
_log = log;
|
||||||
Uri = uri;
|
|
||||||
this.cookies = cookies;
|
|
||||||
this.headers = headers;
|
|
||||||
|
|
||||||
|
Parameters = websocketParameters;
|
||||||
_outgoingMessages = new List<DateTime>();
|
_outgoingMessages = new List<DateTime>();
|
||||||
_receivedMessages = new List<ReceiveItem>();
|
_receivedMessages = new List<ReceiveItem>();
|
||||||
_sendEvent = new AsyncResetEvent();
|
_sendEvent = new AsyncResetEvent();
|
||||||
@@ -198,92 +132,238 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_ctsSource = new CancellationTokenSource();
|
_ctsSource = new CancellationTokenSource();
|
||||||
_receivedMessagesLock = new object();
|
_receivedMessagesLock = new object();
|
||||||
|
|
||||||
|
_closeSem = new SemaphoreSlim(1, 1);
|
||||||
_socket = CreateSocket();
|
_socket = CreateSocket();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual void SetProxy(ApiProxy proxy)
|
|
||||||
{
|
|
||||||
if (!Uri.TryCreate($"{proxy.Host}:{proxy.Port}", UriKind.Absolute, out var uri))
|
|
||||||
throw new ArgumentException("Proxy settings invalid, {proxy.Host}:{proxy.Port} not a valid URI", nameof(proxy));
|
|
||||||
|
|
||||||
_socket.Options.Proxy = uri?.Scheme == null
|
|
||||||
? _socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port)
|
|
||||||
: _socket.Options.Proxy = new WebProxy
|
|
||||||
{
|
|
||||||
Address = uri
|
|
||||||
};
|
|
||||||
|
|
||||||
if (proxy.Login != null)
|
|
||||||
_socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual async Task<bool> ConnectAsync()
|
public virtual async Task<bool> ConnectAsync()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} connecting");
|
if (!await ConnectInternalAsync().ConfigureAwait(false))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
OnOpen?.Invoke();
|
||||||
|
_processTask = ProcessAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create the socket object
|
||||||
|
/// </summary>
|
||||||
|
private ClientWebSocket CreateSocket()
|
||||||
|
{
|
||||||
|
var cookieContainer = new CookieContainer();
|
||||||
|
foreach (var cookie in Parameters.Cookies)
|
||||||
|
cookieContainer.Add(new Cookie(cookie.Key, cookie.Value));
|
||||||
|
|
||||||
|
var socket = new ClientWebSocket();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10));
|
socket.Options.Cookies = cookieContainer;
|
||||||
|
foreach (var header in Parameters.Headers)
|
||||||
|
socket.Options.SetRequestHeader(header.Key, header.Value);
|
||||||
|
socket.Options.KeepAliveInterval = Parameters.KeepAliveInterval ?? TimeSpan.Zero;
|
||||||
|
socket.Options.SetBuffer(65536, 65536); // Setting it to anything bigger than 65536 throws an exception in .net framework
|
||||||
|
if (Parameters.Proxy != null)
|
||||||
|
SetProxy(Parameters.Proxy);
|
||||||
|
}
|
||||||
|
catch (PlatformNotSupportedException)
|
||||||
|
{
|
||||||
|
// Options are not supported on certain platforms (WebAssembly for instance)
|
||||||
|
// best we can do it try to connect without setting options.
|
||||||
|
}
|
||||||
|
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ConnectInternalAsync()
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} connecting");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10));
|
||||||
await _socket.ConnectAsync(Uri, tcs.Token).ConfigureAwait(false);
|
await _socket.ConnectAsync(Uri, tcs.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
Handle(openHandlers);
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} connection failed: " + e.ToLogString());
|
_log.Write(LogLevel.Debug, $"Socket {Id} connection failed: " + e.ToLogString());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} connected to {Uri}");
|
_log.Write(LogLevel.Debug, $"Socket {Id} connected to {Uri}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual async Task ProcessAsync()
|
private async Task ProcessAsync()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} ProcessAsync started");
|
while (!_stopRequested)
|
||||||
var sendTask = SendLoopAsync();
|
{
|
||||||
var receiveTask = ReceiveLoopAsync();
|
_log.Write(LogLevel.Debug, $"Socket {Id} starting processing tasks");
|
||||||
var timeoutTask = Timeout != default ? CheckTimeoutAsync() : Task.CompletedTask;
|
_processState = ProcessState.Processing;
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} processing startup completed");
|
var sendTask = SendLoopAsync();
|
||||||
await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
|
var receiveTask = ReceiveLoopAsync();
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} ProcessAsync finished");
|
var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask;
|
||||||
|
await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} processing tasks finished");
|
||||||
|
|
||||||
|
_processState = ProcessState.WaitingForClose;
|
||||||
|
while (_closeTask == null)
|
||||||
|
await Task.Delay(50).ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _closeTask.ConfigureAwait(false);
|
||||||
|
_closeTask = null;
|
||||||
|
|
||||||
|
if (!Parameters.AutoReconnect)
|
||||||
|
{
|
||||||
|
_processState = ProcessState.Idle;
|
||||||
|
OnClose?.Invoke();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_stopRequested)
|
||||||
|
{
|
||||||
|
_processState = ProcessState.Reconnecting;
|
||||||
|
OnReconnecting?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime;
|
||||||
|
if (sinceLastReconnect < Parameters.ReconnectInterval)
|
||||||
|
await Task.Delay(Parameters.ReconnectInterval - sinceLastReconnect).ConfigureAwait(false);
|
||||||
|
|
||||||
|
while (!_stopRequested)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} attempting to reconnect");
|
||||||
|
var task = GetReconnectionUrl?.Invoke();
|
||||||
|
if (task != null)
|
||||||
|
{
|
||||||
|
var reconnectUri = await task.ConfigureAwait(false);
|
||||||
|
if (reconnectUri != null && Parameters.Uri != reconnectUri)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} reconnect URI set to {reconnectUri}");
|
||||||
|
Parameters.Uri = reconnectUri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_socket = CreateSocket();
|
||||||
|
_ctsSource.Dispose();
|
||||||
|
_ctsSource = new CancellationTokenSource();
|
||||||
|
while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer
|
||||||
|
|
||||||
|
var connected = await ConnectInternalAsync().ConfigureAwait(false);
|
||||||
|
if (!connected)
|
||||||
|
{
|
||||||
|
await Task.Delay(Parameters.ReconnectInterval).ConfigureAwait(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastReconnectTime = DateTime.UtcNow;
|
||||||
|
OnReconnected?.Invoke();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_processState = ProcessState.Idle;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual void Send(string data)
|
public virtual void Send(string data)
|
||||||
{
|
{
|
||||||
if (_ctsSource.IsCancellationRequested)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
throw new InvalidOperationException($"Socket {Id} Can't send data when socket is not connected");
|
return;
|
||||||
|
|
||||||
var bytes = _encoding.GetBytes(data);
|
var bytes = Parameters.Encoding.GetBytes(data);
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} Adding {bytes.Length} to sent buffer");
|
_log.Write(LogLevel.Trace, $"Socket {Id} Adding {bytes.Length} to sent buffer");
|
||||||
_sendBuffer.Enqueue(bytes);
|
_sendBuffer.Enqueue(bytes);
|
||||||
_sendEvent.Set();
|
_sendEvent.Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual async Task ReconnectAsync()
|
||||||
|
{
|
||||||
|
if (_processState != ProcessState.Processing && IsOpen)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} reconnect requested");
|
||||||
|
_closeTask = CloseInternalAsync();
|
||||||
|
await _closeTask.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual async Task CloseAsync()
|
public virtual async Task CloseAsync()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} closing");
|
await _closeSem.WaitAsync().ConfigureAwait(false);
|
||||||
await CloseInternalAsync().ConfigureAwait(false);
|
try
|
||||||
|
{
|
||||||
|
if (_closeTask != null && !_closeTask.IsCompleted)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} CloseAsync() waiting for existing close task");
|
||||||
|
await _closeTask.ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_stopRequested = true;
|
||||||
|
|
||||||
|
if (!IsOpen)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} CloseAsync() socket not open");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} closing");
|
||||||
|
_closeTask = CloseInternalAsync();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_closeSem.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _closeTask.ConfigureAwait(false);
|
||||||
|
if(_processTask != null)
|
||||||
|
await _processTask.ConfigureAwait(false);
|
||||||
|
OnClose?.Invoke();
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} closed");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Internal close method
|
/// Internal close method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private async Task CloseInternalAsync()
|
private async Task CloseInternalAsync()
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//_closeState = CloseState.Closing;
|
||||||
_ctsSource.Cancel();
|
_ctsSource.Cancel();
|
||||||
_sendEvent.Set();
|
_sendEvent.Set();
|
||||||
|
|
||||||
if (_socket.State == WebSocketState.Open)
|
if (_socket.State == WebSocketState.Open)
|
||||||
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
{
|
||||||
|
try
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} closed");
|
{
|
||||||
Handle(closeHandlers);
|
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Can sometimes throw an exception when socket is in aborted state due to timing
|
||||||
|
// Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync
|
||||||
|
// So socket might go to aborted state, might still be open
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(_socket.State == WebSocketState.CloseReceived)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Can sometimes throw an exception when socket is in aborted state due to timing
|
||||||
|
// Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync
|
||||||
|
// So socket might go to aborted state, might still be open
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -291,44 +371,14 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} disposing");
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_log.Write(LogLevel.Debug, $"Socket {Id} disposing");
|
||||||
|
_disposed = true;
|
||||||
_socket.Dispose();
|
_socket.Dispose();
|
||||||
_ctsSource.Dispose();
|
_ctsSource.Dispose();
|
||||||
|
_log.Write(LogLevel.Trace, $"Socket {Id} disposed");
|
||||||
errorHandlers.Clear();
|
|
||||||
openHandlers.Clear();
|
|
||||||
closeHandlers.Clear();
|
|
||||||
messageHandlers.Clear();
|
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} disposed");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Reset()
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} resetting");
|
|
||||||
_ctsSource = new CancellationTokenSource();
|
|
||||||
|
|
||||||
while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer
|
|
||||||
|
|
||||||
_socket = CreateSocket();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create the socket object
|
|
||||||
/// </summary>
|
|
||||||
private ClientWebSocket CreateSocket()
|
|
||||||
{
|
|
||||||
var cookieContainer = new CookieContainer();
|
|
||||||
foreach (var cookie in cookies)
|
|
||||||
cookieContainer.Add(new Cookie(cookie.Key, cookie.Value));
|
|
||||||
|
|
||||||
var socket = new ClientWebSocket();
|
|
||||||
socket.Options.Cookies = cookieContainer;
|
|
||||||
foreach (var header in headers)
|
|
||||||
socket.Options.SetRequestHeader(header.Key, header.Value);
|
|
||||||
socket.Options.KeepAliveInterval = TimeSpan.FromSeconds(10);
|
|
||||||
socket.Options.SetBuffer(65536, 65536); // Setting it to anything bigger than 65536 throws an exception in .net framework
|
|
||||||
return socket;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -351,25 +401,25 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
while (_sendBuffer.TryDequeue(out var data))
|
while (_sendBuffer.TryDequeue(out var data))
|
||||||
{
|
{
|
||||||
if (RatelimitPerSecond != null)
|
if (Parameters.RatelimitPerSecond != null)
|
||||||
{
|
{
|
||||||
// Wait for rate limit
|
// Wait for rate limit
|
||||||
DateTime? start = null;
|
DateTime? start = null;
|
||||||
while (MessagesSentLastSecond() >= RatelimitPerSecond)
|
while (MessagesSentLastSecond() >= Parameters.RatelimitPerSecond)
|
||||||
{
|
{
|
||||||
start ??= DateTime.UtcNow;
|
start ??= DateTime.UtcNow;
|
||||||
await Task.Delay(50).ConfigureAwait(false);
|
await Task.Delay(50).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start != null)
|
if (start != null)
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} sent delayed {Math.Round((DateTime.UtcNow - start.Value).TotalMilliseconds)}ms because of rate limit");
|
_log.Write(LogLevel.Debug, $"Socket {Id} sent delayed {Math.Round((DateTime.UtcNow - start.Value).TotalMilliseconds)}ms because of rate limit");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _socket.SendAsync(new ArraySegment<byte>(data, 0, data.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
|
await _socket.SendAsync(new ArraySegment<byte>(data, 0, data.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
|
||||||
_outgoingMessages.Add(DateTime.UtcNow);
|
_outgoingMessages.Add(DateTime.UtcNow);
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} sent {data.Length} bytes");
|
_log.Write(LogLevel.Trace, $"Socket {Id} sent {data.Length} bytes");
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -378,9 +428,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
catch (Exception ioe)
|
catch (Exception ioe)
|
||||||
{
|
{
|
||||||
// Connection closed unexpectedly, .NET framework
|
// Connection closed unexpectedly, .NET framework
|
||||||
Handle(errorHandlers, ioe);
|
OnError?.Invoke(ioe);
|
||||||
await CloseInternalAsync().ConfigureAwait(false);
|
_closeTask = CloseInternalAsync();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -391,12 +441,12 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// Because this is running in a separate task and not awaited until the socket gets closed
|
// Because this is running in a separate task and not awaited until the socket gets closed
|
||||||
// any exception here will crash the send processing, but do so silently unless the socket get's stopped.
|
// any exception here will crash the send processing, but do so silently unless the socket get's stopped.
|
||||||
// Make sure we at least let the owner know there was an error
|
// Make sure we at least let the owner know there was an error
|
||||||
Handle(errorHandlers, e);
|
OnError?.Invoke(e);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} Send loop finished");
|
_log.Write(LogLevel.Debug, $"Socket {Id} Send loop finished");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,17 +484,17 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
catch (Exception wse)
|
catch (Exception wse)
|
||||||
{
|
{
|
||||||
// Connection closed unexpectedly
|
// Connection closed unexpectedly
|
||||||
Handle(errorHandlers, wse);
|
OnError?.Invoke(wse);
|
||||||
await CloseInternalAsync().ConfigureAwait(false);
|
_closeTask = CloseInternalAsync();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
||||||
{
|
{
|
||||||
// Connection closed unexpectedly
|
// Connection closed unexpectedly
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} received `Close` message");
|
_log.Write(LogLevel.Debug, $"Socket {Id} received `Close` message");
|
||||||
await CloseInternalAsync().ConfigureAwait(false);
|
_closeTask = CloseInternalAsync();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,7 +503,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// We received data, but it is not complete, write it to a memory stream for reassembling
|
// We received data, but it is not complete, write it to a memory stream for reassembling
|
||||||
multiPartMessage = true;
|
multiPartMessage = true;
|
||||||
memoryStream ??= new MemoryStream();
|
memoryStream ??= new MemoryStream();
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
|
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
|
||||||
await memoryStream.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
|
await memoryStream.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -461,13 +511,13 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (!multiPartMessage)
|
if (!multiPartMessage)
|
||||||
{
|
{
|
||||||
// Received a complete message and it's not multi part
|
// Received a complete message and it's not multi part
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in single message");
|
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in single message");
|
||||||
HandleMessage(buffer.Array!, buffer.Offset, receiveResult.Count, receiveResult.MessageType);
|
HandleMessage(buffer.Array!, buffer.Offset, receiveResult.Count, receiveResult.MessageType);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Received the end of a multipart message, write to memory stream for reassembling
|
// Received the end of a multipart message, write to memory stream for reassembling
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
|
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
|
||||||
await memoryStream!.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
|
await memoryStream!.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -495,12 +545,12 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (receiveResult?.EndOfMessage == true)
|
if (receiveResult?.EndOfMessage == true)
|
||||||
{
|
{
|
||||||
// Reassemble complete message from memory stream
|
// Reassemble complete message from memory stream
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} reassembled message of {memoryStream!.Length} bytes");
|
_log.Write(LogLevel.Trace, $"Socket {Id} reassembled message of {memoryStream!.Length} bytes");
|
||||||
HandleMessage(memoryStream!.ToArray(), 0, (int)memoryStream.Length, receiveResult.MessageType);
|
HandleMessage(memoryStream!.ToArray(), 0, (int)memoryStream.Length, receiveResult.MessageType);
|
||||||
memoryStream.Dispose();
|
memoryStream.Dispose();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} discarding incomplete message of {memoryStream!.Length} bytes");
|
_log.Write(LogLevel.Trace, $"Socket {Id} discarding incomplete message of {memoryStream!.Length} bytes");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -509,12 +559,12 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// Because this is running in a separate task and not awaited until the socket gets closed
|
// Because this is running in a separate task and not awaited until the socket gets closed
|
||||||
// any exception here will crash the receive processing, but do so silently unless the socket gets stopped.
|
// any exception here will crash the receive processing, but do so silently unless the socket gets stopped.
|
||||||
// Make sure we at least let the owner know there was an error
|
// Make sure we at least let the owner know there was an error
|
||||||
Handle(errorHandlers, e);
|
OnError?.Invoke(e);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} Receive loop finished");
|
_log.Write(LogLevel.Debug, $"Socket {Id} Receive loop finished");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,54 +580,92 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
string strData;
|
string strData;
|
||||||
if (messageType == WebSocketMessageType.Binary)
|
if (messageType == WebSocketMessageType.Binary)
|
||||||
{
|
{
|
||||||
if (DataInterpreterBytes == null)
|
if (Parameters.DataInterpreterBytes == null)
|
||||||
throw new Exception("Byte interpreter not set while receiving byte data");
|
throw new Exception("Byte interpreter not set while receiving byte data");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var relevantData = new byte[count];
|
var relevantData = new byte[count];
|
||||||
Array.Copy(data, offset, relevantData, 0, count);
|
Array.Copy(data, offset, relevantData, 0, count);
|
||||||
strData = DataInterpreterBytes(relevantData);
|
strData = Parameters.DataInterpreterBytes(relevantData);
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during byte data interpretation: " + e.ToLogString());
|
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during byte data interpretation: " + e.ToLogString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
strData = _encoding.GetString(data, offset, count);
|
strData = Parameters.Encoding.GetString(data, offset, count);
|
||||||
|
|
||||||
if (DataInterpreterString != null)
|
if (Parameters.DataInterpreterString != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
strData = DataInterpreterString(strData);
|
strData = Parameters.DataInterpreterString(strData);
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during string data interpretation: " + e.ToLogString());
|
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during string data interpretation: " + e.ToLogString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Handle(messageHandlers, strData);
|
LastActionTime = DateTime.UtcNow;
|
||||||
|
OnMessage?.Invoke(strData);
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during message processing: " + e.ToLogString());
|
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during message processing: " + e.ToLogString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger the OnMessage event
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected void TriggerOnMessage(string data)
|
||||||
|
{
|
||||||
|
LastActionTime = DateTime.UtcNow;
|
||||||
|
OnMessage?.Invoke(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger the OnError event
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ex"></param>
|
||||||
|
protected void TriggerOnError(Exception ex) => OnError?.Invoke(ex);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger the OnError event
|
||||||
|
/// </summary>
|
||||||
|
protected void TriggerOnOpen() => OnOpen?.Invoke();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger the OnError event
|
||||||
|
/// </summary>
|
||||||
|
protected void TriggerOnClose() => OnClose?.Invoke();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger the OnReconnecting event
|
||||||
|
/// </summary>
|
||||||
|
protected void TriggerOnReconnecting() => OnReconnecting?.Invoke();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger the OnReconnected event
|
||||||
|
/// </summary>
|
||||||
|
protected void TriggerOnReconnected() => OnReconnected?.Invoke();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if there is no data received for a period longer than the specified timeout
|
/// Checks if there is no data received for a period longer than the specified timeout
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected async Task CheckTimeoutAsync()
|
protected async Task CheckTimeoutAsync()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} Starting task checking for no data received for {Timeout}");
|
_log.Write(LogLevel.Debug, $"Socket {Id} Starting task checking for no data received for {Parameters.Timeout}");
|
||||||
|
LastActionTime = DateTime.UtcNow;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
@@ -585,10 +673,10 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (_ctsSource.IsCancellationRequested)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (DateTime.UtcNow - LastActionTime > Timeout)
|
if (DateTime.UtcNow - LastActionTime > Parameters.Timeout)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Warning, $"Socket {Id} No data received for {Timeout}, reconnecting socket");
|
_log.Write(LogLevel.Warning, $"Socket {Id} No data received for {Parameters.Timeout}, reconnecting socket");
|
||||||
_ = CloseAsync().ConfigureAwait(false);
|
_ = ReconnectAsync().ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try
|
try
|
||||||
@@ -607,35 +695,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// Because this is running in a separate task and not awaited until the socket gets closed
|
// Because this is running in a separate task and not awaited until the socket gets closed
|
||||||
// any exception here will stop the timeout checking, but do so silently unless the socket get's stopped.
|
// any exception here will stop the timeout checking, but do so silently unless the socket get's stopped.
|
||||||
// Make sure we at least let the owner know there was an error
|
// Make sure we at least let the owner know there was an error
|
||||||
Handle(errorHandlers, e);
|
OnError?.Invoke(e);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Helper to invoke handlers
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="handlers"></param>
|
|
||||||
protected void Handle(List<Action> handlers)
|
|
||||||
{
|
|
||||||
LastActionTime = DateTime.UtcNow;
|
|
||||||
foreach (var handle in new List<Action>(handlers))
|
|
||||||
handle?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Helper to invoke handlers
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <param name="handlers"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected void Handle<T>(List<Action<T>> handlers, T data)
|
|
||||||
{
|
|
||||||
LastActionTime = DateTime.UtcNow;
|
|
||||||
foreach (var handle in new List<Action<T>>(handlers))
|
|
||||||
handle?.Invoke(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the next identifier
|
/// Get the next identifier
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -671,6 +735,27 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_lastReceivedMessagesUpdate = checkTime;
|
_lastReceivedMessagesUpdate = checkTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set proxy on socket
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="proxy"></param>
|
||||||
|
/// <exception cref="ArgumentException"></exception>
|
||||||
|
protected virtual void SetProxy(ApiProxy proxy)
|
||||||
|
{
|
||||||
|
if (!Uri.TryCreate($"{proxy.Host}:{proxy.Port}", UriKind.Absolute, out var uri))
|
||||||
|
throw new ArgumentException("Proxy settings invalid, {proxy.Host}:{proxy.Port} not a valid URI", nameof(proxy));
|
||||||
|
|
||||||
|
_socket.Options.Proxy = uri?.Scheme == null
|
||||||
|
? _socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port)
|
||||||
|
: _socket.Options.Proxy = new WebProxy
|
||||||
|
{
|
||||||
|
Address = uri
|
||||||
|
};
|
||||||
|
|
||||||
|
if (proxy.Login != null)
|
||||||
|
_socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public T Data { get; set; }
|
public T Data { get; set; }
|
||||||
|
|
||||||
internal DataEvent(T data, DateTime timestamp)
|
/// <summary>
|
||||||
|
/// Ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <param name="timestamp"></param>
|
||||||
|
public DataEvent(T data, DateTime timestamp)
|
||||||
{
|
{
|
||||||
Data = data;
|
Data = data;
|
||||||
Timestamp = timestamp;
|
Timestamp = timestamp;
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Logging;
|
using CryptoExchange.Net.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Sockets
|
namespace CryptoExchange.Net.Sockets
|
||||||
{
|
{
|
||||||
@@ -57,10 +57,22 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
return subscriptions.Count(h => h.UserSubscription); }
|
return subscriptions.Count(h => h.UserSubscription); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a copy of the current subscriptions
|
||||||
|
/// </summary>
|
||||||
|
public SocketSubscription[] Subscriptions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (subscriptionLock)
|
||||||
|
return subscriptions.Where(h => h.UserSubscription).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If the connection has been authenticated
|
/// If the connection has been authenticated
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Authenticated { get; set; }
|
public bool Authenticated { get; internal set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If connection is made
|
/// If connection is made
|
||||||
@@ -80,28 +92,13 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The connection uri
|
/// The connection uri
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Uri Uri => _socket.Uri;
|
public Uri ConnectionUri => _socket.Uri;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The API client the connection is for
|
/// The API client the connection is for
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SocketApiClient ApiClient { get; set; }
|
public SocketApiClient ApiClient { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If the socket should be reconnected upon closing
|
|
||||||
/// </summary>
|
|
||||||
public bool ShouldReconnect { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Current reconnect try, reset when a successful connection is made
|
|
||||||
/// </summary>
|
|
||||||
public int ReconnectTry { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Current resubscribe try, reset when a successful connection is made
|
|
||||||
/// </summary>
|
|
||||||
public int ResubscribeTry { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Time of disconnecting
|
/// Time of disconnecting
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -110,7 +107,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tag for identificaion
|
/// Tag for identificaion
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Tag { get; set; }
|
public string Tag { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If activity is paused
|
/// If activity is paused
|
||||||
@@ -124,22 +121,39 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
pausedActivity = value;
|
pausedActivity = value;
|
||||||
log.Write(LogLevel.Information, $"Socket {SocketId} Paused activity: " + value);
|
log.Write(LogLevel.Information, $"Socket {SocketId} Paused activity: " + value);
|
||||||
if(pausedActivity) ActivityPaused?.Invoke();
|
if(pausedActivity) _ = Task.Run(() => ActivityPaused?.Invoke());
|
||||||
else ActivityUnpaused?.Invoke();
|
else _ = Task.Run(() => ActivityUnpaused?.Invoke());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Status of the socket connection
|
||||||
|
/// </summary>
|
||||||
|
public SocketStatus Status
|
||||||
|
{
|
||||||
|
get => _status;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
if (_status == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var oldStatus = _status;
|
||||||
|
_status = value;
|
||||||
|
log.Write(LogLevel.Debug, $"Socket {SocketId} status changed from {oldStatus} to {_status}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private bool pausedActivity;
|
private bool pausedActivity;
|
||||||
private readonly List<SocketSubscription> subscriptions;
|
private readonly List<SocketSubscription> subscriptions;
|
||||||
private readonly object subscriptionLock = new();
|
private readonly object subscriptionLock = new();
|
||||||
|
|
||||||
private bool lostTriggered;
|
|
||||||
private readonly Log log;
|
private readonly Log log;
|
||||||
private readonly BaseSocketClient socketClient;
|
private readonly BaseSocketClient socketClient;
|
||||||
|
|
||||||
private readonly List<PendingRequest> pendingRequests;
|
private readonly List<PendingRequest> pendingRequests;
|
||||||
private Task? _socketProcessReconnectTask;
|
|
||||||
|
private SocketStatus _status;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The underlying websocket
|
/// The underlying websocket
|
||||||
@@ -152,120 +166,83 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <param name="client">The socket client</param>
|
/// <param name="client">The socket client</param>
|
||||||
/// <param name="apiClient">The api client</param>
|
/// <param name="apiClient">The api client</param>
|
||||||
/// <param name="socket">The socket</param>
|
/// <param name="socket">The socket</param>
|
||||||
public SocketConnection(BaseSocketClient client, SocketApiClient apiClient, IWebsocket socket)
|
/// <param name="tag"></param>
|
||||||
|
public SocketConnection(BaseSocketClient client, SocketApiClient apiClient, IWebsocket socket, string tag)
|
||||||
{
|
{
|
||||||
log = client.log;
|
log = client.log;
|
||||||
socketClient = client;
|
socketClient = client;
|
||||||
ApiClient = apiClient;
|
ApiClient = apiClient;
|
||||||
|
Tag = tag;
|
||||||
|
|
||||||
pendingRequests = new List<PendingRequest>();
|
pendingRequests = new List<PendingRequest>();
|
||||||
|
|
||||||
subscriptions = new List<SocketSubscription>();
|
subscriptions = new List<SocketSubscription>();
|
||||||
|
|
||||||
_socket = socket;
|
_socket = socket;
|
||||||
|
_socket.OnMessage += HandleMessage;
|
||||||
_socket.Timeout = client.ClientOptions.SocketNoDataTimeout;
|
_socket.OnOpen += HandleOpen;
|
||||||
_socket.OnMessage += ProcessMessage;
|
_socket.OnClose += HandleClose;
|
||||||
_socket.OnOpen += SocketOnOpen;
|
_socket.OnReconnecting += HandleReconnecting;
|
||||||
}
|
_socket.OnReconnected += HandleReconnected;
|
||||||
|
_socket.OnError += HandleError;
|
||||||
/// <summary>
|
_socket.GetReconnectionUrl = GetReconnectionUrlAsync;
|
||||||
/// Connect the websocket and start processing
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<bool> ConnectAsync()
|
|
||||||
{
|
|
||||||
var connected = await _socket.ConnectAsync().ConfigureAwait(false);
|
|
||||||
if (connected)
|
|
||||||
StartProcessingTask();
|
|
||||||
|
|
||||||
return connected;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieve the underlying socket
|
/// Handler for a socket opening
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
protected virtual void HandleOpen()
|
||||||
public IWebsocket GetSocket()
|
|
||||||
{
|
{
|
||||||
return _socket;
|
Status = SocketStatus.Connected;
|
||||||
|
PausedActivity = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Trigger a reconnect of the socket connection
|
/// Handler for a socket closing without reconnect
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
protected virtual void HandleClose()
|
||||||
public async Task TriggerReconnectAsync()
|
|
||||||
{
|
{
|
||||||
await _socket.CloseAsync().ConfigureAwait(false);
|
Status = SocketStatus.Closed;
|
||||||
|
Authenticated = false;
|
||||||
|
lock(subscriptionLock)
|
||||||
|
{
|
||||||
|
foreach (var sub in subscriptions)
|
||||||
|
sub.Confirmed = false;
|
||||||
|
}
|
||||||
|
Task.Run(() => ConnectionClosed?.Invoke());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Close the connection
|
/// Handler for a socket losing conenction and starting reconnect
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
protected virtual void HandleReconnecting()
|
||||||
public async Task CloseAsync()
|
|
||||||
{
|
{
|
||||||
ShouldReconnect = false;
|
Status = SocketStatus.Reconnecting;
|
||||||
if (socketClient.socketConnections.ContainsKey(SocketId))
|
DisconnectTime = DateTime.UtcNow;
|
||||||
socketClient.socketConnections.TryRemove(SocketId, out _);
|
Authenticated = false;
|
||||||
|
|
||||||
lock (subscriptionLock)
|
lock (subscriptionLock)
|
||||||
{
|
{
|
||||||
foreach (var subscription in subscriptions)
|
foreach (var sub in subscriptions)
|
||||||
{
|
sub.Confirmed = false;
|
||||||
if (subscription.CancellationTokenRegistration.HasValue)
|
|
||||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
await _socket.CloseAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (_socketProcessReconnectTask != null)
|
_ = Task.Run(() => ConnectionLost?.Invoke());
|
||||||
await _socketProcessReconnectTask.ConfigureAwait(false);
|
|
||||||
|
|
||||||
_socket.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Close a subscription on this connection. If all subscriptions on this connection are closed the connection gets closed as well
|
/// Get the url to connect to when reconnecting
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="subscription">Subscription to close</param>
|
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task CloseAsync(SocketSubscription subscription)
|
protected virtual async Task<Uri?> GetReconnectionUrlAsync()
|
||||||
{
|
{
|
||||||
if (!_socket.IsOpen)
|
return await socketClient.GetReconnectUriAsync(ApiClient, this).ConfigureAwait(false);
|
||||||
return;
|
|
||||||
|
|
||||||
if (subscription.CancellationTokenRegistration.HasValue)
|
|
||||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
|
||||||
|
|
||||||
if (subscription.Confirmed)
|
|
||||||
await socketClient.UnsubscribeAsync(this, subscription).ConfigureAwait(false);
|
|
||||||
|
|
||||||
bool shouldCloseConnection;
|
|
||||||
lock (subscriptionLock)
|
|
||||||
shouldCloseConnection = !subscriptions.Any(r => r.UserSubscription && subscription != r);
|
|
||||||
|
|
||||||
if (shouldCloseConnection)
|
|
||||||
await CloseAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
lock (subscriptionLock)
|
|
||||||
subscriptions.Remove(subscription);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StartProcessingTask()
|
/// <summary>
|
||||||
|
/// Handler for a socket which has reconnected
|
||||||
|
/// </summary>
|
||||||
|
protected virtual async void HandleReconnected()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Trace, $"Starting {SocketId} process/reconnect task");
|
Status = SocketStatus.Resubscribing;
|
||||||
_socketProcessReconnectTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await _socket.ProcessAsync().ConfigureAwait(false);
|
|
||||||
await ReconnectAsync().ConfigureAwait(false);
|
|
||||||
log.Write(LogLevel.Trace, $"Process/reconnect {SocketId} task finished");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ReconnectAsync()
|
|
||||||
{
|
|
||||||
// Fail all pending requests
|
|
||||||
lock (pendingRequests)
|
lock (pendingRequests)
|
||||||
{
|
{
|
||||||
foreach (var pendingRequest in pendingRequests.ToList())
|
foreach (var pendingRequest in pendingRequests.ToList())
|
||||||
@@ -275,135 +252,37 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (socketClient.ClientOptions.AutoReconnect && ShouldReconnect)
|
var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false);
|
||||||
{
|
if (!reconnectSuccessful)
|
||||||
// Should reconnect
|
await _socket.ReconnectAsync().ConfigureAwait(false);
|
||||||
DisconnectTime = DateTime.UtcNow;
|
|
||||||
log.Write(LogLevel.Warning, $"Socket {SocketId} Connection lost, will try to reconnect");
|
|
||||||
if (!lostTriggered)
|
|
||||||
{
|
|
||||||
lostTriggered = true;
|
|
||||||
ConnectionLost?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
while (ShouldReconnect)
|
|
||||||
{
|
|
||||||
if (ReconnectTry > 0)
|
|
||||||
{
|
|
||||||
// Wait a bit before attempting reconnect
|
|
||||||
await Task.Delay(socketClient.ClientOptions.ReconnectInterval).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ShouldReconnect)
|
|
||||||
{
|
|
||||||
// Should reconnect changed to false while waiting to reconnect
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_socket.Reset();
|
|
||||||
if (!await _socket.ConnectAsync().ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
// Reconnect failed
|
|
||||||
ReconnectTry++;
|
|
||||||
ResubscribeTry = 0;
|
|
||||||
if (socketClient.ClientOptions.MaxReconnectTries != null
|
|
||||||
&& ReconnectTry >= socketClient.ClientOptions.MaxReconnectTries)
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Warning, $"Socket {SocketId} failed to reconnect after {ReconnectTry} tries, closing");
|
|
||||||
ShouldReconnect = false;
|
|
||||||
|
|
||||||
if (socketClient.socketConnections.ContainsKey(SocketId))
|
|
||||||
socketClient.socketConnections.TryRemove(SocketId, out _);
|
|
||||||
|
|
||||||
_ = Task.Run(() => ConnectionClosed?.Invoke());
|
|
||||||
// Reached max tries, break loop and leave connection closed
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue to try again
|
|
||||||
log.Write(LogLevel.Debug, $"Socket {SocketId} failed to reconnect{(socketClient.ClientOptions.MaxReconnectTries != null ? $", try {ReconnectTry}/{socketClient.ClientOptions.MaxReconnectTries}" : "")}, will try again in {socketClient.ClientOptions.ReconnectInterval}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Successfully reconnected, start processing
|
|
||||||
StartProcessingTask();
|
|
||||||
|
|
||||||
ReconnectTry = 0;
|
|
||||||
var time = DisconnectTime;
|
|
||||||
DisconnectTime = null;
|
|
||||||
|
|
||||||
log.Write(LogLevel.Information, $"Socket {SocketId} reconnected after {DateTime.UtcNow - time}");
|
|
||||||
|
|
||||||
var reconnectResult = await ProcessReconnectAsync().ConfigureAwait(false);
|
|
||||||
if (!reconnectResult)
|
|
||||||
{
|
|
||||||
// Failed to resubscribe everything
|
|
||||||
ResubscribeTry++;
|
|
||||||
DisconnectTime = time;
|
|
||||||
|
|
||||||
if (socketClient.ClientOptions.MaxResubscribeTries != null &&
|
|
||||||
ResubscribeTry >= socketClient.ClientOptions.MaxResubscribeTries)
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Warning, $"Socket {SocketId} failed to resubscribe after {ResubscribeTry} tries, closing. Last resubscription error: {reconnectResult.Error}");
|
|
||||||
ShouldReconnect = false;
|
|
||||||
|
|
||||||
if (socketClient.socketConnections.ContainsKey(SocketId))
|
|
||||||
socketClient.socketConnections.TryRemove(SocketId, out _);
|
|
||||||
|
|
||||||
_ = Task.Run(() => ConnectionClosed?.Invoke());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
log.Write(LogLevel.Debug, $"Socket {SocketId} resubscribing all subscriptions failed on reconnected socket{(socketClient.ClientOptions.MaxResubscribeTries != null ? $", try {ResubscribeTry}/{socketClient.ClientOptions.MaxResubscribeTries}" : "")}. Error: {reconnectResult.Error}. Disconnecting and reconnecting.");
|
|
||||||
|
|
||||||
// Failed resubscribe, close socket if it is still open
|
|
||||||
if (_socket.IsOpen)
|
|
||||||
await _socket.CloseAsync().ConfigureAwait(false);
|
|
||||||
else
|
|
||||||
DisconnectTime = DateTime.UtcNow;
|
|
||||||
|
|
||||||
// Break out of the loop, the new processing task should reconnect again
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Succesfully reconnected
|
|
||||||
log.Write(LogLevel.Information, $"Socket {SocketId} data connection restored.");
|
|
||||||
ResubscribeTry = 0;
|
|
||||||
if (lostTriggered)
|
|
||||||
{
|
|
||||||
lostTriggered = false;
|
|
||||||
_ = Task.Run(() => ConnectionRestored?.Invoke(time.HasValue ? DateTime.UtcNow - time.Value : TimeSpan.FromSeconds(0))).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!socketClient.ClientOptions.AutoReconnect && ShouldReconnect)
|
Status = SocketStatus.Connected;
|
||||||
_ = Task.Run(() => ConnectionClosed?.Invoke());
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
// No reconnecting needed
|
ConnectionRestored?.Invoke(DateTime.UtcNow - DisconnectTime!.Value);
|
||||||
log.Write(LogLevel.Information, $"Socket {SocketId} closed");
|
DisconnectTime = null;
|
||||||
if (socketClient.socketConnections.ContainsKey(SocketId))
|
});
|
||||||
socketClient.socketConnections.TryRemove(SocketId, out _);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose the connection
|
/// Handler for an error on a websocket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose()
|
/// <param name="e">The exception</param>
|
||||||
|
protected virtual void HandleError(Exception e)
|
||||||
{
|
{
|
||||||
_socket.Dispose();
|
if (e is WebSocketException wse)
|
||||||
|
log.Write(LogLevel.Warning, $"Socket {SocketId} error: Websocket error code {wse.WebSocketErrorCode}, details: " + e.ToLogString());
|
||||||
|
else
|
||||||
|
log.Write(LogLevel.Warning, $"Socket {SocketId} error: " + e.ToLogString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Process a message received by the socket
|
/// Process a message received by the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data">The received data</param>
|
/// <param name="data">The received data</param>
|
||||||
private void ProcessMessage(string data)
|
protected virtual void HandleMessage(string data)
|
||||||
{
|
{
|
||||||
var timestamp = DateTime.UtcNow;
|
var timestamp = DateTime.UtcNow;
|
||||||
log.Write(LogLevel.Trace, $"Socket {SocketId} received data: " + data);
|
log.Write(LogLevel.Trace, $"Socket {SocketId} received data: " + data);
|
||||||
@@ -419,15 +298,13 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
|
|
||||||
var handledResponse = false;
|
var handledResponse = false;
|
||||||
PendingRequest[] requests;
|
|
||||||
lock(pendingRequests)
|
|
||||||
requests = pendingRequests.ToArray();
|
|
||||||
|
|
||||||
// Remove any timed out requests
|
// Remove any timed out requests
|
||||||
foreach (var request in requests.Where(r => r.Completed))
|
PendingRequest[] requests;
|
||||||
|
lock (pendingRequests)
|
||||||
{
|
{
|
||||||
lock (pendingRequests)
|
pendingRequests.RemoveAll(r => r.Completed);
|
||||||
pendingRequests.Remove(request);
|
requests = pendingRequests.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this message is an answer on any pending requests
|
// Check if this message is an answer on any pending requests
|
||||||
@@ -435,7 +312,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
if (pendingRequest.CheckData(tokenData))
|
if (pendingRequest.CheckData(tokenData))
|
||||||
{
|
{
|
||||||
lock (pendingRequests)
|
lock (pendingRequests)
|
||||||
pendingRequests.Remove(pendingRequest);
|
pendingRequests.Remove(pendingRequest);
|
||||||
|
|
||||||
if (!socketClient.ContinueOnQueryResponse)
|
if (!socketClient.ContinueOnQueryResponse)
|
||||||
@@ -447,8 +324,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Message was not a request response, check data handlers
|
// Message was not a request response, check data handlers
|
||||||
var messageEvent = new MessageEvent(this, tokenData, socketClient.ClientOptions.OutputOriginalData ? data: null, timestamp);
|
var messageEvent = new MessageEvent(this, tokenData, socketClient.ClientOptions.OutputOriginalData ? data : null, timestamp);
|
||||||
var (handled, userProcessTime) = HandleData(messageEvent);
|
var (handled, userProcessTime, subscription) = HandleData(messageEvent);
|
||||||
if (!handled && !handledResponse)
|
if (!handled && !handledResponse)
|
||||||
{
|
{
|
||||||
if (!socketClient.UnhandledMessageExpected)
|
if (!socketClient.UnhandledMessageExpected)
|
||||||
@@ -458,20 +335,129 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
var total = DateTime.UtcNow - timestamp;
|
var total = DateTime.UtcNow - timestamp;
|
||||||
if (userProcessTime.TotalMilliseconds > 500)
|
if (userProcessTime.TotalMilliseconds > 500)
|
||||||
log.Write(LogLevel.Debug, $"Socket {SocketId} message processing slow ({(int)total.TotalMilliseconds}ms), consider offloading data handling to another thread. " +
|
log.Write(LogLevel.Debug, $"Socket {SocketId}{(subscription == null ? "" : " subscription " + subscription!.Id)} message processing slow ({(int)total.TotalMilliseconds}ms, {(int)userProcessTime.TotalMilliseconds}ms user code), consider offloading data handling to another thread. " +
|
||||||
"Data from this socket may arrive late or not at all if message processing is continuously slow.");
|
"Data from this socket may arrive late or not at all if message processing is continuously slow.");
|
||||||
|
|
||||||
log.Write(LogLevel.Trace, $"Socket {SocketId} message processed in {(int)total.TotalMilliseconds}ms, ({(int)userProcessTime.TotalMilliseconds}ms user code)");
|
log.Write(LogLevel.Trace, $"Socket {SocketId}{(subscription == null ? "" : " subscription " + subscription!.Id)} message processed in {(int)total.TotalMilliseconds}ms, ({(int)userProcessTime.TotalMilliseconds}ms user code)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connect the websocket
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<bool> ConnectAsync() => await _socket.ConnectAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve the underlying socket
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public IWebsocket GetSocket() => _socket;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger a reconnect of the socket connection
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Close the connection
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task CloseAsync()
|
||||||
|
{
|
||||||
|
if (Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (socketClient.socketConnections.ContainsKey(SocketId))
|
||||||
|
socketClient.socketConnections.TryRemove(SocketId, out _);
|
||||||
|
|
||||||
|
lock (subscriptionLock)
|
||||||
|
{
|
||||||
|
foreach (var subscription in subscriptions)
|
||||||
|
{
|
||||||
|
if (subscription.CancellationTokenRegistration.HasValue)
|
||||||
|
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _socket.CloseAsync().ConfigureAwait(false);
|
||||||
|
_socket.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Close a subscription on this connection. If all subscriptions on this connection are closed the connection gets closed as well
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscription">Subscription to close</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task CloseAsync(SocketSubscription subscription)
|
||||||
|
{
|
||||||
|
lock (subscriptionLock)
|
||||||
|
{
|
||||||
|
if (!subscriptions.Contains(subscription))
|
||||||
|
return;
|
||||||
|
|
||||||
|
subscription.Closed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
log.Write(LogLevel.Debug, $"Socket {SocketId} closing subscription {subscription.Id}");
|
||||||
|
if (subscription.CancellationTokenRegistration.HasValue)
|
||||||
|
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||||
|
|
||||||
|
if (subscription.Confirmed && _socket.IsOpen)
|
||||||
|
await socketClient.UnsubscribeAsync(this, subscription).ConfigureAwait(false);
|
||||||
|
|
||||||
|
bool shouldCloseConnection;
|
||||||
|
lock (subscriptionLock)
|
||||||
|
{
|
||||||
|
if (Status == SocketStatus.Closing)
|
||||||
|
{
|
||||||
|
log.Write(LogLevel.Debug, $"Socket {SocketId} already closing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldCloseConnection = subscriptions.All(r => !r.UserSubscription || r.Closed);
|
||||||
|
if (shouldCloseConnection)
|
||||||
|
Status = SocketStatus.Closing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCloseConnection)
|
||||||
|
{
|
||||||
|
log.Write(LogLevel.Debug, $"Socket {SocketId} closing as there are no more subscriptions");
|
||||||
|
await CloseAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (subscriptionLock)
|
||||||
|
subscriptions.Remove(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dispose the connection
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Status = SocketStatus.Disposed;
|
||||||
|
_socket.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a subscription to this connection
|
/// Add a subscription to this connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="subscription"></param>
|
/// <param name="subscription"></param>
|
||||||
public void AddSubscription(SocketSubscription subscription)
|
public bool AddSubscription(SocketSubscription subscription)
|
||||||
{
|
{
|
||||||
lock(subscriptionLock)
|
lock (subscriptionLock)
|
||||||
|
{
|
||||||
|
if (Status != SocketStatus.None && Status != SocketStatus.Connected)
|
||||||
|
return false;
|
||||||
|
|
||||||
subscriptions.Add(subscription);
|
subscriptions.Add(subscription);
|
||||||
|
if(subscription.UserSubscription)
|
||||||
|
log.Write(LogLevel.Debug, $"Socket {SocketId} adding new subscription with id {subscription.Id}, total subscriptions on connection: {subscriptions.Count(s => s.UserSubscription)}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -500,7 +486,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="messageEvent"></param>
|
/// <param name="messageEvent"></param>
|
||||||
/// <returns>True if the data was successfully handled</returns>
|
/// <returns>True if the data was successfully handled</returns>
|
||||||
private (bool, TimeSpan) HandleData(MessageEvent messageEvent)
|
private (bool, TimeSpan, SocketSubscription?) HandleData(MessageEvent messageEvent)
|
||||||
{
|
{
|
||||||
SocketSubscription? currentSubscription = null;
|
SocketSubscription? currentSubscription = null;
|
||||||
try
|
try
|
||||||
@@ -541,13 +527,13 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (handled, userCodeDuration);
|
return (handled, userCodeDuration, currentSubscription);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Error, $"Socket {SocketId} Exception during message processing\r\nException: {ex.ToLogString()}\r\nData: {messageEvent.JsonData}");
|
log.Write(LogLevel.Error, $"Socket {SocketId} Exception during message processing\r\nException: {ex.ToLogString()}\r\nData: {messageEvent.JsonData}");
|
||||||
currentSubscription?.InvokeExceptionHandler(ex);
|
currentSubscription?.InvokeExceptionHandler(ex);
|
||||||
return (false, TimeSpan.Zero);
|
return (false, TimeSpan.Zero, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,21 +591,24 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handler for a socket opening
|
|
||||||
/// </summary>
|
|
||||||
protected virtual void SocketOnOpen()
|
|
||||||
{
|
|
||||||
ReconnectTry = 0;
|
|
||||||
PausedActivity = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<CallResult<bool>> ProcessReconnectAsync()
|
private async Task<CallResult<bool>> ProcessReconnectAsync()
|
||||||
{
|
{
|
||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
return new CallResult<bool>(new WebError("Socket not connected"));
|
return new CallResult<bool>(new WebError("Socket not connected"));
|
||||||
|
|
||||||
if (Authenticated)
|
bool anySubscriptions = false;
|
||||||
|
lock (subscriptionLock)
|
||||||
|
anySubscriptions = subscriptions.Any(s => s.UserSubscription);
|
||||||
|
|
||||||
|
if (!anySubscriptions)
|
||||||
|
{
|
||||||
|
// No need to resubscribe anything
|
||||||
|
log.Write(LogLevel.Debug, $"Socket {SocketId} Nothing to resubscribe, closing connection");
|
||||||
|
_ = _socket.CloseAsync();
|
||||||
|
return new CallResult<bool>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscriptions.Any(s => s.Authenticated))
|
||||||
{
|
{
|
||||||
// If we reconnected a authenticated connection we need to re-authenticate
|
// If we reconnected a authenticated connection we need to re-authenticate
|
||||||
var authResult = await socketClient.AuthenticateSocketAsync(this).ConfigureAwait(false);
|
var authResult = await socketClient.AuthenticateSocketAsync(this).ConfigureAwait(false);
|
||||||
@@ -629,13 +618,22 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
return authResult;
|
return authResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Authenticated = true;
|
||||||
log.Write(LogLevel.Debug, $"Socket {SocketId} authentication succeeded on reconnected socket.");
|
log.Write(LogLevel.Debug, $"Socket {SocketId} authentication succeeded on reconnected socket.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a list of all subscriptions on the socket
|
// Get a list of all subscriptions on the socket
|
||||||
List<SocketSubscription> subscriptionList;
|
List<SocketSubscription> subscriptionList = new List<SocketSubscription>();
|
||||||
lock (subscriptionLock)
|
lock (subscriptionLock)
|
||||||
subscriptionList = subscriptions.Where(h => h.Request != null).ToList();
|
{
|
||||||
|
foreach (var subscription in subscriptions)
|
||||||
|
{
|
||||||
|
if (subscription.Request != null)
|
||||||
|
subscriptionList.Add(subscription);
|
||||||
|
else
|
||||||
|
subscription.Confirmed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe
|
// Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe
|
||||||
for (var i = 0; i < subscriptionList.Count; i += socketClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket)
|
for (var i = 0; i < subscriptionList.Count; i += socketClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket)
|
||||||
@@ -645,13 +643,16 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
var taskList = new List<Task<CallResult<bool>>>();
|
var taskList = new List<Task<CallResult<bool>>>();
|
||||||
foreach (var subscription in subscriptionList.Skip(i).Take(socketClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket))
|
foreach (var subscription in subscriptionList.Skip(i).Take(socketClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket))
|
||||||
taskList.Add(socketClient.SubscribeAndWaitAsync(this, subscription.Request!, subscription));
|
taskList.Add(socketClient.SubscribeAndWaitAsync(this, subscription.Request!, subscription));
|
||||||
|
|
||||||
await Task.WhenAll(taskList).ConfigureAwait(false);
|
await Task.WhenAll(taskList).ConfigureAwait(false);
|
||||||
if (taskList.Any(t => !t.Result.Success))
|
if (taskList.Any(t => !t.Result.Success))
|
||||||
return taskList.First(t => !t.Result.Success).Result;
|
return taskList.First(t => !t.Result.Success).Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var subscription in subscriptionList)
|
||||||
|
subscription.Confirmed = true;
|
||||||
|
|
||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
return new CallResult<bool>(new WebError("Socket not connected"));
|
return new CallResult<bool>(new WebError("Socket not connected"));
|
||||||
|
|
||||||
@@ -671,5 +672,40 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
return await socketClient.SubscribeAndWaitAsync(this, socketSubscription.Request!, socketSubscription).ConfigureAwait(false);
|
return await socketClient.SubscribeAndWaitAsync(this, socketSubscription.Request!, socketSubscription).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Status of the socket connection
|
||||||
|
/// </summary>
|
||||||
|
public enum SocketStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// None/Initial
|
||||||
|
/// </summary>
|
||||||
|
None,
|
||||||
|
/// <summary>
|
||||||
|
/// Connected
|
||||||
|
/// </summary>
|
||||||
|
Connected,
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnecting
|
||||||
|
/// </summary>
|
||||||
|
Reconnecting,
|
||||||
|
/// <summary>
|
||||||
|
/// Resubscribing on reconnected socket
|
||||||
|
/// </summary>
|
||||||
|
Resubscribing,
|
||||||
|
/// <summary>
|
||||||
|
/// Closing
|
||||||
|
/// </summary>
|
||||||
|
Closing,
|
||||||
|
/// <summary>
|
||||||
|
/// Closed
|
||||||
|
/// </summary>
|
||||||
|
Closed,
|
||||||
|
/// <summary>
|
||||||
|
/// Disposed
|
||||||
|
/// </summary>
|
||||||
|
Disposed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,19 +43,30 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Confirmed { get; set; }
|
public bool Confirmed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether authentication is needed for this subscription
|
||||||
|
/// </summary>
|
||||||
|
public bool Authenticated { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether we're closing this subscription and a socket connection shouldn't be kept open for it
|
||||||
|
/// </summary>
|
||||||
|
public bool Closed { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cancellation token registration, should be disposed when subscription is closed. Used for closing the subscription with
|
/// Cancellation token registration, should be disposed when subscription is closed. Used for closing the subscription with
|
||||||
/// a provided cancelation token
|
/// a provided cancelation token
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CancellationTokenRegistration? CancellationTokenRegistration { get; set; }
|
public CancellationTokenRegistration? CancellationTokenRegistration { get; set; }
|
||||||
|
|
||||||
private SocketSubscription(int id, object? request, string? identifier, bool userSubscription, Action<MessageEvent> dataHandler)
|
private SocketSubscription(int id, object? request, string? identifier, bool userSubscription, bool authenticated, Action<MessageEvent> dataHandler)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
UserSubscription = userSubscription;
|
UserSubscription = userSubscription;
|
||||||
MessageHandler = dataHandler;
|
MessageHandler = dataHandler;
|
||||||
Request = request;
|
Request = request;
|
||||||
Identifier = identifier;
|
Identifier = identifier;
|
||||||
|
Authenticated = authenticated;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -64,12 +75,13 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <param name="id"></param>
|
/// <param name="id"></param>
|
||||||
/// <param name="request"></param>
|
/// <param name="request"></param>
|
||||||
/// <param name="userSubscription"></param>
|
/// <param name="userSubscription"></param>
|
||||||
|
/// <param name="authenticated"></param>
|
||||||
/// <param name="dataHandler"></param>
|
/// <param name="dataHandler"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static SocketSubscription CreateForRequest(int id, object request, bool userSubscription,
|
public static SocketSubscription CreateForRequest(int id, object request, bool userSubscription,
|
||||||
Action<MessageEvent> dataHandler)
|
bool authenticated, Action<MessageEvent> dataHandler)
|
||||||
{
|
{
|
||||||
return new SocketSubscription(id, request, null, userSubscription, dataHandler);
|
return new SocketSubscription(id, request, null, userSubscription, authenticated, dataHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -78,12 +90,13 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <param name="id"></param>
|
/// <param name="id"></param>
|
||||||
/// <param name="identifier"></param>
|
/// <param name="identifier"></param>
|
||||||
/// <param name="userSubscription"></param>
|
/// <param name="userSubscription"></param>
|
||||||
|
/// <param name="authenticated"></param>
|
||||||
/// <param name="dataHandler"></param>
|
/// <param name="dataHandler"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static SocketSubscription CreateForIdentifier(int id, string identifier, bool userSubscription,
|
public static SocketSubscription CreateForIdentifier(int id, string identifier, bool userSubscription,
|
||||||
Action<MessageEvent> dataHandler)
|
bool authenticated, Action<MessageEvent> dataHandler)
|
||||||
{
|
{
|
||||||
return new SocketSubscription(id, null, identifier, userSubscription, dataHandler);
|
return new SocketSubscription(id, null, identifier, userSubscription, authenticated, dataHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event when the connection is closed. This event happens when reconnecting/resubscribing has failed too often based on the <see cref="BaseSocketClientOptions.MaxReconnectTries"/> and <see cref="BaseSocketClientOptions.MaxResubscribeTries"/> options,
|
/// Event when the connection is closed and will not be reconnected
|
||||||
/// or <see cref="BaseSocketClientOptions.AutoReconnect"/> is false. The socket will not be reconnected
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action ConnectionClosed
|
public event Action ConnectionClosed
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Sockets
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parameters for a websocket
|
||||||
|
/// </summary>
|
||||||
|
public class WebSocketParameters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The uri to connect to
|
||||||
|
/// </summary>
|
||||||
|
public Uri Uri { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Headers to send in the connection handshake
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
|
||||||
|
/// <summary>
|
||||||
|
/// Cookies to send in the connection handshake
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>();
|
||||||
|
/// <summary>
|
||||||
|
/// The time to wait between reconnect attempts
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
/// <summary>
|
||||||
|
/// Proxy for the connection
|
||||||
|
/// </summary>
|
||||||
|
public ApiProxy? Proxy { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the socket should automatically reconnect when connection is lost
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoReconnect { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? Timeout { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Interval at which to send ping frames
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? KeepAliveInterval { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The max amount of messages to send per second
|
||||||
|
/// </summary>
|
||||||
|
public int? RatelimitPerSecond { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Origin header value to send in the connection handshake
|
||||||
|
/// </summary>
|
||||||
|
public string? Origin { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
|
||||||
|
/// </summary>
|
||||||
|
public Func<byte[], string>? DataInterpreterBytes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delegate used for processing string data received from socket connections before it is processed by handlers
|
||||||
|
/// </summary>
|
||||||
|
public Func<string, string>? DataInterpreterString { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encoding for sending/receiving data
|
||||||
|
/// </summary>
|
||||||
|
public Encoding Encoding { get; set; } = Encoding.UTF8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uri">Uri</param>
|
||||||
|
/// <param name="autoReconnect">Auto reconnect</param>
|
||||||
|
public WebSocketParameters(Uri uri, bool autoReconnect)
|
||||||
|
{
|
||||||
|
Uri = uri;
|
||||||
|
AutoReconnect = autoReconnect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using System.Collections.Generic;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Logging;
|
using CryptoExchange.Net.Logging;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Sockets
|
namespace CryptoExchange.Net.Sockets
|
||||||
@@ -11,15 +9,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
public class WebsocketFactory : IWebsocketFactory
|
public class WebsocketFactory : IWebsocketFactory
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IWebsocket CreateWebsocket(Log log, string url)
|
public IWebsocket CreateWebsocket(Log log, WebSocketParameters parameters)
|
||||||
{
|
{
|
||||||
return new CryptoExchangeWebSocketClient(log, new Uri(url));
|
return new CryptoExchangeWebSocketClient(log, parameters);
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public IWebsocket CreateWebsocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers)
|
|
||||||
{
|
|
||||||
return new CryptoExchangeWebSocketClient(log, new Uri(url), cookies, headers);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,80 @@ CryptoExchange.Net is a base package which can be used to easily implement crypt
|
|||||||
## Discord
|
## Discord
|
||||||
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
|
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
|
||||||
|
|
||||||
## Donate / Sponsor
|
## Support the project
|
||||||
I develop and maintain this package on my own for free in my spare time. Donations are greatly appreciated. If you prefer to donate any other currency please contact me.
|
I develop and maintain this package on my own for free in my spare time, any support is greatly appreciated.
|
||||||
|
|
||||||
|
### Referral link
|
||||||
|
Use one of the following following referral links to signup to a new exchange to pay a small percentage of the trading fees you pay to support the project instead of paying them straight to the exchange. This doesn't cost you a thing!
|
||||||
|
[Binance](https://accounts.binance.com/en/register?ref=10153680)
|
||||||
|
[Bitfinex](https://www.bitfinex.com/sign-up?refcode=kCCe-CNBO)
|
||||||
|
[Bittrex](https://bittrex.com/discover/join?referralCode=TST-DJM-CSX)
|
||||||
|
[Bybit](https://partner.bybit.com/b/jkorf)
|
||||||
|
[CoinEx](https://www.coinex.com/register?refer_code=hd6gn)
|
||||||
|
[FTX](https://ftx.com/referrals#a=31620192)
|
||||||
|
[Huobi](https://www.huobi.com/en-us/v/register/double-invite/?inviter_id=11343840&invite_code=fxp93)
|
||||||
|
[Kucoin](https://www.kucoin.com/ucenter/signup?rcode=RguMux)
|
||||||
|
|
||||||
|
### Donate
|
||||||
|
Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.
|
||||||
|
|
||||||
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS
|
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS
|
||||||
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2
|
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2
|
||||||
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs
|
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs
|
||||||
|
|
||||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf)
|
### Sponsor
|
||||||
|
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
* Version 5.2.4 - 31 Jul 2022
|
||||||
|
* Added handling of PlatformNotSupportedException when trying to use websocket from WebAssembly
|
||||||
|
* Changed DataEvent to have a public constructor for testing purposes
|
||||||
|
* Fixed EnumConverter serializing values without proper quotes
|
||||||
|
* Fixed websocket connection reconnecting too quickly when resubscribing/reauthenticating fails
|
||||||
|
|
||||||
|
* Version 5.2.3 - 19 Jul 2022
|
||||||
|
* Fixed socket getting disconnected when `no data` timeout is reached instead of being reconnected
|
||||||
|
|
||||||
|
* Version 5.2.2 - 17 Jul 2022
|
||||||
|
* Added support for retrieving a new url when socket connection is lost and reconnection will happen
|
||||||
|
|
||||||
|
* Version 5.2.1 - 16 Jul 2022
|
||||||
|
* Fixed socket reconnect issue
|
||||||
|
* Fixed `message not handled` messages after unsubscribing
|
||||||
|
* Fixed error returning for non-json error responses
|
||||||
|
|
||||||
|
* Version 5.2.0 - 10 Jul 2022
|
||||||
|
* Refactored websocket code, removed some clutter and simplified
|
||||||
|
* Added ReconnectAsync and GetSubscriptionsState methods on socket clients
|
||||||
|
|
||||||
|
* Version 5.1.12 - 12 Jun 2022
|
||||||
|
* Changed time sync so requests no longer wait for it to complete unless it's the first time
|
||||||
|
* Made log client options changable after client creation
|
||||||
|
* Fixed proxy setting not used when reconnecting socket
|
||||||
|
* Changed MaxSocketConnections to a client options
|
||||||
|
* Updated socket reconnection logic
|
||||||
|
|
||||||
|
* Version 5.1.12 - 12 Jun 2022
|
||||||
|
* Changed time sync so requests no longer wait for it to complete unless it's the first time
|
||||||
|
* Made log client options changable after client creation
|
||||||
|
* Fixed proxy setting not used when reconnecting socket
|
||||||
|
* Updated socket reconnection logic
|
||||||
|
|
||||||
|
* Version 5.1.11 - 24 May 2022
|
||||||
|
* Added KeepAliveInterval setting
|
||||||
|
* Fixed port not being copied when setting parameters on request
|
||||||
|
* Fixed inconsistent PackageReference casing in csproj
|
||||||
|
|
||||||
|
* Version 5.1.10 - 22 May 2022
|
||||||
|
* Fixed order book reconnecting while Diposed
|
||||||
|
* Fixed exception when disposing socket client while reconnecting
|
||||||
|
* Added additional null/default checking in DateTimeConverter
|
||||||
|
* Changed ConnectionLost subscription event to run in seperate task to prevent exception/longer operations from intervering with reconnecting
|
||||||
|
|
||||||
|
* Version 5.1.9 - 08 May 2022
|
||||||
|
* Added latency to the timesync calculation
|
||||||
|
* Small fix for exception in socket close handling
|
||||||
|
|
||||||
* Version 5.1.8 - 01 May 2022
|
* Version 5.1.8 - 01 May 2022
|
||||||
* Cleanup socket code, fixed an issue which could cause connections to never reconnect when connection was lost
|
* Cleanup socket code, fixed an issue which could cause connections to never reconnect when connection was lost
|
||||||
* Added support for sending requests which expect an empty response
|
* Added support for sending requests which expect an empty response
|
||||||
|
|||||||
+1
-1
@@ -105,7 +105,7 @@ All updates are wrapped in a `DataEvent<>` object, which contain a `Timestamp`,
|
|||||||
*[WARNING] Do not use `using` statements in combination with constructing a `SocketClient`. Doing so will dispose the `SocketClient` instance when the subscription is done, which will result in the connection getting closed. Instead assign the socket client to a variable outside of the method scope.*
|
*[WARNING] Do not use `using` statements in combination with constructing a `SocketClient`. Doing so will dispose the `SocketClient` instance when the subscription is done, which will result in the connection getting closed. Instead assign the socket client to a variable outside of the method scope.*
|
||||||
|
|
||||||
### Processing subscribe responses
|
### Processing subscribe responses
|
||||||
Subscribing to a stream will return a `CallResult<UpdateSubscription>` object. This should be checked for success the same was as the [rest client](#processing-request-responses). The `UpdateSubscription` object can be used to listen for connection events of the socket connection.
|
Subscribing to a stream will return a `CallResult<UpdateSubscription>` object. This should be checked for success the same way as the [rest client](#processing-request-responses). The `UpdateSubscription` object can be used to listen for connection events of the socket connection.
|
||||||
```csharp
|
```csharp
|
||||||
|
|
||||||
var subscriptionResult = await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
|
var subscriptionResult = await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
|
||||||
|
|||||||
+4
-1
@@ -61,4 +61,7 @@ var client = new BinanceClient(new BinanceClientOptions
|
|||||||
BaseAddress = BinanceApiAddresses.TestNet.UsdFuturesRestClientAddress
|
BaseAddress = BinanceApiAddresses.TestNet.UsdFuturesRestClientAddress
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### How are timezones handled / Timestamps are off by xx
|
||||||
|
Exchange API's treat all timestamps as UTC, both incoming and outgoing. The client libraries do no conversion so be sure to use UTC as well.
|
||||||
+18
-3
@@ -42,11 +42,26 @@ These might not be compatible with other libraries, make sure to check the Crypt
|
|||||||
## Discord
|
## Discord
|
||||||
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
|
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
|
||||||
|
|
||||||
## Donate / Sponsor
|
## Support the project
|
||||||
I develop and maintain this package on my own for free in my spare time. Donations are greatly appreciated. If you prefer to donate any other currency please contact me.
|
I develop and maintain this package on my own for free in my spare time, any support is greatly appreciated.
|
||||||
|
|
||||||
|
### Referral link
|
||||||
|
Use one of the following following referral links to signup to a new exchange to pay a small percentage of the trading fees you pay to support the project instead of paying them straight to the exchange. This doesn't cost you a thing!
|
||||||
|
[Binance](https://accounts.binance.com/en/register?ref=10153680)
|
||||||
|
[Bitfinex](https://www.bitfinex.com/sign-up?refcode=kCCe-CNBO)
|
||||||
|
[Bittrex](https://bittrex.com/discover/join?referralCode=TST-DJM-CSX)
|
||||||
|
[Bybit](https://partner.bybit.com/b/jkorf)
|
||||||
|
[CoinEx](https://www.coinex.com/register?refer_code=hd6gn)
|
||||||
|
[FTX](https://ftx.com/referrals#a=31620192)
|
||||||
|
[Huobi](https://www.huobi.com/en-us/v/register/double-invite/?inviter_id=11343840&invite_code=fxp93)
|
||||||
|
[Kucoin](https://www.kucoin.com/ucenter/signup?rcode=RguMux)
|
||||||
|
|
||||||
|
### Donate
|
||||||
|
Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.
|
||||||
|
|
||||||
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS
|
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS
|
||||||
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2
|
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2
|
||||||
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs
|
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs
|
||||||
|
|
||||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf)
|
### Sponsor
|
||||||
|
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||||
Reference in New Issue
Block a user