1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-12 17:03:10 +00:00

Compare commits

..

43 Commits

Author SHA1 Message Date
JKorf 277be7ab9b Updated version 2022-07-31 21:53:39 +02:00
JKorf 45f3459f59 Made DataEvent ctor public 2022-07-31 21:47:22 +02:00
JKorf 98dad4a8ed Added handling for websocket options not being supported when running on WebAssembly 2022-07-31 21:45:19 +02:00
JKorf 1e5f19271b Updated support docs 2022-07-31 14:51:47 +02:00
JKorf 8abeeb4cf0 Update Clients.md 2022-07-29 18:57:04 +02:00
JKorf cae0cd9ead Fixed EnumConverter serialization writing values without quotes 2022-07-27 21:26:17 +02:00
JKorf 811574ae01 Fixed websocket reconnecting too fast when reconnecting succeeds but resubscribing or authorization fails 2022-07-27 21:25:46 +02:00
JKorf 0ddecf7f8d Updated version 2022-07-19 19:13:26 +02:00
JKorf 5bcf50fb4d Fixed socket getting disconnected when no data timeout reached instead of being reconnected 2022-07-19 19:12:26 +02:00
JKorf 9f0654815d Updated version 2022-07-17 12:50:50 +02:00
JKorf 465e9f04f4 Added support for retrieving a reconnection url when socket connection is lost 2022-07-17 12:49:13 +02:00
JKorf 7c8cbfa4e2 Updated version 2022-07-16 21:06:30 +02:00
JKorf 4c79d13ff9 Set error to the response content when an error response is received which isn't json 2022-07-15 16:55:18 +02:00
JKorf c815fad135 Fix for Message not handled when closing subscription, fix for reconnect loop 2022-07-12 22:06:38 +02:00
JKorf 41f17d0378 Don't close socket after failed auth when already closing 2022-07-11 18:56:51 +02:00
JKorf 50715ff2f7 Squashed commit of the following:
commit 0571ed17a0
Author: JKorf <jankorf91@gmail.com>
Date:   Sun Jul 10 19:56:27 2022 +0200

    Fixed tests

commit 99c331b389
Author: JKorf <jankorf91@gmail.com>
Date:   Sun Jul 10 16:41:14 2022 +0200

    Updated version

commit 70f8bd203a
Author: JKorf <jankorf91@gmail.com>
Date:   Sun Jul 10 16:36:00 2022 +0200

    Finished up websocket refactoring

commit 89b517c936
Author: JKorf <jankorf91@gmail.com>
Date:   Fri Jul 8 20:24:58 2022 +0200

    wip

commit 91e33cc42c
Author: JKorf <jankorf91@gmail.com>
Date:   Thu Jul 7 22:17:55 2022 +0200

    wip
2022-07-10 19:57:10 +02:00
JKorf ea9375d582 Updated version 2022-06-12 15:36:04 +02:00
JKorf 2cf3c93e5e Cleanup 2022-06-12 15:35:35 +02:00
JKorf ca888d8e41 Updated version 2022-06-12 15:31:03 +02:00
JKorf 2040b1c175 Fixed proxy setting not used on reconnecting socket 2022-06-12 15:26:11 +02:00
JKorf d451c18821 No longer waiting for timesyncing to complete when it's not the first request 2022-06-12 15:21:22 +02:00
JKorf c13dfa4461 Updated socket reconnection 2022-06-12 15:10:10 +02:00
JKorf c2080ef75f Made MaxSocketConnections a setting, added support for changing log settings after creating client 2022-06-11 13:31:39 +02:00
Jan Korf 6b252e8024 Update TestSocket.cs 2022-05-24 22:36:55 +02:00
Jan Korf d06bd5f176 Updated version 2022-05-24 18:56:37 +02:00
Jan Korf d55fc8da65 Merge pull request #144 from tamaw/fix/missing-port-on-baseuri
Fixed: copying the port number when using a custom BaseAddress
2022-05-24 15:14:38 +02:00
Tama Waddell 01184f2c5d Added port to the other overloaded method 2022-05-24 21:13:08 +10:00
Jan Korf cadc93c2f0 Merge pull request #143 from andriibratanin/bugfix/fix-nuget-discovery
Fix NuGet packages discovery for some IDEs
2022-05-24 10:21:21 +02:00
Tama Waddell 2600a51461 Included copying the port when using SetParameters 2022-05-24 15:16:06 +10:00
Andrii Bratanin 9e6a86ba8b Fix wrong case in csproj files of tests projects #142 2022-05-24 01:31:25 +03:00
Jan Korf c4430d63fa Added KeepAliveInterval setting for socket connections 2022-05-23 22:05:04 +02:00
Jan Korf f3e1cfef33 Updated version 2022-05-22 15:51:48 +02:00
Jan Korf cc3053719c Make socket ConnectionLost run in a separate task to prevent issue with long running/exceptions in the handler 2022-05-22 15:46:30 +02:00
Jan Korf cd6907e601 Merge branch 'master' of https://github.com/jkorf/CryptoExchange.Net 2022-05-22 14:35:07 +02:00
Jan Korf 8fe00693bd Merge pull request #141 from nathan-datusarator/master
Add checks for Disposed
2022-05-22 11:41:55 +02:00
Jan Korf fb90d1e015 Fixed exception when disposing client in reconnecting state 2022-05-22 11:35:39 +02:00
Jan Korf 4b44861e43 Added additional cases for no null/default handling in DateTimeConverter 2022-05-22 11:35:17 +02:00
Jan Korf e42ca4ab5a Update FAQ.md 2022-05-21 10:17:59 +02:00
Nathan Pfluger 5b97f6dd67 Move Subscription Events into non-lambda so they can be removed on StopAsync 2022-05-12 10:00:44 -07:00
Nathan Pfluger a9813ecb0a Add checks for Disposed 2022-05-12 09:05:27 -07:00
Jan Korf c7069a4049 Updated version 2022-05-08 16:28:17 +02:00
Jan Korf 5683ae0b3c Small fix when closing socket 2022-05-08 16:25:45 +02:00
Jan Korf 1c8cf5ac98 Updated timestamp calculation to include latency 2022-05-08 15:23:47 +02:00
30 changed files with 1146 additions and 741 deletions
@@ -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)
+11
View File
@@ -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();
} }
+11 -5
View File
@@ -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);
+169 -68
View File
@@ -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();
} }
+2 -2
View File
@@ -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);
} }
} }
} }
+4 -4
View File
@@ -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>
+2
View File
@@ -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>
+17 -41
View File
@@ -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);
} }
} }
+15 -9
View File
@@ -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());
}
} }
} }
} }
+29 -15
View File
@@ -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}";
} }
} }
+28 -16
View File
@@ -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>
+6 -1
View File
@@ -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;
+282 -246
View File
@@ -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;
}
}
}
+3 -11
View File
@@ -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);
} }
} }
} }
+67 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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).