mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 00:43:03 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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>
|
||||
|
||||
<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="NUnit" Version="3.13.2"></packagereference>
|
||||
<packagereference Include="NUnit3TestAdapter" Version="4.2.0"></packagereference>
|
||||
<PackageReference Include="NUnit" Version="3.13.2"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0"></PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -38,6 +38,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
public double IncomingKbps => throw new NotImplementedException();
|
||||
|
||||
public Uri Uri => new Uri("");
|
||||
|
||||
public static int lastId = 0;
|
||||
public static object lastIdLock = new object();
|
||||
|
||||
@@ -111,5 +113,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
OnError?.Invoke(error);
|
||||
}
|
||||
|
||||
public async Task ProcessAsync()
|
||||
{
|
||||
while (Connected)
|
||||
await Task.Delay(50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,44 @@ namespace CryptoExchange.Net
|
||||
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>
|
||||
/// Execute a request to the uri and deserialize the response into the provided type parameter
|
||||
/// </summary>
|
||||
@@ -93,6 +131,42 @@ namespace CryptoExchange.Net
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
bool ignoreRatelimit = false
|
||||
) 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();
|
||||
|
||||
@@ -102,7 +176,7 @@ namespace CryptoExchange.Net
|
||||
if (!syncTimeResult)
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
|
||||
return syncTimeResult.As<T>(default);
|
||||
return syncTimeResult.As<IRequest>(default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,20 +186,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);
|
||||
if (!limitResult.Success)
|
||||
return new WebCallResult<T>(limitResult.Error!);
|
||||
return new CallResult<IRequest>(limitResult.Error!);
|
||||
}
|
||||
}
|
||||
|
||||
if (signed && apiClient.AuthenticationProvider == null)
|
||||
{
|
||||
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);
|
||||
var paramsPosition = parameterPosition ?? apiClient.ParameterPositions[method];
|
||||
var request = ConstructRequest(apiClient, uri, method, parameters, signed, paramsPosition, arraySerialization ?? apiClient.arraySerialization, requestId, additionalHeaders);
|
||||
|
||||
|
||||
string? paramString = "";
|
||||
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
||||
paramString = $" with request body '{request.Content}'";
|
||||
@@ -133,12 +207,14 @@ namespace CryptoExchange.Net
|
||||
var headers = request.GetHeaders();
|
||||
if (headers.Any())
|
||||
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
|
||||
|
||||
|
||||
apiClient.TotalRequestsMade++;
|
||||
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>
|
||||
/// Executes the request and returns the result deserialized into the type parameter class
|
||||
/// </summary>
|
||||
@@ -146,8 +222,14 @@ namespace CryptoExchange.Net
|
||||
/// <param name="request">The request object to execute</param>
|
||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="expectedEmptyResponse">If an empty response is expected</param>
|
||||
/// <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
|
||||
{
|
||||
@@ -169,22 +251,52 @@ namespace CryptoExchange.Net
|
||||
response.Close();
|
||||
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
|
||||
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!);
|
||||
if (!expectedEmptyResponse)
|
||||
{
|
||||
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
|
||||
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
|
||||
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||
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!);
|
||||
// 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);
|
||||
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!);
|
||||
|
||||
// Not an error, so continue deserializing
|
||||
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);
|
||||
// Not an error, so continue deserializing
|
||||
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);
|
||||
}
|
||||
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
|
||||
{
|
||||
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
|
||||
var desResult = await DeserializeAsync<T>(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false);
|
||||
responseStream.Close();
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// List of socket connections currently connecting/connected
|
||||
/// </summary>
|
||||
protected internal ConcurrentDictionary<int, SocketConnection> sockets = new();
|
||||
protected internal ConcurrentDictionary<int, SocketConnection> socketConnections = new();
|
||||
/// <summary>
|
||||
/// Semaphore used while creating sockets
|
||||
/// </summary>
|
||||
@@ -40,6 +40,10 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
protected int MaxSocketConnections { get; set; } = 9999;
|
||||
/// <summary>
|
||||
/// Keep alive interval for websocket connection
|
||||
/// </summary>
|
||||
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
|
||||
/// <summary>
|
||||
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
|
||||
/// </summary>
|
||||
protected Func<byte[], string>? dataInterpreterBytes;
|
||||
@@ -85,10 +89,10 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!sockets.Any())
|
||||
if (!socketConnections.Any())
|
||||
return 0;
|
||||
|
||||
return sockets.Sum(s => s.Value.Socket.IncomingKbps);
|
||||
return socketConnections.Sum(s => s.Value.IncomingKbps);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +209,7 @@ namespace CryptoExchange.Net
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
@@ -230,12 +234,12 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
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);
|
||||
}, false);
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Information, $"Socket {socketConnection.Socket.Id} subscription completed");
|
||||
log.Write(LogLevel.Information, $"Socket {socketConnection.SocketId} subscription completed");
|
||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
||||
}
|
||||
|
||||
@@ -317,7 +321,7 @@ namespace CryptoExchange.Net
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
@@ -368,7 +372,7 @@ namespace CryptoExchange.Net
|
||||
if (!result)
|
||||
{
|
||||
await socket.CloseAsync().ConfigureAwait(false);
|
||||
log.Write(LogLevel.Warning, $"Socket {socket.Socket.Id} authentication failed");
|
||||
log.Write(LogLevel.Warning, $"Socket {socket.SocketId} authentication failed");
|
||||
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
||||
return new CallResult<bool>(result.Error);
|
||||
}
|
||||
@@ -443,6 +447,9 @@ namespace CryptoExchange.Net
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
protected internal virtual JToken ProcessTokenData(JToken message)
|
||||
|
||||
|
||||
|
||||
{
|
||||
return message;
|
||||
}
|
||||
@@ -471,7 +478,7 @@ namespace CryptoExchange.Net
|
||||
var desResult = Deserialize<T>(messageEvent.JsonData);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -494,7 +501,7 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
genericHandlers.Add(identifier, action);
|
||||
var subscription = SocketSubscription.CreateForIdentifier(NextId(), identifier, false, action);
|
||||
foreach (var connection in sockets.Values)
|
||||
foreach (var connection in socketConnections.Values)
|
||||
connection.AddSubscription(subscription);
|
||||
}
|
||||
|
||||
@@ -507,13 +514,13 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected virtual 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.Uri.ToString().TrimEnd('/') == address.TrimEnd('/')
|
||||
&& (s.Value.ApiClient.GetType() == apiClient.GetType())
|
||||
&& (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;
|
||||
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 >= 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
|
||||
return result;
|
||||
@@ -548,13 +555,13 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
|
||||
socketConnection.Socket.Dispose();
|
||||
socketConnection.Dispose();
|
||||
return new CallResult<bool>(new CantConnectError());
|
||||
}
|
||||
|
||||
@@ -571,6 +578,7 @@ namespace CryptoExchange.Net
|
||||
if (ClientOptions.Proxy != null)
|
||||
socket.SetProxy(ClientOptions.Proxy);
|
||||
|
||||
socket.KeepAliveInterval = KeepAliveInterval;
|
||||
socket.Timeout = ClientOptions.SocketNoDataTimeout;
|
||||
socket.DataInterpreterBytes = dataInterpreterBytes;
|
||||
socket.DataInterpreterString = dataInterpreterString;
|
||||
@@ -605,27 +613,27 @@ namespace CryptoExchange.Net
|
||||
if (disposing)
|
||||
break;
|
||||
|
||||
foreach (var socket in sockets.Values)
|
||||
foreach (var socketConnection in socketConnections.Values)
|
||||
{
|
||||
if (disposing)
|
||||
break;
|
||||
|
||||
if (!socket.Socket.IsOpen)
|
||||
if (!socketConnection.Connected)
|
||||
continue;
|
||||
|
||||
var obj = objGetter(socket);
|
||||
var obj = objGetter(socketConnection);
|
||||
if (obj == null)
|
||||
continue;
|
||||
|
||||
log.Write(LogLevel.Trace, $"Socket {socket.Socket.Id} sending periodic {identifier}");
|
||||
log.Write(LogLevel.Trace, $"Socket {socketConnection.SocketId} sending periodic {identifier}");
|
||||
|
||||
try
|
||||
{
|
||||
socket.Send(obj);
|
||||
socketConnection.Send(obj);
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -642,7 +650,7 @@ namespace CryptoExchange.Net
|
||||
|
||||
SocketSubscription? subscription = null;
|
||||
SocketConnection? connection = null;
|
||||
foreach(var socket in sockets.Values.ToList())
|
||||
foreach(var socket in socketConnections.Values.ToList())
|
||||
{
|
||||
subscription = socket.GetSubscription(subscriptionId);
|
||||
if (subscription != null)
|
||||
@@ -679,19 +687,15 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAllAsync()
|
||||
{
|
||||
log.Write(LogLevel.Information, $"Closing all {sockets.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
||||
|
||||
await Task.Run(async () =>
|
||||
log.Write(LogLevel.Information, $"Closing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
var socketList = sockets.Values;
|
||||
foreach (var sub in socketList)
|
||||
tasks.Add(sub.CloseAsync());
|
||||
}
|
||||
var socketList = socketConnections.Values;
|
||||
foreach (var sub in socketList)
|
||||
tasks.Add(sub.CloseAsync());
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -703,7 +707,7 @@ namespace CryptoExchange.Net
|
||||
periodicEvent?.Set();
|
||||
periodicEvent?.Dispose();
|
||||
log.Write(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
|
||||
Task.Run(UnsubscribeAllAsync).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
_ = UnsubscribeAllAsync();
|
||||
semaphoreSlim?.Dispose();
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
|
||||
// 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.TimeSyncState.Semaphore.Release();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
@@ -32,13 +31,13 @@ namespace CryptoExchange.Net.Converters
|
||||
if(reader.TokenType is JsonToken.Integer)
|
||||
{
|
||||
var longValue = (long)reader.Value;
|
||||
if (longValue == 0)
|
||||
if (longValue == 0 || longValue == -1)
|
||||
return objectType == typeof(DateTime) ? default(DateTime): null;
|
||||
if (longValue < 1999999999)
|
||||
if (longValue < 19999999999)
|
||||
return ConvertFromSeconds(longValue);
|
||||
if (longValue < 1999999999999)
|
||||
if (longValue < 19999999999999)
|
||||
return ConvertFromMilliseconds(longValue);
|
||||
if (longValue < 1999999999999999)
|
||||
if (longValue < 19999999999999999)
|
||||
return ConvertFromMicroseconds(longValue);
|
||||
|
||||
return ConvertFromNanoseconds(longValue);
|
||||
@@ -46,7 +45,10 @@ namespace CryptoExchange.Net.Converters
|
||||
else if (reader.TokenType is JsonToken.Float)
|
||||
{
|
||||
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 ConvertFromMilliseconds(doubleValue);
|
||||
@@ -57,6 +59,9 @@ namespace CryptoExchange.Net.Converters
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
return null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(stringValue) || stringValue == "0" || stringValue == "-1")
|
||||
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
||||
|
||||
if (stringValue.Length == 8)
|
||||
{
|
||||
// Parse 20211103 format
|
||||
@@ -86,11 +91,11 @@ namespace CryptoExchange.Net.Converters
|
||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
||||
{
|
||||
// Parse 1637745563.000 format
|
||||
if (doubleValue < 1999999999)
|
||||
if (doubleValue < 19999999999)
|
||||
return ConvertFromSeconds(doubleValue);
|
||||
if (doubleValue < 1999999999999)
|
||||
if (doubleValue < 19999999999999)
|
||||
return ConvertFromMilliseconds((long)doubleValue);
|
||||
if (doubleValue < 1999999999999999)
|
||||
if (doubleValue < 19999999999999999)
|
||||
return ConvertFromMicroseconds((long)doubleValue);
|
||||
|
||||
return ConvertFromNanoseconds((long)doubleValue);
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>A base package for implementing cryptocurrency API's</Description>
|
||||
<PackageVersion>5.1.7</PackageVersion>
|
||||
<AssemblyVersion>5.1.7</AssemblyVersion>
|
||||
<FileVersion>5.1.7</FileVersion>
|
||||
<PackageVersion>5.1.11</PackageVersion>
|
||||
<AssemblyVersion>5.1.11</AssemblyVersion>
|
||||
<FileVersion>5.1.11</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<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.1.11 - Added KeepAliveInterval setting, Fixed port not being copied when setting parameters on request, Fixed inconsistent PackageReference casing in csproj</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
||||
@@ -426,6 +426,7 @@ namespace CryptoExchange.Net
|
||||
var uriBuilder = new UriBuilder();
|
||||
uriBuilder.Scheme = baseUri.Scheme;
|
||||
uriBuilder.Host = baseUri.Host;
|
||||
uriBuilder.Port = baseUri.Port;
|
||||
uriBuilder.Path = baseUri.AbsolutePath;
|
||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var parameter in parameters)
|
||||
@@ -454,6 +455,7 @@ namespace CryptoExchange.Net
|
||||
var uriBuilder = new UriBuilder();
|
||||
uriBuilder.Scheme = baseUri.Scheme;
|
||||
uriBuilder.Host = baseUri.Host;
|
||||
uriBuilder.Port = baseUri.Port;
|
||||
uriBuilder.Path = baseUri.AbsolutePath;
|
||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var parameter in parameters)
|
||||
|
||||
@@ -41,10 +41,6 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </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; }
|
||||
@@ -61,9 +57,9 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
Func<string, string>? DataInterpreterString { get; set; }
|
||||
/// <summary>
|
||||
/// The url the socket connects to
|
||||
/// The uri the socket connects to
|
||||
/// </summary>
|
||||
string Url { get; }
|
||||
Uri Uri { get; }
|
||||
/// <summary>
|
||||
/// Whether the socket connection is closed
|
||||
/// </summary>
|
||||
@@ -81,6 +77,10 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
TimeSpan Timeout { get; set; }
|
||||
/// <summary>
|
||||
/// The interval at which to send a ping frame to the server
|
||||
/// </summary>
|
||||
TimeSpan KeepAliveInterval { get; set; }
|
||||
/// <summary>
|
||||
/// Set a proxy to use when connecting
|
||||
/// </summary>
|
||||
/// <param name="proxy"></param>
|
||||
@@ -89,7 +89,12 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// Connect the socket
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<bool> ConnectAsync();
|
||||
Task<bool> ConnectAsync();
|
||||
/// <summary>
|
||||
/// Receive and send messages over the connection. Resulting task should complete when closing the socket.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task ProcessAsync();
|
||||
/// <summary>
|
||||
/// Send data
|
||||
/// </summary>
|
||||
|
||||
@@ -258,24 +258,32 @@ namespace CryptoExchange.Net.OrderBook
|
||||
}
|
||||
|
||||
_subscription = startResult.Data;
|
||||
_subscription.ConnectionLost += () =>
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
|
||||
Status = OrderBookStatus.Reconnecting;
|
||||
Reset();
|
||||
};
|
||||
_subscription.ConnectionClosed += () =>
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
|
||||
Status = OrderBookStatus.Disconnected;
|
||||
_ = StopAsync();
|
||||
};
|
||||
_subscription.ConnectionLost += HandleConnectionLost;
|
||||
_subscription.ConnectionClosed += HandleConnectionClosed;
|
||||
_subscription.ConnectionRestored += HandleConnectionRestored;
|
||||
|
||||
_subscription.ConnectionRestored += async time => await ResyncAsync().ConfigureAwait(false);
|
||||
Status = OrderBookStatus.Synced;
|
||||
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/>
|
||||
public async Task StopAsync()
|
||||
{
|
||||
@@ -286,8 +294,12 @@ namespace CryptoExchange.Net.OrderBook
|
||||
if (_processTask != null)
|
||||
await _processTask.ConfigureAwait(false);
|
||||
|
||||
if (_subscription != null)
|
||||
if (_subscription != null) {
|
||||
await _subscription.CloseAsync().ConfigureAwait(false);
|
||||
_subscription.ConnectionLost -= HandleConnectionLost;
|
||||
_subscription.ConnectionClosed -= HandleConnectionClosed;
|
||||
_subscription.ConnectionRestored -= HandleConnectionRestored;
|
||||
}
|
||||
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="ct">Cancellation token</param>
|
||||
/// <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;
|
||||
while (!bookSet && Status == OrderBookStatus.Syncing)
|
||||
@@ -491,12 +503,12 @@ namespace CryptoExchange.Net.OrderBook
|
||||
if(ct.IsCancellationRequested)
|
||||
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"));
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(10, ct).ConfigureAwait(false);
|
||||
await Task.Delay(50, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ }
|
||||
@@ -601,13 +613,13 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
private async Task ProcessQueue()
|
||||
{
|
||||
while (Status != OrderBookStatus.Disconnected)
|
||||
while (Status != OrderBookStatus.Disconnected && Status != OrderBookStatus.Disposed)
|
||||
{
|
||||
await _queueEvent.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
while (_processQueue.TryDequeue(out var item))
|
||||
{
|
||||
if (Status == OrderBookStatus.Disconnected)
|
||||
if (Status == OrderBookStatus.Disconnected || Status == OrderBookStatus.Disposed)
|
||||
break;
|
||||
|
||||
if (_stopProcessing)
|
||||
|
||||
@@ -23,20 +23,14 @@ namespace CryptoExchange.Net.Sockets
|
||||
public class CryptoExchangeWebSocketClient : IWebsocket
|
||||
{
|
||||
internal static int lastStreamId;
|
||||
private static readonly object streamIdLock = new object();
|
||||
private static readonly object streamIdLock = new();
|
||||
|
||||
private ClientWebSocket _socket;
|
||||
private Task? _sendTask;
|
||||
private Task? _receiveTask;
|
||||
private Task? _timeoutTask;
|
||||
private readonly AsyncResetEvent _sendEvent;
|
||||
private readonly ConcurrentQueue<byte[]> _sendBuffer;
|
||||
private readonly IDictionary<string, string> cookies;
|
||||
private readonly IDictionary<string, string> headers;
|
||||
private CancellationTokenSource _ctsSource;
|
||||
private bool _closing;
|
||||
private bool _startedSent;
|
||||
private bool _startedReceive;
|
||||
|
||||
private readonly List<DateTime> _outgoingMessages;
|
||||
private DateTime _lastReceivedMessagesUpdate;
|
||||
@@ -45,6 +39,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// Received messages, the size and the timstamp
|
||||
/// </summary>
|
||||
protected readonly List<ReceiveItem> _receivedMessages;
|
||||
|
||||
/// <summary>
|
||||
/// Received messages lock
|
||||
/// </summary>
|
||||
@@ -58,19 +53,19 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// Handlers for when an error happens on the socket
|
||||
/// </summary>
|
||||
protected readonly List<Action<Exception>> errorHandlers = new List<Action<Exception>>();
|
||||
protected readonly List<Action<Exception>> errorHandlers = new();
|
||||
/// <summary>
|
||||
/// Handlers for when the socket connection is opened
|
||||
/// </summary>
|
||||
protected readonly List<Action> openHandlers = new List<Action>();
|
||||
protected readonly List<Action> openHandlers = new();
|
||||
/// <summary>
|
||||
/// Handlers for when the connection is closed
|
||||
/// </summary>
|
||||
protected readonly List<Action> closeHandlers = new List<Action>();
|
||||
protected readonly List<Action> closeHandlers = new();
|
||||
/// <summary>
|
||||
/// Handlers for when a message is received
|
||||
/// </summary>
|
||||
protected readonly List<Action<string>> messageHandlers = new List<Action<string>>();
|
||||
protected readonly List<Action<string>> messageHandlers = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Id { get; }
|
||||
@@ -78,9 +73,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <inheritdoc />
|
||||
public string? Origin { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Reconnecting { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The timestamp this socket has been active for the last time
|
||||
/// </summary>
|
||||
@@ -97,13 +89,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
public Func<string, string>? DataInterpreterString { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Url { get; }
|
||||
public Uri Uri { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsClosed => _socket.State == WebSocketState.Closed;
|
||||
|
||||
/// <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
|
||||
@@ -130,6 +122,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <inheritdoc />
|
||||
public TimeSpan Timeout { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan KeepAliveInterval { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public double IncomingKbps
|
||||
{
|
||||
@@ -179,8 +174,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="log">The log object to use</param>
|
||||
/// <param name="url">The url the socket should connect to</param>
|
||||
public CryptoExchangeWebSocketClient(Log log, string url) : this(log, url, new Dictionary<string, string>(), new Dictionary<string, string>())
|
||||
/// <param name="uri">The uri the socket should connect to</param>
|
||||
public CryptoExchangeWebSocketClient(Log log, Uri uri) : this(log, uri, new Dictionary<string, string>(), new Dictionary<string, string>())
|
||||
{
|
||||
}
|
||||
|
||||
@@ -188,14 +183,14 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="log">The log object to use</param>
|
||||
/// <param name="url">The url the socket should connect to</param>
|
||||
/// <param name="uri">The uri the socket should connect to</param>
|
||||
/// <param name="cookies">Cookies to sent in the socket connection request</param>
|
||||
/// <param name="headers">Headers to sent in the socket connection request</param>
|
||||
public CryptoExchangeWebSocketClient(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers)
|
||||
public CryptoExchangeWebSocketClient(Log log, Uri uri, IDictionary<string, string> cookies, IDictionary<string, string> headers)
|
||||
{
|
||||
Id = NextStreamId();
|
||||
this.log = log;
|
||||
Url = url;
|
||||
Uri = uri;
|
||||
this.cookies = cookies;
|
||||
this.headers = headers;
|
||||
|
||||
@@ -212,13 +207,16 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <inheritdoc />
|
||||
public virtual void SetProxy(ApiProxy proxy)
|
||||
{
|
||||
Uri.TryCreate($"{proxy.Host}:{proxy.Port}", UriKind.Absolute, out var uri);
|
||||
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);
|
||||
}
|
||||
@@ -229,8 +227,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
log.Write(LogLevel.Debug, $"Socket {Id} connecting");
|
||||
try
|
||||
{
|
||||
using CancellationTokenSource tcs = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
await _socket.ConnectAsync(new Uri(Url), tcs.Token).ConfigureAwait(false);
|
||||
using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10));
|
||||
await _socket.ConnectAsync(Uri, tcs.Token).ConfigureAwait(false);
|
||||
|
||||
Handle(openHandlers);
|
||||
}
|
||||
@@ -239,35 +237,27 @@ namespace CryptoExchange.Net.Sockets
|
||||
log.Write(LogLevel.Debug, $"Socket {Id} connection failed: " + e.ToLogString());
|
||||
return false;
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Trace, $"Socket {Id} connection succeeded, starting communication");
|
||||
_sendTask = Task.Factory.StartNew(SendLoopAsync, default, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();
|
||||
_receiveTask = Task.Factory.StartNew(ReceiveLoopAsync, default, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();
|
||||
if (Timeout != default)
|
||||
_timeoutTask = Task.Run(CheckTimeoutAsync);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
while (!_startedSent || !_startedReceive)
|
||||
{
|
||||
// Wait for the tasks to have actually started
|
||||
await Task.Delay(10).ConfigureAwait(false);
|
||||
|
||||
if(sw.ElapsedMilliseconds > 5000)
|
||||
{
|
||||
_ = _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", default);
|
||||
log.Write(LogLevel.Debug, $"Socket {Id} startup interupted");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Debug, $"Socket {Id} connected to {Url}");
|
||||
|
||||
log.Write(LogLevel.Debug, $"Socket {Id} connected to {Uri}");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task ProcessAsync()
|
||||
{
|
||||
log.Write(LogLevel.Trace, $"Socket {Id} ProcessAsync started");
|
||||
var sendTask = SendLoopAsync();
|
||||
var receiveTask = ReceiveLoopAsync();
|
||||
var timeoutTask = Timeout != default ? CheckTimeoutAsync() : Task.CompletedTask;
|
||||
log.Write(LogLevel.Trace, $"Socket {Id} processing startup completed");
|
||||
await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
|
||||
log.Write(LogLevel.Trace, $"Socket {Id} ProcessAsync finished");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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");
|
||||
|
||||
var bytes = _encoding.GetBytes(data);
|
||||
@@ -280,36 +270,27 @@ namespace CryptoExchange.Net.Sockets
|
||||
public virtual async Task CloseAsync()
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"Socket {Id} closing");
|
||||
await CloseInternalAsync(true, true).ConfigureAwait(false);
|
||||
await CloseInternalAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal close method, will wait for each task to complete to gracefully close
|
||||
/// Internal close method
|
||||
/// </summary>
|
||||
/// <param name="waitSend"></param>
|
||||
/// <param name="waitReceive"></param>
|
||||
/// <returns></returns>
|
||||
private async Task CloseInternalAsync(bool waitSend, bool waitReceive)
|
||||
private async Task CloseInternalAsync()
|
||||
{
|
||||
if (_closing)
|
||||
return;
|
||||
|
||||
_closing = true;
|
||||
var tasksToAwait = new List<Task>();
|
||||
if (_socket.State == WebSocketState.Open)
|
||||
tasksToAwait.Add(_socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default));
|
||||
|
||||
_ctsSource.Cancel();
|
||||
_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");
|
||||
await Task.WhenAll(tasksToAwait).ConfigureAwait(false);
|
||||
if (_socket.State == WebSocketState.Open)
|
||||
{
|
||||
try
|
||||
{
|
||||
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
|
||||
}
|
||||
log.Write(LogLevel.Debug, $"Socket {Id} closed");
|
||||
Handle(closeHandlers);
|
||||
}
|
||||
@@ -335,7 +316,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"Socket {Id} resetting");
|
||||
_ctsSource = new CancellationTokenSource();
|
||||
_closing = false;
|
||||
|
||||
while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer
|
||||
|
||||
@@ -355,7 +335,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
socket.Options.Cookies = cookieContainer;
|
||||
foreach (var header in headers)
|
||||
socket.Options.SetRequestHeader(header.Key, header.Value);
|
||||
socket.Options.KeepAliveInterval = TimeSpan.FromSeconds(10);
|
||||
socket.Options.KeepAliveInterval = KeepAliveInterval;
|
||||
socket.Options.SetBuffer(65536, 65536); // Setting it to anything bigger than 65536 throws an exception in .net framework
|
||||
return socket;
|
||||
}
|
||||
@@ -366,17 +346,16 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
private async Task SendLoopAsync()
|
||||
{
|
||||
_startedSent = true;
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (_closing)
|
||||
if (_ctsSource.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
await _sendEvent.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
if (_closing)
|
||||
if (_ctsSource.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
while (_sendBuffer.TryDequeue(out var data))
|
||||
@@ -388,7 +367,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
while (MessagesSentLastSecond() >= RatelimitPerSecond)
|
||||
{
|
||||
start ??= DateTime.UtcNow;
|
||||
await Task.Delay(10).ConfigureAwait(false);
|
||||
await Task.Delay(50).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (start != null)
|
||||
@@ -410,7 +389,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
// Connection closed unexpectedly, .NET framework
|
||||
Handle(errorHandlers, ioe);
|
||||
_ = Task.Run(async () => await CloseInternalAsync(false, true).ConfigureAwait(false));
|
||||
await CloseInternalAsync().ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -427,7 +406,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
finally
|
||||
{
|
||||
log.Write(LogLevel.Trace, $"Socket {Id} Send loop finished");
|
||||
_startedSent = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,14 +415,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
private async Task ReceiveLoopAsync()
|
||||
{
|
||||
_startedReceive = true;
|
||||
var buffer = new ArraySegment<byte>(new byte[65536]);
|
||||
var received = 0;
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (_closing)
|
||||
if (_ctsSource.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
MemoryStream? memoryStream = null;
|
||||
@@ -468,7 +445,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
// Connection closed unexpectedly
|
||||
Handle(errorHandlers, wse);
|
||||
_ = Task.Run(async () => await CloseInternalAsync(true, true).ConfigureAwait(false));
|
||||
await CloseInternalAsync().ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -476,7 +453,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
// Connection closed unexpectedly
|
||||
log.Write(LogLevel.Debug, $"Socket {Id} received `Close` message");
|
||||
_ = Task.Run(async () => await CloseInternalAsync(true, true).ConfigureAwait(false));
|
||||
await CloseInternalAsync().ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -515,7 +492,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
break;
|
||||
}
|
||||
|
||||
if (receiveResult == null || _closing)
|
||||
if (receiveResult == null || _ctsSource.IsCancellationRequested)
|
||||
{
|
||||
// Error during receiving or cancellation requested, stop.
|
||||
break;
|
||||
@@ -547,7 +524,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
finally
|
||||
{
|
||||
log.Write(LogLevel.Trace, $"Socket {Id} Receive loop finished");
|
||||
_startedReceive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,7 +591,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (_closing)
|
||||
if (_ctsSource.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
if (DateTime.UtcNow - LastActionTime > Timeout)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,12 +65,22 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// If connection is made
|
||||
/// </summary>
|
||||
public bool Connected { get; private set; }
|
||||
public bool Connected => _socket.IsOpen;
|
||||
|
||||
/// <summary>
|
||||
/// The underlying websocket
|
||||
/// The unique ID of the socket
|
||||
/// </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 Uri => _socket.Uri;
|
||||
|
||||
/// <summary>
|
||||
/// The API client the connection is for
|
||||
@@ -113,7 +123,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (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();
|
||||
else ActivityUnpaused?.Invoke();
|
||||
}
|
||||
@@ -122,13 +132,21 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
private bool pausedActivity;
|
||||
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 BaseSocketClient socketClient;
|
||||
|
||||
private readonly List<PendingRequest> pendingRequests;
|
||||
private Task? _socketProcessReconnectTask;
|
||||
|
||||
private SocketStatus _status;
|
||||
|
||||
/// <summary>
|
||||
/// The underlying websocket
|
||||
/// </summary>
|
||||
private readonly IWebsocket _socket;
|
||||
|
||||
/// <summary>
|
||||
/// New socket connection
|
||||
@@ -145,14 +163,261 @@ namespace CryptoExchange.Net.Sockets
|
||||
pendingRequests = new List<PendingRequest>();
|
||||
|
||||
subscriptions = new List<SocketSubscription>();
|
||||
Socket = socket;
|
||||
_socket = socket;
|
||||
|
||||
Socket.Timeout = client.ClientOptions.SocketNoDataTimeout;
|
||||
Socket.OnMessage += ProcessMessage;
|
||||
Socket.OnClose += SocketOnClose;
|
||||
Socket.OnOpen += SocketOnOpen;
|
||||
_socket.Timeout = client.ClientOptions.SocketNoDataTimeout;
|
||||
_socket.OnMessage += ProcessMessage;
|
||||
_socket.OnOpen += SocketOnOpen;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect the websocket and start processing
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> ConnectAsync()
|
||||
{
|
||||
var connected = await _socket.ConnectAsync().ConfigureAwait(false);
|
||||
if (connected)
|
||||
StartProcessingTask();
|
||||
|
||||
return connected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the underlying socket
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IWebsocket GetSocket()
|
||||
{
|
||||
return _socket;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger a reconnect of the socket connection
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task TriggerReconnectAsync()
|
||||
{
|
||||
await _socket.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close the connection
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task CloseAsync()
|
||||
{
|
||||
ShouldReconnect = false;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
if (_status == SocketStatus.Reconnecting)
|
||||
{
|
||||
// Wait for reconnect task to finish
|
||||
log.Write(LogLevel.Trace, "In reconnecting state, waiting for reconnecting to end");
|
||||
if (_socketProcessReconnectTask != null)
|
||||
await _socketProcessReconnectTask.ConfigureAwait(false);
|
||||
|
||||
await _socket.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Close before waiting for process task to finish
|
||||
await _socket.CloseAsync().ConfigureAwait(false);
|
||||
|
||||
if (_socketProcessReconnectTask != null)
|
||||
await _socketProcessReconnectTask.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)
|
||||
{
|
||||
if (!_socket.IsOpen || _status == SocketStatus.Disposed)
|
||||
return;
|
||||
|
||||
if (subscription.CancellationTokenRegistration.HasValue)
|
||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||
|
||||
if (subscription.Confirmed)
|
||||
await socketClient.UnsubscribeAsync(this, subscription).ConfigureAwait(false);
|
||||
|
||||
bool shouldCloseConnection;
|
||||
lock (subscriptionLock)
|
||||
shouldCloseConnection = !subscriptions.Any(r => r.UserSubscription && subscription != r);
|
||||
|
||||
if (shouldCloseConnection)
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
|
||||
lock (subscriptionLock)
|
||||
subscriptions.Remove(subscription);
|
||||
}
|
||||
|
||||
private void StartProcessingTask()
|
||||
{
|
||||
log.Write(LogLevel.Trace, $"Starting {SocketId} process/reconnect task");
|
||||
_status = SocketStatus.Processing;
|
||||
_socketProcessReconnectTask = Task.Run(async () =>
|
||||
{
|
||||
await _socket.ProcessAsync().ConfigureAwait(false);
|
||||
_status = SocketStatus.Reconnecting;
|
||||
await ReconnectAsync().ConfigureAwait(false);
|
||||
log.Write(LogLevel.Trace, $"Process/reconnect {SocketId} task finished");
|
||||
});
|
||||
}
|
||||
|
||||
private async Task ReconnectAsync()
|
||||
{
|
||||
// Fail all pending requests
|
||||
lock (pendingRequests)
|
||||
{
|
||||
foreach (var pendingRequest in pendingRequests.ToList())
|
||||
{
|
||||
pendingRequest.Fail();
|
||||
pendingRequests.Remove(pendingRequest);
|
||||
}
|
||||
}
|
||||
|
||||
if (socketClient.ClientOptions.AutoReconnect && ShouldReconnect)
|
||||
{
|
||||
// Should reconnect
|
||||
DisconnectTime = DateTime.UtcNow;
|
||||
log.Write(LogLevel.Warning, $"Socket {SocketId} Connection lost, will try to reconnect");
|
||||
if (!lostTriggered)
|
||||
{
|
||||
lostTriggered = true;
|
||||
_ = Task.Run(() => ConnectionLost?.Invoke());
|
||||
}
|
||||
|
||||
while (ShouldReconnect)
|
||||
{
|
||||
if (ReconnectTry > 0)
|
||||
{
|
||||
// Wait a bit before attempting reconnect
|
||||
await Task.Delay(socketClient.ClientOptions.ReconnectInterval).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!ShouldReconnect)
|
||||
{
|
||||
// Should reconnect changed to false while waiting to reconnect
|
||||
return;
|
||||
}
|
||||
|
||||
_socket.Reset();
|
||||
if (!await _socket.ConnectAsync().ConfigureAwait(false))
|
||||
{
|
||||
// Reconnect failed
|
||||
ReconnectTry++;
|
||||
ResubscribeTry = 0;
|
||||
if (socketClient.ClientOptions.MaxReconnectTries != null
|
||||
&& ReconnectTry >= socketClient.ClientOptions.MaxReconnectTries)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"Socket {SocketId} failed to reconnect after {ReconnectTry} tries, closing");
|
||||
ShouldReconnect = false;
|
||||
|
||||
if (socketClient.socketConnections.ContainsKey(SocketId))
|
||||
socketClient.socketConnections.TryRemove(SocketId, out _);
|
||||
|
||||
_ = Task.Run(() => ConnectionClosed?.Invoke());
|
||||
// Reached max tries, break loop and leave connection closed
|
||||
break;
|
||||
}
|
||||
|
||||
// Continue to try again
|
||||
log.Write(LogLevel.Debug, $"Socket {SocketId} failed to reconnect{(socketClient.ClientOptions.MaxReconnectTries != null ? $", try {ReconnectTry}/{socketClient.ClientOptions.MaxReconnectTries}" : "")}, will try again in {socketClient.ClientOptions.ReconnectInterval}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Successfully reconnected, start processing
|
||||
StartProcessingTask();
|
||||
|
||||
ReconnectTry = 0;
|
||||
var time = DisconnectTime;
|
||||
DisconnectTime = null;
|
||||
|
||||
log.Write(LogLevel.Information, $"Socket {SocketId} reconnected after {DateTime.UtcNow - time}");
|
||||
|
||||
var reconnectResult = await ProcessReconnectAsync().ConfigureAwait(false);
|
||||
if (!reconnectResult)
|
||||
{
|
||||
// Failed to resubscribe everything
|
||||
ResubscribeTry++;
|
||||
DisconnectTime = time;
|
||||
|
||||
if (socketClient.ClientOptions.MaxResubscribeTries != null &&
|
||||
ResubscribeTry >= socketClient.ClientOptions.MaxResubscribeTries)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"Socket {SocketId} failed to resubscribe after {ResubscribeTry} tries, closing. Last resubscription error: {reconnectResult.Error}");
|
||||
ShouldReconnect = false;
|
||||
|
||||
if (socketClient.socketConnections.ContainsKey(SocketId))
|
||||
socketClient.socketConnections.TryRemove(SocketId, out _);
|
||||
|
||||
_ = Task.Run(() => ConnectionClosed?.Invoke());
|
||||
}
|
||||
else
|
||||
log.Write(LogLevel.Debug, $"Socket {SocketId} resubscribing all subscriptions failed on reconnected socket{(socketClient.ClientOptions.MaxResubscribeTries != null ? $", try {ResubscribeTry}/{socketClient.ClientOptions.MaxResubscribeTries}" : "")}. Error: {reconnectResult.Error}. Disconnecting and reconnecting.");
|
||||
|
||||
// Failed resubscribe, close socket if it is still open
|
||||
if (_socket.IsOpen)
|
||||
await _socket.CloseAsync().ConfigureAwait(false);
|
||||
else
|
||||
DisconnectTime = DateTime.UtcNow;
|
||||
|
||||
// Break out of the loop, the new processing task should reconnect again
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Succesfully reconnected
|
||||
log.Write(LogLevel.Information, $"Socket {SocketId} data connection restored.");
|
||||
ResubscribeTry = 0;
|
||||
if (lostTriggered)
|
||||
{
|
||||
lostTriggered = false;
|
||||
_ = Task.Run(() => ConnectionRestored?.Invoke(time.HasValue ? DateTime.UtcNow - time.Value : TimeSpan.FromSeconds(0)));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!socketClient.ClientOptions.AutoReconnect && ShouldReconnect)
|
||||
_ = Task.Run(() => ConnectionClosed?.Invoke());
|
||||
|
||||
// No reconnecting needed
|
||||
log.Write(LogLevel.Information, $"Socket {SocketId} closed");
|
||||
if (socketClient.socketConnections.ContainsKey(SocketId))
|
||||
socketClient.socketConnections.TryRemove(SocketId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose the connection
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_status = SocketStatus.Disposed;
|
||||
_socket.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process a message received by the socket
|
||||
/// </summary>
|
||||
@@ -160,7 +425,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
private void ProcessMessage(string data)
|
||||
{
|
||||
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;
|
||||
|
||||
var tokenData = data.ToJToken(log);
|
||||
@@ -202,12 +467,20 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
// Message was not a request response, check data handlers
|
||||
var messageEvent = new MessageEvent(this, tokenData, socketClient.ClientOptions.OutputOriginalData ? data: null, timestamp);
|
||||
if (!HandleData(messageEvent) && !handledResponse)
|
||||
var (handled, userProcessTime) = HandleData(messageEvent);
|
||||
if (!handled && !handledResponse)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
var total = DateTime.UtcNow - timestamp;
|
||||
if (userProcessTime.TotalMilliseconds > 500)
|
||||
log.Write(LogLevel.Debug, $"Socket {SocketId} 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} message processed in {(int)total.TotalMilliseconds}ms, ({(int)userProcessTime.TotalMilliseconds}ms user code)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -246,13 +519,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
/// <param name="messageEvent"></param>
|
||||
/// <returns>True if the data was successfully handled</returns>
|
||||
private bool HandleData(MessageEvent messageEvent)
|
||||
private (bool, TimeSpan) HandleData(MessageEvent messageEvent)
|
||||
{
|
||||
SocketSubscription? currentSubscription = null;
|
||||
try
|
||||
{
|
||||
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
|
||||
List<SocketSubscription> subscriptionsCopy;
|
||||
@@ -267,7 +540,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (socketClient.MessageMatchesHandler(this, messageEvent.JsonData, subscription.Identifier!))
|
||||
{
|
||||
handled = true;
|
||||
var userSw = Stopwatch.StartNew();
|
||||
subscription.MessageHandler(messageEvent);
|
||||
userSw.Stop();
|
||||
userCodeDuration = userSw.Elapsed;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -276,24 +552,21 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
handled = true;
|
||||
messageEvent.JsonData = socketClient.ProcessTokenData(messageEvent.JsonData);
|
||||
var userSw = Stopwatch.StartNew();
|
||||
subscription.MessageHandler(messageEvent);
|
||||
userSw.Stop();
|
||||
userCodeDuration = userSw.Elapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
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;
|
||||
|
||||
return (handled, userCodeDuration);
|
||||
}
|
||||
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);
|
||||
return false;
|
||||
return (false, TimeSpan.Zero);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +585,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
pendingRequests.Add(pending);
|
||||
}
|
||||
Send(obj);
|
||||
var sendOk = Send(obj);
|
||||
if(!sendOk)
|
||||
pending.Fail();
|
||||
|
||||
return pending.Event.WaitAsync(timeout);
|
||||
}
|
||||
|
||||
@@ -322,22 +598,30 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <typeparam name="T">The type of the object to send</typeparam>
|
||||
/// <param name="obj">The object to send</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)
|
||||
Send(str);
|
||||
return Send(str);
|
||||
else
|
||||
Send(JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling }));
|
||||
return Send(JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send string data over the websocket connection
|
||||
/// </summary>
|
||||
/// <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}");
|
||||
Socket.Send(data);
|
||||
log.Write(LogLevel.Trace, $"Socket {SocketId} sending data: {data}");
|
||||
try
|
||||
{
|
||||
_socket.Send(data);
|
||||
return true;
|
||||
}
|
||||
catch(Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -347,140 +631,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
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())
|
||||
{
|
||||
pendingRequest.Fail();
|
||||
pendingRequests.Remove(pendingRequest);
|
||||
}
|
||||
}
|
||||
|
||||
if (socketClient.ClientOptions.AutoReconnect && ShouldReconnect)
|
||||
{
|
||||
if (Socket.Reconnecting)
|
||||
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()
|
||||
{
|
||||
if (!Socket.IsOpen)
|
||||
if (!_socket.IsOpen)
|
||||
return new CallResult<bool>(new WebError("Socket not connected"));
|
||||
|
||||
if (Authenticated)
|
||||
@@ -489,11 +644,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
var authResult = await socketClient.AuthenticateSocketAsync(this).ConfigureAwait(false);
|
||||
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;
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Debug, $"Socket {Socket.Id} authentication succeeded on reconnected socket.");
|
||||
log.Write(LogLevel.Debug, $"Socket {SocketId} authentication succeeded on reconnected socket.");
|
||||
}
|
||||
|
||||
// Get a list of all subscriptions on the socket
|
||||
@@ -504,7 +659,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
// 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)
|
||||
{
|
||||
if (!Socket.IsOpen)
|
||||
if (!_socket.IsOpen)
|
||||
return new CallResult<bool>(new WebError("Socket not connected"));
|
||||
|
||||
var taskList = new List<Task<CallResult<bool>>>();
|
||||
@@ -516,10 +671,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
return taskList.First(t => !t.Result.Success).Result;
|
||||
}
|
||||
|
||||
if (!Socket.IsOpen)
|
||||
if (!_socket.IsOpen)
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -530,100 +685,18 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
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 await socketClient.SubscribeAndWaitAsync(this, socketSubscription.Request!, socketSubscription).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close the connection
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task CloseAsync()
|
||||
|
||||
private enum SocketStatus
|
||||
{
|
||||
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>
|
||||
/// 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)
|
||||
{
|
||||
if (!Socket.IsOpen)
|
||||
return;
|
||||
|
||||
if (subscription.CancellationTokenRegistration.HasValue)
|
||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||
|
||||
if (subscription.Confirmed)
|
||||
await socketClient.UnsubscribeAsync(this, subscription).ConfigureAwait(false);
|
||||
|
||||
bool shouldCloseConnection;
|
||||
lock (subscriptionLock)
|
||||
shouldCloseConnection = !subscriptions.Any(r => r.UserSubscription && subscription != r);
|
||||
|
||||
if (shouldCloseConnection)
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
|
||||
lock (subscriptionLock)
|
||||
subscriptions.Remove(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
None,
|
||||
Processing,
|
||||
Reconnecting,
|
||||
Disposed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// The id of the socket
|
||||
/// </summary>
|
||||
public int SocketId => connection.Socket.Id;
|
||||
public int SocketId => connection.SocketId;
|
||||
|
||||
/// <summary>
|
||||
/// The id of the subscription
|
||||
@@ -103,9 +103,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// Close the socket to cause a reconnect
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal Task ReconnectAsync()
|
||||
public Task ReconnectAsync()
|
||||
{
|
||||
return connection.Socket.CloseAsync();
|
||||
return connection.TriggerReconnectAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Default weboscket factory implementation
|
||||
/// Default websocket factory implementation
|
||||
/// </summary>
|
||||
public class WebsocketFactory : IWebsocketFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IWebsocket CreateWebsocket(Log log, string url)
|
||||
{
|
||||
return new CryptoExchangeWebSocketClient(log, url);
|
||||
return new CryptoExchangeWebSocketClient(log, new Uri(url));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IWebsocket CreateWebsocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers)
|
||||
{
|
||||
return new CryptoExchangeWebSocketClient(log, url, cookies, headers);
|
||||
return new CryptoExchangeWebSocketClient(log, new Uri(url), cookies, headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,26 @@ I develop and maintain this package on my own for free in my spare time. Donatio
|
||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf)
|
||||
|
||||
## Release notes
|
||||
* 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
|
||||
* Moved some Rest parameters from BaseRestClient to RestApiClient to allow different implementations for sub clients
|
||||
|
||||
|
||||
+4
-1
@@ -61,4 +61,7 @@ var client = new BinanceClient(new BinanceClientOptions
|
||||
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.
|
||||
Reference in New Issue
Block a user