Skip to content

Commit 58767e1

Browse files
committed
Release 5.5.0 with netstandard2.0 support
1 parent 298b5ce commit 58767e1

18 files changed

Lines changed: 260 additions & 133 deletions

.github/workflows/dotnet-core-branches.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,19 @@ jobs:
2121
run: dotnet build --configuration Release --no-restore
2222
- name: Test
2323
run: dotnet test --no-restore --verbosity normal
24+
25+
net-framework:
26+
27+
runs-on: windows-latest
28+
29+
steps:
30+
- uses: actions/checkout@v4
31+
- name: Setup .NET SDK
32+
uses: actions/setup-dotnet@v4
33+
with:
34+
dotnet-version: 10.0.x
35+
36+
- name: Build .NET Framework sample
37+
run: dotnet build test_integration\Websocket.Client.Sample.NetFramework\Websocket.Client.Sample.NetFramework.csproj --configuration Release --framework net472
38+
- name: Build .NET Framework benchmarks
39+
run: dotnet build benchmarks\Websocket.Client.Benchmarks\Websocket.Client.Benchmarks.csproj --configuration Release --framework net472

.github/workflows/dotnet-core.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,19 @@ jobs:
3535
run: dotnet nuget push *.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source "https://api.nuget.org/v3/index.json" --skip-duplicate
3636
#- name: Publish library (Github)
3737
# run: dotnet nuget push *.nupkg --api-key ${{secrets.PUBLISH_TO_GITHUB_TOKEN}} --source "https://nuget.pkg.github.com/marfusios/index.json" --skip-duplicate
38+
39+
net-framework:
40+
41+
runs-on: windows-latest
42+
43+
steps:
44+
- uses: actions/checkout@v4
45+
- name: Setup .NET SDK
46+
uses: actions/setup-dotnet@v4
47+
with:
48+
dotnet-version: 10.0.x
49+
50+
- name: Build .NET Framework sample
51+
run: dotnet build test_integration\Websocket.Client.Sample.NetFramework\Websocket.Client.Sample.NetFramework.csproj --configuration Release --framework net472
52+
- name: Build .NET Framework benchmarks
53+
run: dotnet build benchmarks\Websocket.Client.Benchmarks\Websocket.Client.Benchmarks.csproj --configuration Release --framework net472

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<!-- Project defaults -->
44

55
<PropertyGroup>
6-
<Version>5.4.0</Version>
6+
<Version>5.5.0</Version>
77
</PropertyGroup>
88

99
</Project>

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ This is a wrapper over native C# class `ClientWebSocket` with built-in reconnect
1515
### Features
1616

