Skip to content

Commit f6a0245

Browse files
committed
Improvements in Close() (#250)
(cherry picked from commit b766433)
1 parent a6ddf83 commit f6a0245

5 files changed

Lines changed: 117 additions & 43 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,9 @@ A message can be send by calling `SendString` for a text message or `SendBytes`
115115

116116
#### Closing a connection
117117

118-
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.
119-
Whenever a connection is closed the event `Closed` is fired.
118+
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`).
119+
`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.
120+
Whenever a connection is closed the event `ConnectionClosed` is fired.
120121

121122
### Server
122123

‎WebSockets/ReceiveAndControllThread.cs‎

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -100,29 +100,27 @@ private void ProcessIncomingMessage()
100100
break;
101101

102102
case OpCode.ConnectionCloseFrame:
103-
_webSocket.CloseStatus = WebSocketCloseStatus.Empty;
104-
105-
if (buffer.Length > 1)
103+
if (!TryGetPeerCloseStatus(buffer, out WebSocketCloseStatus peerCloseStatus))
106104
{
107-
byte[] closeByteCode = new byte[] { buffer[1], buffer[0] };
108-
UInt16 statusCode = BitConverter.ToUInt16(closeByteCode, 0);
109-
if (statusCode > 999 && statusCode < 1012)
110-
{
111-
_webSocket.CloseStatus = (WebSocketCloseStatus)statusCode;
112-
}
113-
}
105+
_webSocket.HasError = true;
114106

115-
//connection asked to be closed return answer
116-
if (_webSocket.TryMarkCloseReceived())
117-
{
118-
_webSocket.RawClose(WebSocketCloseStatus.NormalClosure, buffer, true);
107+
Debug.WriteLine($"{_webSocket.RemoteEndPoint} sent an invalid close frame");
108+
109+
// no-op if our close message was already queued
110+
_webSocket.RawClose(WebSocketCloseStatus.ProtocolError, Encoding.UTF8.GetBytes("Invalid close frame"), true);
119111
}
120-
else
112+
//connection asked to be closed return answer
113+
else if (_webSocket.TryMarkCloseReceived(peerCloseStatus))
121114
{
122-
// our close message can still be queued behind pending messages (simultaneous close)
123-
_webSocket.WaitForCloseMessageSent();
115+
// echo the status code received (RFC 6455 section 5.5.1), without the peer's reason
116+
// (no status code is sent if none was received)
117+
_webSocket.RawClose(peerCloseStatus, null, true);
124118
}
125119

120+
// our close message can still be queued behind pending messages (simultaneous close)
121+
// returns immediately if it was sent or the connection is already closed
122+
_webSocket.WaitForCloseMessageSent();
123+
126124
// either this is the response to our close, or RawClose lost a race with another close,
127125
// so we can shut down the socket (no-op if already closed)
128126
_webSocket.HardClose();
@@ -149,7 +147,6 @@ private void ProcessIncomingMessage()
149147
}
150148
}
151149

152-
153150
// Runs on the timer thread, concurrently with the receive thread.
154151
// Shared state is accessed through WebSocket helpers that only hold a lock for field access,
155152
// 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)
197194
}
198195
}
199196

197+
// Gets the status code from the payload of a close frame received from the remote endpoint.
198+
// 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).
199+
private static bool TryGetPeerCloseStatus(byte[] buffer, out WebSocketCloseStatus closeStatus)
200+
{
201+
closeStatus = WebSocketCloseStatus.Empty;
202+
203+
if (buffer.Length == 0)
204+
{
205+
// no status code
206+
return true;
207+
}
208+
209+
if (buffer.Length == 1)
210+
{
211+
return false;
212+
}
213+
214+
int statusCode = (buffer[0] << 8) | buffer[1];
215+
216+
if ((statusCode >= 1000 && statusCode <= 1003)
217+
|| (statusCode >= 1007 && statusCode <= 1014)
218+
|| (statusCode >= 3000 && statusCode <= 4999))
219+
{
220+
closeStatus = (WebSocketCloseStatus)statusCode;
221+
222+
return true;
223+
}
224+
225+
return false;
226+
}
227+
200228
private void OnNewMessage(ReceiveMessageFrame message)
201229
{
202230
_webSocket.CallbacksMessageReceivedEventHandler?.Invoke(_webSocket, new MessageReceivedEventArgs() { Frame = message });

‎WebSockets/WebSocket.cs‎

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -218,26 +218,38 @@ public bool SendBytes(byte[] data, int fragmentSize = -1)
218218
}
219219

