mirror of
				https://github.com/JKorf/CryptoExchange.Net
				synced 2025-10-31 02:17:45 +00:00 
			
		
		
		
	wip
This commit is contained in:
		
							parent
							
								
									1739769f87
								
							
						
					
					
						commit
						f514e172d7
					
				| @ -30,7 +30,7 @@ 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> | ||||||
| @ -85,10 +85,10 @@ 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); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -205,7 +205,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")); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| @ -230,12 +230,12 @@ namespace CryptoExchange.Net | |||||||
|             { |             { | ||||||
|                 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 completed"); | ||||||
|             return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription)); |             return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription)); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -317,7 +317,7 @@ namespace CryptoExchange.Net | |||||||
| 
 | 
 | ||||||
|             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")); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| @ -368,7 +368,7 @@ namespace CryptoExchange.Net | |||||||
|             if (!result) |             if (!result) | ||||||
|             { |             { | ||||||
|                 await socket.CloseAsync().ConfigureAwait(false); |                 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; |                 result.Error!.Message = "Authentication failed: " + result.Error.Message; | ||||||
|                 return new CallResult<bool>(result.Error); |                 return new CallResult<bool>(result.Error); | ||||||
|             } |             } | ||||||
| @ -471,7 +471,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; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
| @ -494,7 +494,7 @@ namespace CryptoExchange.Net | |||||||
|         { |         { | ||||||
|             genericHandlers.Add(identifier, action); |             genericHandlers.Add(identifier, action); | ||||||
|             var subscription = SocketSubscription.CreateForIdentifier(NextId(), identifier, false, action); |             var subscription = SocketSubscription.CreateForIdentifier(NextId(), identifier, false, action); | ||||||
|             foreach (var connection in sockets.Values) |             foreach (var connection in socketConnections.Values) | ||||||
|                 connection.AddSubscription(subscription); |                 connection.AddSubscription(subscription); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -507,13 +507,13 @@ namespace CryptoExchange.Net | |||||||
|         /// <returns></returns> |         /// <returns></returns> | ||||||
|         protected virtual SocketConnection GetSocketConnection(SocketApiClient apiClient, string address, bool authenticated) |         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.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 >= 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 result; | ||||||
| @ -548,13 +548,13 @@ 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()); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -605,19 +605,19 @@ namespace CryptoExchange.Net | |||||||
|                     if (disposing) |                     if (disposing) | ||||||
|                         break; |                         break; | ||||||
|                      |                      | ||||||
|                     foreach (var socket in sockets.Values) |                     foreach (var socket in socketConnections.Values) | ||||||
|                     { |                     { | ||||||
|                         if (disposing) |                         if (disposing) | ||||||
|                             break; |                             break; | ||||||
| 
 | 
 | ||||||
|                         if (!socket.Socket.IsOpen) |                         if (!socket.Connected) | ||||||
|                             continue; |                             continue; | ||||||
| 
 | 
 | ||||||
|                         var obj = objGetter(socket); |                         var obj = objGetter(socket); | ||||||
|                         if (obj == null) |                         if (obj == null) | ||||||
|                             continue; |                             continue; | ||||||
| 
 | 
 | ||||||
|                         log.Write(LogLevel.Trace, $"Socket {socket.Socket.Id} sending periodic {identifier}"); |                         log.Write(LogLevel.Trace, $"Socket {socket.SocketId} sending periodic {identifier}"); | ||||||
| 
 | 
 | ||||||
|                         try |                         try | ||||||
|                         { |                         { | ||||||
| @ -625,7 +625,7 @@ namespace CryptoExchange.Net | |||||||
|                         } |                         } | ||||||
|                         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 {socket.SocketId} Periodic send {identifier} failed: " + ex.ToLogString()); | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| @ -642,7 +642,7 @@ namespace CryptoExchange.Net | |||||||
| 
 | 
 | ||||||
|             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) | ||||||
| @ -679,13 +679,13 @@ 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, $"Closing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions"); | ||||||
| 
 | 
 | ||||||
|             await Task.Run(async () => |             await Task.Run(async () => | ||||||
|             { |             { | ||||||
|                 var tasks = new List<Task>(); |                 var tasks = new List<Task>(); | ||||||
|                 { |                 { | ||||||
|                     var socketList = sockets.Values; |                     var socketList = socketConnections.Values; | ||||||
|                     foreach (var sub in socketList) |                     foreach (var sub in socketList) | ||||||
|                         tasks.Add(sub.CloseAsync()); |                         tasks.Add(sub.CloseAsync()); | ||||||
|                 } |                 } | ||||||
|  | |||||||
| @ -41,10 +41,6 @@ namespace CryptoExchange.Net.Interfaces | |||||||
|         /// </summary> |         /// </summary> | ||||||
|         Encoding? Encoding { get; set; } |         Encoding? Encoding { get; set; } | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Whether socket is in the process of reconnecting |  | ||||||
|         /// </summary> |  | ||||||
|         bool Reconnecting { get; set; } |  | ||||||
|         /// <summary> |  | ||||||
|         /// The max amount of outgoing messages per second |         /// The max amount of outgoing messages per second | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         int? RatelimitPerSecond { get; set; } |         int? RatelimitPerSecond { get; set; } | ||||||
| @ -61,9 +57,9 @@ namespace CryptoExchange.Net.Interfaces | |||||||
|         /// </summary> |         /// </summary> | ||||||
|         Func<string, string>? DataInterpreterString { get; set; } |         Func<string, string>? DataInterpreterString { get; set; } | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// The url the socket connects to |         /// The uri the socket connects to | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         string Url { get; } |         Uri Uri { get; } | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Whether the socket connection is closed |         /// Whether the socket connection is closed | ||||||
|         /// </summary> |         /// </summary> | ||||||
| @ -90,6 +86,7 @@ namespace CryptoExchange.Net.Interfaces | |||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <returns></returns> |         /// <returns></returns> | ||||||
|         Task<bool> ConnectAsync(); |         Task<bool> ConnectAsync(); | ||||||
|  |         Task ProcessAsync(); | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Send data |         /// Send data | ||||||
|         /// </summary> |         /// </summary> | ||||||
|  | |||||||
| @ -483,7 +483,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 +491,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) | ||||||
|                 { } |                 { } | ||||||
|  | |||||||
| @ -26,17 +26,11 @@ namespace CryptoExchange.Net.Sockets | |||||||
|         private static readonly object streamIdLock = new object(); |         private static readonly object streamIdLock = new object(); | ||||||
| 
 | 
 | ||||||
|         private ClientWebSocket _socket; |         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 IDictionary<string, string> cookies; | ||||||
|         private readonly IDictionary<string, string> headers; |         private readonly IDictionary<string, string> headers; | ||||||
|         private CancellationTokenSource _ctsSource; |         private CancellationTokenSource _ctsSource; | ||||||
|         private bool _closing; |  | ||||||
|         private bool _startedSent; |  | ||||||
|         private bool _startedReceive; |  | ||||||
| 
 | 
 | ||||||
|         private readonly List<DateTime> _outgoingMessages; |         private readonly List<DateTime> _outgoingMessages; | ||||||
|         private DateTime _lastReceivedMessagesUpdate; |         private DateTime _lastReceivedMessagesUpdate; | ||||||
| @ -78,9 +72,6 @@ namespace CryptoExchange.Net.Sockets | |||||||
|         /// <inheritdoc /> |         /// <inheritdoc /> | ||||||
|         public string? Origin { get; set; } |         public string? Origin { get; set; } | ||||||
| 
 | 
 | ||||||
|         /// <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> | ||||||
| @ -97,13 +88,13 @@ namespace CryptoExchange.Net.Sockets | |||||||
|         public Func<string, string>? DataInterpreterString { get; set; } |         public Func<string, string>? DataInterpreterString { get; set; } | ||||||
| 
 | 
 | ||||||
|         /// <inheritdoc /> |         /// <inheritdoc /> | ||||||
|         public string Url { get; } |         public Uri Uri { get; } | ||||||
| 
 | 
 | ||||||
|         /// <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> |         /// <summary> | ||||||
|         /// Ssl protocols supported. NOT USED BY THIS IMPLEMENTATION |         /// Ssl protocols supported. NOT USED BY THIS IMPLEMENTATION | ||||||
| @ -179,8 +170,8 @@ namespace CryptoExchange.Net.Sockets | |||||||
|         /// 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="uri">The uri the socket should connect to</param> | ||||||
|         public CryptoExchangeWebSocketClient(Log log, string url) : this(log, url, new Dictionary<string, string>(), new Dictionary<string, string>()) |         public CryptoExchangeWebSocketClient(Log log, Uri uri) : this(log, uri, new Dictionary<string, string>(), new Dictionary<string, string>()) | ||||||
|         { |         { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -188,14 +179,14 @@ namespace CryptoExchange.Net.Sockets | |||||||
|         /// 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="uri">The uri the socket should connect to</param> | ||||||
|         /// <param name="cookies">Cookies to sent in the socket connection request</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> |         /// <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(); |             Id = NextStreamId(); | ||||||
|             this.log = log; |             this.log = log; | ||||||
|             Url = url; |             Uri = uri; | ||||||
|             this.cookies = cookies; |             this.cookies = cookies; | ||||||
|             this.headers = headers; |             this.headers = headers; | ||||||
| 
 | 
 | ||||||
| @ -212,13 +203,16 @@ namespace CryptoExchange.Net.Sockets | |||||||
|         /// <inheritdoc /> |         /// <inheritdoc /> | ||||||
|         public virtual void SetProxy(ApiProxy proxy) |         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 = uri?.Scheme == null | ||||||
|                 ? _socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port) |                 ? _socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port) | ||||||
|                 : _socket.Options.Proxy = new WebProxy |                 : _socket.Options.Proxy = new WebProxy | ||||||
|                 { |                 { | ||||||
|                     Address = uri |                     Address = uri | ||||||
|                 }; |                 }; | ||||||
|  | 
 | ||||||
|             if (proxy.Login != null) |             if (proxy.Login != null) | ||||||
|                 _socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password); |                 _socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password); | ||||||
|         } |         } | ||||||
| @ -230,7 +224,7 @@ namespace CryptoExchange.Net.Sockets | |||||||
|             try |             try | ||||||
|             { |             { | ||||||
|                 using CancellationTokenSource tcs = new CancellationTokenSource(TimeSpan.FromSeconds(10));                 |                 using CancellationTokenSource tcs = new CancellationTokenSource(TimeSpan.FromSeconds(10));                 | ||||||
|                 await _socket.ConnectAsync(new Uri(Url), tcs.Token).ConfigureAwait(false); |                 await _socket.ConnectAsync(Uri, tcs.Token).ConfigureAwait(false); | ||||||
|                  |                  | ||||||
|                 Handle(openHandlers); |                 Handle(openHandlers); | ||||||
|             } |             } | ||||||
| @ -240,34 +234,24 @@ namespace CryptoExchange.Net.Sockets | |||||||
|                 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(); |  | ||||||
|             _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}"); |  | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         public virtual async Task ProcessAsync() | ||||||
|  |         { | ||||||
|  |             var sendTask = SendLoopAsync(); | ||||||
|  |             var receiveTask = ReceiveLoopAsync(); | ||||||
|  |             var timeoutTask = Timeout != default ? CheckTimeoutAsync() : Task.CompletedTask; | ||||||
|  |             log.Write(LogLevel.Debug, $"Socket {Id} processing started"); | ||||||
|  |             await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         /// <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"); |                 throw new InvalidOperationException($"Socket {Id} Can't send data when socket is not connected"); | ||||||
| 
 | 
 | ||||||
|             var bytes = _encoding.GetBytes(data); |             var bytes = _encoding.GetBytes(data); | ||||||
| @ -280,36 +264,21 @@ namespace CryptoExchange.Net.Sockets | |||||||
|         public virtual async Task CloseAsync() |         public virtual async Task CloseAsync() | ||||||
|         { |         { | ||||||
|             log.Write(LogLevel.Debug, $"Socket {Id} closing"); |             log.Write(LogLevel.Debug, $"Socket {Id} closing"); | ||||||
|             await CloseInternalAsync(true, true).ConfigureAwait(false); |             await CloseInternalAsync().ConfigureAwait(false); | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Internal close method, will wait for each task to complete to gracefully close |         /// Internal close method | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         /// <param name="waitSend"></param> |  | ||||||
|         /// <param name="waitReceive"></param> |  | ||||||
|         /// <returns></returns> |         /// <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(); |             _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); |                 await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false); | ||||||
|  | 
 | ||||||
|             log.Write(LogLevel.Debug, $"Socket {Id} closed"); |             log.Write(LogLevel.Debug, $"Socket {Id} closed"); | ||||||
|             Handle(closeHandlers); |             Handle(closeHandlers); | ||||||
|         } |         } | ||||||
| @ -335,7 +304,6 @@ namespace CryptoExchange.Net.Sockets | |||||||
|         { |         { | ||||||
|             log.Write(LogLevel.Debug, $"Socket {Id} resetting"); |             log.Write(LogLevel.Debug, $"Socket {Id} resetting"); | ||||||
|             _ctsSource = new CancellationTokenSource(); |             _ctsSource = new CancellationTokenSource(); | ||||||
|             _closing = false; |  | ||||||
| 
 | 
 | ||||||
|             while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer |             while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer | ||||||
| 
 | 
 | ||||||
| @ -366,17 +334,16 @@ 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)) | ||||||
| @ -388,7 +355,7 @@ namespace CryptoExchange.Net.Sockets | |||||||
|                             while (MessagesSentLastSecond() >= RatelimitPerSecond) |                             while (MessagesSentLastSecond() >= 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) | ||||||
| @ -410,7 +377,7 @@ namespace CryptoExchange.Net.Sockets | |||||||
|                         { |                         { | ||||||
|                             // Connection closed unexpectedly, .NET framework                       |                             // Connection closed unexpectedly, .NET framework                       | ||||||
|                             Handle(errorHandlers, ioe); |                             Handle(errorHandlers, ioe); | ||||||
|                             _ = Task.Run(async () => await CloseInternalAsync(false, true).ConfigureAwait(false)); |                             await CloseInternalAsync().ConfigureAwait(false); | ||||||
|                             break; |                             break; | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
| @ -427,7 +394,6 @@ namespace CryptoExchange.Net.Sockets | |||||||
|             finally |             finally | ||||||
|             { |             { | ||||||
|                 log.Write(LogLevel.Trace, $"Socket {Id} Send loop finished"); |                 log.Write(LogLevel.Trace, $"Socket {Id} Send loop finished"); | ||||||
|                 _startedSent = false; |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -437,14 +403,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; | ||||||
| @ -468,7 +433,7 @@ namespace CryptoExchange.Net.Sockets | |||||||
|                         { |                         { | ||||||
|                             // Connection closed unexpectedly         |                             // Connection closed unexpectedly         | ||||||
|                             Handle(errorHandlers, wse); |                             Handle(errorHandlers, wse); | ||||||
|                             _ = Task.Run(async () => await CloseInternalAsync(true, true).ConfigureAwait(false)); |                             await CloseInternalAsync().ConfigureAwait(false); | ||||||
|                             break; |                             break; | ||||||
|                         } |                         } | ||||||
| 
 | 
 | ||||||
| @ -476,7 +441,7 @@ namespace CryptoExchange.Net.Sockets | |||||||
|                         { |                         { | ||||||
|                             // 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)); |                             await CloseInternalAsync().ConfigureAwait(false); | ||||||
|                             break; |                             break; | ||||||
|                         } |                         } | ||||||
| 
 | 
 | ||||||
| @ -515,7 +480,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; | ||||||
| @ -547,7 +512,6 @@ namespace CryptoExchange.Net.Sockets | |||||||
|             finally |             finally | ||||||
|             { |             { | ||||||
|                 log.Write(LogLevel.Trace, $"Socket {Id} Receive loop finished"); |                 log.Write(LogLevel.Trace, $"Socket {Id} Receive loop finished"); | ||||||
|                 _startedReceive = false; |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -615,7 +579,7 @@ namespace CryptoExchange.Net.Sockets | |||||||
|             {  |             {  | ||||||
|                 while (true) |                 while (true) | ||||||
|                 { |                 { | ||||||
|                     if (_closing) |                     if (_ctsSource.IsCancellationRequested) | ||||||
|                         return; |                         return; | ||||||
| 
 | 
 | ||||||
|                     if (DateTime.UtcNow - LastActionTime > Timeout) |                     if (DateTime.UtcNow - LastActionTime > Timeout) | ||||||
|  | |||||||
| @ -65,12 +65,16 @@ namespace CryptoExchange.Net.Sockets | |||||||
|         /// <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 underlying websocket | ||||||
|         /// </summary> |         /// </summary> | ||||||
|         public IWebsocket Socket { get; set; } |         private IWebsocket Socket { get; set; } | ||||||
|  |         public int SocketId => Socket.Id; | ||||||
|  | 
 | ||||||
|  |         public double IncomingKbps => Socket.IncomingKbps; | ||||||
|  |         public Uri Uri => Socket.Uri; | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// The API client the connection is for |         /// The API client the connection is for | ||||||
| @ -129,6 +133,7 @@ namespace CryptoExchange.Net.Sockets | |||||||
|         private readonly BaseSocketClient socketClient; |         private readonly BaseSocketClient socketClient; | ||||||
| 
 | 
 | ||||||
|         private readonly List<PendingRequest> pendingRequests; |         private readonly List<PendingRequest> pendingRequests; | ||||||
|  |         private Task _socketProcessReconnectTask; | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// New socket connection |         /// New socket connection | ||||||
| @ -149,10 +154,215 @@ namespace CryptoExchange.Net.Sockets | |||||||
| 
 | 
 | ||||||
|             Socket.Timeout = client.ClientOptions.SocketNoDataTimeout; |             Socket.Timeout = client.ClientOptions.SocketNoDataTimeout; | ||||||
|             Socket.OnMessage += ProcessMessage; |             Socket.OnMessage += ProcessMessage; | ||||||
|             Socket.OnClose += SocketOnClose; |  | ||||||
|             Socket.OnOpen += SocketOnOpen; |             Socket.OnOpen += SocketOnOpen; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|  |         public async Task<bool> ConnectAsync() | ||||||
|  |         { | ||||||
|  |             var connected = await Socket.ConnectAsync().ConfigureAwait(false); | ||||||
|  |             if (connected) | ||||||
|  |                 StartProcessingTask(); | ||||||
|  | 
 | ||||||
|  |             return connected; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         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(Socket.Id)) | ||||||
|  |                 socketClient.socketConnections.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(); | ||||||
|  |             await _socketProcessReconnectTask.ConfigureAwait(false); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /// <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); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private void StartProcessingTask() | ||||||
|  |         { | ||||||
|  |             _socketProcessReconnectTask = Task.Run(async () => | ||||||
|  |             { | ||||||
|  |                 await Socket.ProcessAsync().ConfigureAwait(false); | ||||||
|  |                 await ReconnectAsync().ConfigureAwait(false); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         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 {Socket.Id} Connection lost, will try to reconnect"); | ||||||
|  |                 if (!lostTriggered) | ||||||
|  |                 { | ||||||
|  |                     lostTriggered = true; | ||||||
|  |                     ConnectionLost?.Invoke(); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 while (ShouldReconnect) | ||||||
|  |                 { | ||||||
|  |                     if (ReconnectTry > 0) | ||||||
|  |                     { | ||||||
|  |                         // Wait a bit before attempting reconnect | ||||||
|  |                         await Task.Delay(socketClient.ClientOptions.ReconnectInterval).ConfigureAwait(false); | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     if (!ShouldReconnect) | ||||||
|  |                     { | ||||||
|  |                         // Should reconnect changed to false while waiting to reconnect | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     Socket.Reset(); | ||||||
|  |                     if (!await Socket.ConnectAsync().ConfigureAwait(false)) | ||||||
|  |                     { | ||||||
|  |                         // Reconnect failed | ||||||
|  |                         ReconnectTry++; | ||||||
|  |                         ResubscribeTry = 0; | ||||||
|  |                         if (socketClient.ClientOptions.MaxReconnectTries != null | ||||||
|  |                         && ReconnectTry >= socketClient.ClientOptions.MaxReconnectTries) | ||||||
|  |                         { | ||||||
|  |                             log.Write(LogLevel.Warning, $"Socket {Socket.Id} failed to reconnect after {ReconnectTry} tries, closing"); | ||||||
|  |                             ShouldReconnect = false; | ||||||
|  | 
 | ||||||
|  |                             if (socketClient.socketConnections.ContainsKey(Socket.Id)) | ||||||
|  |                                 socketClient.socketConnections.TryRemove(Socket.Id, out _); | ||||||
|  | 
 | ||||||
|  |                             _ = Task.Run(() => ConnectionClosed?.Invoke()); | ||||||
|  |                             // Reached max tries, break loop and leave connection closed | ||||||
|  |                             break; | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         // Continue to try again | ||||||
|  |                         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, start processing | ||||||
|  |                     StartProcessingTask(); | ||||||
|  | 
 | ||||||
|  |                     ReconnectTry = 0; | ||||||
|  |                     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) | ||||||
|  |                     { | ||||||
|  |                         // Failed to resubscribe everything | ||||||
|  |                         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.socketConnections.ContainsKey(Socket.Id)) | ||||||
|  |                                 socketClient.socketConnections.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."); | ||||||
|  | 
 | ||||||
|  |                         // 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 {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; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                 if (!socketClient.ClientOptions.AutoReconnect && ShouldReconnect) | ||||||
|  |                     _ = Task.Run(() => ConnectionClosed?.Invoke()); | ||||||
|  | 
 | ||||||
|  |                 // No reconnecting needed | ||||||
|  |                 log.Write(LogLevel.Information, $"Socket {Socket.Id} closed"); | ||||||
|  |                 if (socketClient.socketConnections.ContainsKey(Socket.Id)) | ||||||
|  |                     socketClient.socketConnections.TryRemove(Socket.Id, out _); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public void Dispose() | ||||||
|  |         { | ||||||
|  |             Socket.Dispose(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
|         /// Process a message received by the socket |         /// Process a message received by the socket | ||||||
|         /// </summary> |         /// </summary> | ||||||
| @ -202,12 +412,20 @@ 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) = 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 {Socket.Id} Message not handled: " + tokenData); | ||||||
|                 UnhandledMessage?.Invoke(tokenData); |                 UnhandledMessage?.Invoke(tokenData); | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|  |             var total = DateTime.UtcNow - timestamp; | ||||||
|  |             if (userProcessTime.TotalMilliseconds > 500) | ||||||
|  |                 log.Write(LogLevel.Debug, $"Socket {Socket.Id} message processing slow ({(int)total.TotalMilliseconds}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."); | ||||||
|  |              | ||||||
|  |             log.Write(LogLevel.Trace, $"Socket {Socket.Id} message processed in {(int)total.TotalMilliseconds}ms, ({(int)userProcessTime.TotalMilliseconds}ms user code)"); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /// <summary> |         /// <summary> | ||||||
| @ -246,13 +464,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) 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 +485,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 +497,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); | ||||||
|                 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 {Socket.Id} Exception during message processing\r\nException: {ex.ToLogString()}\r\nData: {messageEvent.JsonData}"); | ||||||
|                 currentSubscription?.InvokeExceptionHandler(ex); |                 currentSubscription?.InvokeExceptionHandler(ex); | ||||||
|                 return false; |                 return (false, TimeSpan.Zero); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -347,135 +565,6 @@ namespace CryptoExchange.Net.Sockets | |||||||
|         { |         { | ||||||
|             ReconnectTry = 0; |             ReconnectTry = 0; | ||||||
|             PausedActivity = false; |             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() |         private async Task<CallResult<bool>> ProcessReconnectAsync() | ||||||
| @ -535,56 +624,6 @@ namespace CryptoExchange.Net.Sockets | |||||||
| 
 | 
 | ||||||
|             return await socketClient.SubscribeAndWaitAsync(this, socketSubscription.Request!, socketSubscription).ConfigureAwait(false); |             return await socketClient.SubscribeAndWaitAsync(this, socketSubscription.Request!, socketSubscription).ConfigureAwait(false); | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         /// <summary> |  | ||||||
|         /// 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> |  | ||||||
|         /// 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 |     internal class PendingRequest | ||||||
|  | |||||||
| @ -72,7 +72,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 +103,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> | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| using System.Collections.Generic; | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
| using CryptoExchange.Net.Interfaces; | using CryptoExchange.Net.Interfaces; | ||||||
| using CryptoExchange.Net.Logging; | using CryptoExchange.Net.Logging; | ||||||
| 
 | 
 | ||||||
| @ -12,13 +13,13 @@ namespace CryptoExchange.Net.Sockets | |||||||
|         /// <inheritdoc /> |         /// <inheritdoc /> | ||||||
|         public IWebsocket CreateWebsocket(Log log, string url) |         public IWebsocket CreateWebsocket(Log log, string url) | ||||||
|         { |         { | ||||||
|             return new CryptoExchangeWebSocketClient(log, url); |             return new CryptoExchangeWebSocketClient(log, new Uri(url)); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /// <inheritdoc /> |         /// <inheritdoc /> | ||||||
|         public IWebsocket CreateWebsocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers) |         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); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user