mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 08:53:01 +00:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 277be7ab9b | |||
| 45f3459f59 | |||
| 98dad4a8ed | |||
| 1e5f19271b | |||
| 8abeeb4cf0 | |||
| cae0cd9ead | |||
| 811574ae01 | |||
| 0ddecf7f8d | |||
| 5bcf50fb4d | |||
| 9f0654815d | |||
| 465e9f04f4 | |||
| 7c8cbfa4e2 | |||
| 4c79d13ff9 | |||
| c815fad135 | |||
| 41f17d0378 | |||
| 50715ff2f7 | |||
| ea9375d582 | |||
| 2cf3c93e5e | |||
| ca888d8e41 | |||
| 2040b1c175 | |||
| d451c18821 | |||
| c13dfa4461 | |||
| c2080ef75f | |||
| 6b252e8024 | |||
| d06bd5f176 | |||
| d55fc8da65 | |||
| 01184f2c5d | |||
| cadc93c2f0 | |||
| 2600a51461 | |||
| 9e6a86ba8b | |||
| c4430d63fa | |||
| f3e1cfef33 | |||
| cc3053719c | |||
| cd6907e601 | |||
| 8fe00693bd | |||
| fb90d1e015 | |||
| 4b44861e43 | |||
| e42ca4ab5a | |||
| 5b97f6dd67 | |||
| a9813ecb0a | |||
| c7069a4049 | |||
| 5683ae0b3c | |||
| 1c8cf5ac98 | |||
| ad7231ec56 | |||
| 7e4a607391 | |||
| 2d470d18e2 | |||
| cb9a766c3b | |||
| 94b8184f7b | |||
| 270ea06f24 | |||
| 536afa92da | |||
| 11c48b3341 | |||
| f514e172d7 |
@@ -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; }
|
||||||
@@ -38,6 +44,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
public double IncomingKbps => throw new NotImplementedException();
|
public double IncomingKbps => throw new NotImplementedException();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
@@ -89,6 +99,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
{
|
{
|
||||||
Connected = false;
|
Connected = false;
|
||||||
DisconnectTime = DateTime.UtcNow;
|
DisconnectTime = DateTime.UtcNow;
|
||||||
|
Reconnecting = true;
|
||||||
OnClose?.Invoke();
|
OnClose?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,5 +122,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
{
|
{
|
||||||
OnError?.Invoke(error);
|
OnError?.Invoke(error);
|
||||||
}
|
}
|
||||||
|
public Task ReconnectAsync() => Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
{
|
{
|
||||||
SubClient = new TestSubSocketClient(exchangeOptions, exchangeOptions.SubOptions);
|
SubClient = new TestSubSocketClient(exchangeOptions, exchangeOptions.SubOptions);
|
||||||
SocketFactory = new Mock<IWebsocketFactory>().Object;
|
SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||||
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
|
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestSocket CreateSocket()
|
public TestSocket CreateSocket()
|
||||||
{
|
{
|
||||||
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
|
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||||
return (TestSocket)CreateSocket("123");
|
return (TestSocket)CreateSocket("https://localhost:123/");
|
||||||
}
|
}
|
||||||
|
|
||||||
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
|
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ namespace CryptoExchange.Net
|
|||||||
log = new Log(name);
|
log = new Log(name);
|
||||||
log.UpdateWriters(options.LogWriters);
|
log.UpdateWriters(options.LogWriters);
|
||||||
log.Level = options.LogLevel;
|
log.Level = options.LogLevel;
|
||||||
|
options.OnLoggingChanged += HandleLogConfigChange;
|
||||||
|
|
||||||
ClientOptions = options;
|
ClientOptions = options;
|
||||||
|
|
||||||
@@ -282,12 +283,22 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle a change in the client options log config
|
||||||
|
/// </summary>
|
||||||
|
private void HandleLogConfigChange()
|
||||||
|
{
|
||||||
|
log.UpdateWriters(ClientOptions.LogWriters);
|
||||||
|
log.Level = ClientOptions.LogLevel;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose
|
/// Dispose
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Dispose()
|
public virtual void Dispose()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Debug, "Disposing client");
|
log.Write(LogLevel.Debug, "Disposing client");
|
||||||
|
ClientOptions.OnLoggingChanged -= HandleLogConfigChange;
|
||||||
foreach (var client in ApiClients)
|
foreach (var client in ApiClients)
|
||||||
client.Dispose();
|
client.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,44 @@ namespace CryptoExchange.Net
|
|||||||
apiClient.SetApiCredentials(credentials);
|
apiClient.SetApiCredentials(credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute a request to the uri and returns if it was successful
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiClient">The API client the request is for</param>
|
||||||
|
/// <param name="uri">The uri to send the request to</param>
|
||||||
|
/// <param name="method">The method of the request</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="parameters">The parameters of the request</param>
|
||||||
|
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||||
|
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||||
|
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||||
|
/// <param name="requestWeight">Credits used for the request</param>
|
||||||
|
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||||
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
|
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[return: NotNull]
|
||||||
|
protected virtual async Task<WebCallResult> SendRequestAsync(RestApiClient apiClient,
|
||||||
|
Uri uri,
|
||||||
|
HttpMethod method,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Dictionary<string, object>? parameters = null,
|
||||||
|
bool signed = false,
|
||||||
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
|
int requestWeight = 1,
|
||||||
|
JsonSerializer? deserializer = null,
|
||||||
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
|
bool ignoreRatelimit = false)
|
||||||
|
{
|
||||||
|
var request = await PrepareRequestAsync(apiClient, uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||||
|
if (!request)
|
||||||
|
return new WebCallResult(request.Error!);
|
||||||
|
|
||||||
|
var result = await GetResponseAsync<object>(apiClient, request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
|
||||||
|
return result.AsDataless();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Execute a request to the uri and deserialize the response into the provided type parameter
|
/// Execute a request to the uri and deserialize the response into the provided type parameter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -93,16 +131,58 @@ namespace CryptoExchange.Net
|
|||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
bool ignoreRatelimit = false
|
bool ignoreRatelimit = false
|
||||||
) where T : class
|
) where T : class
|
||||||
|
{
|
||||||
|
var request = await PrepareRequestAsync(apiClient, uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||||
|
if (!request)
|
||||||
|
return new WebCallResult<T>(request.Error!);
|
||||||
|
|
||||||
|
return await GetResponseAsync<T>(apiClient, request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares a request to be sent to the server
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiClient">The API client the request is for</param>
|
||||||
|
/// <param name="uri">The uri to send the request to</param>
|
||||||
|
/// <param name="method">The method of the request</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="parameters">The parameters of the request</param>
|
||||||
|
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||||
|
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||||
|
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||||
|
/// <param name="requestWeight">Credits used for the request</param>
|
||||||
|
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||||
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
|
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<CallResult<IRequest>> PrepareRequestAsync(RestApiClient apiClient,
|
||||||
|
Uri uri,
|
||||||
|
HttpMethod method,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Dictionary<string, object>? parameters = null,
|
||||||
|
bool signed = false,
|
||||||
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
|
int requestWeight = 1,
|
||||||
|
JsonSerializer? deserializer = null,
|
||||||
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
|
bool ignoreRatelimit = false)
|
||||||
{
|
{
|
||||||
var requestId = NextId();
|
var requestId = NextId();
|
||||||
|
|
||||||
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<T>(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,20 +192,20 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
var limitResult = await limiter.LimitRequestAsync(log, uri.AbsolutePath, method, signed, apiClient.Options.ApiCredentials?.Key, apiClient.Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
|
var limitResult = await limiter.LimitRequestAsync(log, uri.AbsolutePath, method, signed, apiClient.Options.ApiCredentials?.Key, apiClient.Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
|
||||||
if (!limitResult.Success)
|
if (!limitResult.Success)
|
||||||
return new WebCallResult<T>(limitResult.Error!);
|
return new CallResult<IRequest>(limitResult.Error!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signed && apiClient.AuthenticationProvider == null)
|
if (signed && apiClient.AuthenticationProvider == null)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
|
log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
|
||||||
return new WebCallResult<T>(new NoApiCredentialsError());
|
return new CallResult<IRequest>(new NoApiCredentialsError());
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Write(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
|
log.Write(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
|
||||||
var paramsPosition = parameterPosition ?? apiClient.ParameterPositions[method];
|
var paramsPosition = parameterPosition ?? apiClient.ParameterPositions[method];
|
||||||
var request = ConstructRequest(apiClient, uri, method, parameters, signed, paramsPosition, arraySerialization ?? apiClient.arraySerialization, requestId, additionalHeaders);
|
var request = ConstructRequest(apiClient, uri, method, parameters, signed, paramsPosition, arraySerialization ?? apiClient.arraySerialization, requestId, additionalHeaders);
|
||||||
|
|
||||||
string? paramString = "";
|
string? paramString = "";
|
||||||
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
||||||
paramString = $" with request body '{request.Content}'";
|
paramString = $" with request body '{request.Content}'";
|
||||||
@@ -133,12 +213,14 @@ namespace CryptoExchange.Net
|
|||||||
var headers = request.GetHeaders();
|
var headers = request.GetHeaders();
|
||||||
if (headers.Any())
|
if (headers.Any())
|
||||||
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
|
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
|
||||||
|
|
||||||
apiClient.TotalRequestsMade++;
|
apiClient.TotalRequestsMade++;
|
||||||
log.Write(LogLevel.Trace, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(ClientOptions.Proxy == null ? "" : $" via proxy {ClientOptions.Proxy.Host}")}");
|
log.Write(LogLevel.Trace, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(ClientOptions.Proxy == null ? "" : $" via proxy {ClientOptions.Proxy.Host}")}");
|
||||||
return await GetResponseAsync<T>(apiClient, request, deserializer, cancellationToken).ConfigureAwait(false);
|
return new CallResult<IRequest>(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes the request and returns the result deserialized into the type parameter class
|
/// Executes the request and returns the result deserialized into the type parameter class
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -146,8 +228,14 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="request">The request object to execute</param>
|
/// <param name="request">The request object to execute</param>
|
||||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="expectedEmptyResponse">If an empty response is expected</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(BaseApiClient apiClient, IRequest request, JsonSerializer? deserializer, CancellationToken cancellationToken)
|
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
|
||||||
|
BaseApiClient apiClient,
|
||||||
|
IRequest request,
|
||||||
|
JsonSerializer? deserializer,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
bool expectedEmptyResponse)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -169,22 +257,52 @@ namespace CryptoExchange.Net
|
|||||||
response.Close();
|
response.Close();
|
||||||
log.Write(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms{(log.Level == LogLevel.Trace ? (": "+data): "")}");
|
log.Write(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms{(log.Level == LogLevel.Trace ? (": "+data): "")}");
|
||||||
|
|
||||||
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
|
if (!expectedEmptyResponse)
|
||||||
var parseResult = ValidateJson(data);
|
{
|
||||||
if (!parseResult.Success)
|
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
var parseResult = ValidateJson(data);
|
||||||
|
if (!parseResult.Success)
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||||
|
|
||||||
// Let the library implementation see if it is an error response, and if so parse the error
|
// Let the library implementation see if it is an error response, and if so parse the error
|
||||||
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||||
if (error != null)
|
if (error != null)
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||||
|
|
||||||
// Not an error, so continue deserializing
|
// Not an error, so continue deserializing
|
||||||
var deserializeResult = Deserialize<T>(parseResult.Data, deserializer, request.RequestId);
|
var deserializeResult = Deserialize<T>(parseResult.Data, deserializer, request.RequestId);
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data: null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(data))
|
||||||
|
{
|
||||||
|
var parseResult = ValidateJson(data);
|
||||||
|
if (!parseResult.Success)
|
||||||
|
// Not empty, and not json
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||||
|
|
||||||
|
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||||
|
if (error != null)
|
||||||
|
// Error response
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty success response; okay
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, default);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if (expectedEmptyResponse)
|
||||||
|
{
|
||||||
|
// We expected an empty response and the request is successful and don't manually parse errors, so assume it's correct
|
||||||
|
responseStream.Close();
|
||||||
|
response.Close();
|
||||||
|
|
||||||
|
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
|
||||||
|
}
|
||||||
|
|
||||||
// Success status code, and we don't have to check for errors. Continue deserializing directly from the stream
|
// Success status code, and we don't have to check for errors. Continue deserializing directly from the stream
|
||||||
var desResult = await DeserializeAsync<T>(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false);
|
var desResult = await DeserializeAsync<T>(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false);
|
||||||
responseStream.Close();
|
responseStream.Close();
|
||||||
@@ -202,7 +320,7 @@ namespace CryptoExchange.Net
|
|||||||
responseStream.Close();
|
responseStream.Close();
|
||||||
response.Close();
|
response.Close();
|
||||||
var parseResult = ValidateJson(data);
|
var parseResult = ValidateJson(data);
|
||||||
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : parseResult.Error!;
|
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : new ServerError(data)!;
|
||||||
if(error.Code == null || error.Code == 0)
|
if(error.Code == null || error.Code == 0)
|
||||||
error.Code = (int)response.StatusCode;
|
error.Code = (int)response.StatusCode;
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using System.Collections.Concurrent;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.WebSockets;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
@@ -11,7 +11,6 @@ using CryptoExchange.Net.Interfaces;
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
@@ -30,15 +29,15 @@ namespace CryptoExchange.Net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// List of socket connections currently connecting/connected
|
/// List of socket connections currently connecting/connected
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal ConcurrentDictionary<int, SocketConnection> sockets = new();
|
protected internal ConcurrentDictionary<int, SocketConnection> socketConnections = new();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Semaphore used while creating sockets
|
/// Semaphore used while creating sockets
|
||||||
/// </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>
|
||||||
@@ -85,10 +84,24 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (!sockets.Any())
|
if (!socketConnections.Any())
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return sockets.Sum(s => s.Value.Socket.IncomingKbps);
|
return socketConnections.Sum(s => s.Value.IncomingKbps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int CurrentConnections => socketConnections.Count;
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int CurrentSubscriptions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!socketConnections.Any())
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return socketConnections.Sum(s => s.Value.SubscriptionCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -205,7 +230,7 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
if (socketConnection.PausedActivity)
|
if (socketConnection.PausedActivity)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Warning, $"Socket {socketConnection.Socket.Id} has been paused, can't subscribe at this moment");
|
log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't subscribe at this moment");
|
||||||
return new CallResult<UpdateSubscription>( new ServerError("Socket is paused"));
|
return new CallResult<UpdateSubscription>( new ServerError("Socket is paused"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,17 +251,16 @@ 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 () =>
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Information, $"Socket {socketConnection.Socket.Id} Cancellation token set, closing subscription");
|
log.Write(LogLevel.Information, $"Socket {socketConnection.SocketId} Cancellation token set, closing subscription");
|
||||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Write(LogLevel.Information, $"Socket {socketConnection.Socket.Id} 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,15 +339,13 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (socketConnection.PausedActivity)
|
if (socketConnection.PausedActivity)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Warning, $"Socket {socketConnection.Socket.Id} has been paused, can't send query at this moment");
|
log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't send query at this moment");
|
||||||
return new CallResult<T>(new ServerError("Socket is paused"));
|
return new CallResult<T>(new ServerError("Socket is paused"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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.Socket.Id} 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);
|
||||||
}
|
}
|
||||||
@@ -456,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)
|
||||||
{
|
{
|
||||||
@@ -471,7 +503,7 @@ namespace CryptoExchange.Net
|
|||||||
var desResult = Deserialize<T>(messageEvent.JsonData);
|
var desResult = Deserialize<T>(messageEvent.JsonData);
|
||||||
if (!desResult)
|
if (!desResult)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Warning, $"Socket {connection.Socket.Id} Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
|
log.Write(LogLevel.Warning, $"Socket {connection.SocketId} Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,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 sockets.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>
|
||||||
@@ -505,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 = sockets.Where(s => s.Value.Socket.Url.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 || (sockets.Count >= MaxSocketConnections && sockets.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>
|
||||||
@@ -548,16 +615,33 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<bool>> ConnectSocketAsync(SocketConnection socketConnection)
|
protected virtual async Task<CallResult<bool>> ConnectSocketAsync(SocketConnection socketConnection)
|
||||||
{
|
{
|
||||||
if (await socketConnection.Socket.ConnectAsync().ConfigureAwait(false))
|
if (await socketConnection.ConnectAsync().ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
sockets.TryAdd(socketConnection.Socket.Id, socketConnection);
|
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
|
||||||
return new CallResult<bool>(true);
|
return new CallResult<bool>(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
socketConnection.Socket.Dispose();
|
socketConnection.Dispose();
|
||||||
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>
|
||||||
@@ -565,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,27 +674,27 @@ namespace CryptoExchange.Net
|
|||||||
if (disposing)
|
if (disposing)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
foreach (var socket in sockets.Values)
|
foreach (var socketConnection in socketConnections.Values)
|
||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
if (!socket.Socket.IsOpen)
|
if (!socketConnection.Connected)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var obj = objGetter(socket);
|
var obj = objGetter(socketConnection);
|
||||||
if (obj == null)
|
if (obj == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
log.Write(LogLevel.Trace, $"Socket {socket.Socket.Id} sending periodic {identifier}");
|
log.Write(LogLevel.Trace, $"Socket {socketConnection.SocketId} sending periodic {identifier}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
socket.Send(obj);
|
socketConnection.Send(obj);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Warning, $"Socket {socket.Socket.Id} Periodic send {identifier} failed: " + ex.ToLogString());
|
log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} Periodic send {identifier} failed: " + ex.ToLogString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -639,10 +708,9 @@ 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 sockets.Values.ToList())
|
foreach(var socket in socketConnections.Values.ToList())
|
||||||
{
|
{
|
||||||
subscription = socket.GetSubscription(subscriptionId);
|
subscription = socket.GetSubscription(subscriptionId);
|
||||||
if (subscription != null)
|
if (subscription != null)
|
||||||
@@ -655,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,19 +747,48 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task UnsubscribeAllAsync()
|
public virtual async Task UnsubscribeAllAsync()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Information, $"Closing all {sockets.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
log.Write(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
||||||
|
var tasks = new List<Task>();
|
||||||
await Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
var tasks = new List<Task>();
|
var socketList = socketConnections.Values;
|
||||||
{
|
foreach (var sub in socketList)
|
||||||
var socketList = sockets.Values;
|
tasks.Add(sub.CloseAsync());
|
||||||
foreach (var sub in socketList)
|
}
|
||||||
tasks.Add(sub.CloseAsync());
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
}).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>
|
||||||
@@ -702,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)
|
||||||
Task.Run(UnsubscribeAllAsync).ConfigureAwait(false).GetAwaiter().GetResult();
|
{
|
||||||
|
log.Write(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
|
||||||
|
_ = UnsubscribeAllAsync();
|
||||||
|
}
|
||||||
semaphoreSlim?.Dispose();
|
semaphoreSlim?.Dispose();
|
||||||
base.Dispose();
|
base.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace CryptoExchange.Net
|
|||||||
/// Get time sync info for an API client
|
/// Get time sync info for an API client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected abstract TimeSyncInfo GetTimeSyncInfo();
|
public abstract TimeSyncInfo GetTimeSyncInfo();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get time offset for an API client
|
/// Get time offset for an API client
|
||||||
@@ -92,7 +92,7 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate time offset between local and server
|
// Calculate time offset between local and server
|
||||||
var offset = result.Data - localTime;
|
var offset = result.Data - (localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2));
|
||||||
timeSyncParams.UpdateTimeOffset(offset);
|
timeSyncParams.UpdateTimeOffset(offset);
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
@@ -32,13 +31,13 @@ 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 < 1999999999)
|
if (longValue < 19999999999)
|
||||||
return ConvertFromSeconds(longValue);
|
return ConvertFromSeconds(longValue);
|
||||||
if (longValue < 1999999999999)
|
if (longValue < 19999999999999)
|
||||||
return ConvertFromMilliseconds(longValue);
|
return ConvertFromMilliseconds(longValue);
|
||||||
if (longValue < 1999999999999999)
|
if (longValue < 19999999999999999)
|
||||||
return ConvertFromMicroseconds(longValue);
|
return ConvertFromMicroseconds(longValue);
|
||||||
|
|
||||||
return ConvertFromNanoseconds(longValue);
|
return ConvertFromNanoseconds(longValue);
|
||||||
@@ -46,7 +45,10 @@ 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 < 1999999999)
|
if (doubleValue == 0 || doubleValue == -1)
|
||||||
|
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
||||||
|
|
||||||
|
if (doubleValue < 19999999999)
|
||||||
return ConvertFromSeconds(doubleValue);
|
return ConvertFromSeconds(doubleValue);
|
||||||
|
|
||||||
return ConvertFromMilliseconds(doubleValue);
|
return ConvertFromMilliseconds(doubleValue);
|
||||||
@@ -57,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
|
||||||
@@ -86,11 +91,11 @@ namespace CryptoExchange.Net.Converters
|
|||||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
||||||
{
|
{
|
||||||
// Parse 1637745563.000 format
|
// Parse 1637745563.000 format
|
||||||
if (doubleValue < 1999999999)
|
if (doubleValue < 19999999999)
|
||||||
return ConvertFromSeconds(doubleValue);
|
return ConvertFromSeconds(doubleValue);
|
||||||
if (doubleValue < 1999999999999)
|
if (doubleValue < 19999999999999)
|
||||||
return ConvertFromMilliseconds((long)doubleValue);
|
return ConvertFromMilliseconds((long)doubleValue);
|
||||||
if (doubleValue < 1999999999999999)
|
if (doubleValue < 19999999999999999)
|
||||||
return ConvertFromMicroseconds((long)doubleValue);
|
return ConvertFromMicroseconds((long)doubleValue);
|
||||||
|
|
||||||
return ConvertFromNanoseconds((long)doubleValue);
|
return ConvertFromNanoseconds((long)doubleValue);
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ namespace CryptoExchange.Net.Converters
|
|||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
var stringValue = GetString(value);
|
var stringValue = GetString(value);
|
||||||
writer.WriteRawValue(stringValue);
|
writer.WriteValue(stringValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,16 @@
|
|||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>A base package for implementing cryptocurrency API's</Description>
|
<Description>A base package for implementing cryptocurrency API's</Description>
|
||||||
<PackageVersion>5.1.7</PackageVersion>
|
<PackageVersion>5.2.4</PackageVersion>
|
||||||
<AssemblyVersion>5.1.7</AssemblyVersion>
|
<AssemblyVersion>5.2.4</AssemblyVersion>
|
||||||
<FileVersion>5.1.7</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.7 - Moved some Rest parameters from BaseRestClient to RestApiClient to allow different implementations for sub clients</PackageReleaseNotes>
|
<PackageReleaseNotes>5.2.4 - Added handling of PlatformNotSupportedException when trying to use websocket from WebAssembly, Changed DataEvent to have a public constructor for testing purposes, Fixed EnumConverter serializing values without proper quotes, Fixed websocket connection reconnecting too quickly when resubscribing/reauthenticating fails</PackageReleaseNotes>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<LangVersion>9.0</LangVersion>
|
<LangVersion>9.0</LangVersion>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
|||||||
@@ -426,6 +426,7 @@ namespace CryptoExchange.Net
|
|||||||
var uriBuilder = new UriBuilder();
|
var uriBuilder = new UriBuilder();
|
||||||
uriBuilder.Scheme = baseUri.Scheme;
|
uriBuilder.Scheme = baseUri.Scheme;
|
||||||
uriBuilder.Host = baseUri.Host;
|
uriBuilder.Host = baseUri.Host;
|
||||||
|
uriBuilder.Port = baseUri.Port;
|
||||||
uriBuilder.Path = baseUri.AbsolutePath;
|
uriBuilder.Path = baseUri.AbsolutePath;
|
||||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||||
foreach (var parameter in parameters)
|
foreach (var parameter in parameters)
|
||||||
@@ -454,6 +455,7 @@ namespace CryptoExchange.Net
|
|||||||
var uriBuilder = new UriBuilder();
|
var uriBuilder = new UriBuilder();
|
||||||
uriBuilder.Scheme = baseUri.Scheme;
|
uriBuilder.Scheme = baseUri.Scheme;
|
||||||
uriBuilder.Host = baseUri.Host;
|
uriBuilder.Host = baseUri.Host;
|
||||||
|
uriBuilder.Port = baseUri.Port;
|
||||||
uriBuilder.Path = baseUri.AbsolutePath;
|
uriBuilder.Path = baseUri.AbsolutePath;
|
||||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||||
foreach (var parameter in parameters)
|
foreach (var parameter in parameters)
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double IncomingKbps { get; }
|
public double IncomingKbps { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current amount of connections to the API from this client. A connection can have multiple subscriptions.
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentConnections { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current amount of subscriptions running from the client
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentSubscriptions { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unsubscribe from a stream using the subscription id received when starting the subscription
|
/// Unsubscribe from a stream using the subscription id received when starting the subscription
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Sockets;
|
||||||
using System;
|
using System;
|
||||||
using System.Security.Authentication;
|
using System.Security.Authentication;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -27,43 +28,31 @@ 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>
|
|
||||||
/// Whether socket is in the process of reconnecting
|
|
||||||
/// </summary>
|
|
||||||
bool Reconnecting { 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
|
/// The uri the socket connects to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Func<byte[], string>? DataInterpreterBytes { get; set; }
|
Uri Uri { get; }
|
||||||
/// <summary>
|
|
||||||
/// Handler for string data
|
|
||||||
/// </summary>
|
|
||||||
Func<string, string>? DataInterpreterString { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The url the socket connects to
|
|
||||||
/// </summary>
|
|
||||||
string Url { get; }
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the socket connection is closed
|
/// Whether the socket connection is closed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -73,19 +62,6 @@ 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>
|
||||||
@@ -96,9 +72,10 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
void Send(string data);
|
void Send(string data);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reset socket when a connection is lost to prepare for a new connection
|
/// Reconnect the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void Reset();
|
/// <returns></returns>
|
||||||
|
Task ReconnectAsync();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Close the connection
|
/// Close the connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using CryptoExchange.Net.Logging;
|
||||||
using CryptoExchange.Net.Logging;
|
using CryptoExchange.Net.Sockets;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
namespace CryptoExchange.Net.Interfaces
|
||||||
{
|
{
|
||||||
@@ -12,17 +12,8 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// Create a websocket for an url
|
/// Create a websocket for an url
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="log">The logger</param>
|
/// <param name="log">The logger</param>
|
||||||
/// <param name="url">The url the socket is fo</param>
|
/// <param name="parameters">The parameters to use for the connection</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
IWebsocket CreateWebsocket(Log log, string url);
|
IWebsocket CreateWebsocket(Log log, WebSocketParameters parameters);
|
||||||
/// <summary>
|
|
||||||
/// Create a websocket for an url
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="log">The logger</param>
|
|
||||||
/// <param name="url">The url the socket is fo</param>
|
|
||||||
/// <param name="cookies">Cookies to be send in the initial request</param>
|
|
||||||
/// <param name="headers">Headers to be send in the initial request</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
IWebsocket CreateWebsocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ namespace CryptoExchange.Net.Logging
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string ClientName { get; set; }
|
public string ClientName { get; set; }
|
||||||
|
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -42,7 +44,8 @@ namespace CryptoExchange.Net.Logging
|
|||||||
/// <param name="textWriters"></param>
|
/// <param name="textWriters"></param>
|
||||||
public void UpdateWriters(List<ILogger> textWriters)
|
public void UpdateWriters(List<ILogger> textWriters)
|
||||||
{
|
{
|
||||||
writers = textWriters;
|
lock (_lock)
|
||||||
|
writers = textWriters;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -56,16 +59,19 @@ namespace CryptoExchange.Net.Logging
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var logMessage = $"{ClientName,-10} | {message}";
|
var logMessage = $"{ClientName,-10} | {message}";
|
||||||
foreach (var writer in writers.ToList())
|
lock (_lock)
|
||||||
{
|
{
|
||||||
try
|
foreach (var writer in writers)
|
||||||
{
|
{
|
||||||
writer.Log(logLevel, logMessage);
|
try
|
||||||
}
|
{
|
||||||
catch (Exception e)
|
writer.Log(logLevel, logMessage);
|
||||||
{
|
}
|
||||||
// Can't write to the logging so where else to output..
|
catch (Exception e)
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Failed to write log to writer {writer.GetType()}: " + e.ToLogString());
|
{
|
||||||
|
// Can't write to the logging so where else to output..
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Failed to write log to writer {writer.GetType()}: " + e.ToLogString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,15 +14,35 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class BaseOptions
|
public class BaseOptions
|
||||||
{
|
{
|
||||||
|
internal event Action? OnLoggingChanged;
|
||||||
|
|
||||||
|
private LogLevel _logLevel = LogLevel.Information;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The minimum log level to output
|
/// The minimum log level to output
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public LogLevel LogLevel { get; set; } = LogLevel.Information;
|
public LogLevel LogLevel
|
||||||
|
{
|
||||||
|
get => _logLevel;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_logLevel = value;
|
||||||
|
OnLoggingChanged?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ILogger> _logWriters = new List<ILogger> { new DebugLogger() };
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The log writers
|
/// The log writers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<ILogger> LogWriters { get; set; } = new List<ILogger> { new DebugLogger() };
|
public List<ILogger> LogWriters
|
||||||
|
{
|
||||||
|
get => _logWriters;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_logWriters = value;
|
||||||
|
OnLoggingChanged?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
||||||
@@ -156,16 +176,6 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The maximum number of times to try to reconnect, default null will retry indefinitely
|
|
||||||
/// </summary>
|
|
||||||
public int? MaxReconnectTries { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The maximum number of times to try to resubscribe after reconnecting
|
|
||||||
/// </summary>
|
|
||||||
public int? MaxResubscribeTries { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -189,6 +199,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? SocketSubscriptionsCombineTarget { get; set; }
|
public int? SocketSubscriptionsCombineTarget { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxSocketConnections { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -207,18 +222,17 @@ namespace CryptoExchange.Net.Objects
|
|||||||
|
|
||||||
AutoReconnect = baseOptions.AutoReconnect;
|
AutoReconnect = baseOptions.AutoReconnect;
|
||||||
ReconnectInterval = baseOptions.ReconnectInterval;
|
ReconnectInterval = baseOptions.ReconnectInterval;
|
||||||
MaxReconnectTries = baseOptions.MaxReconnectTries;
|
|
||||||
MaxResubscribeTries = baseOptions.MaxResubscribeTries;
|
|
||||||
MaxConcurrentResubscriptionsPerSocket = baseOptions.MaxConcurrentResubscriptionsPerSocket;
|
MaxConcurrentResubscriptionsPerSocket = baseOptions.MaxConcurrentResubscriptionsPerSocket;
|
||||||
SocketResponseTimeout = baseOptions.SocketResponseTimeout;
|
SocketResponseTimeout = baseOptions.SocketResponseTimeout;
|
||||||
SocketNoDataTimeout = baseOptions.SocketNoDataTimeout;
|
SocketNoDataTimeout = baseOptions.SocketNoDataTimeout;
|
||||||
SocketSubscriptionsCombineTarget = baseOptions.SocketSubscriptionsCombineTarget;
|
SocketSubscriptionsCombineTarget = baseOptions.SocketSubscriptionsCombineTarget;
|
||||||
|
MaxSocketConnections = baseOptions.MaxSocketConnections;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, MaxReconnectTries: {MaxReconnectTries}, MaxResubscribeTries: {MaxResubscribeTries}, MaxConcurrentResubscriptionsPerSocket: {MaxConcurrentResubscriptionsPerSocket}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketNoDataTimeout: {SocketNoDataTimeout}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}";
|
return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, MaxConcurrentResubscriptionsPerSocket: {MaxConcurrentResubscriptionsPerSocket}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketNoDataTimeout: {SocketNoDataTimeout}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}, MaxSocketConnections: {MaxSocketConnections}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -258,24 +258,32 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
}
|
}
|
||||||
|
|
||||||
_subscription = startResult.Data;
|
_subscription = startResult.Data;
|
||||||
_subscription.ConnectionLost += () =>
|
_subscription.ConnectionLost += HandleConnectionLost;
|
||||||
{
|
_subscription.ConnectionClosed += HandleConnectionClosed;
|
||||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
|
_subscription.ConnectionRestored += HandleConnectionRestored;
|
||||||
Status = OrderBookStatus.Reconnecting;
|
|
||||||
Reset();
|
|
||||||
};
|
|
||||||
_subscription.ConnectionClosed += () =>
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
|
|
||||||
Status = OrderBookStatus.Disconnected;
|
|
||||||
_ = StopAsync();
|
|
||||||
};
|
|
||||||
|
|
||||||
_subscription.ConnectionRestored += async time => await ResyncAsync().ConfigureAwait(false);
|
|
||||||
Status = OrderBookStatus.Synced;
|
Status = OrderBookStatus.Synced;
|
||||||
return new CallResult<bool>(true);
|
return new CallResult<bool>(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleConnectionLost() {
|
||||||
|
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
|
||||||
|
if (Status != OrderBookStatus.Disposed) {
|
||||||
|
Status = OrderBookStatus.Reconnecting;
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleConnectionClosed() {
|
||||||
|
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
|
||||||
|
Status = OrderBookStatus.Disconnected;
|
||||||
|
_ = StopAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void HandleConnectionRestored(TimeSpan _) {
|
||||||
|
await ResyncAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task StopAsync()
|
public async Task StopAsync()
|
||||||
{
|
{
|
||||||
@@ -286,8 +294,12 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
if (_processTask != null)
|
if (_processTask != null)
|
||||||
await _processTask.ConfigureAwait(false);
|
await _processTask.ConfigureAwait(false);
|
||||||
|
|
||||||
if (_subscription != null)
|
if (_subscription != null) {
|
||||||
await _subscription.CloseAsync().ConfigureAwait(false);
|
await _subscription.CloseAsync().ConfigureAwait(false);
|
||||||
|
_subscription.ConnectionLost -= HandleConnectionLost;
|
||||||
|
_subscription.ConnectionClosed -= HandleConnectionClosed;
|
||||||
|
_subscription.ConnectionRestored -= HandleConnectionRestored;
|
||||||
|
}
|
||||||
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} stopped");
|
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,7 +495,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
/// <param name="timeout">Max wait time</param>
|
/// <param name="timeout">Max wait time</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected async Task<CallResult<bool>> WaitForSetOrderBookAsync(int timeout, CancellationToken ct)
|
protected async Task<CallResult<bool>> WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var startWait = DateTime.UtcNow;
|
var startWait = DateTime.UtcNow;
|
||||||
while (!bookSet && Status == OrderBookStatus.Syncing)
|
while (!bookSet && Status == OrderBookStatus.Syncing)
|
||||||
@@ -491,12 +503,12 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
if(ct.IsCancellationRequested)
|
if(ct.IsCancellationRequested)
|
||||||
return new CallResult<bool>(new CancellationRequestedError());
|
return new CallResult<bool>(new CancellationRequestedError());
|
||||||
|
|
||||||
if ((DateTime.UtcNow - startWait).TotalMilliseconds > timeout)
|
if (DateTime.UtcNow - startWait > timeout)
|
||||||
return new CallResult<bool>(new ServerError("Timeout while waiting for data"));
|
return new CallResult<bool>(new ServerError("Timeout while waiting for data"));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(10, ct).ConfigureAwait(false);
|
await Task.Delay(50, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{ }
|
{ }
|
||||||
@@ -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,29 +20,38 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CryptoExchangeWebSocketClient : IWebsocket
|
public class CryptoExchangeWebSocketClient : IWebsocket
|
||||||
{
|
{
|
||||||
internal static int lastStreamId;
|
enum ProcessState
|
||||||
private static readonly object streamIdLock = new object();
|
{
|
||||||
|
Idle,
|
||||||
|
Processing,
|
||||||
|
WaitingForClose,
|
||||||
|
Reconnecting
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static int lastStreamId;
|
||||||
|
private static readonly object streamIdLock = new();
|
||||||
|
|
||||||
private ClientWebSocket _socket;
|
|
||||||
private Task? _sendTask;
|
|
||||||
private Task? _receiveTask;
|
|
||||||
private Task? _timeoutTask;
|
|
||||||
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 bool _closing;
|
|
||||||
private bool _startedSent;
|
|
||||||
private bool _startedReceive;
|
|
||||||
|
|
||||||
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
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly List<ReceiveItem> _receivedMessages;
|
protected readonly List<ReceiveItem> _receivedMessages;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Received messages lock
|
/// Received messages lock
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -53,82 +60,27 @@ 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 List<Action<Exception>>();
|
|
||||||
/// <summary>
|
|
||||||
/// Handlers for when the socket connection is opened
|
|
||||||
/// </summary>
|
|
||||||
protected readonly List<Action> openHandlers = new List<Action>();
|
|
||||||
/// <summary>
|
|
||||||
/// Handlers for when the connection is closed
|
|
||||||
/// </summary>
|
|
||||||
protected readonly List<Action> closeHandlers = new List<Action>();
|
|
||||||
/// <summary>
|
|
||||||
/// Handlers for when a message is received
|
|
||||||
/// </summary>
|
|
||||||
protected readonly List<Action<string>> messageHandlers = new List<Action<string>>();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int Id { get; }
|
public int Id { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string? Origin { get; set; }
|
public WebSocketParameters Parameters { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool Reconnecting { get; set; }
|
|
||||||
|
|
||||||
/// <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 string Url { get; }
|
public Uri Uri => Parameters.Uri;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsClosed => _socket.State == WebSocketState.Closed;
|
public bool IsClosed => _socket.State == WebSocketState.Closed;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsOpen => _socket.State == WebSocketState.Open && !_closing;
|
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
|
||||||
@@ -148,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="url">The url the socket should connect to</param>
|
/// <param name="websocketParameters">The parameters for this socket</param>
|
||||||
public CryptoExchangeWebSocketClient(Log log, string url) : this(log, url, 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="url">The url 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, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers)
|
|
||||||
{
|
{
|
||||||
Id = NextStreamId();
|
Id = NextStreamId();
|
||||||
this.log = log;
|
_log = log;
|
||||||
Url = url;
|
|
||||||
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();
|
||||||
@@ -206,112 +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)
|
|
||||||
{
|
|
||||||
Uri.TryCreate($"{proxy.Host}:{proxy.Port}", UriKind.Absolute, out var uri);
|
|
||||||
_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 CancellationTokenSource(TimeSpan.FromSeconds(10));
|
socket.Options.Cookies = cookieContainer;
|
||||||
await _socket.ConnectAsync(new Uri(Url), tcs.Token).ConfigureAwait(false);
|
foreach (var header in Parameters.Headers)
|
||||||
|
socket.Options.SetRequestHeader(header.Key, header.Value);
|
||||||
Handle(openHandlers);
|
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);
|
||||||
}
|
}
|
||||||
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.Trace, $"Socket {Id} connection succeeded, starting communication");
|
_log.Write(LogLevel.Debug, $"Socket {Id} connected to {Uri}");
|
||||||
_sendTask = Task.Factory.StartNew(SendLoopAsync, default, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();
|
return true;
|
||||||
_receiveTask = Task.Factory.StartNew(ReceiveLoopAsync, default, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();
|
}
|
||||||
if (Timeout != default)
|
|
||||||
_timeoutTask = Task.Run(CheckTimeoutAsync);
|
|
||||||
|
|
||||||
var sw = Stopwatch.StartNew();
|
/// <inheritdoc />
|
||||||
while (!_startedSent || !_startedReceive)
|
private async Task ProcessAsync()
|
||||||
|
{
|
||||||
|
while (!_stopRequested)
|
||||||
{
|
{
|
||||||
// Wait for the tasks to have actually started
|
_log.Write(LogLevel.Debug, $"Socket {Id} starting processing tasks");
|
||||||
await Task.Delay(10).ConfigureAwait(false);
|
_processState = ProcessState.Processing;
|
||||||
|
var sendTask = SendLoopAsync();
|
||||||
|
var receiveTask = ReceiveLoopAsync();
|
||||||
|
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");
|
||||||
|
|
||||||
if(sw.ElapsedMilliseconds > 5000)
|
_processState = ProcessState.WaitingForClose;
|
||||||
|
while (_closeTask == null)
|
||||||
|
await Task.Delay(50).ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _closeTask.ConfigureAwait(false);
|
||||||
|
_closeTask = null;
|
||||||
|
|
||||||
|
if (!Parameters.AutoReconnect)
|
||||||
{
|
{
|
||||||
_ = _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", default);
|
_processState = ProcessState.Idle;
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} startup interupted");
|
OnClose?.Invoke();
|
||||||
return false;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} connected to {Url}");
|
_processState = ProcessState.Idle;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual void Send(string data)
|
public virtual void Send(string data)
|
||||||
{
|
{
|
||||||
if (_closing)
|
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 />
|
/// <inheritdoc />
|
||||||
public virtual async Task CloseAsync()
|
public virtual async Task ReconnectAsync()
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} closing");
|
if (_processState != ProcessState.Processing && IsOpen)
|
||||||
await CloseInternalAsync(true, true).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Internal close method, will wait for each task to complete to gracefully close
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="waitSend"></param>
|
|
||||||
/// <param name="waitReceive"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
private async Task CloseInternalAsync(bool waitSend, bool waitReceive)
|
|
||||||
{
|
|
||||||
if (_closing)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_closing = true;
|
_log.Write(LogLevel.Debug, $"Socket {Id} reconnect requested");
|
||||||
var tasksToAwait = new List<Task>();
|
_closeTask = CloseInternalAsync();
|
||||||
if (_socket.State == WebSocketState.Open)
|
await _closeTask.ConfigureAwait(false);
|
||||||
tasksToAwait.Add(_socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default));
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual async Task CloseAsync()
|
||||||
|
{
|
||||||
|
await _closeSem.WaitAsync().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>
|
||||||
|
/// Internal close method
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
private async Task CloseInternalAsync()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//_closeState = CloseState.Closing;
|
||||||
_ctsSource.Cancel();
|
_ctsSource.Cancel();
|
||||||
_sendEvent.Set();
|
_sendEvent.Set();
|
||||||
if (waitSend)
|
|
||||||
tasksToAwait.Add(_sendTask!);
|
|
||||||
if (waitReceive)
|
|
||||||
tasksToAwait.Add(_receiveTask!);
|
|
||||||
if (_timeoutTask != null)
|
|
||||||
tasksToAwait.Add(_timeoutTask);
|
|
||||||
|
|
||||||
log.Write(LogLevel.Trace, $"Socket {Id} waiting for communication loops to finish");
|
if (_socket.State == WebSocketState.Open)
|
||||||
await Task.WhenAll(tasksToAwait).ConfigureAwait(false);
|
{
|
||||||
log.Write(LogLevel.Debug, $"Socket {Id} closed");
|
try
|
||||||
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>
|
||||||
@@ -319,45 +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();
|
|
||||||
_closing = false;
|
|
||||||
|
|
||||||
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>
|
||||||
@@ -366,40 +387,39 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private async Task SendLoopAsync()
|
private async Task SendLoopAsync()
|
||||||
{
|
{
|
||||||
_startedSent = true;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (_closing)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
await _sendEvent.WaitAsync().ConfigureAwait(false);
|
await _sendEvent.WaitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
if (_closing)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
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(10).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)
|
||||||
{
|
{
|
||||||
@@ -408,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);
|
||||||
_ = Task.Run(async () => await CloseInternalAsync(false, true).ConfigureAwait(false));
|
_closeTask = CloseInternalAsync();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -421,13 +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");
|
||||||
_startedSent = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,14 +456,13 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private async Task ReceiveLoopAsync()
|
private async Task ReceiveLoopAsync()
|
||||||
{
|
{
|
||||||
_startedReceive = true;
|
|
||||||
var buffer = new ArraySegment<byte>(new byte[65536]);
|
var buffer = new ArraySegment<byte>(new byte[65536]);
|
||||||
var received = 0;
|
var received = 0;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (_closing)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
MemoryStream? memoryStream = null;
|
MemoryStream? memoryStream = null;
|
||||||
@@ -466,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);
|
||||||
_ = Task.Run(async () => await CloseInternalAsync(true, true).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");
|
||||||
_ = Task.Run(async () => await CloseInternalAsync(true, true).ConfigureAwait(false));
|
_closeTask = CloseInternalAsync();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,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
|
||||||
@@ -493,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;
|
||||||
@@ -515,7 +533,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (receiveResult == null || _closing)
|
if (receiveResult == null || _ctsSource.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
// Error during receiving or cancellation requested, stop.
|
// Error during receiving or cancellation requested, stop.
|
||||||
break;
|
break;
|
||||||
@@ -527,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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -541,13 +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");
|
||||||
_startedReceive = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,65 +580,103 @@ 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)
|
||||||
{
|
{
|
||||||
if (_closing)
|
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
|
||||||
@@ -640,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>
|
||||||
@@ -704,6 +735,27 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_lastReceivedMessagesUpdate = checkTime;
|
_lastReceivedMessagesUpdate = checkTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set proxy on socket
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="proxy"></param>
|
||||||
|
/// <exception cref="ArgumentException"></exception>
|
||||||
|
protected virtual void SetProxy(ApiProxy proxy)
|
||||||
|
{
|
||||||
|
if (!Uri.TryCreate($"{proxy.Host}:{proxy.Port}", UriKind.Absolute, out var uri))
|
||||||
|
throw new ArgumentException("Proxy settings invalid, {proxy.Host}:{proxy.Port} not a valid URI", nameof(proxy));
|
||||||
|
|
||||||
|
_socket.Options.Proxy = uri?.Scheme == null
|
||||||
|
? _socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port)
|
||||||
|
: _socket.Options.Proxy = new WebProxy
|
||||||
|
{
|
||||||
|
Address = uri
|
||||||
|
};
|
||||||
|
|
||||||
|
if (proxy.Login != null)
|
||||||
|
_socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public T Data { get; set; }
|
public T Data { get; set; }
|
||||||
|
|
||||||
internal DataEvent(T data, DateTime timestamp)
|
/// <summary>
|
||||||
|
/// Ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <param name="timestamp"></param>
|
||||||
|
public DataEvent(T data, DateTime timestamp)
|
||||||
{
|
{
|
||||||
Data = data;
|
Data = data;
|
||||||
Timestamp = timestamp;
|
Timestamp = timestamp;
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Sockets
|
||||||
|
{
|
||||||
|
internal class PendingRequest
|
||||||
|
{
|
||||||
|
public Func<JToken, bool> Handler { get; }
|
||||||
|
public JToken? Result { get; private set; }
|
||||||
|
public bool Completed { get; private set; }
|
||||||
|
public AsyncResetEvent Event { get; }
|
||||||
|
public TimeSpan Timeout { get; }
|
||||||
|
|
||||||
|
private CancellationTokenSource cts;
|
||||||
|
|
||||||
|
public PendingRequest(Func<JToken, bool> handler, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
Handler = handler;
|
||||||
|
Event = new AsyncResetEvent(false, false);
|
||||||
|
Timeout = timeout;
|
||||||
|
|
||||||
|
cts = new CancellationTokenSource(timeout);
|
||||||
|
cts.Token.Register(Fail, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CheckData(JToken data)
|
||||||
|
{
|
||||||
|
if (Handler(data))
|
||||||
|
{
|
||||||
|
Result = data;
|
||||||
|
Completed = true;
|
||||||
|
Event.Set();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Fail()
|
||||||
|
{
|
||||||
|
Completed = true;
|
||||||
|
Event.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,41 +57,48 @@ 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
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Connected { get; private set; }
|
public bool Connected => _socket.IsOpen;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The underlying websocket
|
/// The unique ID of the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IWebsocket Socket { get; set; }
|
public int SocketId => _socket.Id;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current kilobytes per second of data being received, averaged over the last 3 seconds
|
||||||
|
/// </summary>
|
||||||
|
public double IncomingKbps => _socket.IncomingKbps;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The connection uri
|
||||||
|
/// </summary>
|
||||||
|
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>
|
||||||
@@ -100,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
|
||||||
@@ -113,54 +120,172 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (pausedActivity != value)
|
if (pausedActivity != value)
|
||||||
{
|
{
|
||||||
pausedActivity = value;
|
pausedActivity = value;
|
||||||
log.Write(LogLevel.Information, $"Socket {Socket.Id} 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 object();
|
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 SocketStatus _status;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The underlying websocket
|
||||||
|
/// </summary>
|
||||||
|
private readonly IWebsocket _socket;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// New socket connection
|
/// New socket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <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.Timeout = client.ClientOptions.SocketNoDataTimeout;
|
_socket = socket;
|
||||||
Socket.OnMessage += ProcessMessage;
|
_socket.OnMessage += HandleMessage;
|
||||||
Socket.OnClose += SocketOnClose;
|
_socket.OnOpen += HandleOpen;
|
||||||
Socket.OnOpen += SocketOnOpen;
|
_socket.OnClose += HandleClose;
|
||||||
|
_socket.OnReconnecting += HandleReconnecting;
|
||||||
|
_socket.OnReconnected += HandleReconnected;
|
||||||
|
_socket.OnError += HandleError;
|
||||||
|
_socket.GetReconnectionUrl = GetReconnectionUrlAsync;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for a socket opening
|
||||||
|
/// </summary>
|
||||||
|
protected virtual void HandleOpen()
|
||||||
|
{
|
||||||
|
Status = SocketStatus.Connected;
|
||||||
|
PausedActivity = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for a socket closing without reconnect
|
||||||
|
/// </summary>
|
||||||
|
protected virtual void HandleClose()
|
||||||
|
{
|
||||||
|
Status = SocketStatus.Closed;
|
||||||
|
Authenticated = false;
|
||||||
|
lock(subscriptionLock)
|
||||||
|
{
|
||||||
|
foreach (var sub in subscriptions)
|
||||||
|
sub.Confirmed = false;
|
||||||
|
}
|
||||||
|
Task.Run(() => ConnectionClosed?.Invoke());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for a socket losing conenction and starting reconnect
|
||||||
|
/// </summary>
|
||||||
|
protected virtual void HandleReconnecting()
|
||||||
|
{
|
||||||
|
Status = SocketStatus.Reconnecting;
|
||||||
|
DisconnectTime = DateTime.UtcNow;
|
||||||
|
Authenticated = false;
|
||||||
|
lock (subscriptionLock)
|
||||||
|
{
|
||||||
|
foreach (var sub in subscriptions)
|
||||||
|
sub.Confirmed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = Task.Run(() => ConnectionLost?.Invoke());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the url to connect to when reconnecting
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<Uri?> GetReconnectionUrlAsync()
|
||||||
|
{
|
||||||
|
return await socketClient.GetReconnectUriAsync(ApiClient, this).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for a socket which has reconnected
|
||||||
|
/// </summary>
|
||||||
|
protected virtual async void HandleReconnected()
|
||||||
|
{
|
||||||
|
Status = SocketStatus.Resubscribing;
|
||||||
|
lock (pendingRequests)
|
||||||
|
{
|
||||||
|
foreach (var pendingRequest in pendingRequests.ToList())
|
||||||
|
{
|
||||||
|
pendingRequest.Fail();
|
||||||
|
pendingRequests.Remove(pendingRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false);
|
||||||
|
if (!reconnectSuccessful)
|
||||||
|
await _socket.ReconnectAsync().ConfigureAwait(false);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Status = SocketStatus.Connected;
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
ConnectionRestored?.Invoke(DateTime.UtcNow - DisconnectTime!.Value);
|
||||||
|
DisconnectTime = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for an error on a websocket
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="e">The exception</param>
|
||||||
|
protected virtual void HandleError(Exception e)
|
||||||
|
{
|
||||||
|
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 {Socket.Id} received data: " + data);
|
log.Write(LogLevel.Trace, $"Socket {SocketId} received data: " + data);
|
||||||
if (string.IsNullOrEmpty(data)) return;
|
if (string.IsNullOrEmpty(data)) return;
|
||||||
|
|
||||||
var tokenData = data.ToJToken(log);
|
var tokenData = data.ToJToken(log);
|
||||||
@@ -173,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
|
||||||
@@ -189,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)
|
||||||
@@ -201,23 +324,140 @@ 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);
|
||||||
if (!HandleData(messageEvent) && !handledResponse)
|
var (handled, userProcessTime, subscription) = HandleData(messageEvent);
|
||||||
|
if (!handled && !handledResponse)
|
||||||
{
|
{
|
||||||
if (!socketClient.UnhandledMessageExpected)
|
if (!socketClient.UnhandledMessageExpected)
|
||||||
log.Write(LogLevel.Warning, $"Socket {Socket.Id} Message not handled: " + tokenData);
|
log.Write(LogLevel.Warning, $"Socket {SocketId} Message not handled: " + tokenData);
|
||||||
UnhandledMessage?.Invoke(tokenData);
|
UnhandledMessage?.Invoke(tokenData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var total = DateTime.UtcNow - timestamp;
|
||||||
|
if (userProcessTime.TotalMilliseconds > 500)
|
||||||
|
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.");
|
||||||
|
|
||||||
|
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>
|
||||||
@@ -246,13 +486,13 @@ 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 HandleData(MessageEvent messageEvent)
|
private (bool, TimeSpan, SocketSubscription?) HandleData(MessageEvent messageEvent)
|
||||||
{
|
{
|
||||||
SocketSubscription? currentSubscription = null;
|
SocketSubscription? currentSubscription = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var handled = false;
|
var handled = false;
|
||||||
var sw = Stopwatch.StartNew();
|
TimeSpan userCodeDuration = TimeSpan.Zero;
|
||||||
|
|
||||||
// Loop the subscriptions to check if any of them signal us that the message is for them
|
// Loop the subscriptions to check if any of them signal us that the message is for them
|
||||||
List<SocketSubscription> subscriptionsCopy;
|
List<SocketSubscription> subscriptionsCopy;
|
||||||
@@ -267,7 +507,10 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (socketClient.MessageMatchesHandler(this, messageEvent.JsonData, subscription.Identifier!))
|
if (socketClient.MessageMatchesHandler(this, messageEvent.JsonData, subscription.Identifier!))
|
||||||
{
|
{
|
||||||
handled = true;
|
handled = true;
|
||||||
|
var userSw = Stopwatch.StartNew();
|
||||||
subscription.MessageHandler(messageEvent);
|
subscription.MessageHandler(messageEvent);
|
||||||
|
userSw.Stop();
|
||||||
|
userCodeDuration = userSw.Elapsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -276,24 +519,21 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
handled = true;
|
handled = true;
|
||||||
messageEvent.JsonData = socketClient.ProcessTokenData(messageEvent.JsonData);
|
messageEvent.JsonData = socketClient.ProcessTokenData(messageEvent.JsonData);
|
||||||
|
var userSw = Stopwatch.StartNew();
|
||||||
subscription.MessageHandler(messageEvent);
|
subscription.MessageHandler(messageEvent);
|
||||||
|
userSw.Stop();
|
||||||
|
userCodeDuration = userSw.Elapsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sw.Stop();
|
return (handled, userCodeDuration, currentSubscription);
|
||||||
if (sw.ElapsedMilliseconds > 500)
|
|
||||||
log.Write(LogLevel.Debug, $"Socket {Socket.Id} message processing slow ({sw.ElapsedMilliseconds}ms), consider offloading data handling to another thread. " +
|
|
||||||
"Data from this socket may arrive late or not at all if message processing is continuously slow.");
|
|
||||||
else
|
|
||||||
log.Write(LogLevel.Trace, $"Socket {Socket.Id} message processed in {sw.ElapsedMilliseconds}ms");
|
|
||||||
return handled;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Error, $"Socket {Socket.Id} 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;
|
return (false, TimeSpan.Zero, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +552,10 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
pendingRequests.Add(pending);
|
pendingRequests.Add(pending);
|
||||||
}
|
}
|
||||||
Send(obj);
|
var sendOk = Send(obj);
|
||||||
|
if(!sendOk)
|
||||||
|
pending.Fail();
|
||||||
|
|
||||||
return pending.Event.WaitAsync(timeout);
|
return pending.Event.WaitAsync(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,204 +565,98 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <typeparam name="T">The type of the object to send</typeparam>
|
/// <typeparam name="T">The type of the object to send</typeparam>
|
||||||
/// <param name="obj">The object to send</param>
|
/// <param name="obj">The object to send</param>
|
||||||
/// <param name="nullValueHandling">How null values should be serialized</param>
|
/// <param name="nullValueHandling">How null values should be serialized</param>
|
||||||
public virtual void Send<T>(T obj, NullValueHandling nullValueHandling = NullValueHandling.Ignore)
|
public virtual bool Send<T>(T obj, NullValueHandling nullValueHandling = NullValueHandling.Ignore)
|
||||||
{
|
{
|
||||||
if(obj is string str)
|
if(obj is string str)
|
||||||
Send(str);
|
return Send(str);
|
||||||
else
|
else
|
||||||
Send(JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling }));
|
return Send(JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send string data over the websocket connection
|
/// Send string data over the websocket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data">The data to send</param>
|
/// <param name="data">The data to send</param>
|
||||||
public virtual void Send(string data)
|
public virtual bool Send(string data)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Trace, $"Socket {Socket.Id} sending data: {data}");
|
log.Write(LogLevel.Trace, $"Socket {SocketId} sending data: {data}");
|
||||||
Socket.Send(data);
|
try
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handler for a socket opening
|
|
||||||
/// </summary>
|
|
||||||
protected virtual void SocketOnOpen()
|
|
||||||
{
|
|
||||||
ReconnectTry = 0;
|
|
||||||
PausedActivity = false;
|
|
||||||
Connected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handler for a socket closing. Reconnects the socket if needed, or removes it from the active socket list if not
|
|
||||||
/// </summary>
|
|
||||||
protected virtual void SocketOnClose()
|
|
||||||
{
|
|
||||||
lock (pendingRequests)
|
|
||||||
{
|
{
|
||||||
foreach(var pendingRequest in pendingRequests.ToList())
|
_socket.Send(data);
|
||||||
{
|
return true;
|
||||||
pendingRequest.Fail();
|
|
||||||
pendingRequests.Remove(pendingRequest);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch(Exception)
|
||||||
if (socketClient.ClientOptions.AutoReconnect && ShouldReconnect)
|
|
||||||
{
|
{
|
||||||
if (Socket.Reconnecting)
|
return false;
|
||||||
return; // Already reconnecting
|
|
||||||
|
|
||||||
Socket.Reconnecting = true;
|
|
||||||
|
|
||||||
DisconnectTime = DateTime.UtcNow;
|
|
||||||
log.Write(LogLevel.Warning, $"Socket {Socket.Id} Connection lost, will try to reconnect");
|
|
||||||
if (!lostTriggered)
|
|
||||||
{
|
|
||||||
lostTriggered = true;
|
|
||||||
ConnectionLost?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
|
||||||
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
|
|
||||||
Socket.Reconnecting = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Socket.Reset();
|
|
||||||
if (!await Socket.ConnectAsync().ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
ReconnectTry++;
|
|
||||||
ResubscribeTry = 0;
|
|
||||||
if (socketClient.ClientOptions.MaxReconnectTries != null
|
|
||||||
&& ReconnectTry >= socketClient.ClientOptions.MaxReconnectTries)
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Warning, $"Socket {Socket.Id} failed to reconnect after {ReconnectTry} tries, closing");
|
|
||||||
ShouldReconnect = false;
|
|
||||||
|
|
||||||
if (socketClient.sockets.ContainsKey(Socket.Id))
|
|
||||||
socketClient.sockets.TryRemove(Socket.Id, out _);
|
|
||||||
|
|
||||||
_ = Task.Run(() => ConnectionClosed?.Invoke());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Write(LogLevel.Debug, $"Socket {Socket.Id} failed to reconnect{(socketClient.ClientOptions.MaxReconnectTries != null ? $", try {ReconnectTry}/{socketClient.ClientOptions.MaxReconnectTries}": "")}, will try again in {socketClient.ClientOptions.ReconnectInterval}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Successfully reconnected
|
|
||||||
var time = DisconnectTime;
|
|
||||||
DisconnectTime = null;
|
|
||||||
|
|
||||||
log.Write(LogLevel.Information, $"Socket {Socket.Id} reconnected after {DateTime.UtcNow - time}");
|
|
||||||
|
|
||||||
var reconnectResult = await ProcessReconnectAsync().ConfigureAwait(false);
|
|
||||||
if (!reconnectResult)
|
|
||||||
{
|
|
||||||
ResubscribeTry++;
|
|
||||||
DisconnectTime = time;
|
|
||||||
|
|
||||||
if (socketClient.ClientOptions.MaxResubscribeTries != null &&
|
|
||||||
ResubscribeTry >= socketClient.ClientOptions.MaxResubscribeTries)
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Warning, $"Socket {Socket.Id} failed to resubscribe after {ResubscribeTry} tries, closing. Last resubscription error: {reconnectResult.Error}");
|
|
||||||
ShouldReconnect = false;
|
|
||||||
|
|
||||||
if (socketClient.sockets.ContainsKey(Socket.Id))
|
|
||||||
socketClient.sockets.TryRemove(Socket.Id, out _);
|
|
||||||
|
|
||||||
_ = Task.Run(() => ConnectionClosed?.Invoke());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
log.Write(LogLevel.Debug, $"Socket {Socket.Id} resubscribing all subscriptions failed on reconnected socket{(socketClient.ClientOptions.MaxResubscribeTries != null ? $", try {ResubscribeTry}/{socketClient.ClientOptions.MaxResubscribeTries}" : "")}. Error: {reconnectResult.Error}. Disconnecting and reconnecting.");
|
|
||||||
|
|
||||||
if (Socket.IsOpen)
|
|
||||||
await Socket.CloseAsync().ConfigureAwait(false);
|
|
||||||
else
|
|
||||||
DisconnectTime = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Information, $"Socket {Socket.Id} 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Socket.Reconnecting = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!socketClient.ClientOptions.AutoReconnect && ShouldReconnect)
|
|
||||||
_ = Task.Run(() => ConnectionClosed?.Invoke());
|
|
||||||
|
|
||||||
// No reconnecting needed
|
|
||||||
log.Write(LogLevel.Information, $"Socket {Socket.Id} closed");
|
|
||||||
if (socketClient.sockets.ContainsKey(Socket.Id))
|
|
||||||
socketClient.sockets.TryRemove(Socket.Id, out _);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
if (!authResult)
|
if (!authResult)
|
||||||
{
|
{
|
||||||
log.Write(LogLevel.Warning, $"Socket {Socket.Id} authentication failed on reconnected socket. Disconnecting and reconnecting.");
|
log.Write(LogLevel.Warning, $"Socket {SocketId} authentication failed on reconnected socket. Disconnecting and reconnecting.");
|
||||||
return authResult;
|
return authResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Write(LogLevel.Debug, $"Socket {Socket.Id} authentication succeeded on reconnected socket.");
|
Authenticated = true;
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
if (!Socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
return new CallResult<bool>(new WebError("Socket not connected"));
|
return new CallResult<bool>(new WebError("Socket not connected"));
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Socket.IsOpen)
|
foreach (var subscription in subscriptionList)
|
||||||
|
subscription.Confirmed = true;
|
||||||
|
|
||||||
|
if (!_socket.IsOpen)
|
||||||
return new CallResult<bool>(new WebError("Socket not connected"));
|
return new CallResult<bool>(new WebError("Socket not connected"));
|
||||||
|
|
||||||
log.Write(LogLevel.Debug, $"Socket {Socket.Id} all subscription successfully resubscribed on reconnected socket.");
|
log.Write(LogLevel.Debug, $"Socket {SocketId} all subscription successfully resubscribed on reconnected socket.");
|
||||||
return new CallResult<bool>(true);
|
return new CallResult<bool>(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,100 +667,45 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
internal async Task<CallResult<bool>> ResubscribeAsync(SocketSubscription socketSubscription)
|
internal async Task<CallResult<bool>> ResubscribeAsync(SocketSubscription socketSubscription)
|
||||||
{
|
{
|
||||||
if (!Socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
return new CallResult<bool>(new UnknownError("Socket is not connected"));
|
return new CallResult<bool>(new UnknownError("Socket is not connected"));
|
||||||
|
|
||||||
return await socketClient.SubscribeAndWaitAsync(this, socketSubscription.Request!, socketSubscription).ConfigureAwait(false);
|
return await socketClient.SubscribeAndWaitAsync(this, socketSubscription.Request!, socketSubscription).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Close the connection
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task CloseAsync()
|
|
||||||
{
|
|
||||||
Connected = false;
|
|
||||||
ShouldReconnect = false;
|
|
||||||
if (socketClient.sockets.ContainsKey(Socket.Id))
|
|
||||||
socketClient.sockets.TryRemove(Socket.Id, out _);
|
|
||||||
|
|
||||||
lock (subscriptionLock)
|
|
||||||
{
|
|
||||||
foreach (var subscription in subscriptions)
|
|
||||||
{
|
|
||||||
if (subscription.CancellationTokenRegistration.HasValue)
|
|
||||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await Socket.CloseAsync().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
|
/// Status of the socket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="subscription">Subscription to close</param>
|
public enum SocketStatus
|
||||||
/// <returns></returns>
|
|
||||||
public async Task CloseAsync(SocketSubscription subscription)
|
|
||||||
{
|
{
|
||||||
if (!Socket.IsOpen)
|
/// <summary>
|
||||||
return;
|
/// None/Initial
|
||||||
|
/// </summary>
|
||||||
if (subscription.CancellationTokenRegistration.HasValue)
|
None,
|
||||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
/// <summary>
|
||||||
|
/// Connected
|
||||||
if (subscription.Confirmed)
|
/// </summary>
|
||||||
await socketClient.UnsubscribeAsync(this, subscription).ConfigureAwait(false);
|
Connected,
|
||||||
|
/// <summary>
|
||||||
bool shouldCloseConnection;
|
/// Reconnecting
|
||||||
lock (subscriptionLock)
|
/// </summary>
|
||||||
shouldCloseConnection = !subscriptions.Any(r => r.UserSubscription && subscription != r);
|
Reconnecting,
|
||||||
|
/// <summary>
|
||||||
if (shouldCloseConnection)
|
/// Resubscribing on reconnected socket
|
||||||
await CloseAsync().ConfigureAwait(false);
|
/// </summary>
|
||||||
|
Resubscribing,
|
||||||
lock (subscriptionLock)
|
/// <summary>
|
||||||
subscriptions.Remove(subscription);
|
/// Closing
|
||||||
}
|
/// </summary>
|
||||||
}
|
Closing,
|
||||||
|
/// <summary>
|
||||||
internal class PendingRequest
|
/// Closed
|
||||||
{
|
/// </summary>
|
||||||
public Func<JToken, bool> Handler { get; }
|
Closed,
|
||||||
public JToken? Result { get; private set; }
|
/// <summary>
|
||||||
public bool Completed { get; private set; }
|
/// Disposed
|
||||||
public AsyncResetEvent Event { get; }
|
/// </summary>
|
||||||
public TimeSpan Timeout { get; }
|
Disposed
|
||||||
|
|
||||||
private CancellationTokenSource cts;
|
|
||||||
|
|
||||||
public PendingRequest(Func<JToken, bool> handler, TimeSpan timeout)
|
|
||||||
{
|
|
||||||
Handler = handler;
|
|
||||||
Event = new AsyncResetEvent(false, false);
|
|
||||||
Timeout = timeout;
|
|
||||||
|
|
||||||
cts = new CancellationTokenSource(timeout);
|
|
||||||
cts.Token.Register(Fail, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CheckData(JToken data)
|
|
||||||
{
|
|
||||||
if (Handler(data))
|
|
||||||
{
|
|
||||||
Result = data;
|
|
||||||
Completed = true;
|
|
||||||
Event.Set();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Fail()
|
|
||||||
{
|
|
||||||
Completed = true;
|
|
||||||
Event.Set();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -72,7 +71,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The id of the socket
|
/// The id of the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int SocketId => connection.Socket.Id;
|
public int SocketId => connection.SocketId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The id of the subscription
|
/// The id of the subscription
|
||||||
@@ -103,9 +102,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// Close the socket to cause a reconnect
|
/// Close the socket to cause a reconnect
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
internal Task ReconnectAsync()
|
public Task ReconnectAsync()
|
||||||
{
|
{
|
||||||
return connection.Socket.CloseAsync();
|
return connection.TriggerReconnectAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Sockets
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parameters for a websocket
|
||||||
|
/// </summary>
|
||||||
|
public class WebSocketParameters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The uri to connect to
|
||||||
|
/// </summary>
|
||||||
|
public Uri Uri { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Headers to send in the connection handshake
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
|
||||||
|
/// <summary>
|
||||||
|
/// Cookies to send in the connection handshake
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>();
|
||||||
|
/// <summary>
|
||||||
|
/// The time to wait between reconnect attempts
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
/// <summary>
|
||||||
|
/// Proxy for the connection
|
||||||
|
/// </summary>
|
||||||
|
public ApiProxy? Proxy { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the socket should automatically reconnect when connection is lost
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoReconnect { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? Timeout { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Interval at which to send ping frames
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? KeepAliveInterval { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The max amount of messages to send per second
|
||||||
|
/// </summary>
|
||||||
|
public int? RatelimitPerSecond { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Origin header value to send in the connection handshake
|
||||||
|
/// </summary>
|
||||||
|
public string? Origin { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
|
||||||
|
/// </summary>
|
||||||
|
public Func<byte[], string>? DataInterpreterBytes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delegate used for processing string data received from socket connections before it is processed by handlers
|
||||||
|
/// </summary>
|
||||||
|
public Func<string, string>? DataInterpreterString { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encoding for sending/receiving data
|
||||||
|
/// </summary>
|
||||||
|
public Encoding Encoding { get; set; } = Encoding.UTF8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uri">Uri</param>
|
||||||
|
/// <param name="autoReconnect">Auto reconnect</param>
|
||||||
|
public WebSocketParameters(Uri uri, bool autoReconnect)
|
||||||
|
{
|
||||||
|
Uri = uri;
|
||||||
|
AutoReconnect = autoReconnect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,17 @@
|
|||||||
using System.Collections.Generic;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Logging;
|
using CryptoExchange.Net.Logging;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Sockets
|
namespace CryptoExchange.Net.Sockets
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default weboscket factory implementation
|
/// Default websocket factory implementation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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, 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, url, cookies, headers);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,85 @@ 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
|
||||||
|
* 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
|
||||||
|
|
||||||
* Version 5.1.7 - 14 Apr 2022
|
* Version 5.1.7 - 14 Apr 2022
|
||||||
* Moved some Rest parameters from BaseRestClient to RestApiClient to allow different implementations for sub clients
|
* Moved some Rest parameters from BaseRestClient to RestApiClient to allow different implementations for sub clients
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -105,7 +105,7 @@ All updates are wrapped in a `DataEvent<>` object, which contain a `Timestamp`,
|
|||||||
*[WARNING] Do not use `using` statements in combination with constructing a `SocketClient`. Doing so will dispose the `SocketClient` instance when the subscription is done, which will result in the connection getting closed. Instead assign the socket client to a variable outside of the method scope.*
|
*[WARNING] Do not use `using` statements in combination with constructing a `SocketClient`. Doing so will dispose the `SocketClient` instance when the subscription is done, which will result in the connection getting closed. Instead assign the socket client to a variable outside of the method scope.*
|
||||||
|
|
||||||
### Processing subscribe responses
|
### Processing subscribe responses
|
||||||
Subscribing to a stream will return a `CallResult<UpdateSubscription>` object. This should be checked for success the same was as the [rest client](#processing-request-responses). The `UpdateSubscription` object can be used to listen for connection events of the socket connection.
|
Subscribing to a stream will return a `CallResult<UpdateSubscription>` object. This should be checked for success the same way as the [rest client](#processing-request-responses). The `UpdateSubscription` object can be used to listen for connection events of the socket connection.
|
||||||
```csharp
|
```csharp
|
||||||
|
|
||||||
var subscriptionResult = await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
|
var subscriptionResult = await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
|
||||||
|
|||||||
+4
-1
@@ -61,4 +61,7 @@ var client = new BinanceClient(new BinanceClientOptions
|
|||||||
BaseAddress = BinanceApiAddresses.TestNet.UsdFuturesRestClientAddress
|
BaseAddress = BinanceApiAddresses.TestNet.UsdFuturesRestClientAddress
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### How are timezones handled / Timestamps are off by xx
|
||||||
|
Exchange API's treat all timestamps as UTC, both incoming and outgoing. The client libraries do no conversion so be sure to use UTC as well.
|
||||||
+18
-3
@@ -42,11 +42,26 @@ These might not be compatible with other libraries, make sure to check the Crypt
|
|||||||
## Discord
|
## Discord
|
||||||
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
|
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
|
||||||
|
|
||||||
## Donate / Sponsor
|
## Support the project
|
||||||
I develop and maintain this package on my own for free in my spare time. Donations are greatly appreciated. If you prefer to donate any other currency please contact me.
|
I develop and maintain this package on my own for free in my spare time, any support is greatly appreciated.
|
||||||
|
|
||||||
|
### Referral link
|
||||||
|
Use one of the following following referral links to signup to a new exchange to pay a small percentage of the trading fees you pay to support the project instead of paying them straight to the exchange. This doesn't cost you a thing!
|
||||||
|
[Binance](https://accounts.binance.com/en/register?ref=10153680)
|
||||||
|
[Bitfinex](https://www.bitfinex.com/sign-up?refcode=kCCe-CNBO)
|
||||||
|
[Bittrex](https://bittrex.com/discover/join?referralCode=TST-DJM-CSX)
|
||||||
|
[Bybit](https://partner.bybit.com/b/jkorf)
|
||||||
|
[CoinEx](https://www.coinex.com/register?refer_code=hd6gn)
|
||||||
|
[FTX](https://ftx.com/referrals#a=31620192)
|
||||||
|
[Huobi](https://www.huobi.com/en-us/v/register/double-invite/?inviter_id=11343840&invite_code=fxp93)
|
||||||
|
[Kucoin](https://www.kucoin.com/ucenter/signup?rcode=RguMux)
|
||||||
|
|
||||||
|
### Donate
|
||||||
|
Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.
|
||||||
|
|
||||||
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS
|
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS
|
||||||
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2
|
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2
|
||||||
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs
|
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs
|
||||||
|
|
||||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf)
|
### Sponsor
|
||||||
|
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||||
Reference in New Issue
Block a user