220220
/// <summary>
221-
/// Will start closing the WebSocket connection using the close handshake defined in the WebSocket protocol specification section 7.
221+
/// Will start closing the WebSocket connection using the close handshake defined in the <see cref="WebSocket"/> protocol specification section 7.
222222
/// </summary>
223223
/// <param name="closeStatus">Indicates the reason for closing the WebSocket connection.</param>
224224
/// <param name="statusDescription">Specifies a human readable explanation as to why the connection is closed.</param>
225225
/// <remarks>
226-
/// WebSocketCloseStatus.EndpointUnavailable will close the WebSocket synchronous without awaiting response.
226+
/// <para>
227+
/// This method doesn't block. The close message is sent after the messages already queued and <see cref="ConnectionClosed"/> is raised
228+
/// when the remote endpoint answers the close message, or after <see cref="ServerTimeout"/> if it doesn't.
229+
/// </para>
230+
/// <para>
231+
/// <see cref="WebSocketCloseStatus.EndpointUnavailable"/> will close the <see cref="WebSocket"/> synchronous without awaiting response.
232+
/// The call blocks until the close message is sent, waiting at most <see cref="ServerTimeout"/> (5 seconds if it's infinite).
233+
/// If that time expires first, the connection is closed anyway and the close message may not have been sent.
234+
/// Messages still queued may not be sent.
235+
/// </para>
236+
/// <para>
237+
/// Only has effect if the connection is open. With <see cref="WebSocketCloseStatus.Empty"/> no status code is sent, so <paramref name="statusDescription"/> is ignored.
238+
/// </para>
227239
/// </remarks>
228-
public void Close(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.Empty, string statusDescription = null)
240+
public void Close(
241+
WebSocketCloseStatus closeStatus = WebSocketCloseStatus.Empty,
242+
string statusDescription = null)
229243
{
230-
if (State != WebSocketState.Open)
231-
{
232-
//already closing or closed
233-
return;
234-
}
235-
236244
bool closeImmediately = closeStatus == WebSocketCloseStatus.EndpointUnavailable;
237245

238-
// state transition (and ClosingTime) is done atomically by TryBeginClose
239-
// a normal close is sent after the messages already queued, so these aren't lost
240-
RawClose(closeStatus, statusDescription == null ? null : Encoding.UTF8.GetBytes(statusDescription), closeImmediately, !closeImmediately);
246+
// state check and transition (and ClosingTime) are done atomically by TryBeginClose
247+
RawClose(
248+
closeStatus,
249+
statusDescription == null ? null : Encoding.UTF8.GetBytes(statusDescription),
250+
closeImmediately,
251+
!closeImmediately,
252+
true);
241253
}
242254

243255
/// <summary>
@@ -253,11 +265,19 @@ public void Abort()
253265
HardClose();
254266
}
255267

