diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingWriteStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingWriteStream.cs index 7a612f64c3854..7db23377bc596 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingWriteStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingWriteStream.cs @@ -11,6 +11,7 @@ internal sealed partial class HttpConnection : IDisposable { private sealed class ChunkedEncodingWriteStream : HttpContentWriteStream { + private static readonly byte[] s_crlfBytes = "\r\n"u8.ToArray(); private static readonly byte[] s_finalChunkBytes = "0\r\n\r\n"u8.ToArray(); public ChunkedEncodingWriteStream(HttpConnection connection) : base(connection) @@ -31,12 +32,14 @@ public override void Write(ReadOnlySpan buffer) } // Write chunk length in hex followed by \r\n - connection.WriteHexInt32Async(buffer.Length, async: false).GetAwaiter().GetResult(); - connection.WriteTwoBytesAsync((byte)'\r', (byte)'\n', async: false).GetAwaiter().GetResult(); + ValueTask writeTask = connection.WriteHexInt32Async(buffer.Length, async: false); + Debug.Assert(writeTask.IsCompleted); + writeTask.GetAwaiter().GetResult(); + connection.Write(s_crlfBytes); // Write chunk contents followed by \r\n connection.Write(buffer); - connection.WriteTwoBytesAsync((byte)'\r', (byte)'\n', async: false).GetAwaiter().GetResult(); + connection.Write(s_crlfBytes); } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken ignored) @@ -62,11 +65,11 @@ static async ValueTask WriteChunkAsync(HttpConnection connection, ReadOnlyMemory { // Write chunk length in hex followed by \r\n await connection.WriteHexInt32Async(buffer.Length, async: true).ConfigureAwait(false); - await connection.WriteTwoBytesAsync((byte)'\r', (byte)'\n', async: true).ConfigureAwait(false); + await connection.WriteAsync(s_crlfBytes).ConfigureAwait(false); // Write chunk contents followed by \r\n - await connection.WriteAsync(buffer, async: true).ConfigureAwait(false); - await connection.WriteTwoBytesAsync((byte)'\r', (byte)'\n', async: true).ConfigureAwait(false); + await connection.WriteAsync(buffer).ConfigureAwait(false); + await connection.WriteAsync(s_crlfBytes).ConfigureAwait(false); } } @@ -75,7 +78,16 @@ public override Task FinishAsync(bool async) // Send 0 byte chunk to indicate end, then final CrLf HttpConnection connection = GetConnectionOrThrow(); _connection = null; - return connection.WriteBytesAsync(s_finalChunkBytes, async); + + if (async) + { + return connection.WriteAsync(s_finalChunkBytes).AsTask(); + } + else + { + connection.Write(s_finalChunkBytes); + return Task.CompletedTask; + } } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthWriteStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthWriteStream.cs index 33add1358ab51..077fc8c6d5d3c 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthWriteStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthWriteStream.cs @@ -28,9 +28,6 @@ public override void Write(ReadOnlySpan buffer) throw new HttpRequestException(SR.net_http_content_write_larger_than_content_length); } - // Have the connection write the data, skipping the buffer. Importantly, this will - // force a flush of anything already in the buffer, i.e. any remaining request headers - // that are still buffered. HttpConnection connection = GetConnectionOrThrow(); Debug.Assert(connection._currentRequest != null); connection.Write(buffer); @@ -45,12 +42,9 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo return ValueTask.FromException(new HttpRequestException(SR.net_http_content_write_larger_than_content_length)); } - // Have the connection write the data, skipping the buffer. Importantly, this will - // force a flush of anything already in the buffer, i.e. any remaining request headers - // that are still buffered. HttpConnection connection = GetConnectionOrThrow(); Debug.Assert(connection._currentRequest != null); - return connection.WriteAsync(buffer, async: true); + return connection.WriteAsync(buffer); } public override Task FinishAsync(bool async) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs index 1068d040ff15d..eef8947f384d3 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs @@ -3,12 +3,10 @@ using System.Buffers; using System.Buffers.Text; -using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Net.Http.Headers; -using System.Net.Security; using System.Net.Sockets; using System.Runtime.CompilerServices; using System.Text; @@ -41,10 +39,6 @@ internal sealed partial class HttpConnection : HttpConnectionBase /// private const int MaxChunkBytesAllowed = 16 * 1024; - private static readonly byte[] s_contentLength0NewlineAsciiBytes = "Content-Length: 0\r\n"u8.ToArray(); - private static readonly byte[] s_spaceHttp10NewlineAsciiBytes = " HTTP/1.0\r\n"u8.ToArray(); - private static readonly byte[] s_spaceHttp11NewlineAsciiBytes = " HTTP/1.1\r\n"u8.ToArray(); - private static readonly byte[] s_httpSchemeAndDelimiter = "http://"u8.ToArray(); private static readonly ulong s_http10Bytes = BitConverter.ToUInt64("HTTP/1.0"u8); private static readonly ulong s_http11Bytes = BitConverter.ToUInt64("HTTP/1.1"u8); @@ -53,11 +47,12 @@ internal sealed partial class HttpConnection : HttpConnectionBase private readonly TransportContext? _transportContext; private HttpRequestMessage? _currentRequest; - private readonly byte[] _writeBuffer; - private int _writeOffset; + private ArrayBuffer _writeBuffer; private int _allowedReadLineBytes; + /// Reusable array used to get the values for each header being written to the wire. - private string[] _headerValues = Array.Empty(); + [ThreadStatic] + private static string[]? t_headerValues; private ValueTask? _readAheadTask; private int _readAheadTaskLock; // 0 == free, 1 == held @@ -88,7 +83,7 @@ public HttpConnection( _transportContext = transportContext; - _writeBuffer = new byte[InitialWriteBufferSize]; + _writeBuffer = new ArrayBuffer(InitialWriteBufferSize, usePool: false); _readBuffer = new ArrayBuffer(InitialReadBufferSize, usePool: false); _idleSinceTickCount = Environment.TickCount64; @@ -269,143 +264,239 @@ private void ConsumeFromRemainingBuffer(int bytesToConsume) _readBuffer.Discard(bytesToConsume); } - private async ValueTask WriteHeadersAsync(HttpHeaders headers, string? cookiesFromContainer, bool async) + private void WriteHeaders(HttpRequestMessage request, HttpMethod normalizedMethod) { - Debug.Assert(_currentRequest != null); + Debug.Assert(request.RequestUri is not null); + + // Write the request line + WriteAsciiString(normalizedMethod.Method); + _writeBuffer.EnsureAvailableSpace(1); + _writeBuffer.AvailableSpan[0] = (byte)' '; + _writeBuffer.Commit(1); - if (headers.GetEntriesArray() is HeaderEntry[] entries) + if (ReferenceEquals(normalizedMethod, HttpMethod.Connect)) { - for (int i = 0; i < headers.Count; i++) + // RFC 7231 #section-4.3.6. + // Write only CONNECT foo.com:345 HTTP/1.1 + if (!request.HasHeaders || request.Headers.Host is not string host) { - HeaderEntry header = entries[i]; - - if (header.Key.KnownHeader is KnownHeader knownHeader) - { - await WriteBytesAsync(knownHeader.AsciiBytesWithColonSpace, async).ConfigureAwait(false); - } - else - { - await WriteAsciiStringAsync(header.Key.Name, async).ConfigureAwait(false); - await WriteTwoBytesAsync((byte)':', (byte)' ', async).ConfigureAwait(false); - } - - int headerValuesCount = HttpHeaders.GetStoreValuesIntoStringArray(header.Key, header.Value, ref _headerValues); - Debug.Assert(headerValuesCount > 0, "No values for header??"); - if (headerValuesCount > 0) - { - Encoding? valueEncoding = _pool.Settings._requestHeaderEncodingSelector?.Invoke(header.Key.Name, _currentRequest); - - await WriteStringAsync(_headerValues[0], async, valueEncoding).ConfigureAwait(false); - - if (cookiesFromContainer != null && header.Key.Equals(KnownHeaders.Cookie)) - { - await WriteTwoBytesAsync((byte)';', (byte)' ', async).ConfigureAwait(false); - await WriteStringAsync(cookiesFromContainer, async, valueEncoding).ConfigureAwait(false); + throw new HttpRequestException(SR.net_http_request_no_host); + } - cookiesFromContainer = null; - } + WriteAsciiString(host); + } + else + { + if (Kind == HttpConnectionKind.Proxy) + { + // Proxied requests contain full URL + Debug.Assert(request.RequestUri.Scheme == Uri.UriSchemeHttp); + WriteBytes("http://"u8); + WriteHost(request.RequestUri); + } - // Some headers such as User-Agent and Server use space as a separator (see: ProductInfoHeaderParser) - if (headerValuesCount > 1) - { - HttpHeaderParser? parser = header.Key.Parser; - string separator = HttpHeaderParser.DefaultSeparator; - if (parser != null && parser.SupportsMultipleValues) - { - separator = parser.Separator!; - } + WriteAsciiString(request.RequestUri.PathAndQuery); + } - for (int j = 1; j < headerValuesCount; j++) - { - await WriteAsciiStringAsync(separator, async).ConfigureAwait(false); - await WriteStringAsync(_headerValues[j], async, valueEncoding).ConfigureAwait(false); - } - } - } + // Fall back to 1.1 for all versions other than 1.0 + Debug.Assert(request.Version.Major >= 0 && request.Version.Minor >= 0); // guaranteed by Version class + bool isHttp10 = request.Version.Minor == 0 && request.Version.Major == 1; + WriteBytes(isHttp10 ? " HTTP/1.0\r\n"u8 : " HTTP/1.1\r\n"u8); - await WriteTwoBytesAsync((byte)'\r', (byte)'\n', async).ConfigureAwait(false); + // Write special additional headers. If a host isn't in the headers list, then a Host header + // wasn't set, so as it's required by HTTP 1.1 spec, send one based on the Request Uri. + if (!request.HasHeaders || request.Headers.Host is null) + { + if (_pool.HostHeaderLineBytes is byte[] hostHeaderLineBytes) + { + Debug.Assert(Kind != HttpConnectionKind.Proxy); + WriteBytes(hostHeaderLineBytes); + } + else + { + Debug.Assert(Kind == HttpConnectionKind.Proxy); + WriteBytes(KnownHeaders.Host.AsciiBytesWithColonSpace); + WriteHost(request.RequestUri); + WriteCRLF(); } } - if (cookiesFromContainer != null) + // Determine cookies to send + string? cookiesFromContainer = null; + if (_pool.Settings._useCookies) { - await WriteAsciiStringAsync(HttpKnownHeaderNames.Cookie, async).ConfigureAwait(false); - await WriteTwoBytesAsync((byte)':', (byte)' ', async).ConfigureAwait(false); - - Encoding? valueEncoding = _pool.Settings._requestHeaderEncodingSelector?.Invoke(HttpKnownHeaderNames.Cookie, _currentRequest); - await WriteStringAsync(cookiesFromContainer, async, valueEncoding).ConfigureAwait(false); - - await WriteTwoBytesAsync((byte)'\r', (byte)'\n', async).ConfigureAwait(false); + cookiesFromContainer = _pool.Settings._cookieContainer!.GetCookieHeader(request.RequestUri); + if (cookiesFromContainer == "") + { + cookiesFromContainer = null; + } } - } - private async ValueTask WriteHostHeaderAsync(Uri uri, bool async) - { - await WriteBytesAsync(KnownHeaders.Host.AsciiBytesWithColonSpace, async).ConfigureAwait(false); + // Write request headers + if (request.HasHeaders || cookiesFromContainer is not null) + { + WriteHeaderCollection(request.Headers, cookiesFromContainer); + } - if (_pool.HostHeaderValueBytes != null) + // Write content headers + if (request.Content is HttpContent content) { - Debug.Assert(Kind != HttpConnectionKind.Proxy); - await WriteBytesAsync(_pool.HostHeaderValueBytes, async).ConfigureAwait(false); + WriteHeaderCollection(content.Headers); } else { - Debug.Assert(Kind == HttpConnectionKind.Proxy); + // Write out Content-Length: 0 header to indicate no body, + // unless this is a method that never has a body. + if (normalizedMethod.MustHaveRequestBody) + { + WriteBytes("Content-Length: 0\r\n"u8); + } + } + + // CRLF for end of headers. + WriteCRLF(); + void WriteHost(Uri requestUri) + { // Uri.IdnHost is missing '[', ']' characters around IPv6 address // and it also contains ScopeID for Link-Local addresses - if (uri.HostNameType == UriHostNameType.IPv6) + string host = requestUri.HostNameType == UriHostNameType.IPv6 ? requestUri.Host : requestUri.IdnHost; + WriteAsciiString(host); + + if (!requestUri.IsDefaultPort) { - await WriteAsciiStringAsync(uri.Host, async).ConfigureAwait(false); + _writeBuffer.EnsureAvailableSpace(6); + Span buffer = _writeBuffer.AvailableSpan; + buffer[0] = (byte)':'; + bool success = Utf8Formatter.TryFormat(requestUri.Port, buffer.Slice(1), out int bytesWritten); + Debug.Assert(success); + _writeBuffer.Commit(bytesWritten + 1); + } + } + } + + private void WriteHeaderCollection(HttpHeaders headers, string? cookiesFromContainer = null) + { + Debug.Assert(_currentRequest is not null); + + HeaderEncodingSelector? encodingSelector = _pool.Settings._requestHeaderEncodingSelector; + ref string[]? headerValues = ref t_headerValues; + + foreach (HeaderEntry header in headers.GetEntries()) + { + if (header.Key.KnownHeader is KnownHeader knownHeader) + { + WriteBytes(knownHeader.AsciiBytesWithColonSpace); } else { - await WriteAsciiStringAsync(uri.IdnHost, async).ConfigureAwait(false); + WriteAsciiString(header.Key.Name); + WriteBytes(": "u8); + } + + int headerValuesCount = HttpHeaders.GetStoreValuesIntoStringArray(header.Key, header.Value, ref headerValues); + Debug.Assert(headerValuesCount > 0, "No values for header??"); + + Encoding? valueEncoding = encodingSelector?.Invoke(header.Key.Name, _currentRequest); + + WriteString(headerValues[0], valueEncoding); + + if (cookiesFromContainer is not null && header.Key.Equals(KnownHeaders.Cookie)) + { + WriteBytes("; "u8); // Cookies use "; " as the separator + WriteString(cookiesFromContainer, valueEncoding); + cookiesFromContainer = null; } - if (!uri.IsDefaultPort) + // Some headers such as User-Agent and Server use space as a separator (see: ProductInfoHeaderParser) + if (headerValuesCount > 1) { - await WriteByteAsync((byte)':', async).ConfigureAwait(false); - await WriteDecimalInt32Async(uri.Port, async).ConfigureAwait(false); + HttpHeaderParser? parser = header.Key.Parser; + string separator = HttpHeaderParser.DefaultSeparator; + if (parser != null && parser.SupportsMultipleValues) + { + separator = parser.Separator!; + } + + for (int i = 1; i < headerValuesCount; i++) + { + WriteAsciiString(separator); + WriteString(headerValues[i], valueEncoding); + } } + + WriteCRLF(); } - await WriteTwoBytesAsync((byte)'\r', (byte)'\n', async).ConfigureAwait(false); + if (cookiesFromContainer is not null) + { + WriteBytes(KnownHeaders.Cookie.AsciiBytesWithColonSpace); + WriteString(cookiesFromContainer, encodingSelector?.Invoke(HttpKnownHeaderNames.Cookie, _currentRequest)); + WriteCRLF(); + } } - private Task WriteDecimalInt32Async(int value, bool async) + private void WriteCRLF() { - // Try to format into our output buffer directly. - if (Utf8Formatter.TryFormat(value, new Span(_writeBuffer, _writeOffset, _writeBuffer.Length - _writeOffset), out int bytesWritten)) - { - _writeOffset += bytesWritten; - return Task.CompletedTask; - } + _writeBuffer.EnsureAvailableSpace(2); + Span buffer = _writeBuffer.AvailableSpan; + buffer[1] = (byte)'\n'; + buffer[0] = (byte)'\r'; + _writeBuffer.Commit(2); + } - // If we don't have enough room, do it the slow way. - return WriteAsciiStringAsync(value.ToString(), async); + private void WriteBytes(ReadOnlySpan bytes) + { + _writeBuffer.EnsureAvailableSpace(bytes.Length); + bytes.CopyTo(_writeBuffer.AvailableSpan); + _writeBuffer.Commit(bytes.Length); } - private Task WriteHexInt32Async(int value, bool async) + private void WriteAsciiString(string s) { - // Try to format into our output buffer directly. - if (Utf8Formatter.TryFormat(value, new Span(_writeBuffer, _writeOffset, _writeBuffer.Length - _writeOffset), out int bytesWritten, 'X')) + _writeBuffer.EnsureAvailableSpace(s.Length); + int length = Encoding.ASCII.GetBytes(s, _writeBuffer.AvailableSpan); + Debug.Assert(length == s.Length); + Debug.Assert(Encoding.ASCII.GetString(_writeBuffer.AvailableSpan.Slice(0, length)) == s); + _writeBuffer.Commit(length); + } + + private void WriteString(string s, Encoding? encoding) + { + if (encoding is null) { - _writeOffset += bytesWritten; - return Task.CompletedTask; + _writeBuffer.EnsureAvailableSpace(s.Length); + Span buffer = _writeBuffer.AvailableSpan; + for (int i = 0; i < s.Length; i++) + { + char c = s[i]; + if (!char.IsAscii(c)) + { + ThrowForInvalidCharEncoding(); + } + buffer[i] = (byte)c; + } + _writeBuffer.Commit(s.Length); + } + else + { + _writeBuffer.EnsureAvailableSpace(encoding.GetMaxByteCount(s.Length)); + int length = encoding.GetBytes(s, _writeBuffer.AvailableSpan); + _writeBuffer.Commit(length); } - // If we don't have enough room, do it the slow way. - return WriteAsciiStringAsync(value.ToString("X", CultureInfo.InvariantCulture), async); + static void ThrowForInvalidCharEncoding() => + throw new HttpRequestException(SR.net_http_request_invalid_char_encoding); } public async Task SendAsyncCore(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { - TaskCompletionSource? allowExpect100ToContinue = null; - Task? sendRequestContentTask = null; Debug.Assert(_currentRequest == null, $"Expected null {nameof(_currentRequest)}."); Debug.Assert(_readBuffer.ActiveLength == 0, "Unexpected data in read buffer"); + TaskCompletionSource? allowExpect100ToContinue = null; + Task? sendRequestContentTask = null; + _currentRequest = request; HttpMethod normalizedMethod = HttpMethod.Normalize(request.Method); @@ -419,98 +510,7 @@ public async Task SendAsyncCore(HttpRequestMessage request, { if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.RequestHeadersStart(); - Debug.Assert(request.RequestUri != null); - // Write request line - await WriteStringAsync(normalizedMethod.Method, async).ConfigureAwait(false); - await WriteByteAsync((byte)' ', async).ConfigureAwait(false); - - if (ReferenceEquals(normalizedMethod, HttpMethod.Connect)) - { - // RFC 7231 #section-4.3.6. - // Write only CONNECT foo.com:345 HTTP/1.1 - if (!request.HasHeaders || request.Headers.Host == null) - { - throw new HttpRequestException(SR.net_http_request_no_host); - } - await WriteAsciiStringAsync(request.Headers.Host, async).ConfigureAwait(false); - } - else - { - if (Kind == HttpConnectionKind.Proxy) - { - // Proxied requests contain full URL - Debug.Assert(request.RequestUri.Scheme == Uri.UriSchemeHttp); - await WriteBytesAsync(s_httpSchemeAndDelimiter, async).ConfigureAwait(false); - - // TODO https://github.com/dotnet/runtime/issues/25782: - // Uri.IdnHost is missing '[', ']' characters around IPv6 address. - // So, we need to add them manually for now. - if (request.RequestUri.HostNameType == UriHostNameType.IPv6) - { - await WriteByteAsync((byte)'[', async).ConfigureAwait(false); - await WriteAsciiStringAsync(request.RequestUri.IdnHost, async).ConfigureAwait(false); - await WriteByteAsync((byte)']', async).ConfigureAwait(false); - } - else - { - await WriteAsciiStringAsync(request.RequestUri.IdnHost, async).ConfigureAwait(false); - } - - if (!request.RequestUri.IsDefaultPort) - { - await WriteByteAsync((byte)':', async).ConfigureAwait(false); - await WriteDecimalInt32Async(request.RequestUri.Port, async).ConfigureAwait(false); - } - } - await WriteStringAsync(request.RequestUri.PathAndQuery, async).ConfigureAwait(false); - } - - // Fall back to 1.1 for all versions other than 1.0 - Debug.Assert(request.Version.Major >= 0 && request.Version.Minor >= 0); // guaranteed by Version class - bool isHttp10 = request.Version.Minor == 0 && request.Version.Major == 1; - await WriteBytesAsync(isHttp10 ? s_spaceHttp10NewlineAsciiBytes : s_spaceHttp11NewlineAsciiBytes, async).ConfigureAwait(false); - - // Determine cookies to send - string? cookiesFromContainer = null; - if (_pool.Settings._useCookies) - { - cookiesFromContainer = _pool.Settings._cookieContainer!.GetCookieHeader(request.RequestUri); - if (cookiesFromContainer == "") - { - cookiesFromContainer = null; - } - } - - // Write special additional headers. If a host isn't in the headers list, then a Host header - // wasn't sent, so as it's required by HTTP 1.1 spec, send one based on the Request Uri. - if (!request.HasHeaders || request.Headers.Host == null) - { - await WriteHostHeaderAsync(request.RequestUri, async).ConfigureAwait(false); - } - - // Write request headers - if (request.HasHeaders || cookiesFromContainer != null) - { - await WriteHeadersAsync(request.Headers, cookiesFromContainer, async).ConfigureAwait(false); - } - - if (request.Content == null) - { - // Write out Content-Length: 0 header to indicate no body, - // unless this is a method that never has a body. - if (normalizedMethod.MustHaveRequestBody) - { - await WriteBytesAsync(s_contentLength0NewlineAsciiBytes, async).ConfigureAwait(false); - } - } - else - { - // Write content headers - await WriteHeadersAsync(request.Content.Headers, cookiesFromContainer: null, async).ConfigureAwait(false); - } - - // CRLF for end of headers. - await WriteTwoBytesAsync((byte)'\r', (byte)'\n', async).ConfigureAwait(false); + WriteHeaders(request, normalizedMethod); if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.RequestHeadersStop(); @@ -1310,21 +1310,14 @@ private void ProcessKeepAliveHeader(string keepAlive) private void WriteToBuffer(ReadOnlySpan source) { - Debug.Assert(source.Length <= _writeBuffer.Length - _writeOffset); - source.CopyTo(new Span(_writeBuffer, _writeOffset, source.Length)); - _writeOffset += source.Length; - } - - private void WriteToBuffer(ReadOnlyMemory source) - { - Debug.Assert(source.Length <= _writeBuffer.Length - _writeOffset); - source.Span.CopyTo(new Span(_writeBuffer, _writeOffset, source.Length)); - _writeOffset += source.Length; + Debug.Assert(source.Length <= _writeBuffer.AvailableLength); + source.CopyTo(_writeBuffer.AvailableSpan); + _writeBuffer.Commit(source.Length); } private void Write(ReadOnlySpan source) { - int remaining = _writeBuffer.Length - _writeOffset; + int remaining = _writeBuffer.AvailableLength; if (source.Length <= remaining) { @@ -1333,7 +1326,7 @@ private void Write(ReadOnlySpan source) return; } - if (_writeOffset != 0) + if (_writeBuffer.ActiveLength != 0) { // Fit what we can in the current write buffer and flush it. WriteToBuffer(source.Slice(0, remaining)); @@ -1341,7 +1334,7 @@ private void Write(ReadOnlySpan source) Flush(); } - if (source.Length >= _writeBuffer.Length) + if (source.Length >= _writeBuffer.Capacity) { // Large write. No sense buffering this. Write directly to stream. WriteToStream(source); @@ -1353,43 +1346,66 @@ private void Write(ReadOnlySpan source) } } - private async ValueTask WriteAsync(ReadOnlyMemory source, bool async) + private ValueTask WriteAsync(ReadOnlyMemory source) { - int remaining = _writeBuffer.Length - _writeOffset; + int remaining = _writeBuffer.AvailableLength; if (source.Length <= remaining) { // Fits in current write buffer. Just copy and return. - WriteToBuffer(source); - return; + WriteToBuffer(source.Span); + return default; } - if (_writeOffset != 0) + if (_writeBuffer.ActiveLength != 0) { // Fit what we can in the current write buffer and flush it. - WriteToBuffer(source.Slice(0, remaining)); + WriteToBuffer(source.Span.Slice(0, remaining)); source = source.Slice(remaining); - await FlushAsync(async).ConfigureAwait(false); - } - if (source.Length >= _writeBuffer.Length) - { - // Large write. No sense buffering this. Write directly to stream. - await WriteToStreamAsync(source, async).ConfigureAwait(false); + ValueTask flushTask = FlushAsync(async: true); + + if (flushTask.IsCompletedSuccessfully) + { + flushTask.GetAwaiter().GetResult(); + + if (source.Length <= _writeBuffer.Capacity) + { + WriteToBuffer(source.Span); + return default; + } + + // Fall-through to WriteToStreamAsync + } + else + { + return AwaitFlushAndWriteAsync(flushTask, source); + } } - else + + // Large write. No sense buffering this. Write directly to stream. + return WriteToStreamAsync(source, async: true); + + async ValueTask AwaitFlushAndWriteAsync(ValueTask flushTask, ReadOnlyMemory source) { - // Copy remainder into buffer - WriteToBuffer(source); + await flushTask.ConfigureAwait(false); + + if (source.Length <= _writeBuffer.Capacity) + { + WriteToBuffer(source.Span); + } + else + { + await WriteToStreamAsync(source, async: true).ConfigureAwait(false); + } } } private void WriteWithoutBuffering(ReadOnlySpan source) { - if (_writeOffset != 0) + if (_writeBuffer.ActiveLength != 0) { - int remaining = _writeBuffer.Length - _writeOffset; - if (source.Length <= remaining) + if (source.Length <= _writeBuffer.AvailableLength) { // There's something already in the write buffer, but the content // we're writing can also fit after it in the write buffer. Copy @@ -1410,21 +1426,20 @@ private void WriteWithoutBuffering(ReadOnlySpan source) private ValueTask WriteWithoutBufferingAsync(ReadOnlyMemory source, bool async) { - if (_writeOffset == 0) + if (_writeBuffer.ActiveLength == 0) { // There's nothing in the write buffer we need to flush. // Just write the supplied data out to the stream. return WriteToStreamAsync(source, async); } - int remaining = _writeBuffer.Length - _writeOffset; - if (source.Length <= remaining) + if (source.Length <= _writeBuffer.AvailableLength) { // There's something already in the write buffer, but the content // we're writing can also fit after it in the write buffer. Copy // the content to the write buffer and then flush it, so that we // can do a single send rather than two. - WriteToBuffer(source); + WriteToBuffer(source.Span); return FlushAsync(async); } @@ -1439,188 +1454,47 @@ private async ValueTask FlushThenWriteWithoutBufferingAsync(ReadOnlyMemory await WriteToStreamAsync(source, async).ConfigureAwait(false); } - private Task WriteByteAsync(byte b, bool async) - { - if (_writeOffset < _writeBuffer.Length) - { - _writeBuffer[_writeOffset++] = b; - return Task.CompletedTask; - } - return WriteByteSlowAsync(b, async); - } - - private async Task WriteByteSlowAsync(byte b, bool async) - { - Debug.Assert(_writeOffset == _writeBuffer.Length); - await WriteToStreamAsync(_writeBuffer, async).ConfigureAwait(false); - - _writeBuffer[0] = b; - _writeOffset = 1; - } - - private Task WriteTwoBytesAsync(byte b1, byte b2, bool async) - { - if (_writeOffset <= _writeBuffer.Length - 2) - { - byte[] buffer = _writeBuffer; - buffer[_writeOffset++] = b1; - buffer[_writeOffset++] = b2; - return Task.CompletedTask; - } - return WriteTwoBytesSlowAsync(b1, b2, async); - } - - private async Task WriteTwoBytesSlowAsync(byte b1, byte b2, bool async) - { - await WriteByteAsync(b1, async).ConfigureAwait(false); - await WriteByteAsync(b2, async).ConfigureAwait(false); - } - - private Task WriteBytesAsync(byte[] bytes, bool async) - { - if (_writeOffset <= _writeBuffer.Length - bytes.Length) - { - Buffer.BlockCopy(bytes, 0, _writeBuffer, _writeOffset, bytes.Length); - _writeOffset += bytes.Length; - return Task.CompletedTask; - } - return WriteBytesSlowAsync(bytes, bytes.Length, async); - } - - private async Task WriteBytesSlowAsync(byte[] bytes, int length, bool async) + private ValueTask WriteHexInt32Async(int value, bool async) { - int offset = 0; - while (true) - { - int remaining = length - offset; - int toCopy = Math.Min(remaining, _writeBuffer.Length - _writeOffset); - Buffer.BlockCopy(bytes, offset, _writeBuffer, _writeOffset, toCopy); - _writeOffset += toCopy; - offset += toCopy; - - Debug.Assert(offset <= length, $"Expected {nameof(offset)} to be <= {length}, got {offset}"); - Debug.Assert(_writeOffset <= _writeBuffer.Length, $"Expected {nameof(_writeOffset)} to be <= {_writeBuffer.Length}, got {_writeOffset}"); - if (offset == length) - { - break; - } - else if (_writeOffset == _writeBuffer.Length) - { - await WriteToStreamAsync(_writeBuffer, async).ConfigureAwait(false); - _writeOffset = 0; - } - } - } - - private Task WriteStringAsync(string s, bool async) - { - // If there's enough space in the buffer to just copy all of the string's bytes, do so. - // Unlike WriteAsciiStringAsync, validate each char along the way. - int offset = _writeOffset; - if (s.Length <= _writeBuffer.Length - offset) - { - byte[] writeBuffer = _writeBuffer; - foreach (char c in s) - { - if ((c & 0xFF80) != 0) - { - throw new HttpRequestException(SR.net_http_request_invalid_char_encoding); - } - writeBuffer[offset++] = (byte)c; - } - _writeOffset = offset; - return Task.CompletedTask; - } - - // Otherwise, fall back to doing a normal slow string write; we could optimize away - // the extra checks later, but the case where we cross a buffer boundary should be rare. - return WriteStringAsyncSlow(s, async); - } - - private Task WriteStringAsync(string s, bool async, Encoding? encoding) - { - if (encoding is null) - { - return WriteStringAsync(s, async); - } - - // If there's enough space in the buffer to just copy all of the string's bytes, do so. - if (encoding.GetMaxByteCount(s.Length) <= _writeBuffer.Length - _writeOffset) - { - _writeOffset += encoding.GetBytes(s, _writeBuffer.AsSpan(_writeOffset)); - return Task.CompletedTask; - } - - // Otherwise, fall back to doing a normal slow string write - return WriteStringWithEncodingAsyncSlow(s, async, encoding); - } - - private async Task WriteStringWithEncodingAsyncSlow(string s, bool async, Encoding encoding) - { - // Avoid calculating the length if the rented array would be small anyway - int length = s.Length <= 512 - ? encoding.GetMaxByteCount(s.Length) - : encoding.GetByteCount(s); - - byte[] rentedBuffer = ArrayPool.Shared.Rent(length); - try - { - int written = encoding.GetBytes(s, rentedBuffer); - await WriteBytesSlowAsync(rentedBuffer, written, async).ConfigureAwait(false); - } - finally + // Try to format into our output buffer directly. + if (Utf8Formatter.TryFormat(value, _writeBuffer.AvailableSpan, out int bytesWritten, 'X')) { - ArrayPool.Shared.Return(rentedBuffer); + _writeBuffer.Commit(bytesWritten); + return default; } - } - private Task WriteAsciiStringAsync(string s, bool async) - { - // If there's enough space in the buffer to just copy all of the string's bytes, do so. - int offset = _writeOffset; - if (s.Length <= _writeBuffer.Length - offset) + // If we don't have enough room, do it the slow way. + if (async) { - OperationStatus operationStatus = Ascii.FromUtf16(s, _writeBuffer.AsSpan(offset), out int bytesWritten); - Debug.Assert(operationStatus == OperationStatus.Done); - _writeOffset = offset + bytesWritten; - - return Task.CompletedTask; + return WriteAsync(Encoding.ASCII.GetBytes(value.ToString("X", CultureInfo.InvariantCulture))); } - - // Otherwise, fall back to doing a normal slow string write; we could optimize away - // the extra checks later, but the case where we cross a buffer boundary should be rare. - return WriteStringAsyncSlow(s, async); - } - - private async Task WriteStringAsyncSlow(string s, bool async) - { - if (!Ascii.IsValid(s)) + else { - throw new HttpRequestException(SR.net_http_request_invalid_char_encoding); - } + // We should have enough capacity to write any hex-encoded int after flushing the buffer. + Debug.Assert(_writeBuffer.Capacity >= 8); - for (int i = 0; i < s.Length; i++) - { - await WriteByteAsync((byte)s[i], async).ConfigureAwait(false); + Flush(); + return WriteHexInt32Async(value, async: false); } } private void Flush() { - if (_writeOffset > 0) + ReadOnlySpan bytes = _writeBuffer.ActiveSpan; + if (bytes.Length > 0) { - WriteToStream(new ReadOnlySpan(_writeBuffer, 0, _writeOffset)); - _writeOffset = 0; + _writeBuffer.Discard(bytes.Length); + WriteToStream(bytes); } } private ValueTask FlushAsync(bool async) { - if (_writeOffset > 0) + ReadOnlyMemory bytes = _writeBuffer.ActiveMemory; + if (bytes.Length > 0) { - ValueTask t = WriteToStreamAsync(new ReadOnlyMemory(_writeBuffer, 0, _writeOffset), async); - _writeOffset = 0; - return t; + _writeBuffer.Discard(bytes.Length); + return WriteToStreamAsync(bytes, async); } return default; } @@ -2089,7 +1963,7 @@ internal void DetachFromPool() private void CompleteResponse() { Debug.Assert(_currentRequest != null, "Expected the connection to be associated with a request."); - Debug.Assert(_writeOffset == 0, "Everything in write buffer should have been flushed."); + Debug.Assert(_writeBuffer.ActiveLength == 0, "Everything in write buffer should have been flushed."); // Disassociate the connection from a request. _currentRequest = null; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index c10a3bf1ea032..ecb084e149ddf 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -108,7 +108,7 @@ internal sealed class HttpConnectionPool : IDisposable internal uint _lastSeenHttp3MaxHeaderListSize; /// For non-proxy connection pools, this is the host name in bytes; for proxies, null. - private readonly byte[]? _hostHeaderValueBytes; + private readonly byte[]? _hostHeaderLineBytes; /// Options specialized and cached for this pool and its key. private readonly SslClientAuthenticationOptions? _sslOptionsHttp11; private readonly SslClientAuthenticationOptions? _sslOptionsHttp2; @@ -242,8 +242,15 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK _originAuthority.HostValue; // Note the IDN hostname should always be ASCII, since it's already been IDNA encoded. - _hostHeaderValueBytes = Encoding.ASCII.GetBytes(hostHeader); - Debug.Assert(Encoding.ASCII.GetString(_hostHeaderValueBytes) == hostHeader); + byte[] hostHeaderLine = new byte[6 + hostHeader.Length + 2]; // Host: foo\r\n + "Host: "u8.CopyTo(hostHeaderLine); + Encoding.ASCII.GetBytes(hostHeader, hostHeaderLine.AsSpan(6)); + hostHeaderLine[^2] = (byte)'\r'; + hostHeaderLine[^1] = (byte)'\n'; + _hostHeaderLineBytes = hostHeaderLine; + + Debug.Assert(Encoding.ASCII.GetString(_hostHeaderLineBytes) == $"Host: {hostHeader}\r\n"); + if (sslHostName == null) { _http2EncodedAuthorityHostHeader = HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(H2StaticTable.Authority, hostHeader); @@ -348,7 +355,7 @@ private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnection public bool IsSecure => _kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel || _kind == HttpConnectionKind.SslSocksTunnel; public Uri? ProxyUri => _proxyUri; public ICredentials? ProxyCredentials => _poolManager.ProxyCredentials; - public byte[]? HostHeaderValueBytes => _hostHeaderValueBytes; + public byte[]? HostHeaderLineBytes => _hostHeaderLineBytes; public CredentialCache? PreAuthCredentials { get; } ///