1717
* installation via NuGet ([Websocket.Client](https://www.nuget.org/packages/Websocket.Client))
18-
* targeting .NET Standard 2.1, .NET 6, .NET 7, .NET 8, .NET 9, and .NET 10
18+
* targeting .NET Standard 2.0, .NET Standard 2.1, .NET 6, .NET 7, .NET 8, .NET 9, and .NET 10
19+
* full .NET Framework support through the .NET Standard 2.0 asset; the included framework sample targets .NET Framework 4.7.2 on Windows
1920
* reactive extensions ([Rx.NET](https://github.com/Reactive-Extensions/Rx.NET))
2021
* integrated logging abstraction (`Microsoft.Extensions.Logging`)
2122
* using Channels for high performance sending queue
@@ -43,6 +44,7 @@ Representative BenchmarkDotNet results show meaningful improvements on typical s
4344
* disabled trace logging: `28.72 ns / 64 B` to approximately `0 ns / 0 B`
4445
* queued text request envelope: `31.54 ns / 24 B` to `29.65 ns / 0 B`
4546
* stream-backed binary `ResponseMessage.ToString()` at 32 KB: `1.149 us / 32872 B` to `44.60 ns / 104 B`
47+
* .NET Framework 4.8.1 client receive path, via the `netstandard2.0` asset: 1000 x 1024 B text messages in `1.062 ms / 2312.21 KB`
4648

4749
For very large text messages, the resulting `string` allocation dominates the receive cost, so the library focuses on avoiding unnecessary intermediate allocations and avoiding retention of oversized receive buffers after traffic spikes.
4850

Websocket.Client.sln

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 17
4-
VisualStudioVersion = 17.0.31903.59
3+
# Visual Studio Version 18
4+
VisualStudioVersion = 18.7.11811.120 insiders
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{360B18BD-653A-4824-9261-C88167EEBAD2}"
77
EndProject
@@ -33,6 +33,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks",
3333
EndProject
3434
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Websocket.Client.Benchmarks", "benchmarks\Websocket.Client.Benchmarks\Websocket.Client.Benchmarks.csproj", "{4A962092-136C-489E-A1A7-B40D8506E048}"
3535
EndProject
36+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Websocket.Client.Sample.NetFramework", "test_integration\Websocket.Client.Sample.NetFramework\Websocket.Client.Sample.NetFramework.csproj", "{8742C4B4-85C3-F5D6-4391-68397361775F}"
37+
EndProject
3638
Global
3739
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3840
Debug|Any CPU = Debug|Any CPU
@@ -115,6 +117,18 @@ Global
115117
{4A962092-136C-489E-A1A7-B40D8506E048}.Release|x64.Build.0 = Release|Any CPU
116118
{4A962092-136C-489E-A1A7-B40D8506E048}.Release|x86.ActiveCfg = Release|Any CPU
117119
{4A962092-136C-489E-A1A7-B40D8506E048}.Release|x86.Build.0 = Release|Any CPU
120+
{8742C4B4-85C3-F5D6-4391-68397361775F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
121+
{8742C4B4-85C3-F5D6-4391-68397361775F}.Debug|Any CPU.Build.0 = Debug|Any CPU
122+
{8742C4B4-85C3-F5D6-4391-68397361775F}.Debug|x64.ActiveCfg = Debug|Any CPU
123+
{8742C4B4-85C3-F5D6-4391-68397361775F}.Debug|x64.Build.0 = Debug|Any CPU
124+
{8742C4B4-85C3-F5D6-4391-68397361775F}.Debug|x86.ActiveCfg = Debug|Any CPU
125+
{8742C4B4-85C3-F5D6-4391-68397361775F}.Debug|x86.Build.0 = Debug|Any CPU
126+
{8742C4B4-85C3-F5D6-4391-68397361775F}.Release|Any CPU.ActiveCfg = Release|Any CPU
127+
{8742C4B4-85C3-F5D6-4391-68397361775F}.Release|Any CPU.Build.0 = Release|Any CPU
128+
{8742C4B4-85C3-F5D6-4391-68397361775F}.Release|x64.ActiveCfg = Release|Any CPU
129+
{8742C4B4-85C3-F5D6-4391-68397361775F}.Release|x64.Build.0 = Release|Any CPU
130+
{8742C4B4-85C3-F5D6-4391-68397361775F}.Release|x86.ActiveCfg = Release|Any CPU
131+
{8742C4B4-85C3-F5D6-4391-68397361775F}.Release|x86.Build.0 = Release|Any CPU
118132
EndGlobalSection
119133
GlobalSection(SolutionProperties) = preSolution
120134
HideSolutionNode = FALSE
@@ -126,6 +140,7 @@ Global
126140
{085B9246-446B-4F61-A6EF-0D8949CA546D} = {42E4078C-4FBE-4B03-B889-1573332945A9}
127141
{AFAC261C-E5B5-4558-A869-6AEF5F439060} = {42E4078C-4FBE-4B03-B889-1573332945A9}
128142
{4A962092-136C-489E-A1A7-B40D8506E048} = {66320409-64EC-F7C5-3DEF-65E7510DAAD1}
143+
{8742C4B4-85C3-F5D6-4391-68397361775F} = {42E4078C-4FBE-4B03-B889-1573332945A9}
129144
EndGlobalSection
130145
GlobalSection(ExtensibilityGlobals) = postSolution
131146
SolutionGuid = {EE3E0B94-CF20-4C22-B50B-E720E8DFD675}

benchmarks/README.md

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,37 @@
22

33
This folder contains BenchmarkDotNet benchmarks for allocation-sensitive Websocket.Client hot paths.
44

5-
The latest representative run below was captured on Windows 11, AMD Ryzen 9 3900X, .NET SDK 10.0.201, running the benchmarks on .NET 10.0.5 with BenchmarkDotNet `ShortRun`.
5+
The representative .NET 10 results below were captured on Windows 11, AMD Ryzen 9 3900X, .NET SDK 10.0.201, running the benchmarks on .NET 10.0.5 with BenchmarkDotNet `ShortRun`.
66

7-
Run all benchmarks:
7+
The .NET Framework results were captured on the same machine with .NET SDK 10.0.300, BenchmarkDotNet 0.15.8, and .NET Framework 4.8.1 through the benchmark project's `net472` target.
8+
9+
Run all .NET 10 benchmarks:
10+
11+
```powershell
12+
dotnet run --configuration Release --framework net10.0 --project benchmarks\Websocket.Client.Benchmarks -- --filter "*"
13+
```
14+
15+
Run the most relevant .NET 10 receive/send benchmarks:
816

917
```powershell
10-
dotnet run --configuration Release --project benchmarks\Websocket.Client.Benchmarks -- --filter "*"
18+
dotnet run --configuration Release --framework net10.0 --project benchmarks\Websocket.Client.Benchmarks -- --filter "*ReceiveBufferBenchmarks*"
19+
dotnet run --configuration Release --framework net10.0 --project benchmarks\Websocket.Client.Benchmarks -- --filter "*TextSendEncodingBenchmarks*"
20+
dotnet run --configuration Release --framework net10.0 --project benchmarks\Websocket.Client.Benchmarks -- --filter "*TraceLoggingBenchmarks*"
21+
dotnet run --configuration Release --framework net10.0 --project benchmarks\Websocket.Client.Benchmarks -- --filter "*RequestMessageBenchmarks*"
22+
dotnet run --configuration Release --framework net10.0 --project benchmarks\Websocket.Client.Benchmarks -- --filter "*SendSequenceFramingBenchmarks*"
1123
```
1224

13-
Run the most relevant receive/send benchmarks:
25+
Run the .NET Framework client receive benchmark on Windows:
1426

1527
```powershell
16-
dotnet run --configuration Release --project benchmarks\Websocket.Client.Benchmarks -- --filter "*ReceiveBufferBenchmarks*"
17-
dotnet run --configuration Release --project benchmarks\Websocket.Client.Benchmarks -- --filter "*TextSendEncodingBenchmarks*"
18-
dotnet run --configuration Release --project benchmarks\Websocket.Client.Benchmarks -- --filter "*TraceLoggingBenchmarks*"
19-
dotnet run --configuration Release --project benchmarks\Websocket.Client.Benchmarks -- --filter "*RequestMessageBenchmarks*"
20-
dotnet run --configuration Release --project benchmarks\Websocket.Client.Benchmarks -- --filter "*SendSequenceFramingBenchmarks*"
28+
dotnet run --configuration Release --framework net472 --project benchmarks\Websocket.Client.Benchmarks -- --filter "*ClientReceiveBenchmarks*"
2129
```
2230

2331
Results are written to `BenchmarkDotNet.Artifacts\results`.
2432

2533
## What The Benchmarks Cover
2634

27-
- `ReceiveBufferBenchmarks` compares the old per-message `RecyclableMemoryStream` receive path, PR #158's `ArrayBufferWriter<byte>` path, and the current pooled receive buffer.
35+
- `ReceiveBufferBenchmarks` compares the old per-message `RecyclableMemoryStream` receive path, PR #158's `ArrayBufferWriter<byte>` path, and the current pooled receive buffer. This benchmark is .NET 10-only because the comparison uses `ArrayBufferWriter<byte>`.
2836
- `TextSendEncodingBenchmarks` compares outgoing text encoding through `Encoding.GetBytes(string)` against encoding into an `ArrayPool<byte>` buffer.
2937
- `ResponseMessageBenchmarks` measures the `ResponseMessage.ToString()` stream-copy fix.
3038
- `ObservablePropertyBenchmarks` measures cached observable properties versus creating an `AsObservable()` wrapper on every access.
@@ -109,9 +117,22 @@ Use the Ratio and Allocated columns for the quick answer. Values below 1.00 in R
109117

110118
`ClientReceiveBenchmarks` measures the current public `WebsocketClient` receive flow using an in-memory scripted `WebSocket`. It is not a before/after benchmark; it is a package-level smoke benchmark for the real client path.
111119

120+
#### .NET 10.0
121+
112122
| Messages | Message size | Mean | Allocated |
113123
| ---: | ---: | ---: | ---: |
114124
| 100 | 64 B | 10.19 us | 25.42 KB |
115125
| 100 | 1024 B | 25.09 us | 212.93 KB |
116126
| 1000 | 64 B | 81.07 us | 201.21 KB |
117127
| 1000 | 1024 B | 215.61 us | 2076.21 KB |
128+
129+
#### .NET Framework 4.8.1
130+
131+
The `net472` benchmark target consumes the library's `netstandard2.0` asset, so this measures the compatibility path used by full .NET Framework applications.
132+
133+
| Messages | Message size | Mean | Allocated |
134+
| ---: | ---: | ---: | ---: |
135+
| 100 | 64 B | 63.85 us | 51.31 KB |
136+
| 100 | 1024 B | 137.56 us | 242.04 KB |
137+
| 1000 | 64 B | 447.49 us | 421.71 KB |
138+
| 1000 | 1024 B | 1.062 ms | 2312.21 KB |

benchmarks/Websocket.Client.Benchmarks/ClientReceiveBenchmarks.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,27 @@ public async Task<int> Current_ClientReceiveTextMessages()
5555

5656
await client.Start().ConfigureAwait(false);
5757
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
58+
#if NET472
59+
return await WaitAsync(completion.Task, timeout.Token).ConfigureAwait(false);
60+
#else
5861
return await completion.Task.WaitAsync(timeout.Token).ConfigureAwait(false);
62+
#endif
5963
}
6064

65+
#if NET472
66+
private static async Task<T> WaitAsync<T>(Task<T> task, CancellationToken cancellationToken)
67+
{
68+
var cancellationTask = Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
69+
var completedTask = await Task.WhenAny(task, cancellationTask).ConfigureAwait(false);
70+
71+
if (completedTask == task)
72+
return await task.ConfigureAwait(false);
73+
74+
cancellationToken.ThrowIfCancellationRequested();
75+
throw new TimeoutException("The benchmark operation timed out.");
76+
}
77+
#endif
78+
6179
private sealed class ScriptedTextWebSocket : WebSocket
6280
{
6381
private readonly byte[][] _messages;
@@ -109,10 +127,15 @@ public override void Dispose()
109127

110128
public override async Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken)
111129
{
130+
#if NET472
131+
return await ReceiveSegmentAsync(buffer, cancellationToken).ConfigureAwait(false);
132+
#else
112133
var result = await ReceiveAsync(buffer.AsMemory(), cancellationToken).ConfigureAwait(false);
113134
return new WebSocketReceiveResult(result.Count, result.MessageType, result.EndOfMessage);
135+
#endif
114136
}
115137

138+
#if !NET472
116139
public override ValueTask<ValueWebSocketReceiveResult> ReceiveAsync(Memory<byte> buffer, CancellationToken cancellationToken)
117140
{
118141
if (_messageIndex >= _messages.Length)
@@ -141,16 +164,51 @@ public override ValueTask<ValueWebSocketReceiveResult> ReceiveAsync(Memory<byte>
141164

142165
return ValueTask.FromResult(new ValueWebSocketReceiveResult(count, WebSocketMessageType.Text, endOfMessage));
143166
}
167+
#endif
144168

145169
public override Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken)
146170
{
147171
return Task.CompletedTask;
148172
}
149173

174+
#if NET472
175+
private async Task<WebSocketReceiveResult> ReceiveSegmentAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken)
176+
{
177+
if (_messageIndex >= _messages.Length)
178+
{
179+
if (_closeReturned)
180+
{
181+
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken).ConfigureAwait(false);
182+
return new WebSocketReceiveResult(0, WebSocketMessageType.Close, true);
183+
}
184+
185+
_closeReturned = true;
186+
_state = WebSocketState.CloseReceived;
187+
_closeStatus = WebSocketCloseStatus.NormalClosure;
188+
_closeStatusDescription = "Benchmark complete";
189+
return new WebSocketReceiveResult(0, WebSocketMessageType.Close, true);
190+
}
191+
192+
var message = _messages[_messageIndex];
193+
var count = Math.Min(buffer.Count, message.Length - _messageOffset);
194+
Buffer.BlockCopy(message, _messageOffset, buffer.Array!, buffer.Offset, count);
195+
_messageOffset += count;
196+
197+
var endOfMessage = _messageOffset == message.Length;
198+
if (endOfMessage)
199+
{
200+
_messageIndex++;
201+
_messageOffset = 0;
202+
}
203+
204+
return new WebSocketReceiveResult(count, WebSocketMessageType.Text, endOfMessage);
205+
}
206+
#else
150207
private static async ValueTask<ValueWebSocketReceiveResult> WaitForCancellation(CancellationToken cancellationToken)
151208
{
152209
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken).ConfigureAwait(false);
153210
return new ValueWebSocketReceiveResult(0, WebSocketMessageType.Close, true);
154211
}
212+
#endif
155213
}
156214
}