256-
// CloseImediately will Send a close message and not await this message.
268+
// CloseImediately will Send a close message and not await the answer from the remote endpoint:
269+
// it waits at most ServerTimeout (5 seconds if infinite) for the close message to be sent, then closes the connection,
270+
// even if the close message wasn't sent. Messages still queued may not be sent.
257271
// afterPendingMessages will send the close message after the messages already queued, instead of before them.
258-
internal void RawClose(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.Empty, byte[] buffer = null, bool CloseImmediately = false, bool afterPendingMessages = false)
272+
// requireOpen will only close an open connection (not one answering a close message from the remote endpoint).
273+
internal void RawClose(
274+
WebSocketCloseStatus closeStatus = WebSocketCloseStatus.Empty,
275+
byte[] buffer = null,
276+
bool CloseImmediately = false,
277+
bool afterPendingMessages = false,
278+
bool requireOpen = false)
259279
{
260-
if (!QueueCloseFrame(closeStatus, buffer, afterPendingMessages))
280+
if (!QueueCloseFrame(closeStatus, buffer, afterPendingMessages, requireOpen))
261281
{
262282
//already closing or closed
263283
return;
@@ -296,9 +316,11 @@ internal void WaitForCloseMessageSent()
296316

297317
// Non blocking close, used by the timeout checker.
298318
// The connection is hard closed by a later timeout check, once the close message is sent or ServerTimeout expires.
299-
internal void BeginClose(WebSocketCloseStatus closeStatus, byte[] buffer)
319+
internal void BeginClose(
320+
WebSocketCloseStatus closeStatus,
321+
byte[] buffer)
300322
{
301-
if (QueueCloseFrame(closeStatus, buffer, false))
323+
if (QueueCloseFrame(closeStatus, buffer, false, false))
302324
{
303325
lock (_stateLock)
304326
{
@@ -307,9 +329,13 @@ internal void BeginClose(WebSocketCloseStatus closeStatus, byte[] buffer)
307329
}
308330
}
309331

310-
private bool QueueCloseFrame(WebSocketCloseStatus closeStatus, byte[] buffer, bool afterPendingMessages)
332+
private bool QueueCloseFrame(
333+
WebSocketCloseStatus closeStatus,
334+
byte[] buffer,
335+
bool afterPendingMessages,
336+
bool requireOpen)
311337
{
312-
if (!TryBeginClose(closeStatus))
338+
if (!TryBeginClose(closeStatus, requireOpen))
313339
{
314340
return false;
315341
}
@@ -348,12 +374,14 @@ private bool QueueCloseFrame(WebSocketCloseStatus closeStatus, byte[] buffer, bo
348374
}
349375

350376
// Atomically moves to CloseSent, making sure only one close message is ever queued.
351-
private bool TryBeginClose(WebSocketCloseStatus closeStatus)
377+
private bool TryBeginClose(
378+
WebSocketCloseStatus closeStatus,
379+
bool requireOpen)
352380
{
353381
lock (_stateLock)
354382
{
355383
if (_closeFrameQueued
356-
|| !(State == WebSocketState.Open || State == WebSocketState.CloseReceived))
384+
|| !(State == WebSocketState.Open || (!requireOpen && State == WebSocketState.CloseReceived)))
357385
{
358386
return false;
359387
}
@@ -371,10 +399,12 @@ private bool TryBeginClose(WebSocketCloseStatus closeStatus)
371399

372400
// Called when the peer sent a close message.
373401
// Returns true if the close message has to be answered, false if the connection is already closing and can be hard closed.
374-
internal bool TryMarkCloseReceived()
402+
internal bool TryMarkCloseReceived(WebSocketCloseStatus peerCloseStatus)
375403
{
376404
lock (_stateLock)
377405
{
406+
CloseStatus = peerCloseStatus;
407+
378408
if (_closeFrameQueued
379409
|| !(State == WebSocketState.Open || State == WebSocketState.CloseReceived))
380410
{

‎WebSockets/WebSocketReceiver.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ internal byte[] ReadBuffer(int messageLength, byte[] masks = null)
182182
private ReceiveMessageFrame SetMessageError(ReceiveMessageFrame frame, string errorMsg, WebSocketCloseStatus closeCode)
183183
{
184184
frame.ErrorMessage = errorMsg;
185+
frame.CloseStatus = closeCode;
185186
return frame;
186187
}
187188

‎WebSockets/WebSocketSender.cs‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ internal class WebSocketSender
2929

3030
internal bool CloseMessageSent { get; private set; } = false;
3131

32+
private bool _stopRequested = false;
33+
3234
internal bool ControlMessagesPresent
3335
{
3436
get
@@ -72,6 +74,8 @@ internal void QueueMessage(SendMessageFrame sendMessage, bool afterPendingMessag
7274

7375
internal void StopSender()
7476
{
77+
// set before signaling, the event is shared with QueueMessage so a signal alone doesn't mean stop
78+
_stopRequested = true;
7579
CloseMessageSent = true;
7680

7781
_shutdownEvent.Set();
@@ -140,6 +144,12 @@ private bool ProcessMessageFrames()
140144

141145
for (int i = 0; i < numberOfFrames; i++)
142146
{
147+
if (CloseMessageSent)
148+
{
149+
// close message was sent (or sender stopped) while sending fragments, stop sending
150+
break;
151+
}
152+
143153
//start frame fin = 0 Opcode normal
144154
if (i == 0)
145155
{
@@ -225,7 +235,11 @@ private void OnCloseFrameSent()
225235
CloseMessageSent = true;
226236

227237
// wait until resources can be released.
228-
_shutdownEvent.WaitOne();
238+
// loop because the event can still be signaled by a message queued before the close
239+
while (!_stopRequested)
240+
{
241+
_shutdownEvent.WaitOne();
242+
}
229243
}
230244

231245

0 commit comments

Comments
 (0)