private async Task CloseAsyncPrivate(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)
{
// Send the close message. Skip sending a close frame if we're currently in a CloseSent state,
// for example having just done a CloseOutputAsync.
if (!_sentCloseFrame)
{
await SendCloseFrameAsync(closeStatus, statusDescription, cancellationToken).ConfigureAwait(false);
}
// We should now either be in a CloseSent case (because we just sent one), or in a CloseReceived state, in case
// there was a concurrent receive that ended up handling an immediate close frame response from the server.
// Of course it could also be Aborted if something happened concurrently to cause things to blow up.
Debug.Assert(
State == WebSocketState.CloseSent ||
State == WebSocketState.CloseReceived ||
State == WebSocketState.Aborted,
$"Unexpected state {State}.");
// Wait until we've received a close response
byte[] closeBuffer = new byte[MaxMessageHeaderLength + MaxControlPayloadLength];
while (!_receivedCloseFrame)
{
Debug.Assert(!Monitor.IsEntered(StateUpdateLock), $"{nameof(StateUpdateLock)} must never be held when acquiring {nameof(ReceiveAsyncLock)}");
Task<WebSocketReceiveResult> receiveTask;
lock (ReceiveAsyncLock)
{
// Now that we're holding the ReceiveAsyncLock, double-check that we've not yet received the close frame.
// It could have been received between our check above and now due to a concurrent receive completing.
if (_receivedCloseFrame)
{
break;
}
// We've not yet processed a received close frame, which means we need to wait for a received close to complete.
// There may already be one in flight, in which case we want to just wait for that one rather than kicking off
// another (we don't support concurrent receive operations). We need to kick off a new receive if either we've
// never issued a receive or if the last issued receive completed for reasons other than a close frame. There is
// a race condition here, e.g. if there's a in-flight receive that completes after we check, but that's fine: worst
// case is we then await it, find that it's not what we need, and try again.
receiveTask = _lastReceiveAsync;
if (receiveTask == null ||
(receiveTask.Status == TaskStatus.RanToCompletion && receiveTask.Result.MessageType != WebSocketMessageType.Close))
{
_lastReceiveAsync = receiveTask = ReceiveAsyncPrivate(new ArraySegment<byte>(closeBuffer), cancellationToken);
}
}
// Wait for whatever receive task we have. We'll then loop around again to re-check our state.
Debug.Assert(receiveTask != null);
await receiveTask.ConfigureAwait(false);
}
// We're closed. Close the connection and update the status.
lock (StateUpdateLock)
{
DisposeCore();
if (_state < WebSocketState.Closed)
{
_state = WebSocketState.Closed;
}
}
}