Websocket handshake perf (#12386)

This commit is contained in:
Brennan 2019-10-31 17:43:06 -07:00 committed by GitHub
parent 0e8fea6fee
commit 0a61879cf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 146 additions and 21 deletions

View File

@ -293,6 +293,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SpaServices.Extensions.Tests", "SpaServices.Extensions\test\Microsoft.AspNetCore.SpaServices.Extensions.Tests.csproj", "{D0CB733B-4CE8-4F6C-BBB9-548EA1A96966}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebSockets.Microbenchmarks", "perf\Microbenchmarks\Microsoft.AspNetCore.WebSockets.Microbenchmarks.csproj", "{C4D624B3-749E-41D8-A43B-B304BC3885EA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Perf", "Perf", "{4623F52E-2070-4631-8DEE-7D2F48733FFD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -1599,6 +1603,18 @@ Global
{D0CB733B-4CE8-4F6C-BBB9-548EA1A96966}.Release|x64.Build.0 = Release|Any CPU
{D0CB733B-4CE8-4F6C-BBB9-548EA1A96966}.Release|x86.ActiveCfg = Release|Any CPU
{D0CB733B-4CE8-4F6C-BBB9-548EA1A96966}.Release|x86.Build.0 = Release|Any CPU
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Debug|x64.ActiveCfg = Debug|Any CPU
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Debug|x64.Build.0 = Debug|Any CPU
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Debug|x86.ActiveCfg = Debug|Any CPU
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Debug|x86.Build.0 = Debug|Any CPU
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Release|Any CPU.Build.0 = Release|Any CPU
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Release|x64.ActiveCfg = Release|Any CPU
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Release|x64.Build.0 = Release|Any CPU
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Release|x86.ActiveCfg = Release|Any CPU
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1725,6 +1741,7 @@ Global
{46B4FE62-06A1-4D54-B3E8-D8B4B3560075} = {ACA6DDB9-7592-47CE-A740-D15BF307E9E0}
{92E11EBB-759E-4DA8-AB61-A9977D9F97D0} = {ACA6DDB9-7592-47CE-A740-D15BF307E9E0}
{D0CB733B-4CE8-4F6C-BBB9-548EA1A96966} = {D6FA4ABE-E685-4EDD-8B06-D8777E76B472}
{C4D624B3-749E-41D8-A43B-B304BC3885EA} = {4623F52E-2070-4631-8DEE-7D2F48733FFD}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {83786312-A93B-4BB4-AB06-7C6913A59AFA}

View File

@ -23,6 +23,15 @@ namespace Microsoft.AspNetCore.WebSockets
HeaderNames.SecWebSocketVersion
};
// "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
private static ReadOnlySpan<byte> _encodedWebSocketKey => new byte[]
{
(byte)'2', (byte)'5', (byte)'8', (byte)'E', (byte)'A', (byte)'F', (byte)'A', (byte)'5', (byte)'-',
(byte)'E', (byte)'9', (byte)'1', (byte)'4', (byte)'-', (byte)'4', (byte)'7', (byte)'D', (byte)'A',
(byte)'-', (byte)'9', (byte)'5', (byte)'C', (byte)'A', (byte)'-', (byte)'C', (byte)'5', (byte)'A',
(byte)'B', (byte)'0', (byte)'D', (byte)'C', (byte)'8', (byte)'5', (byte)'B', (byte)'1', (byte)'1'
};
// Verify Method, Upgrade, Connection, version, key, etc..
public static bool CheckSupportedWebSocketRequest(string method, IEnumerable<KeyValuePair<string, string>> headers)
{
@ -87,34 +96,34 @@ namespace Microsoft.AspNetCore.WebSockets
{
return false;
}
try
{
byte[] data = Convert.FromBase64String(value);
return data.Length == 16;
}
catch (Exception)
{
return false;
}
Span<byte> temp = stackalloc byte[16];
var success = Convert.TryFromBase64String(value, temp, out var written);
return success && written == 16;
}
public static string CreateResponseKey(string requestKey)
{
// "The value of this header field is constructed by concatenating /key/, defined above in step 4
// in Section 4.2.2, with the string "258EAFA5- E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of
// in Section 4.2.2, with the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of
// this concatenated value to obtain a 20-byte value and base64-encoding"
// https://tools.ietf.org/html/rfc6455#section-4.2.2
if (requestKey == null)
{
throw new ArgumentNullException(nameof(requestKey));
}
using (var algorithm = SHA1.Create())
{
string merged = requestKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
byte[] mergedBytes = Encoding.UTF8.GetBytes(merged);
byte[] hashedBytes = algorithm.ComputeHash(mergedBytes);
// requestKey is already verified to be small (24 bytes) by 'IsRequestKeyValid()' and everything is 1:1 mapping to UTF8 bytes
// so this can be hardcoded to 60 bytes for the requestKey + static websocket string
Span<byte> mergedBytes = stackalloc byte[60];
Encoding.UTF8.GetBytes(requestKey, mergedBytes);
_encodedWebSocketKey.CopyTo(mergedBytes.Slice(24));
Span<byte> hashedBytes = stackalloc byte[20];
var success = algorithm.TryComputeHash(mergedBytes, hashedBytes, out var written);
if (!success || written != 20)
{
throw new InvalidOperationException("Could not compute the hash for the 'Sec-WebSocket-Accept' header.");
}
return Convert.ToBase64String(hashedBytes);
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core web socket middleware for use on top of opaque servers.</Description>
@ -17,4 +17,8 @@
<Reference Include="Microsoft.Extensions.Options" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.AspNetCore.WebSockets.Tests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.WebSockets.MicroBenchmarks" />
</ItemGroup>
</Project>

View File

@ -11,7 +11,6 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
@ -146,7 +145,7 @@ namespace Microsoft.AspNetCore.WebSockets
}
}
string key = string.Join(", ", _context.Request.Headers[HeaderNames.SecWebSocketKey]);
string key = _context.Request.Headers[HeaderNames.SecWebSocketKey];
HandshakeHelpers.GenerateResponseHeaders(key, subProtocol, _context.Response.Headers);

View File

@ -0,0 +1,41 @@
// 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 Xunit;
namespace Microsoft.AspNetCore.WebSockets.Tests
{
public class HandshakeTests
{
[Fact]
public void CreatesCorrectResponseKey()
{
// Example taken from https://tools.ietf.org/html/rfc6455#section-1.3
var key = "dGhlIHNhbXBsZSBub25jZQ==";
var expectedResponse = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=";
var response = HandshakeHelpers.CreateResponseKey(key);
Assert.Equal(expectedResponse, response);
}
[Theory]
[InlineData("VUfWn1u2Ot0AICM6f+/8Zg==")]
public void AcceptsValidRequestKeys(string key)
{
Assert.True(HandshakeHelpers.IsRequestKeyValid(key));
}
[Theory]
// 17 bytes when decoded
[InlineData("dGhpcyBpcyAxNyBieXRlcy4=")]
// 15 bytes when decoded
[InlineData("dGhpcyBpcyAxNWJ5dGVz")]
[InlineData("")]
[InlineData("24 length not base64 str")]
public void RejectsInvalidRequestKeys(string key)
{
Assert.False(HandshakeHelpers.IsRequestKeyValid(key));
}
}
}

View File

@ -0,0 +1 @@
[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark]

View File

@ -0,0 +1,41 @@
// 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 BenchmarkDotNet.Attributes;
namespace Microsoft.AspNetCore.WebSockets.Microbenchmarks
{
public class HandshakeBenchmark
{
private string[] _requestKeys = {
"F8/qpj9RYr2/sIymdDvlmw==",
"PyQi8nyMkKnI7JKiAJ/IrA==",
"CUe0z8ItSBRtgJlPqP1+SQ==",
"w9vo1A9oM56M31qPQYKL6g==",
"+vqFGD9U04QOxKdWHrduTQ==",
"xsfuh2ZOm5O7zTzFPWJGUA==",
"TvmUzr4DgBLcDYX88kEAyw==",
"EZ5tcEOxWm7tF6adFXLSQg==",
"bkmoBhqwbbRzL8H9hvH1tQ==",
"EUwBrmmwivd5czsxz9eRzQ==",
};
[Benchmark(OperationsPerInvoke = 10)]
public void CreateResponseKey()
{
foreach (var key in _requestKeys)
{
HandshakeHelpers.CreateResponseKey(key);
}
}
[Benchmark(OperationsPerInvoke = 10)]
public void IsRequestKeyValid()
{
foreach (var key in _requestKeys)
{
HandshakeHelpers.IsRequestKeyValid(key);
}
}
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Reference Include="BenchmarkDotNet" />
<Reference Include="Microsoft.AspNetCore.BenchmarkRunner.Sources" />
<Reference Include="Microsoft.AspNetCore.WebSockets" />
</ItemGroup>
</Project>