diff --git a/README.md b/README.md
index 34ae46b..3321051 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. 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 06ca5d6..2270b77 100644
--- a/WebSockets/ReceiveAndControllThread.cs
+++ b/WebSockets/ReceiveAndControllThread.cs
@@ -100,29 +100,27 @@ private void ProcessIncomingMessage()
break;
case OpCode.ConnectionCloseFrame:
- _webSocket.CloseStatus = 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)
- {
- _webSocket.CloseStatus = (WebSocketCloseStatus)statusCode;
- }
- }
+ _webSocket.HasError = true;
- //connection asked to be closed return answer
- if (_webSocket.TryMarkCloseReceived())
- {
- _webSocket.RawClose(WebSocketCloseStatus.NormalClosure, buffer, 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);
}
- else
+ //connection asked to be closed return answer
+ else if (_webSocket.TryMarkCloseReceived(peerCloseStatus))
{
- // our close message can still be queued behind pending messages (simultaneous close)
- _webSocket.WaitForCloseMessageSent();
+ // 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);
}
+ // 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)
_webSocket.HardClose();
@@ -149,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.
@@ -197,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 2ff8b44..63557dd 100644
--- a/WebSockets/WebSocket.cs
+++ b/WebSockets/WebSocket.cs
@@ -218,26 +218,38 @@ 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, 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.
+ ///
///
- 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);
}
///
@@ -253,11 +265,19 @@ 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.
- 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 +316,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 +329,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 +374,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 +399,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();
+ }
}