Websocket handshake perf (#12386)
This commit is contained in:
parent
0e8fea6fee
commit
0a61879cf7
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark]
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue