337 lines
11 KiB
C#
337 lines
11 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Buffers;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.IO.Pipelines;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Connections;
|
|
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
|
|
{
|
|
internal sealed class SocketConnection : TransportConnection, IDisposable
|
|
{
|
|
private static readonly int MinAllocBufferSize = KestrelMemoryPool.MinimumSegmentSize / 2;
|
|
private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
|
private static readonly bool IsMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
|
|
|
|
private readonly Socket _socket;
|
|
private readonly PipeScheduler _scheduler;
|
|
private readonly ISocketsTrace _trace;
|
|
private readonly SocketReceiver _receiver;
|
|
private readonly SocketSender _sender;
|
|
private readonly CancellationTokenSource _connectionClosedTokenSource = new CancellationTokenSource();
|
|
|
|
private readonly object _shutdownLock = new object();
|
|
private volatile bool _aborted;
|
|
private volatile ConnectionAbortedException _abortReason;
|
|
private long _totalBytesWritten;
|
|
|
|
internal SocketConnection(Socket socket, MemoryPool<byte> memoryPool, PipeScheduler scheduler, ISocketsTrace trace)
|
|
{
|
|
Debug.Assert(socket != null);
|
|
Debug.Assert(memoryPool != null);
|
|
Debug.Assert(trace != null);
|
|
|
|
_socket = socket;
|
|
MemoryPool = memoryPool;
|
|
_scheduler = scheduler;
|
|
_trace = trace;
|
|
|
|
var localEndPoint = (IPEndPoint)_socket.LocalEndPoint;
|
|
var remoteEndPoint = (IPEndPoint)_socket.RemoteEndPoint;
|
|
|
|
LocalAddress = localEndPoint.Address;
|
|
LocalPort = localEndPoint.Port;
|
|
|
|
RemoteAddress = remoteEndPoint.Address;
|
|
RemotePort = remoteEndPoint.Port;
|
|
|
|
ConnectionClosed = _connectionClosedTokenSource.Token;
|
|
|
|
// On *nix platforms, Sockets already dispatches to the ThreadPool.
|
|
// Yes, the IOQueues are still used for the PipeSchedulers. This is intentional.
|
|
// https://github.com/aspnet/KestrelHttpServer/issues/2573
|
|
var awaiterScheduler = IsWindows ? _scheduler : PipeScheduler.Inline;
|
|
|
|
_receiver = new SocketReceiver(_socket, awaiterScheduler);
|
|
_sender = new SocketSender(_socket, awaiterScheduler);
|
|
}
|
|
|
|
public override MemoryPool<byte> MemoryPool { get; }
|
|
public override PipeScheduler InputWriterScheduler => _scheduler;
|
|
public override PipeScheduler OutputReaderScheduler => _scheduler;
|
|
public override long TotalBytesWritten => Interlocked.Read(ref _totalBytesWritten);
|
|
|
|
public async Task StartAsync()
|
|
{
|
|
try
|
|
{
|
|
// Spawn send and receive logic
|
|
var receiveTask = DoReceive();
|
|
var sendTask = DoSend();
|
|
|
|
// Now wait for both to complete
|
|
await receiveTask;
|
|
await sendTask;
|
|
|
|
_receiver.Dispose();
|
|
_sender.Dispose();
|
|
ThreadPool.QueueUserWorkItem(state => ((SocketConnection)state).CancelConnectionClosedToken(), this);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_trace.LogError(0, ex, $"Unexpected exception in {nameof(SocketConnection)}.{nameof(StartAsync)}.");
|
|
}
|
|
}
|
|
|
|
public override void Abort(ConnectionAbortedException abortReason)
|
|
{
|
|
_abortReason = abortReason;
|
|
Output.CancelPendingRead();
|
|
|
|
// Try to gracefully close the socket to match libuv behavior.
|
|
Shutdown();
|
|
}
|
|
|
|
// Only called after connection middleware is complete which means the ConnectionClosed token has fired.
|
|
public void Dispose()
|
|
{
|
|
_connectionClosedTokenSource.Dispose();
|
|
}
|
|
|
|
private async Task DoReceive()
|
|
{
|
|
Exception error = null;
|
|
|
|
try
|
|
{
|
|
await ProcessReceives();
|
|
}
|
|
catch (SocketException ex) when (IsConnectionResetError(ex.SocketErrorCode))
|
|
{
|
|
// A connection reset can be reported as SocketError.ConnectionAborted on Windows
|
|
if (!_aborted)
|
|
{
|
|
error = new ConnectionResetException(ex.Message, ex);
|
|
_trace.ConnectionReset(ConnectionId);
|
|
}
|
|
}
|
|
catch (SocketException ex) when (IsConnectionAbortError(ex.SocketErrorCode))
|
|
{
|
|
if (!_aborted)
|
|
{
|
|
// Calling Dispose after ReceiveAsync can cause an "InvalidArgument" error on *nix.
|
|
_trace.ConnectionError(ConnectionId, error);
|
|
}
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
if (!_aborted)
|
|
{
|
|
_trace.ConnectionError(ConnectionId, error);
|
|
}
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
error = ex;
|
|
_trace.ConnectionError(ConnectionId, error);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = new IOException(ex.Message, ex);
|
|
_trace.ConnectionError(ConnectionId, error);
|
|
}
|
|
finally
|
|
{
|
|
if (_aborted)
|
|
{
|
|
error = error ?? _abortReason ?? new ConnectionAbortedException();
|
|
}
|
|
|
|
Input.Complete(error);
|
|
}
|
|
}
|
|
|
|
private async Task ProcessReceives()
|
|
{
|
|
while (true)
|
|
{
|
|
// MacOS blocked on https://github.com/dotnet/corefx/issues/31766
|
|
if (!IsMacOS)
|
|
{
|
|
// Wait for data before allocating a buffer.
|
|
await _receiver.WaitForDataAsync();
|
|
}
|
|
|
|
// Ensure we have some reasonable amount of buffer space
|
|
var buffer = Input.GetMemory(MinAllocBufferSize);
|
|
|
|
var bytesReceived = await _receiver.ReceiveAsync(buffer);
|
|
|
|
if (bytesReceived == 0)
|
|
{
|
|
// FIN
|
|
_trace.ConnectionReadFin(ConnectionId);
|
|
break;
|
|
}
|
|
|
|
Input.Advance(bytesReceived);
|
|
|
|
var flushTask = Input.FlushAsync();
|
|
|
|
if (!flushTask.IsCompleted)
|
|
{
|
|
_trace.ConnectionPause(ConnectionId);
|
|
|
|
await flushTask;
|
|
|
|
_trace.ConnectionResume(ConnectionId);
|
|
}
|
|
|
|
var result = flushTask.GetAwaiter().GetResult();
|
|
if (result.IsCompleted)
|
|
{
|
|
// Pipe consumer is shut down, do we stop writing
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task DoSend()
|
|
{
|
|
Exception error = null;
|
|
|
|
try
|
|
{
|
|
await ProcessSends();
|
|
}
|
|
catch (SocketException ex) when (IsConnectionResetError(ex.SocketErrorCode))
|
|
{
|
|
// A connection reset can be reported as SocketError.ConnectionAborted on Windows
|
|
error = null;
|
|
_trace.ConnectionReset(ConnectionId);
|
|
}
|
|
catch (SocketException ex) when (IsConnectionAbortError(ex.SocketErrorCode))
|
|
{
|
|
error = null;
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
error = null;
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
error = ex;
|
|
_trace.ConnectionError(ConnectionId, error);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = new IOException(ex.Message, ex);
|
|
_trace.ConnectionError(ConnectionId, error);
|
|
}
|
|
finally
|
|
{
|
|
Shutdown();
|
|
|
|
// Complete the output after disposing the socket
|
|
Output.Complete(error);
|
|
}
|
|
}
|
|
|
|
private async Task ProcessSends()
|
|
{
|
|
while (true)
|
|
{
|
|
var result = await Output.ReadAsync();
|
|
|
|
if (result.IsCanceled)
|
|
{
|
|
break;
|
|
}
|
|
|
|
var buffer = result.Buffer;
|
|
|
|
var end = buffer.End;
|
|
var isCompleted = result.IsCompleted;
|
|
if (!buffer.IsEmpty)
|
|
{
|
|
await _sender.SendAsync(buffer);
|
|
}
|
|
|
|
// This is not interlocked because there could be a concurrent writer.
|
|
// Instead it's to prevent read tearing on 32-bit systems.
|
|
Interlocked.Add(ref _totalBytesWritten, buffer.Length);
|
|
|
|
Output.AdvanceTo(end);
|
|
|
|
if (isCompleted)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Shutdown()
|
|
{
|
|
lock (_shutdownLock)
|
|
{
|
|
if (!_aborted)
|
|
{
|
|
// Make sure to close the connection only after the _aborted flag is set.
|
|
// Without this, the RequestsCanBeAbortedMidRead test will sometimes fail when
|
|
// a BadHttpRequestException is thrown instead of a TaskCanceledException.
|
|
_aborted = true;
|
|
_trace.ConnectionWriteFin(ConnectionId);
|
|
|
|
try
|
|
{
|
|
// Try to gracefully close the socket even for aborts to match libuv behavior.
|
|
_socket.Shutdown(SocketShutdown.Both);
|
|
}
|
|
catch
|
|
{
|
|
// Ignore any errors from Socket.Shutdown since we're tearing down the connection anyway.
|
|
}
|
|
|
|
_socket.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CancelConnectionClosedToken()
|
|
{
|
|
try
|
|
{
|
|
_connectionClosedTokenSource.Cancel();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_trace.LogError(0, ex, $"Unexpected exception in {nameof(SocketConnection)}.{nameof(CancelConnectionClosedToken)}.");
|
|
}
|
|
}
|
|
|
|
private static bool IsConnectionResetError(SocketError errorCode)
|
|
{
|
|
return errorCode == SocketError.ConnectionReset ||
|
|
errorCode == SocketError.ConnectionAborted ||
|
|
errorCode == SocketError.Shutdown;
|
|
}
|
|
|
|
private static bool IsConnectionAbortError(SocketError errorCode)
|
|
{
|
|
return errorCode == SocketError.OperationAborted ||
|
|
errorCode == SocketError.Interrupted ||
|
|
errorCode == SocketError.InvalidArgument;
|
|
}
|
|
}
|
|
}
|