Do over the websocket transport (#1481)
* Do over the websocket transport - Unify client and server logic (no code sharing yet) - Removed use of cancellation tokens to communicate shutdown and instead used the pipe reader and socket abort. - Added CloseTimeout to HttpOptions
This commit is contained in:
parent
7d035f48bc
commit
32baa655b9
|
|
@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Http
|
||||||
public HttpMessageHandler HttpMessageHandler { get; set; }
|
public HttpMessageHandler HttpMessageHandler { get; set; }
|
||||||
public IReadOnlyCollection<KeyValuePair<string, string>> Headers { get; set; }
|
public IReadOnlyCollection<KeyValuePair<string, string>> Headers { get; set; }
|
||||||
public Func<string> AccessTokenFactory { get; set; }
|
public Func<string> AccessTokenFactory { get; set; }
|
||||||
|
public TimeSpan CloseTimeout { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a delegate that will be invoked with the <see cref="ClientWebSocketOptions"/> object used
|
/// Gets or sets a delegate that will be invoked with the <see cref="ClientWebSocketOptions"/> object used
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Sockets.Client.Http;
|
using Microsoft.AspNetCore.Sockets.Client.Http;
|
||||||
|
|
@ -19,9 +18,9 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
{
|
{
|
||||||
private readonly ClientWebSocket _webSocket;
|
private readonly ClientWebSocket _webSocket;
|
||||||
private IDuplexPipe _application;
|
private IDuplexPipe _application;
|
||||||
private readonly CancellationTokenSource _transportCts = new CancellationTokenSource();
|
|
||||||
private readonly CancellationTokenSource _receiveCts = new CancellationTokenSource();
|
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private readonly TimeSpan _closeTimeout;
|
||||||
|
private volatile bool _aborted;
|
||||||
|
|
||||||
public Task Running { get; private set; } = Task.CompletedTask;
|
public Task Running { get; private set; } = Task.CompletedTask;
|
||||||
|
|
||||||
|
|
@ -51,6 +50,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
|
|
||||||
httpOptions?.WebSocketOptions?.Invoke(_webSocket.Options);
|
httpOptions?.WebSocketOptions?.Invoke(_webSocket.Options);
|
||||||
|
|
||||||
|
_closeTimeout = httpOptions?.CloseTimeout ?? TimeSpan.FromSeconds(5);
|
||||||
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<WebSocketsTransport>();
|
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<WebSocketsTransport>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,27 +77,69 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
_logger.StartTransport(Mode.Value);
|
_logger.StartTransport(Mode.Value);
|
||||||
|
|
||||||
await Connect(url);
|
await Connect(url);
|
||||||
var sendTask = SendMessages();
|
|
||||||
var receiveTask = ReceiveMessages();
|
|
||||||
|
|
||||||
// TODO: Handle TCP connection errors
|
// TODO: Handle TCP connection errors
|
||||||
// https://github.com/SignalR/SignalR/blob/1fba14fa3437e24c204dfaf8a18db3fce8acad3c/src/Microsoft.AspNet.SignalR.Core/Owin/WebSockets/WebSocketHandler.cs#L248-L251
|
// https://github.com/SignalR/SignalR/blob/1fba14fa3437e24c204dfaf8a18db3fce8acad3c/src/Microsoft.AspNet.SignalR.Core/Owin/WebSockets/WebSocketHandler.cs#L248-L251
|
||||||
Running = Task.WhenAll(sendTask, receiveTask).ContinueWith(t =>
|
Running = ProcessSocketAsync(_webSocket);
|
||||||
{
|
|
||||||
_webSocket.Dispose();
|
|
||||||
_logger.TransportStopped(t.Exception?.InnerException);
|
|
||||||
|
|
||||||
_application.Output.Complete(t.Exception?.InnerException);
|
|
||||||
_application.Input.Complete();
|
|
||||||
|
|
||||||
return t;
|
|
||||||
}).Unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ReceiveMessages()
|
private async Task ProcessSocketAsync(WebSocket socket)
|
||||||
{
|
{
|
||||||
_logger.StartReceive();
|
using (socket)
|
||||||
|
{
|
||||||
|
// Begin sending and receiving. Receiving must be started first because ExecuteAsync enables SendAsync.
|
||||||
|
var receiving = StartReceiving(socket);
|
||||||
|
var sending = StartSending(socket);
|
||||||
|
|
||||||
|
// Wait for send or receive to complete
|
||||||
|
var trigger = await Task.WhenAny(receiving, sending);
|
||||||
|
|
||||||
|
if (trigger == receiving)
|
||||||
|
{
|
||||||
|
// We're waiting for the application to finish and there are 2 things it could be doing
|
||||||
|
// 1. Waiting for application data
|
||||||
|
// 2. Waiting for a websocket send to complete
|
||||||
|
|
||||||
|
// Cancel the application so that ReadAsync yields
|
||||||
|
_application.Input.CancelPendingRead();
|
||||||
|
|
||||||
|
using (var delayCts = new CancellationTokenSource())
|
||||||
|
{
|
||||||
|
var resultTask = await Task.WhenAny(sending, Task.Delay(_closeTimeout, delayCts.Token));
|
||||||
|
|
||||||
|
if (resultTask != sending)
|
||||||
|
{
|
||||||
|
_aborted = true;
|
||||||
|
|
||||||
|
// Abort the websocket if we're stuck in a pending send to the client
|
||||||
|
socket.Abort();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Cancel the timeout
|
||||||
|
delayCts.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We're waiting on the websocket to close and there are 2 things it could be doing
|
||||||
|
// 1. Waiting for websocket data
|
||||||
|
// 2. Waiting on a flush to complete (backpressure being applied)
|
||||||
|
|
||||||
|
_aborted = true;
|
||||||
|
|
||||||
|
// Abort the websocket if we're stuck in a pending receive from the client
|
||||||
|
socket.Abort();
|
||||||
|
|
||||||
|
// Cancel any pending flush so that we can quit
|
||||||
|
_application.Output.CancelPendingFlush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartReceiving(WebSocket socket)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
|
|
@ -105,15 +147,14 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
var memory = _application.Output.GetMemory();
|
var memory = _application.Output.GetMemory();
|
||||||
|
|
||||||
#if NETCOREAPP2_1
|
#if NETCOREAPP2_1
|
||||||
var receiveResult = await _webSocket.ReceiveAsync(memory, _receiveCts.Token);
|
var receiveResult = await socket.ReceiveAsync(memory, CancellationToken.None);
|
||||||
#else
|
#else
|
||||||
var isArray = memory.TryGetArray(out var arraySegment);
|
var isArray = memory.TryGetArray(out var arraySegment);
|
||||||
Debug.Assert(isArray);
|
Debug.Assert(isArray);
|
||||||
|
|
||||||
// Exceptions are handled above where the send and receive tasks are being run.
|
// Exceptions are handled above where the send and receive tasks are being run.
|
||||||
var receiveResult = await _webSocket.ReceiveAsync(arraySegment, _receiveCts.Token);
|
var receiveResult = await socket.ReceiveAsync(arraySegment, CancellationToken.None);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
||||||
{
|
{
|
||||||
_logger.WebSocketClosed(_webSocket.CloseStatus);
|
_logger.WebSocketClosed(_webSocket.CloseStatus);
|
||||||
|
|
@ -132,7 +173,14 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
|
|
||||||
if (receiveResult.EndOfMessage)
|
if (receiveResult.EndOfMessage)
|
||||||
{
|
{
|
||||||
await _application.Output.FlushAsync(_transportCts.Token);
|
var flushResult = await _application.Output.FlushAsync();
|
||||||
|
|
||||||
|
// We canceled in the middle of applying back pressure
|
||||||
|
// or if the consumer is done
|
||||||
|
if (flushResult.IsCancelled || flushResult.IsCompleted)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -140,71 +188,111 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
{
|
{
|
||||||
_logger.ReceiveCanceled();
|
_logger.ReceiveCanceled();
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!_aborted)
|
||||||
|
{
|
||||||
|
_application.Output.Complete(ex);
|
||||||
|
|
||||||
|
// We re-throw here so we can communicate that there was an error when sending
|
||||||
|
// the close frame
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// We're done writing
|
// We're done writing
|
||||||
|
_application.Output.Complete();
|
||||||
|
|
||||||
_logger.ReceiveStopped();
|
_logger.ReceiveStopped();
|
||||||
_transportCts.Cancel();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendMessages()
|
private async Task StartSending(WebSocket socket)
|
||||||
{
|
{
|
||||||
_logger.SendStarted();
|
|
||||||
|
|
||||||
var webSocketMessageType =
|
var webSocketMessageType =
|
||||||
Mode == TransferMode.Binary
|
Mode == TransferMode.Binary
|
||||||
? WebSocketMessageType.Binary
|
? WebSocketMessageType.Binary
|
||||||
: WebSocketMessageType.Text;
|
: WebSocketMessageType.Text;
|
||||||
|
|
||||||
|
Exception error = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var result = await _application.Input.ReadAsync(_transportCts.Token);
|
var result = await _application.Input.ReadAsync();
|
||||||
var buffer = result.Buffer;
|
var buffer = result.Buffer;
|
||||||
|
|
||||||
|
// Get a frame from the application
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (result.IsCancelled)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (!buffer.IsEmpty)
|
if (!buffer.IsEmpty)
|
||||||
{
|
{
|
||||||
_logger.ReceivedFromApp(buffer.Length);
|
try
|
||||||
|
{
|
||||||
|
_logger.ReceivedFromApp(buffer.Length);
|
||||||
|
|
||||||
await _webSocket.SendAsync(buffer, webSocketMessageType, _transportCts.Token);
|
if (WebSocketCanSend(socket))
|
||||||
|
{
|
||||||
|
await socket.SendAsync(buffer, webSocketMessageType);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!_aborted)
|
||||||
|
{
|
||||||
|
_logger.ErrorSendingMessage(ex);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (result.IsCompleted)
|
else if (result.IsCompleted)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
_logger.SendMessageCanceled();
|
|
||||||
await CloseWebSocket();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorSendingMessage(ex);
|
|
||||||
await CloseWebSocket();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_application.Input.AdvanceTo(buffer.End);
|
_application.Input.AdvanceTo(buffer.End);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.SendCanceled();
|
error = ex;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
if (WebSocketCanSend(socket))
|
||||||
|
{
|
||||||
|
// We're done sending, send the close frame to the client if the websocket is still open
|
||||||
|
await socket.CloseOutputAsync(error != null ? WebSocketCloseStatus.InternalServerError : WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
_application.Input.Complete();
|
||||||
|
|
||||||
_logger.SendStopped();
|
_logger.SendStopped();
|
||||||
TriggerCancel();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool WebSocketCanSend(WebSocket ws)
|
||||||
|
{
|
||||||
|
return !(ws.State == WebSocketState.Aborted ||
|
||||||
|
ws.State == WebSocketState.Closed ||
|
||||||
|
ws.State == WebSocketState.CloseSent);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task Connect(Uri url)
|
private async Task Connect(Uri url)
|
||||||
{
|
{
|
||||||
var uriBuilder = new UriBuilder(url);
|
var uriBuilder = new UriBuilder(url);
|
||||||
|
|
@ -224,7 +312,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
{
|
{
|
||||||
_logger.TransportStopping();
|
_logger.TransportStopping();
|
||||||
|
|
||||||
await CloseWebSocket();
|
// Cancel any pending reads from the application, this should start the entire shutdown process
|
||||||
|
_application.Input.CancelPendingRead();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -235,38 +324,5 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
// exceptions have been handled in the Running task continuation by closing the channel with the exception
|
// exceptions have been handled in the Running task continuation by closing the channel with the exception
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CloseWebSocket()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Best effort - it's still possible (but not likely) that the transport is being closed via StopAsync
|
|
||||||
// while the webSocket is being closed due to an error.
|
|
||||||
if (_webSocket.State != WebSocketState.Closed)
|
|
||||||
{
|
|
||||||
_logger.ClosingWebSocket();
|
|
||||||
|
|
||||||
// We intentionally don't pass _transportCts.Token to CloseOutputAsync. The token can be cancelled
|
|
||||||
// for reasons not related to webSocket in which case we would not close the websocket gracefully.
|
|
||||||
await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
|
|
||||||
|
|
||||||
// shutdown the transport after a timeout in case the server does not send close frame
|
|
||||||
TriggerCancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// This is benign - the exception can happen due to the race described above because we would
|
|
||||||
// try closing the webSocket twice.
|
|
||||||
_logger.ClosingWebSocketFailed(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TriggerCancel()
|
|
||||||
{
|
|
||||||
// Give server 5 seconds to respond with a close frame for graceful close.
|
|
||||||
_receiveCts.CancelAfter(TimeSpan.FromSeconds(5));
|
|
||||||
_transportCts.Cancel();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Transports
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IDuplexPipe _application;
|
private readonly IDuplexPipe _application;
|
||||||
private readonly DefaultConnectionContext _connection;
|
private readonly DefaultConnectionContext _connection;
|
||||||
|
private volatile bool _aborted;
|
||||||
|
|
||||||
public WebSocketsTransport(WebSocketOptions options, IDuplexPipe application, DefaultConnectionContext connection, ILoggerFactory loggerFactory)
|
public WebSocketsTransport(WebSocketOptions options, IDuplexPipe application, DefaultConnectionContext connection, ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
|
|
@ -68,41 +69,68 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Transports
|
||||||
var receiving = StartReceiving(socket);
|
var receiving = StartReceiving(socket);
|
||||||
var sending = StartSending(socket);
|
var sending = StartSending(socket);
|
||||||
|
|
||||||
// Wait for something to shut down.
|
// Wait for send or receive to complete
|
||||||
var trigger = await Task.WhenAny(
|
var trigger = await Task.WhenAny(receiving, sending);
|
||||||
receiving,
|
|
||||||
sending);
|
|
||||||
|
|
||||||
var failed = trigger.IsCanceled || trigger.IsFaulted;
|
|
||||||
var task = Task.CompletedTask;
|
|
||||||
if (trigger == receiving)
|
if (trigger == receiving)
|
||||||
{
|
{
|
||||||
task = sending;
|
|
||||||
_logger.WaitingForSend();
|
_logger.WaitingForSend();
|
||||||
|
|
||||||
|
// We're waiting for the application to finish and there are 2 things it could be doing
|
||||||
|
// 1. Waiting for application data
|
||||||
|
// 2. Waiting for a websocket send to complete
|
||||||
|
|
||||||
|
// Cancel the application so that ReadAsync yields
|
||||||
|
_application.Input.CancelPendingRead();
|
||||||
|
|
||||||
|
using (var delayCts = new CancellationTokenSource())
|
||||||
|
{
|
||||||
|
var resultTask = await Task.WhenAny(sending, Task.Delay(_options.CloseTimeout, delayCts.Token));
|
||||||
|
|
||||||
|
if (resultTask != sending)
|
||||||
|
{
|
||||||
|
// We timed out so now we're in ungraceful shutdown mode
|
||||||
|
_logger.CloseTimedOut();
|
||||||
|
|
||||||
|
// Abort the websocket if we're stuck in a pending send to the client
|
||||||
|
_aborted = true;
|
||||||
|
|
||||||
|
socket.Abort();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
delayCts.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
task = receiving;
|
|
||||||
_logger.WaitingForClose();
|
_logger.WaitingForClose();
|
||||||
|
|
||||||
|
// We're waiting on the websocket to close and there are 2 things it could be doing
|
||||||
|
// 1. Waiting for websocket data
|
||||||
|
// 2. Waiting on a flush to complete (backpressure being applied)
|
||||||
|
|
||||||
|
using (var delayCts = new CancellationTokenSource())
|
||||||
|
{
|
||||||
|
var resultTask = await Task.WhenAny(receiving, Task.Delay(_options.CloseTimeout, delayCts.Token));
|
||||||
|
|
||||||
|
if (resultTask != receiving)
|
||||||
|
{
|
||||||
|
// Abort the websocket if we're stuck in a pending receive from the client
|
||||||
|
_aborted = true;
|
||||||
|
|
||||||
|
socket.Abort();
|
||||||
|
|
||||||
|
// Cancel any pending flush so that we can quit
|
||||||
|
_application.Output.CancelPendingFlush();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
delayCts.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await socket.CloseOutputAsync(failed ? WebSocketCloseStatus.InternalServerError : WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
|
||||||
|
|
||||||
var resultTask = await Task.WhenAny(task, Task.Delay(_options.CloseTimeout));
|
|
||||||
|
|
||||||
if (resultTask != task)
|
|
||||||
{
|
|
||||||
_logger.CloseTimedOut();
|
|
||||||
socket.Abort();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Observe any exceptions from second completed task
|
|
||||||
task.GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Observe any exceptions from original completed task
|
|
||||||
trigger.GetAwaiter().GetResult();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task StartReceiving(WebSocket socket)
|
private async Task StartReceiving(WebSocket socket)
|
||||||
|
|
@ -133,10 +161,32 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Transports
|
||||||
|
|
||||||
if (receiveResult.EndOfMessage)
|
if (receiveResult.EndOfMessage)
|
||||||
{
|
{
|
||||||
await _application.Output.FlushAsync();
|
var flushResult = await _application.Output.FlushAsync();
|
||||||
|
|
||||||
|
// We canceled in the middle of applying back pressure
|
||||||
|
// or if the consumer is done
|
||||||
|
if (flushResult.IsCancelled || flushResult.IsCompleted)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Ignore aborts, don't treat them like transport errors
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!_aborted)
|
||||||
|
{
|
||||||
|
_application.Output.Complete(ex);
|
||||||
|
|
||||||
|
// We re-throw here so we can communicate that there was an error when sending
|
||||||
|
// the close frame
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// We're done writing
|
// We're done writing
|
||||||
|
|
@ -144,54 +194,81 @@ namespace Microsoft.AspNetCore.Sockets.Internal.Transports
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task StartSending(WebSocket ws)
|
private async Task StartSending(WebSocket socket)
|
||||||
{
|
{
|
||||||
while (true)
|
Exception error = null;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var result = await _application.Input.ReadAsync();
|
while (true)
|
||||||
var buffer = result.Buffer;
|
|
||||||
|
|
||||||
// Get a frame from the application
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (!buffer.IsEmpty)
|
var result = await _application.Input.ReadAsync();
|
||||||
|
var buffer = result.Buffer;
|
||||||
|
|
||||||
|
// Get a frame from the application
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
try
|
if (result.IsCancelled)
|
||||||
{
|
{
|
||||||
_logger.SendPayload(buffer.Length);
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
var webSocketMessageType = (_connection.TransferMode == TransferMode.Binary
|
if (!buffer.IsEmpty)
|
||||||
? WebSocketMessageType.Binary
|
{
|
||||||
: WebSocketMessageType.Text);
|
try
|
||||||
|
|
||||||
if (WebSocketCanSend(ws))
|
|
||||||
{
|
{
|
||||||
await ws.SendAsync(buffer, webSocketMessageType);
|
_logger.SendPayload(buffer.Length);
|
||||||
|
|
||||||
|
var webSocketMessageType = (_connection.TransferMode == TransferMode.Binary
|
||||||
|
? WebSocketMessageType.Binary
|
||||||
|
: WebSocketMessageType.Text);
|
||||||
|
|
||||||
|
if (WebSocketCanSend(socket))
|
||||||
|
{
|
||||||
|
await socket.SendAsync(buffer, webSocketMessageType);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!_aborted)
|
||||||
|
{
|
||||||
|
_logger.ErrorWritingFrame(ex);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (WebSocketException socketException) when (!WebSocketCanSend(ws))
|
else if (result.IsCompleted)
|
||||||
{
|
{
|
||||||
// this can happen when we send the CloseFrame to the client and try to write afterwards
|
|
||||||
_logger.SendFailed(socketException);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorWritingFrame(ex);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (result.IsCompleted)
|
finally
|
||||||
{
|
{
|
||||||
break;
|
_application.Input.AdvanceTo(buffer.End);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
_application.Input.AdvanceTo(buffer.End);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
error = ex;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Send the close frame before calling into user code
|
||||||
|
if (WebSocketCanSend(socket))
|
||||||
|
{
|
||||||
|
// We're done sending, send the close frame to the client if the websocket is still open
|
||||||
|
await socket.CloseOutputAsync(error != null ? WebSocketCloseStatus.InternalServerError : WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
_application.Input.Complete();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool WebSocketCanSend(WebSocket ws)
|
private static bool WebSocketCanSend(WebSocket ws)
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,9 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Complete the client side if there's an error
|
||||||
|
_output.TryComplete();
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TransportFailsWhenClientDisconnectsAbnormally()
|
public async Task TransportCommunicatesErrorToApplicationWhenClientDisconnectsAbnormally()
|
||||||
{
|
{
|
||||||
using (StartLog(out var loggerFactory, LogLevel.Debug))
|
using (StartLog(out var loggerFactory, LogLevel.Debug))
|
||||||
{
|
{
|
||||||
|
|
@ -119,12 +119,21 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
||||||
{
|
{
|
||||||
async Task CompleteApplicationAfterTransportCompletes()
|
async Task CompleteApplicationAfterTransportCompletes()
|
||||||
{
|
{
|
||||||
// Wait until the transport completes so that we can end the application
|
try
|
||||||
var result = await connection.Transport.Input.ReadAsync();
|
{
|
||||||
connection.Transport.Input.AdvanceTo(result.Buffer.End);
|
// Wait until the transport completes so that we can end the application
|
||||||
|
var result = await connection.Transport.Input.ReadAsync();
|
||||||
// Complete the application so that the connection unwinds without aborting
|
connection.Transport.Input.AdvanceTo(result.Buffer.End);
|
||||||
connection.Transport.Output.Complete();
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Assert.IsType<WebSocketError>(ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Complete the application so that the connection unwinds without aborting
|
||||||
|
connection.Transport.Output.Complete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectionContext = new DefaultConnectionContext(string.Empty, null, null);
|
var connectionContext = new DefaultConnectionContext(string.Empty, null, null);
|
||||||
|
|
@ -144,7 +153,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
||||||
feature.Client.SendAbort();
|
feature.Client.SendAbort();
|
||||||
|
|
||||||
// Wait for the transport
|
// Wait for the transport
|
||||||
await Assert.ThrowsAsync<WebSocketException>(() => transport).OrTimeout();
|
await transport.OrTimeout();
|
||||||
|
|
||||||
await client.OrTimeout();
|
await client.OrTimeout();
|
||||||
}
|
}
|
||||||
|
|
@ -178,8 +187,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
||||||
// Close from the client
|
// Close from the client
|
||||||
await feature.Client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
await feature.Client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||||
|
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => transport.OrTimeout());
|
await transport.OrTimeout();
|
||||||
Assert.Equal("Catastrophic failure.", ex.Message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -247,7 +255,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
|
||||||
// fail the client to server channel
|
// fail the client to server channel
|
||||||
connection.Transport.Output.Complete(new Exception());
|
connection.Transport.Output.Complete(new Exception());
|
||||||
|
|
||||||
await Assert.ThrowsAsync<Exception>(() => transport).OrTimeout();
|
await transport.OrTimeout();
|
||||||
|
|
||||||
Assert.Equal(WebSocketState.Aborted, serverSocket.State);
|
Assert.Equal(WebSocketState.Aborted, serverSocket.State);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue