Skip to content

Commit 0a61879

Browse files
Websocket handshake perf (#12386)
1 parent 0e8fea6 commit 0a61879

File tree

8 files changed

+146
-21
lines changed

8 files changed

+146
-21
lines changed

src/Middleware/Middleware.sln

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server
293293
EndProject
294294
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}"
295295
EndProject
296+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebSockets.Microbenchmarks", "perf\Microbenchmarks\Microsoft.AspNetCore.WebSockets.Microbenchmarks.csproj", "{C4D624B3-749E-41D8-A43B-B304BC3885EA}"
297+
EndProject
298+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Perf", "Perf", "{4623F52E-2070-4631-8DEE-7D2F48733FFD}"
299+
EndProject
296300
Global
297301
GlobalSection(SolutionConfigurationPlatforms) = preSolution
298302
Debug|Any CPU = Debug|Any CPU
@@ -1599,6 +1603,18 @@ Global
15991603
{D0CB733B-4CE8-4F6C-BBB9-548EA1A96966}.Release|x64.Build.0 = Release|Any CPU
16001604
{D0CB733B-4CE8-4F6C-BBB9-548EA1A96966}.Release|x86.ActiveCfg = Release|Any CPU
16011605
{D0CB733B-4CE8-4F6C-BBB9-548EA1A96966}.Release|x86.Build.0 = Release|Any CPU
1606+
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
1607+
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
1608+
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Debug|x64.ActiveCfg = Debug|Any CPU
1609+
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Debug|x64.Build.0 = Debug|Any CPU
1610+
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Debug|x86.ActiveCfg = Debug|Any CPU
1611+
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Debug|x86.Build.0 = Debug|Any CPU
1612+
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
1613+
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Release|Any CPU.Build.0 = Release|Any CPU
1614+
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Release|x64.ActiveCfg = Release|Any CPU
1615+
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Release|x64.Build.0 = Release|Any CPU
1616+
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Release|x86.ActiveCfg = Release|Any CPU
1617+
{C4D624B3-749E-41D8-A43B-B304BC3885EA}.Release|x86.Build.0 = Release|Any CPU
16021618
EndGlobalSection
16031619
GlobalSection(SolutionProperties) = preSolution
16041620
HideSolutionNode = FALSE
@@ -1725,6 +1741,7 @@ Global
17251741
{46B4FE62-06A1-4D54-B3E8-D8B4B3560075} = {ACA6DDB9-7592-47CE-A740-D15BF307E9E0}
17261742
{92E11EBB-759E-4DA8-AB61-A9977D9F97D0} = {ACA6DDB9-7592-47CE-A740-D15BF307E9E0}
17271743
{D0CB733B-4CE8-4F6C-BBB9-548EA1A96966} = {D6FA4ABE-E685-4EDD-8B06-D8777E76B472}
1744+
{C4D624B3-749E-41D8-A43B-B304BC3885EA} = {4623F52E-2070-4631-8DEE-7D2F48733FFD}
17281745
EndGlobalSection
17291746
GlobalSection(ExtensibilityGlobals) = postSolution
17301747
SolutionGuid = {83786312-A93B-4BB4-AB06-7C6913A59AFA}

src/Middleware/WebSockets/src/HandshakeHelpers.cs

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ internal static class HandshakeHelpers
2323
HeaderNames.SecWebSocketVersion
2424
};
2525

26+
// "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
27+
private static ReadOnlySpan<byte> _encodedWebSocketKey => new byte[]
28+
{
29+
(byte)'2', (byte)'5', (byte)'8', (byte)'E', (byte)'A', (byte)'F', (byte)'A', (byte)'5', (byte)'-',
30+
(byte)'E', (byte)'9', (byte)'1', (byte)'4', (byte)'-', (byte)'4', (byte)'7', (byte)'D', (byte)'A',
31+
(byte)'-', (byte)'9', (byte)'5', (byte)'C', (byte)'A', (byte)'-', (byte)'C', (byte)'5', (byte)'A',
32+
(byte)'B', (byte)'0', (byte)'D', (byte)'C', (byte)'8', (byte)'5', (byte)'B', (byte)'1', (byte)'1'
33+
};
34+
2635
// Verify Method, Upgrade, Connection, version, key, etc..
2736
public static bool CheckSupportedWebSocketRequest(string method, IEnumerable<KeyValuePair<string, string>> headers)
2837
{
@@ -87,34 +96,34 @@ public static bool IsRequestKeyValid(string value)
8796
{
8897
return false;
8998
}
90-
try
91-
{
92-
byte[] data = Convert.FromBase64String(value);
93-
return data.Length == 16;
94-
}
95-
catch (Exception)
96-
{
97-
return false;
98-
}
99+
100+
Span<byte> temp = stackalloc byte[16];
101+
var success = Convert.TryFromBase64String(value, temp, out var written);
102+
return success && written == 16;
99103
}
100104

