using CryptoExchange.Net.Interfaces; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using CryptoExchange.Net.Objects; using System.Net.WebSockets; using CryptoExchange.Net.Objects.Sockets; using System.Diagnostics; using CryptoExchange.Net.Clients; using CryptoExchange.Net.Logging.Extensions; using System.Threading; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Authentication; namespace CryptoExchange.Net.Sockets { /// /// A single socket connection to the server /// public class SocketConnection { /// /// State of a the connection /// /// The id of the socket connection /// The connection URI /// Number of subscriptions on this socket /// Socket status /// If the connection is authenticated /// Download speed over this socket /// Number of non-completed queries /// State for each subscription on this socket public record SocketConnectionState( int Id, string Address, int Subscriptions, SocketStatus Status, bool Authenticated, double DownloadSpeed, int PendingQueries, List SubscriptionStates ); /// /// Connection lost event /// public event Action? ConnectionLost; /// /// Connection closed and no reconnect is happening /// public event Action? ConnectionClosed; /// /// Failed to resubscribe all subscription on the reconnected socket /// public event Action? ResubscribingFailed; /// /// Connecting restored event /// public event Action? ConnectionRestored; /// /// The connection is paused event /// public event Action? ActivityPaused; /// /// The connection is unpaused event /// public event Action? ActivityUnpaused; /// /// Unhandled message event /// public event Action? UnhandledMessage; /// /// Connection was rate limited and couldn't be established /// public Func? ConnectRateLimitedAsync; /// /// The amount of subscriptions on this connection /// public int UserSubscriptionCount { get { lock(_listenersLock) return _listeners.OfType().Count(h => h.UserSubscription); } } /// /// Get a copy of the current message subscriptions /// public Subscription[] Subscriptions { get { lock(_listenersLock) return _listeners.OfType().Where(h => h.UserSubscription).ToArray(); } } /// /// If the connection has been authenticated /// public bool Authenticated { get; set; } /// /// If connection is made /// public bool Connected => _socket.IsOpen; /// /// The unique ID of the socket /// public int SocketId => _socket.Id; /// /// The current kilobytes per second of data being received, averaged over the last 3 seconds /// public double IncomingKbps => _socket.IncomingKbps; /// /// The connection uri /// public Uri ConnectionUri => _socket.Uri; /// /// The API client the connection is for /// public SocketApiClient ApiClient { get; set; } /// /// Time of disconnecting /// public DateTime? DisconnectTime { get; set; } /// /// Tag for identification /// public string Tag { get; set; } /// /// Additional properties for this connection /// public Dictionary Properties { get; set; } /// /// If activity is paused /// public bool PausedActivity { get => _pausedActivity; set { if (_pausedActivity != value) { _pausedActivity = value; _logger.ActivityPaused(SocketId, value); if(_pausedActivity) _ = Task.Run(() => ActivityPaused?.Invoke()); else _ = Task.Run(() => ActivityUnpaused?.Invoke()); } } } /// /// Status of the socket connection /// public SocketStatus Status { get => _status; private set { if (_status == value) return; var oldStatus = _status; _status = value; _logger.SocketStatusChanged(SocketId, oldStatus, value); } } /// /// Info on whether this connection is a dedicated request connection /// public DedicatedConnectionState DedicatedRequestConnection { get; internal set; } = new DedicatedConnectionState(); /// /// Current subscription topics on this connection /// public string[] Topics { get { lock (_listenersLock) return _listeners.OfType().Select(x => x.Topic).Where(t => t != null).ToArray()!; } } private bool _pausedActivity; private readonly object _listenersLock; private readonly List _listeners; private readonly ILogger _logger; private SocketStatus _status; private readonly IMessageSerializer _serializer; private readonly IByteMessageAccessor _accessor; /// /// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similar. Not necessary. /// protected Task? periodicTask; /// /// Wait event for the periodicTask /// protected AsyncResetEvent? periodicEvent; /// /// The underlying websocket /// private readonly IWebsocket _socket; /// /// New socket connection /// /// The logger /// The api client /// The socket /// public SocketConnection(ILogger logger, SocketApiClient apiClient, IWebsocket socket, string tag) { _logger = logger; ApiClient = apiClient; Tag = tag; Properties = new Dictionary(); _socket = socket; _socket.OnStreamMessage += HandleStreamMessage; _socket.OnRequestSent += HandleRequestSentAsync; _socket.OnRequestRateLimited += HandleRequestRateLimitedAsync; _socket.OnConnectRateLimited += HandleConnectRateLimitedAsync; _socket.OnOpen += HandleOpenAsync; _socket.OnClose += HandleCloseAsync; _socket.OnReconnecting += HandleReconnectingAsync; _socket.OnReconnected += HandleReconnectedAsync; _socket.OnError += HandleErrorAsync; _socket.GetReconnectionUrl = GetReconnectionUrlAsync; _listenersLock = new object(); _listeners = new List(); _serializer = apiClient.CreateSerializer(); _accessor = apiClient.CreateAccessor(); } /// /// Handler for a socket opening /// protected virtual Task HandleOpenAsync() { Status = SocketStatus.Connected; PausedActivity = false; return Task.CompletedTask; } /// /// Handler for a socket closing without reconnect /// protected virtual Task HandleCloseAsync() { Status = SocketStatus.Closed; Authenticated = false; lock (_listenersLock) { foreach (var subscription in _listeners.OfType().Where(l => l.UserSubscription)) subscription.Reset(); foreach (var query in _listeners.OfType().ToList()) { query.Fail(new WebError("Connection interrupted")); _listeners.Remove(query); } } _ = Task.Run(() => ConnectionClosed?.Invoke()); return Task.CompletedTask; } /// /// Handler for a socket losing connection and starting reconnect /// protected virtual Task HandleReconnectingAsync() { Status = SocketStatus.Reconnecting; DisconnectTime = DateTime.UtcNow; Authenticated = false; lock (_listenersLock) { foreach (var subscription in _listeners.OfType().Where(l => l.UserSubscription)) subscription.Reset(); foreach (var query in _listeners.OfType().ToList()) { query.Fail(new WebError("Connection interrupted")); _listeners.Remove(query); } } _ = Task.Run(() => ConnectionLost?.Invoke()); return Task.CompletedTask; } /// /// Get the url to connect to when reconnecting /// /// protected virtual async Task GetReconnectionUrlAsync() { return await ApiClient.GetReconnectUriAsync(this).ConfigureAwait(false); } /// /// Handler for a socket which has reconnected /// protected virtual Task HandleReconnectedAsync() { Status = SocketStatus.Resubscribing; lock (_listenersLock) { foreach (var query in _listeners.OfType().ToList()) { query.Fail(new WebError("Connection interrupted")); _listeners.Remove(query); } } // Can't wait for this as it would cause a deadlock _ = Task.Run(async () => { try { var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false); if (!reconnectSuccessful) { _logger.FailedReconnectProcessing(SocketId, reconnectSuccessful.Error!.ToString()); _ = Task.Run(() => ResubscribingFailed?.Invoke(reconnectSuccessful.Error)); _ = _socket.ReconnectAsync().ConfigureAwait(false); } else { Status = SocketStatus.Connected; _ = Task.Run(() => { ConnectionRestored?.Invoke(DateTime.UtcNow - DisconnectTime!.Value); DisconnectTime = null; }); } } catch(Exception ex) { _logger.UnknownExceptionWhileProcessingReconnection(SocketId, ex); _ = _socket.ReconnectAsync().ConfigureAwait(false); } }); return Task.CompletedTask; } /// /// Handler for an error on a websocket /// /// The exception protected virtual Task HandleErrorAsync(Exception e) { if (e is WebSocketException wse) _logger.WebSocketErrorCodeAndDetails(SocketId, wse.WebSocketErrorCode, wse.Message, wse); else _logger.WebSocketError(SocketId, e.Message, e); return Task.CompletedTask; } /// /// Handler for whenever a request is rate limited and rate limit behavior is set to fail /// /// /// protected virtual Task HandleRequestRateLimitedAsync(int requestId) { Query? query; lock (_listenersLock) { query = _listeners.OfType().FirstOrDefault(x => x.Id == requestId); } if (query == null) return Task.CompletedTask; query.Fail(new ClientRateLimitError("Connection rate limit reached")); return Task.CompletedTask; } /// /// Handler for whenever a connection was rate limited and couldn't be established /// /// protected async virtual Task HandleConnectRateLimitedAsync() { if (ConnectRateLimitedAsync is not null) await ConnectRateLimitedAsync().ConfigureAwait(false); } /// /// Handler for whenever a request is sent over the websocket /// /// Id of the request sent protected virtual Task HandleRequestSentAsync(int requestId) { Query? query; lock (_listenersLock) { query = _listeners.OfType().FirstOrDefault(x => x.Id == requestId); } if (query == null) { _logger.MessageSentNotPending(SocketId, requestId); return Task.CompletedTask; } query.IsSend(query.RequestTimeout ?? ApiClient.ClientOptions.RequestTimeout); return Task.CompletedTask; } /// /// Handle a message /// /// /// /// protected virtual async Task HandleStreamMessage(WebSocketMessageType type, ReadOnlyMemory data) { var sw = Stopwatch.StartNew(); var receiveTime = DateTime.UtcNow; string? originalData = null; // 1. Decrypt/Preprocess if necessary data = ApiClient.PreprocessStreamMessage(this, type, data); // 2. Read data into accessor _accessor.Read(data); try { bool outputOriginalData = ApiClient.ApiOptions.OutputOriginalData ?? ApiClient.ClientOptions.OutputOriginalData; if (outputOriginalData) { originalData = _accessor.GetOriginalString(); _logger.ReceivedData(SocketId, originalData); } // 3. Determine the identifying properties of this message var listenId = ApiClient.GetListenerIdentifier(_accessor); if (listenId == null) { originalData = outputOriginalData ? _accessor.GetOriginalString() : "[OutputOriginalData is false]"; if (!ApiClient.UnhandledMessageExpected) _logger.FailedToEvaluateMessage(SocketId, originalData); UnhandledMessage?.Invoke(_accessor); return; } // 4. Get the listeners interested in this message List processors; lock (_listenersLock) processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId)).ToList(); if (processors.Count == 0) { if (!ApiClient.UnhandledMessageExpected) { List listenerIds; lock (_listenersLock) listenerIds = _listeners.SelectMany(l => l.ListenerIdentifiers).ToList(); _logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds)); UnhandledMessage?.Invoke(_accessor); } return; } _logger.ProcessorMatched(SocketId, processors.Count, listenId); var totalUserTime = 0; Dictionary? desCache = null; if (processors.Count > 1) { // Only instantiate a cache if there are multiple processors desCache = new Dictionary(); } foreach (var processor in processors) { // 5. Determine the type to deserialize to for this processor var messageType = processor.GetMessageType(_accessor); if (messageType == null) { _logger.ReceivedMessageNotRecognized(SocketId, processor.Id); continue; } if (processor is Subscription subscriptionProcessor && !subscriptionProcessor.Confirmed) { // If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed subscriptionProcessor.Confirmed = true; // This doesn't trigger a waiting subscribe query, should probably also somehow set the wait event for that } // 6. Deserialize the message object? deserialized = null; desCache?.TryGetValue(messageType, out deserialized); if (deserialized == null) { var desResult = processor.Deserialize(_accessor, messageType); if (!desResult) { _logger.FailedToDeserializeMessage(SocketId, desResult.Error?.ToString(), desResult.Error?.Exception); continue; } deserialized = desResult.Data; desCache?.Add(messageType, deserialized); } // 7. Hand of the message to the subscription try { var innerSw = Stopwatch.StartNew(); await processor.Handle(this, new DataEvent(deserialized, null, null, originalData, receiveTime, null)).ConfigureAwait(false); if (processor is Query query && query.RequiredResponses != 1) _logger.LogDebug($"[Sckt {SocketId}] [Req {query.Id}] responses: {query.CurrentResponses}/{query.RequiredResponses}"); totalUserTime += (int)innerSw.ElapsedMilliseconds; } catch (Exception ex) { _logger.UserMessageProcessingFailed(SocketId, ex.Message, ex); if (processor is Subscription subscription) subscription.InvokeExceptionHandler(ex); } } _logger.MessageProcessed(SocketId, sw.ElapsedMilliseconds, sw.ElapsedMilliseconds - totalUserTime); } finally { _accessor.Clear(); } } /// /// Connect the websocket /// /// public async Task ConnectAsync(CancellationToken ct) => await _socket.ConnectAsync(ct).ConfigureAwait(false); /// /// Retrieve the underlying socket /// /// public IWebsocket GetSocket() => _socket; /// /// Trigger a reconnect of the socket connection /// /// public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false); /// /// Update the proxy setting and reconnect /// /// New proxy setting public async Task UpdateProxy(ApiProxy? proxy) { _socket.UpdateProxy(proxy); await TriggerReconnectAsync().ConfigureAwait(false); } /// /// Close the connection /// /// public async Task CloseAsync() { if (Status == SocketStatus.Closed || Status == SocketStatus.Disposed) return; if (ApiClient.socketConnections.ContainsKey(SocketId)) ApiClient.socketConnections.TryRemove(SocketId, out _); lock (_listenersLock) { foreach (var subscription in _listeners.OfType()) { if (subscription.CancellationTokenRegistration.HasValue) subscription.CancellationTokenRegistration.Value.Dispose(); } } await _socket.CloseAsync().ConfigureAwait(false); _socket.Dispose(); } /// /// Close a subscription on this connection. If all subscriptions on this connection are closed the connection gets closed as well /// /// Subscription to close /// public async Task CloseAsync(Subscription subscription) { // If we are resubscribing this subscription at this moment we'll want to wait for a bit until it is finished to avoid concurrency issues while (subscription.IsResubscribing) await Task.Delay(50).ConfigureAwait(false); subscription.Closed = true; if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed) return; _logger.ClosingSubscription(SocketId, subscription.Id); if (subscription.CancellationTokenRegistration.HasValue) subscription.CancellationTokenRegistration.Value.Dispose(); bool anyDuplicateSubscription; lock (_listenersLock) anyDuplicateSubscription = _listeners.OfType().Any(x => x != subscription && x.ListenerIdentifiers.All(l => subscription.ListenerIdentifiers.Contains(l))); bool shouldCloseConnection; lock (_listenersLock) shouldCloseConnection = _listeners.OfType().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection; if (!anyDuplicateSubscription) { bool needUnsub; lock (_listenersLock) needUnsub = _listeners.Contains(subscription) && !shouldCloseConnection; if (needUnsub && _socket.IsOpen) await UnsubscribeAsync(subscription).ConfigureAwait(false); } else { _logger.NotUnsubscribingSubscriptionBecauseDuplicateRunning(SocketId); } if (Status == SocketStatus.Closing) { _logger.AlreadyClosing(SocketId); return; } if (shouldCloseConnection) { Status = SocketStatus.Closing; _logger.ClosingNoMoreSubscriptions(SocketId); await CloseAsync().ConfigureAwait(false); } lock (_listenersLock) _listeners.Remove(subscription); } /// /// Dispose the connection /// public void Dispose() { Status = SocketStatus.Disposed; periodicEvent?.Set(); periodicEvent?.Dispose(); _socket.Dispose(); } /// /// Whether or not a new subscription can be added to this connection /// /// public bool CanAddSubscription() => Status == SocketStatus.None || Status == SocketStatus.Connected; /// /// Add a subscription to this connection /// /// public bool AddSubscription(Subscription subscription) { if (Status != SocketStatus.None && Status != SocketStatus.Connected) return false; lock (_listenersLock) _listeners.Add(subscription); if (subscription.UserSubscription) _logger.AddingNewSubscription(SocketId, subscription.Id, UserSubscriptionCount); return true; } /// /// Get a subscription on this connection by id /// /// public Subscription? GetSubscription(int id) { lock (_listenersLock) return _listeners.OfType().SingleOrDefault(s => s.Id == id); } /// /// Get the state of the connection /// /// public SocketConnectionState GetState(bool includeSubDetails) { return new SocketConnectionState( SocketId, ConnectionUri.AbsoluteUri, UserSubscriptionCount, Status, Authenticated, IncomingKbps, PendingQueries: _listeners.OfType().Count(x => !x.Completed), includeSubDetails ? Subscriptions.Select(sub => sub.GetState()).ToList() : new List() ); } /// /// Send a query request and wait for an answer /// /// Query to send /// Wait event for when the socket message handler can continue /// Cancellation token /// public virtual async Task SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default) { await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false); return query.Result ?? new CallResult(new ServerError("Timeout")); } /// /// Send a query request and wait for an answer /// /// Expected result type /// The type returned to the caller /// Query to send /// Wait event for when the socket message handler can continue /// Cancellation token /// public virtual async Task> SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default) { await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false); return query.TypedResult ?? new CallResult(new ServerError("Timeout")); } private async Task SendAndWaitIntAsync(Query query, AsyncResetEvent? continueEvent, CancellationToken ct = default) { lock(_listenersLock) _listeners.Add(query); query.ContinueAwaiter = continueEvent; var sendResult = Send(query.Id, query.Request, query.Weight); if (!sendResult) { query.Fail(sendResult.Error!); lock (_listenersLock) _listeners.Remove(query); return; } try { while (!ct.IsCancellationRequested) { if (!_socket.IsOpen) { query.Fail(new WebError("Socket not open")); return; } if (query.Completed) return; await query.WaitAsync(TimeSpan.FromMilliseconds(500), ct).ConfigureAwait(false); if (query.Completed) return; } if (ct.IsCancellationRequested) { query.Fail(new CancellationRequestedError()); return; } } finally { lock (_listenersLock) _listeners.Remove(query); } } /// /// Send data over the websocket connection /// /// The type of the object to send /// The request id /// The object to send /// The weight of the message public virtual CallResult Send(int requestId, T obj, int weight) { var data = obj is string str ? str : _serializer.Serialize(obj!); return Send(requestId, data, weight); } /// /// Send string data over the websocket connection /// /// The data to send /// The weight of the message /// The id of the request public virtual CallResult Send(int requestId, string data, int weight) { if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value) { var info = $"Message to send exceeds the max server message size ({ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit"; _logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info); return new CallResult(new InvalidOperationError(info)); } if (!_socket.IsOpen) { _logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] failed to send, socket no longer open", SocketId, requestId); return new CallResult(new WebError("Failed to send message, socket no longer open")); } _logger.SendingData(SocketId, requestId, data); try { if (!_socket.Send(requestId, data, weight)) return new CallResult(new WebError("Failed to send message, connection not open")); return CallResult.SuccessResult; } catch(Exception ex) { return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex)); } } private async Task ProcessReconnectAsync() { if (!_socket.IsOpen) return new CallResult(new WebError("Socket not connected")); if (!DedicatedRequestConnection.IsDedicatedRequestConnection) { bool anySubscriptions; lock (_listenersLock) anySubscriptions = _listeners.OfType().Any(s => s.UserSubscription); if (!anySubscriptions) { // No need to resubscribe anything _logger.NothingToResubscribeCloseConnection(SocketId); _ = _socket.CloseAsync(); return CallResult.SuccessResult; } } bool anyAuthenticated; lock (_listenersLock) { anyAuthenticated = _listeners.OfType().Any(s => s.Authenticated) || (DedicatedRequestConnection.IsDedicatedRequestConnection && DedicatedRequestConnection.Authenticated); } if (anyAuthenticated) { // If we reconnected a authenticated connection we need to re-authenticate var authResult = await ApiClient.AuthenticateSocketAsync(this).ConfigureAwait(false); if (!authResult) { _logger.FailedAuthenticationDisconnectAndRecoonect(SocketId); return authResult; } Authenticated = true; _logger.AuthenticationSucceeded(SocketId); } // Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe int batch = 0; int batchSize = ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket; while (true) { if (!_socket.IsOpen) return new CallResult(new WebError("Socket not connected")); List subList; lock (_listenersLock) subList = _listeners.OfType().Where(x => !x.Closed).Skip(batch * batchSize).Take(batchSize).ToList(); if (subList.Count == 0) break; var taskList = new List>(); foreach (var subscription in subList) { subscription.ConnectionInvocations = 0; if (subscription.Closed) // Can be closed during resubscribing continue; subscription.IsResubscribing = true; var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false); if (!result) { _logger.FailedRequestRevitalization(SocketId, result.Error?.ToString()); subscription.IsResubscribing = false; return result; } var subQuery = subscription.GetSubQuery(this); if (subQuery == null) { subscription.IsResubscribing = false; continue; } var waitEvent = new AsyncResetEvent(false); taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) => { subscription.IsResubscribing = false; subscription.HandleSubQueryResponse(subQuery.Response!); waitEvent.Set(); if (r.Result.Success) subscription.Confirmed = true; return r.Result; })); } await Task.WhenAll(taskList).ConfigureAwait(false); if (taskList.Any(t => !t.Result.Success)) return taskList.First(t => !t.Result.Success).Result; batch++; } if (!_socket.IsOpen) return new CallResult(new WebError("Socket not connected")); _logger.AllSubscriptionResubscribed(SocketId); return CallResult.SuccessResult; } internal async Task UnsubscribeAsync(Subscription subscription) { var unsubscribeRequest = subscription.GetUnsubQuery(); if (unsubscribeRequest == null) return; await SendAndWaitQueryAsync(unsubscribeRequest).ConfigureAwait(false); _logger.SubscriptionUnsubscribed(SocketId, subscription.Id); } internal async Task ResubscribeAsync(Subscription subscription) { if (!_socket.IsOpen) return new CallResult(new WebError("Socket is not connected")); var subQuery = subscription.GetSubQuery(this); if (subQuery == null) return CallResult.SuccessResult; var result = await SendAndWaitQueryAsync(subQuery).ConfigureAwait(false); subscription.HandleSubQueryResponse(subQuery.Response!); return result; } /// /// Periodically sends data over a socket connection /// /// Identifier for the periodic send /// How often /// Method returning the query to send /// The callback for processing the response public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) { if (queryDelegate == null) throw new ArgumentNullException(nameof(queryDelegate)); periodicEvent = new AsyncResetEvent(); periodicTask = Task.Run(async () => { while (Status != SocketStatus.Disposed && Status != SocketStatus.Closed && Status != SocketStatus.Closing) { await periodicEvent.WaitAsync(interval).ConfigureAwait(false); if (Status == SocketStatus.Disposed || Status == SocketStatus.Closed || Status == SocketStatus.Closing) { break; } if (!Connected) continue; var query = queryDelegate(this); if (query == null) continue; _logger.SendingPeriodic(SocketId, identifier); try { var result = await SendAndWaitQueryAsync(query).ConfigureAwait(false); callback?.Invoke(this, result); } catch (Exception ex) { _logger.PeriodicSendFailed(SocketId, identifier, ex.Message, ex); } } }); } /// /// Status of the socket connection /// public enum SocketStatus { /// /// None/Initial /// None, /// /// Connected /// Connected, /// /// Reconnecting /// Reconnecting, /// /// Resubscribing on reconnected socket /// Resubscribing, /// /// Closing /// Closing, /// /// Closed /// Closed, /// /// Disposed /// Disposed } } }