// Does all the real work of making a connection. Currently, this blocks on the initial connection. // I'd rather there be a cleaner interface for this, where the Task itself is being polled and the state changes over when it's done. private async Task DoConnection() { if (_status != Status.ReadyToConnect) { throw new Exception("Not in status=ReadyToConnect."); } _lastErrorMsg = string.Empty; Uri uri = new Uri(_connectUrl); // I think this can throw exceptions for bad formatting? // Creates a websocket connection and lets you start sending or receiving messages on separate threads. ClientWebSocket wsClient = null; try { wsClient = new ClientWebSocket(); using (CancellationTokenSource connectTimeout = new CancellationTokenSource(_connectTimeoutMS)) { // Apply all the headers that were passed in. foreach (KeyValuePair <string, string> kvp in _connectHeaders) { wsClient.Options.SetRequestHeader(kvp.Key, kvp.Value); } _status = Status.Connecting; await wsClient.ConnectAsync(uri, connectTimeout.Token).ConfigureAwait(false); } _status = Status.Connected; _rgws = new RGWebSocket(OnRecvTextMsg, OnRecvBinaryMsg, OnDisconnect, _logCb, uri.ToString(), wsClient); _logCb?.Invoke($"UWS Connected to {_connectUrl}", 1); } catch (AggregateException age) { if (age.InnerException is OperationCanceledException) { _lastErrorMsg = "Connection timed out."; _logCb?.Invoke(_lastErrorMsg, 0); } else if (age.InnerException is WebSocketException) { _lastErrorMsg = ((WebSocketException)age.InnerException).Message; _logCb?.Invoke(_lastErrorMsg, 0); } else { _lastErrorMsg = age.Message; _logCb?.Invoke(_lastErrorMsg, 0); } wsClient?.Dispose(); // cleanup _status = Status.Disconnected; } catch (Exception e) { _lastErrorMsg = e.Message; _logCb?.Invoke(_lastErrorMsg, 0); wsClient?.Dispose(); // cleanup _status = Status.Disconnected; } }
//------------------- // Privates. These calls occur on non-main-threads, so messages get queued up and you POLL them out in the Receive call above on the main thread. private Task OnReceiveText(RGWebSocket rgws, string msg) { _incomingMessages.Add(new wsMessage() { stringMsg = msg, binMsg = null }); _logger($"UWS Recv {msg.Length} bytes txt", 3); return(Task.CompletedTask); }
// This callback holds the reference to PooledArray, so it must be decremented to free it (eventually) after it's consumed. private Task OnReceiveBinary(RGWebSocket rgws, PooledArray msg) { msg.IncRef(); // bump the refcount since we aren't done with it yet, and RGWebSocket can decrement it without freeing the buffer _incomingMessages.Add(new wsMessage() { stringMsg = string.Empty, binMsg = msg }); _logger($"UWS Recv {msg.Length} bytes bin", 3); return(Task.CompletedTask); }
// We capture the callback so we can manage the websocket set internally. private void OnDisconnection(RGWebSocket rgws) { RGWebSocket ws; if (_websockets.TryRemove(rgws._uniqueId, out ws)) { _disconnected.TryAdd(rgws._uniqueId, rgws); _disconnectionCount.Release(); _onDisconnect(rgws); // let the caller know it's disconnected now } }
// A simple blocking way to make sure this is all torn down. public void Shutdown() { if (_rgws != null) { _rgws.Shutdown(); _rgws.Dispose(); _rgws = null; } _logger("UWS shutdown.", 1); _status = Status.ReadyToConnect; }
// Add this websocket to the list of those we need to remove and unblock the cleanup thread private void OnDisconnection(RGWebSocket rgws) { _logger($"{rgws._displayId} OnDisconnection call.", 3); _disconnected.Add(rgws); // finally, put it on the list of things to be disposed of _cleanupSocket.Set(); // Let the Cleanup process know there's something to do }
//------------------- // Task: when a connection is requested, depending on whether it's an HTTP request or WebSocket request, do different things. private async Task HandleConnection(HttpListenerContext httpContext) { // Allow debugging to actually happen, where you have unlimited time to check things without breaking a connection. -1 means don't cancel over time. int timeoutMS = Debugger.IsAttached ? -1 : _connectionMS; if (httpContext.Request.IsWebSocketRequest) { // Kick off an async task to upgrade the web socket and do send/recv messaging, but fail if it takes more than a second to finish. try { _logger("WebSocketServer.HandleConnection - websocket detected. Upgrading connection.", 2); using (CancellationTokenSource upgradeTimeout = new CancellationTokenSource(timeoutMS)) { HttpListenerWebSocketContext webSocketContext = await Task.Run(async() => { return(await httpContext.AcceptWebSocketAsync(null, _idleSeconds).ConfigureAwait(false)); }, upgradeTimeout.Token); _logger("WebSocketServer.HandleConnection - websocket detected. Upgraded.", 3); // Note, we hook our own OnDisconnect before proxying it on to the ConnectionManager. Note, due to heavy congestion and C# scheduling, it's entirely possible that this is already a closed socket, and is immediately flagged for destruction. RGWebSocket rgws = new RGWebSocket(httpContext, _connectionManager.OnReceiveText, _connectionManager.OnReceiveBinary, OnDisconnection, _logger, httpContext.Request.RemoteEndPoint.ToString(), webSocketContext.WebSocket); _websockets.TryAdd(rgws._uniqueId, rgws); await _connectionManager.OnConnection(rgws).ConfigureAwait(false); _logger($"WebSocketServer.HandleConnection - websocket detected. Upgrade completed. {rgws._displayId}", 3); } } catch (OperationCanceledException ex) // timeout { _logger($"WebSocketServer.HandleConnection - websocket upgrade timeout {ex.Message}", 0); httpContext.Response.StatusCode = 500; httpContext.Response.Close(); // this breaks the connection, otherwise it may linger forever } catch (Exception ex) // anything else { _logger($"WebSocketServer.HandleConnection - websocket upgrade exception {ex.Message}", 0); httpContext.Response.StatusCode = 500; httpContext.Response.Close(); // this breaks the connection, otherwise it may linger forever } } else // let the application specify what the HTTP response is, but we do the async write here to free up the app to do other things { try { _logger($"WebSocketServer.HandleConnection - normal http request {httpContext.Request.RawUrl}", 3); using (CancellationTokenSource responseTimeout = new CancellationTokenSource(timeoutMS)) { // Remember to set httpContext.Response.StatusCode, httpContext.Response.ContentLength64, and httpContenxtResponse.OutputStream await _httpRequestCallback(httpContext).ConfigureAwait(false); } } catch (OperationCanceledException ex) // timeout { _logger($"WebSocketServer.HandleConnection - websocket upgrade timeout {ex.Message}", 0); httpContext.Response.StatusCode = 500; // httpContext.Response.Abort(); // this breaks the connection, otherwise it may linger forever } catch (Exception ex) // anything else { _logger($"WebSocketServer.HandleConnection - http callback handler exception {ex.Message}", 0); httpContext.Response.StatusCode = 500; // httpContext.Response.Abort(); // this breaks the connection, otherwise it may linger forever } finally { httpContext.Response.Close(); // This frees all the memory associated with this connection. } } }
// Forcibly disposes the RGWS public void Dispose() { _rgws?.Dispose(); _rgws = null; }
// At this point, it's a done deal. Both Recv and Send are completed, nothing to synchronize. This is called by Send after Recv is finished. private void OnDisconnect(RGWebSocket rgws) { _logCb?.Invoke("UWS Disconnected.", 1); _status = Status.Disconnected; }
private void OnRecvBinaryMsg(RGWebSocket rgws, byte[] msg) { _incomingMessages.Enqueue(new Tuple <string, byte[]>(string.Empty, msg)); _logCb?.Invoke($"UWS Recv {msg.Length} bytes bin", 2); }
//------------------- // Privates. These calls occur on non-main-threads, so messages get queued up and you POLL them out in the Receive call above on the main thread. private void OnRecvTextMsg(RGWebSocket rgws, string msg) { _incomingMessages.Enqueue(new Tuple <string, byte[]>(msg, null)); _logCb?.Invoke($"UWS Recv {msg.Length} bytes txt", 2); }
//------------------- // Task: when a connection is requested, depending on whether it's an HTTP request or WebSocket request, do different things. private async Task HandleConnection(Task <HttpListenerContext> listenerContext) { HttpListenerContext httpContext = listenerContext.Result; if (httpContext.Request.IsWebSocketRequest) { // Kick off an async task to upgrade the web socket and do send/recv messaging, but fail if it takes more than a second to finish. try { _logger?.Invoke("WebSocketServer.HandleConnection - websocket detected. Upgrading connection.", 1); using (CancellationTokenSource upgradeTimeout = new CancellationTokenSource(_connectionMS)) { HttpListenerWebSocketContext webSocketContext = await Task.Run(async() => { return(await httpContext.AcceptWebSocketAsync(null).ConfigureAwait(false)); }, upgradeTimeout.Token); _logger?.Invoke("WebSocketServer.HandleConnection - websocket detected. Upgraded.", 1); RGWebSocket rgws = new RGWebSocket(_onReceiveMsgText, _onReceiveMsgBinary, OnDisconnection, _logger, httpContext.Request.RemoteEndPoint.ToString(), webSocketContext.WebSocket, 25); _websockets.TryAdd(rgws._uniqueId, rgws); _websocketConnection(rgws); } } catch (OperationCanceledException) // timeout { _logger?.Invoke("WebSocketServer.HandleConnection - websocket upgrade timeout", 1); httpContext.Response.StatusCode = 500; httpContext.Response.Close(); } catch // anything else { _logger?.Invoke("WebSocketServer.HandleConnection - websocket upgrade exception", 1); httpContext.Response.StatusCode = 500; httpContext.Response.Close(); } } else // let the application specify what the HTTP response is, but we do the async write here to free up the app to do other things { try { _logger?.Invoke("WebSocketServer.HandleConnection - normal http request", 1); using (CancellationTokenSource responseTimeout = new CancellationTokenSource(_connectionMS)) { byte[] buffer = null; httpContext.Response.StatusCode = _httpRequestCallback(httpContext, out buffer); httpContext.Response.ContentLength64 = buffer.Length; await httpContext.Response.OutputStream.WriteAsync(buffer, 0, buffer.Length, responseTimeout.Token); } } catch (OperationCanceledException) // timeout { _logger?.Invoke("WebSocketServer.HandleConnection - http response timeout", 1); httpContext.Response.StatusCode = 500; } catch // anything else { _logger?.Invoke("WebSocketServer.HandleConnection - http callback handler exception", 1); httpContext.Response.StatusCode = 500; } finally { httpContext.Response.Close(); } } }
// At this point, it's a done deal. Both Recv and Send are completed, nothing to synchronize. This is called at the bottom of the Send thread after Recv is completed. // However, it is possible that the Recv/Send threads shutdown before the RGWS constructor is even finished private void OnDisconnect(RGWebSocket rgws) { _logger("UWS Disconnected.", 1); _status = Status.Disconnected; _disconnectCallback?.Invoke(this); // This callback needs to NOT modify any tracking structures, because it may be called as early as DURING the RGWS constructor. Just set flags }