101105
public static string CreateResponseKey(string requestKey)
102106
{
103107
// "The value of this header field is constructed by concatenating /key/, defined above in step 4
104-
// in Section 4.2.2, with the string "258EAFA5- E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of
108+
// in Section 4.2.2, with the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of
105109
// this concatenated value to obtain a 20-byte value and base64-encoding"
106110
// https://tools.ietf.org/html/rfc6455#section-4.2.2
107111

108-
if (requestKey == null)
109-
{
110-
throw new ArgumentNullException(nameof(requestKey));
111-
}
112-
113112
using (var algorithm = SHA1.Create())
114113
{
115-
string merged = requestKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
116-
byte[] mergedBytes = Encoding.UTF8.GetBytes(merged);
117-
byte[] hashedBytes = algorithm.ComputeHash(mergedBytes);
114+
// requestKey is already verified to be small (24 bytes) by 'IsRequestKeyValid()' and everything is 1:1 mapping to UTF8 bytes
115+
// so this can be hardcoded to 60 bytes for the requestKey + static websocket string
116+
Span<byte> mergedBytes = stackalloc byte[60];
117+
Encoding.UTF8.GetBytes(requestKey, mergedBytes);
118+
_encodedWebSocketKey.CopyTo(mergedBytes.Slice(24));
119+
120+
Span<byte> hashedBytes = stackalloc byte[20];
121+
var success = algorithm.TryComputeHash(mergedBytes, hashedBytes, out var written);
122+
if (!success || written != 20)
123+
{
124+
throw new InvalidOperationException("Could not compute the hash for the 'Sec-WebSocket-Accept' header.");
125+
}
126+
118127
return Convert.ToBase64String(hashedBytes);
119128
}
120129
}

src/Middleware/WebSockets/src/Microsoft.AspNetCore.WebSockets.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<Description>ASP.NET Core web socket middleware for use on top of opaque servers.</Description>
@@ -17,4 +17,8 @@
1717
<Reference Include="Microsoft.Extensions.Options" />
1818
</ItemGroup>
1919

20+
<ItemGroup>
21+
<InternalsVisibleTo Include="Microsoft.AspNetCore.WebSockets.Tests" />
22+
<InternalsVisibleTo Include="Microsoft.AspNetCore.WebSockets.MicroBenchmarks" />
23+
</ItemGroup>
2024
</Project>

src/Middleware/WebSockets/src/WebSocketMiddleware.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
using Microsoft.AspNetCore.Http;
1212
using Microsoft.AspNetCore.Http.Features;
1313
using Microsoft.Extensions.Logging;
14-
using Microsoft.Extensions.Logging.Abstractions;
1514
using Microsoft.Extensions.Options;
1615
using Microsoft.Extensions.Primitives;
1716
using Microsoft.Net.Http.Headers;
@@ -146,7 +145,7 @@ public async Task<WebSocket> AcceptAsync(WebSocketAcceptContext acceptContext)
146145
}
147146
}
148147

149-
string key = string.Join(", ", _context.Request.Headers[HeaderNames.SecWebSocketKey]);
148+
string key = _context.Request.Headers[HeaderNames.SecWebSocketKey];
150149

151150
HandshakeHelpers.GenerateResponseHeaders(key, subProtocol, _context.Response.Headers);
152151

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Xunit;
5+
6+
namespace Microsoft.AspNetCore.WebSockets.Tests
7+
{
8+
public class HandshakeTests
9+
{
10+
[Fact]
11+
public void CreatesCorrectResponseKey()
12+
{
13+
// Example taken from https://tools.ietf.org/html/rfc6455#section-1.3
14+
var key = "dGhlIHNhbXBsZSBub25jZQ==";
15+
var expectedResponse = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=";
16+
17+
var response = HandshakeHelpers.CreateResponseKey(key);
18+
19+
Assert.Equal(expectedResponse, response);
20+
}
21+
22+
[Theory]
23+
[InlineData("VUfWn1u2Ot0AICM6f+/8Zg==")]
24+
public void AcceptsValidRequestKeys(string key)
25+
{
26+
Assert.True(HandshakeHelpers.IsRequestKeyValid(key));
27+
}
28+
29+
[Theory]
30+
// 17 bytes when decoded
31+
[InlineData("dGhpcyBpcyAxNyBieXRlcy4=")]
32+
// 15 bytes when decoded
33+
[InlineData("dGhpcyBpcyAxNWJ5dGVz")]
34+
[InlineData("")]
35+
[InlineData("24 length not base64 str")]
36+
public void RejectsInvalidRequestKeys(string key)
37+
{
38+
Assert.False(HandshakeHelpers.IsRequestKeyValid(key));
39+
}
40+
}
41+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using BenchmarkDotNet.Attributes;
5+
6+
namespace Microsoft.AspNetCore.WebSockets.Microbenchmarks
7+
{
8+
public class HandshakeBenchmark
9+
{
10+
private string[] _requestKeys = {
11+
"F8/qpj9RYr2/sIymdDvlmw==",
12+
"PyQi8nyMkKnI7JKiAJ/IrA==",
13+
"CUe0z8ItSBRtgJlPqP1+SQ==",
14+
"w9vo1A9oM56M31qPQYKL6g==",
15+
"+vqFGD9U04QOxKdWHrduTQ==",
16+
"xsfuh2ZOm5O7zTzFPWJGUA==",
17+
"TvmUzr4DgBLcDYX88kEAyw==",
18+
"EZ5tcEOxWm7tF6adFXLSQg==",
19+
"bkmoBhqwbbRzL8H9hvH1tQ==",
20+
"EUwBrmmwivd5czsxz9eRzQ==",
21+
};
22+
23+
[Benchmark(OperationsPerInvoke = 10)]
24+
public void CreateResponseKey()
25+
{
26+
foreach (var key in _requestKeys)
27+
{
28+
HandshakeHelpers.CreateResponseKey(key);
29+
}
30+
}
31+
32+
[Benchmark(OperationsPerInvoke = 10)]
33+
public void IsRequestKeyValid()
34+
{
35+
foreach (var key in _requestKeys)
36+
{
37+
HandshakeHelpers.IsRequestKeyValid(key);
38+
}
39+
}
40+
}
41+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<Reference Include="BenchmarkDotNet" />
10+
<Reference Include="Microsoft.AspNetCore.BenchmarkRunner.Sources" />
11+
<Reference Include="Microsoft.AspNetCore.WebSockets" />
12+
</ItemGroup>
13+
</Project>

0 commit comments

Comments
 (0)