benchmarks/Websocket.Client.Benchmarks/ReceiveBufferBenchmarks.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
namespace Websocket.Client.Benchmarks;
77

8+
#if !NET472
89
[MemoryDiagnoser]
910
[ShortRunJob]
1011
[RankColumn]
@@ -97,3 +98,4 @@ private void WriteChunks(IBufferWriter<byte> writer)
9798
}
9899
}
99100
}
101+
#endif

benchmarks/Websocket.Client.Benchmarks/Websocket.Client.Benchmarks.csproj

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net10.0</TargetFramework>
5+
<TargetFrameworks>net10.0</TargetFrameworks>
6+
<TargetFrameworks Condition="'$(OS)' == 'Windows_NT'">$(TargetFrameworks);net472</TargetFrameworks>
67
<Nullable>enable</Nullable>
8+
<LangVersion>latest</LangVersion>
79
<ImplicitUsings>enable</ImplicitUsings>
810
<IsPackable>false</IsPackable>
911
</PropertyGroup>
@@ -13,6 +15,11 @@
1315
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
1416
</ItemGroup>
1517

18+
<ItemGroup Condition="'$(TargetFramework)' == 'net472'">
19+
<PackageReference Include="System.Memory" Version="4.6.3" />
20+
<PackageReference Include="System.Threading.Channels" Version="10.0.8" />
21+
</ItemGroup>
22+
1623
<ItemGroup>
1724
<ProjectReference Include="..\..\src\Websocket.Client\Websocket.Client.csproj" />
1825
</ItemGroup>

src/Websocket.Client/PooledBufferWriter.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ private PooledBufferWriter(int initialCapacity, ArrayPool<byte> pool)
2525

2626
public ReadOnlySpan<byte> WrittenSpan => _buffer.AsSpan(0, _written);
2727

28+
public ArraySegment<byte> WrittenSegment => new ArraySegment<byte>(_buffer, 0, _written);
29+
2830
public int Capacity => _buffer.Length;
2931

3032
public void Advance(int count)
@@ -50,6 +52,12 @@ public Span<byte> GetSpan(int sizeHint = 0)
5052
return _buffer.AsSpan(_written);
5153
}
5254

55+
public ArraySegment<byte> GetSegment(int sizeHint = 0)
56+
{
57+
Ensure(sizeHint);
58+
return new ArraySegment<byte>(_buffer, _written, _buffer.Length - _written);
59+
}
60+
5361
public void Clear()
5462
{
5563
_written = 0;

0 commit comments

Comments
 (0)