From f2f06b9bd7d85a2a2883177235b89d86d1ee9718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Sim=C3=B5es?= Date: Thu, 24 Sep 2026 15:53:12 +0100 Subject: [PATCH 1/2] Improvements in Close() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit – The reply to a peer-started close now carries only the peer's status code. - Protocol errors are now closed with a proper status code and reason. - Calling Close() when the connection isn't Open does nothing, including while answering the peer close. - Update Intellisesne commetns and README. --- README.md | 5 +- WebSockets/ReceiveAndControllThread.cs | 12 +++-- WebSockets/WebSocket.cs | 68 ++++++++++++++++++-------- WebSockets/WebSocketReceiver.cs | 1 + WebSockets/WebSocketSender.cs | 16 +++++- 5 files changed, 73 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 34ae46b..3be02c4 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,9 @@ A message can be send by calling `SendString` for a text message or `SendBytes` #### Closing a connection -The connection can be closed by calling `Close`. Calling this method will send a closing message over the line. You can optional specify a `WebSocketCloseStatus` and description on the reason for closing for debugging purposes. -Whenever a connection is closed the event `Closed` is fired. +The connection can be closed by calling `Close`. Calling this method will send a closing message over the line. You can optional specify a `WebSocketCloseStatus` and description on the reason for closing for debugging purposes (the description is only sent together with a status, it's ignored with the default `WebSocketCloseStatus.Empty`). +`Close` doesn't block: the closing message is sent after the messages already queued and the connection is closed when the other end answers it, or after `ServerTimeout`. Closing with `WebSocketCloseStatus.EndpointUnavailable` is synchronous: the call returns after the closing message is sent and the connection is closed, without waiting for an answer. +Whenever a connection is closed the event `ConnectionClosed` is fired. ### Server diff --git a/WebSockets/ReceiveAndControllThread.cs b/WebSockets/ReceiveAndControllThread.cs index 06ca5d6..337a48e 100644 --- a/WebSockets/ReceiveAndControllThread.cs +++ b/WebSockets/ReceiveAndControllThread.cs @@ -100,22 +100,24 @@ private void ProcessIncomingMessage() break; case OpCode.ConnectionCloseFrame: - _webSocket.CloseStatus = WebSocketCloseStatus.Empty; + var peerCloseStatus = WebSocketCloseStatus.Empty; if (buffer.Length > 1) { byte[] closeByteCode = new byte[] { buffer[1], buffer[0] }; UInt16 statusCode = BitConverter.ToUInt16(closeByteCode, 0); - if (statusCode > 999 && statusCode < 1012) + if (statusCode > 999 && statusCode < 1012) { - _webSocket.CloseStatus = (WebSocketCloseStatus)statusCode; + peerCloseStatus = (WebSocketCloseStatus)statusCode; } } //connection asked to be closed return answer - if (_webSocket.TryMarkCloseReceived()) + if (_webSocket.TryMarkCloseReceived(peerCloseStatus)) { - _webSocket.RawClose(WebSocketCloseStatus.NormalClosure, buffer, true); + // echo the status code received (RFC 6455 section 5.5.1), without the peer's reason + // (no status code is sent if none was received) + _webSocket.RawClose(peerCloseStatus, null, true); } else { diff --git a/WebSockets/WebSocket.cs b/WebSockets/WebSocket.cs index 2ff8b44..49792ec 100644 --- a/WebSockets/WebSocket.cs +++ b/WebSockets/WebSocket.cs @@ -218,26 +218,36 @@ public bool SendBytes(byte[] data, int fragmentSize = -1) } /// - /// Will start closing the WebSocket connection using the close handshake defined in the WebSocket protocol specification section 7. + /// Will start closing the WebSocket connection using the close handshake defined in the protocol specification section 7. /// /// Indicates the reason for closing the WebSocket connection. /// Specifies a human readable explanation as to why the connection is closed. /// - /// WebSocketCloseStatus.EndpointUnavailable will close the WebSocket synchronous without awaiting response. + /// + /// This method doesn't block. The close message is sent after the messages already queued and is raised + /// when the remote endpoint answers the close message, or after if it doesn't. + /// + /// + /// will close the synchronous without awaiting response. + /// The call blocks until the close message is sent (bounded by ) and messages still queued may not be sent. + /// + /// + /// Only has effect if the connection is open. With no status code is sent, so is ignored. + /// /// - public void Close(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.Empty, string statusDescription = null) + public void Close( + WebSocketCloseStatus closeStatus = WebSocketCloseStatus.Empty, + string statusDescription = null) { - if (State != WebSocketState.Open) - { - //already closing or closed - return; - } - bool closeImmediately = closeStatus == WebSocketCloseStatus.EndpointUnavailable; - // state transition (and ClosingTime) is done atomically by TryBeginClose - // a normal close is sent after the messages already queued, so these aren't lost - RawClose(closeStatus, statusDescription == null ? null : Encoding.UTF8.GetBytes(statusDescription), closeImmediately, !closeImmediately); + // state check and transition (and ClosingTime) are done atomically by TryBeginClose + RawClose( + closeStatus, + statusDescription == null ? null : Encoding.UTF8.GetBytes(statusDescription), + closeImmediately, + !closeImmediately, + true); } /// @@ -255,9 +265,15 @@ public void Abort() // CloseImediately will Send a close message and not await this message. // afterPendingMessages will send the close message after the messages already queued, instead of before them. - internal void RawClose(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.Empty, byte[] buffer = null, bool CloseImmediately = false, bool afterPendingMessages = false) + // requireOpen will only close an open connection (not one answering a close message from the remote endpoint). + internal void RawClose( + WebSocketCloseStatus closeStatus = WebSocketCloseStatus.Empty, + byte[] buffer = null, + bool CloseImmediately = false, + bool afterPendingMessages = false, + bool requireOpen = false) { - if (!QueueCloseFrame(closeStatus, buffer, afterPendingMessages)) + if (!QueueCloseFrame(closeStatus, buffer, afterPendingMessages, requireOpen)) { //already closing or closed return; @@ -296,9 +312,11 @@ internal void WaitForCloseMessageSent() // Non blocking close, used by the timeout checker. // The connection is hard closed by a later timeout check, once the close message is sent or ServerTimeout expires. - internal void BeginClose(WebSocketCloseStatus closeStatus, byte[] buffer) + internal void BeginClose( + WebSocketCloseStatus closeStatus, + byte[] buffer) { - if (QueueCloseFrame(closeStatus, buffer, false)) + if (QueueCloseFrame(closeStatus, buffer, false, false)) { lock (_stateLock) { @@ -307,9 +325,13 @@ internal void BeginClose(WebSocketCloseStatus closeStatus, byte[] buffer) } } - private bool QueueCloseFrame(WebSocketCloseStatus closeStatus, byte[] buffer, bool afterPendingMessages) + private bool QueueCloseFrame( + WebSocketCloseStatus closeStatus, + byte[] buffer, + bool afterPendingMessages, + bool requireOpen) { - if (!TryBeginClose(closeStatus)) + if (!TryBeginClose(closeStatus, requireOpen)) { return false; } @@ -348,12 +370,14 @@ private bool QueueCloseFrame(WebSocketCloseStatus closeStatus, byte[] buffer, bo } // Atomically moves to CloseSent, making sure only one close message is ever queued. - private bool TryBeginClose(WebSocketCloseStatus closeStatus) + private bool TryBeginClose( + WebSocketCloseStatus closeStatus, + bool requireOpen) { lock (_stateLock) { if (_closeFrameQueued - || !(State == WebSocketState.Open || State == WebSocketState.CloseReceived)) + || !(State == WebSocketState.Open || (!requireOpen && State == WebSocketState.CloseReceived))) { return false; } @@ -371,10 +395,12 @@ private bool TryBeginClose(WebSocketCloseStatus closeStatus) // Called when the peer sent a close message. // Returns true if the close message has to be answered, false if the connection is already closing and can be hard closed. - internal bool TryMarkCloseReceived() + internal bool TryMarkCloseReceived(WebSocketCloseStatus peerCloseStatus) { lock (_stateLock) { + CloseStatus = peerCloseStatus; + if (_closeFrameQueued || !(State == WebSocketState.Open || State == WebSocketState.CloseReceived)) { diff --git a/WebSockets/WebSocketReceiver.cs b/WebSockets/WebSocketReceiver.cs index 24bf610..de0eb92 100644 --- a/WebSockets/WebSocketReceiver.cs +++ b/WebSockets/WebSocketReceiver.cs @@ -182,6 +182,7 @@ internal byte[] ReadBuffer(int messageLength, byte[] masks = null) private ReceiveMessageFrame SetMessageError(ReceiveMessageFrame frame, string errorMsg, WebSocketCloseStatus closeCode) { frame.ErrorMessage = errorMsg; + frame.CloseStatus = closeCode; return frame; } diff --git a/WebSockets/WebSocketSender.cs b/WebSockets/WebSocketSender.cs index 2d4abf8..803b9ce 100644 --- a/WebSockets/WebSocketSender.cs +++ b/WebSockets/WebSocketSender.cs @@ -29,6 +29,8 @@ internal class WebSocketSender internal bool CloseMessageSent { get; private set; } = false; + private bool _stopRequested = false; + internal bool ControlMessagesPresent { get @@ -72,6 +74,8 @@ internal void QueueMessage(SendMessageFrame sendMessage, bool afterPendingMessag internal void StopSender() { + // set before signaling, the event is shared with QueueMessage so a signal alone doesn't mean stop + _stopRequested = true; CloseMessageSent = true; _shutdownEvent.Set(); @@ -140,6 +144,12 @@ private bool ProcessMessageFrames() for (int i = 0; i < numberOfFrames; i++) { + if (CloseMessageSent) + { + // close message was sent (or sender stopped) while sending fragments, stop sending + break; + } + //start frame fin = 0 Opcode normal if (i == 0) { @@ -225,7 +235,11 @@ private void OnCloseFrameSent() CloseMessageSent = true; // wait until resources can be released. - _shutdownEvent.WaitOne(); + // loop because the event can still be signaled by a message queued before the close + while (!_stopRequested) + { + _shutdownEvent.WaitOne(); + } } From 0f48c3b4d3356429dcf20128303bf9232c36187f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Sim=C3=B5es?= Date: Thu, 24 Sep 2026 16:29:45 +0100 Subject: [PATCH 2/2] Better handling error codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - invalid close frames from the peer are now answered with 1002. - Close codes in the 3000–4999 range are now echoed back to the peer, instead of being replaced by an empty close. --- README.md | 2 +- WebSockets/ReceiveAndControllThread.cs | 60 ++++++++++++++++++-------- WebSockets/WebSocket.cs | 8 +++- 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 3be02c4..3321051 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ A message can be send by calling `SendString` for a text message or `SendBytes` #### Closing a connection The connection can be closed by calling `Close`. Calling this method will send a closing message over the line. You can optional specify a `WebSocketCloseStatus` and description on the reason for closing for debugging purposes (the description is only sent together with a status, it's ignored with the default `WebSocketCloseStatus.Empty`). -`Close` doesn't block: the closing message is sent after the messages already queued and the connection is closed when the other end answers it, or after `ServerTimeout`. Closing with `WebSocketCloseStatus.EndpointUnavailable` is synchronous: the call returns after the closing message is sent and the connection is closed, without waiting for an answer. +`Close` doesn't block: the closing message is sent after the messages already queued and the connection is closed when the other end answers it, or after `ServerTimeout`. Closing with `WebSocketCloseStatus.EndpointUnavailable` is synchronous: the call returns after the closing message is sent and the connection is closed, without waiting for an answer. It waits at most `ServerTimeout` for the closing message to be sent; if that expires first the connection is closed anyway and the closing message may not have been sent. Messages still queued may not be sent. Whenever a connection is closed the event `ConnectionClosed` is fired. ### Server diff --git a/WebSockets/ReceiveAndControllThread.cs b/WebSockets/ReceiveAndControllThread.cs index 337a48e..2270b77 100644 --- a/WebSockets/ReceiveAndControllThread.cs +++ b/WebSockets/ReceiveAndControllThread.cs @@ -100,30 +100,26 @@ private void ProcessIncomingMessage() break; case OpCode.ConnectionCloseFrame: - var peerCloseStatus = WebSocketCloseStatus.Empty; - - if (buffer.Length > 1) + if (!TryGetPeerCloseStatus(buffer, out WebSocketCloseStatus peerCloseStatus)) { - byte[] closeByteCode = new byte[] { buffer[1], buffer[0] }; - UInt16 statusCode = BitConverter.ToUInt16(closeByteCode, 0); - if (statusCode > 999 && statusCode < 1012) - { - peerCloseStatus = (WebSocketCloseStatus)statusCode; - } - } + _webSocket.HasError = true; + + Debug.WriteLine($"{_webSocket.RemoteEndPoint} sent an invalid close frame"); + // no-op if our close message was already queued + _webSocket.RawClose(WebSocketCloseStatus.ProtocolError, Encoding.UTF8.GetBytes("Invalid close frame"), true); + } //connection asked to be closed return answer - if (_webSocket.TryMarkCloseReceived(peerCloseStatus)) + else if (_webSocket.TryMarkCloseReceived(peerCloseStatus)) { // echo the status code received (RFC 6455 section 5.5.1), without the peer's reason // (no status code is sent if none was received) _webSocket.RawClose(peerCloseStatus, null, true); } - else - { - // our close message can still be queued behind pending messages (simultaneous close) - _webSocket.WaitForCloseMessageSent(); - } + + // our close message can still be queued behind pending messages (simultaneous close) + // returns immediately if it was sent or the connection is already closed + _webSocket.WaitForCloseMessageSent(); // either this is the response to our close, or RawClose lost a race with another close, // so we can shut down the socket (no-op if already closed) @@ -151,7 +147,6 @@ private void ProcessIncomingMessage() } } - // Runs on the timer thread, concurrently with the receive thread. // Shared state is accessed through WebSocket helpers that only hold a lock for field access, // and nothing here blocks, so a receive thread holding a lock can't stall this check. @@ -199,6 +194,37 @@ private void CheckTimeouts(object state) } } + // Gets the status code from the payload of a close frame received from the remote endpoint. + // Returns false if the payload is invalid: 1 byte long, or with a status code that can't be sent in a close frame (RFC 6455 section 7.4). + private static bool TryGetPeerCloseStatus(byte[] buffer, out WebSocketCloseStatus closeStatus) + { + closeStatus = WebSocketCloseStatus.Empty; + + if (buffer.Length == 0) + { + // no status code + return true; + } + + if (buffer.Length == 1) + { + return false; + } + + int statusCode = (buffer[0] << 8) | buffer[1]; + + if ((statusCode >= 1000 && statusCode <= 1003) + || (statusCode >= 1007 && statusCode <= 1014) + || (statusCode >= 3000 && statusCode <= 4999)) + { + closeStatus = (WebSocketCloseStatus)statusCode; + + return true; + } + + return false; + } + private void OnNewMessage(ReceiveMessageFrame message) { _webSocket.CallbacksMessageReceivedEventHandler?.Invoke(_webSocket, new MessageReceivedEventArgs() { Frame = message }); diff --git a/WebSockets/WebSocket.cs b/WebSockets/WebSocket.cs index 49792ec..63557dd 100644 --- a/WebSockets/WebSocket.cs +++ b/WebSockets/WebSocket.cs @@ -229,7 +229,9 @@ public bool SendBytes(byte[] data, int fragmentSize = -1) /// /// /// will close the synchronous without awaiting response. - /// The call blocks until the close message is sent (bounded by ) and messages still queued may not be sent. + /// The call blocks until the close message is sent, waiting at most (5 seconds if it's infinite). + /// If that time expires first, the connection is closed anyway and the close message may not have been sent. + /// Messages still queued may not be sent. /// /// /// Only has effect if the connection is open. With no status code is sent, so is ignored. @@ -263,7 +265,9 @@ public void Abort() HardClose(); } - // CloseImediately will Send a close message and not await this message. + // CloseImediately will Send a close message and not await the answer from the remote endpoint: + // it waits at most ServerTimeout (5 seconds if infinite) for the close message to be sent, then closes the connection, + // even if the close message wasn't sent. Messages still queued may not be sent. // afterPendingMessages will send the close message after the messages already queued, instead of before them. // requireOpen will only close an open connection (not one answering a close message from the remote endpoint). internal void RawClose(