Skip to content

Commit 474a672

Browse files
committed
✨ Initial feature complete version
1 parent cb8103a commit 474a672

11 files changed

Lines changed: 1191 additions & 0 deletions

WebSocketChannel.sln

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,29 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
99
readme.md = readme.md
1010
EndProjectSection
1111
EndProject
12+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebSocketChannel", "src\WebSocketChannel\WebSocketChannel.csproj", "{36D496E4-50C8-4156-8A9F-D525A3C19746}"
13+
EndProject
14+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "src\Tests\Tests.csproj", "{517F1129-4EA6-46FA-827B-42CF5EB0DE09}"
15+
EndProject
16+
Global
17+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
18+
Debug|Any CPU = Debug|Any CPU
19+
Release|Any CPU = Release|Any CPU
20+
EndGlobalSection
21+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
22+
{36D496E4-50C8-4156-8A9F-D525A3C19746}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23+
{36D496E4-50C8-4156-8A9F-D525A3C19746}.Debug|Any CPU.Build.0 = Debug|Any CPU
24+
{36D496E4-50C8-4156-8A9F-D525A3C19746}.Release|Any CPU.ActiveCfg = Release|Any CPU
25+
{36D496E4-50C8-4156-8A9F-D525A3C19746}.Release|Any CPU.Build.0 = Release|Any CPU
26+
{517F1129-4EA6-46FA-827B-42CF5EB0DE09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27+
{517F1129-4EA6-46FA-827B-42CF5EB0DE09}.Debug|Any CPU.Build.0 = Debug|Any CPU
28+
{517F1129-4EA6-46FA-827B-42CF5EB0DE09}.Release|Any CPU.ActiveCfg = Release|Any CPU
29+
{517F1129-4EA6-46FA-827B-42CF5EB0DE09}.Release|Any CPU.Build.0 = Release|Any CPU
30+
EndGlobalSection
31+
GlobalSection(SolutionProperties) = preSolution
32+
HideSolutionNode = FALSE
33+
EndGlobalSection
34+
GlobalSection(ExtensibilityGlobals) = postSolution
35+
SolutionGuid = {D4B0A4E9-B519-4C31-939F-0F5BF858248F}
36+
EndGlobalSection
1237
EndGlobal

readme.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
![Icon](https://raw.githubusercontent.com/devlooped/WebSocketChannel/main/assets/img/icon.png) WebSocketChannel
2+
============
3+
4+
High-performance [System.Threading.Channels](https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/) API adapter for System.Net.WebSockets
5+
6+
[![Version](https://img.shields.io/nuget/v/WebSocketChannel.svg?color=royalblue)](https://www.nuget.org/packages/WebSocketChannel)
7+
[![Downloads](https://img.shields.io/nuget/dt/WebSocketChannel.svg?color=green)](https://www.nuget.org/packages/WebSocketChannel)
8+
[![License](https://img.shields.io/github/license/devlooped/WebSocketChannel.svg?color=blue)](https://github.com/devlooped/WebSocketChannel/blob/main/license.txt)
9+
[![Build](https://github.com/devlooped/WebSocketChannel/workflows/build/badge.svg?branch=main)](https://github.com/devlooped/WebSocketChannel/actions)
10+
11+
# Usage
12+
13+
```csharp
14+
var client = new ClientWebSocket();
15+
await client.ConnectAsync(serverUri, CancellationToken.None);
16+
17+
Channel<ReadOnlyMemory<byte>> channel = client.CreateChannel();
18+
19+
await channel.Writer.WriteAsync(Encoding.UTF8.GetBytes("hello").AsMemory());
20+
21+
// Read single message when it arrives
22+
ReadOnlyMemory<byte> response = await channel.Reader.ReadAsync();
23+
24+
// Read all messages while underlying websocket is open
25+
await foreach (var item in channel.Reader.ReadAllAsync())
26+
{
27+
Console.WriteLine(Encoding.UTF8.GetString(item.Span));
28+
}
29+
30+
// Completing the writer closes the underlying websocket cleanly
31+
channel.Writer.Complete();
32+
33+
// Can also complete reporting an error for the remote party
34+
channel.Writer.Complete(new InvalidOperationException("Bad format"));
35+
```
36+
37+
38+
The `WebSocketChannel` can also be used on the server. The following example is basically
39+
taken from the documentation on [WebSockets in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/websockets?view=aspnetcore-5.0#configure-the-middleware)
40+
and adapted to use a `WebSocketChannel` to echo messages to the client:
41+
42+
```csharp
43+
app.Use(async (context, next) =>
44+
{
45+
if (context.Request.Path == "/ws")
46+
{
47+
if (context.WebSockets.IsWebSocketRequest)
48+
{
49+
using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
50+
var channel = WebSocketChannel.Create(webSocket);
51+
try
52+
{
53+
await foreach (var item in channel.Reader.ReadAllAsync(context.RequestAborted))
54+
{
55+
await channel.Writer.WriteAsync(item, context.RequestAborted);
56+
}
57+
}
58+
catch (OperationCanceledException)
59+
{
60+
try
61+
{
62+
await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, default);
63+
}
64+
catch { } // Best effort to try closing cleanly. Client may be entirely gone.
65+
}
66+
}
67+
else
68+
{
69+
context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
70+
}
71+
}
72+
else
73+
{
74+
await next();
75+
}
76+
});
77+
```
78+
79+
80+
# Dogfooding
81+
82+
[![CI Version](https://img.shields.io/endpoint?url=https://shields.kzu.io/vpre/WebSocketChannel/main&label=nuget.ci&color=brightgreen)](https://pkg.kzu.io/index.json)
83+
[![Build](https://github.com/devlooped/WebSocketChannel/workflows/build/badge.svg?branch=main)](https://github.com/devlooped/WebSocketChannel/actions)
84+
85+
We also produce CI packages from branches and pull requests so you can dogfood builds as quickly as they are produced.
86+
87+
The CI feed is `https://pkg.kzu.io/index.json`.
88+
89+
The versioning scheme for packages is:
90+
91+
- PR builds: *42.42.42-pr*`[NUMBER]`
92+
- Branch builds: *42.42.42-*`[BRANCH]`.`[COMMITS]`
93+
94+
95+
96+
## Sponsors
97+
98+
[![sponsored](https://raw.githubusercontent.com/devlooped/oss/main/assets/images/sponsors.svg)](https://github.com/sponsors/devlooped) [![clarius](https://raw.githubusercontent.com/clarius/branding/main/logo/byclarius.svg)](https://github.com/clarius)[![clarius](https://raw.githubusercontent.com/clarius/branding/main/logo/logo.svg)](https://github.com/clarius)
99+
100+
*[get mentioned here too](https://github.com/sponsors/devlooped)!*
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"iisSettings": {
3+
"windowsAuthentication": false,
4+
"anonymousAuthentication": true,
5+
"iisExpress": {
6+
"applicationUrl": "http://localhost:52411/",
7+
"sslPort": 44389
8+
}
9+
},
10+
"profiles": {
11+
"IIS Express": {
12+
"commandName": "IISExpress",
13+
"launchBrowser": true,
14+
"environmentVariables": {
15+
"ASPNETCORE_ENVIRONMENT": "Development"
16+
}
17+
},
18+
"Tests": {
19+
"commandName": "Project",
20+
"launchBrowser": true,
21+
"environmentVariables": {
22+
"ASPNETCORE_ENVIRONMENT": "Development"
23+
},
24+
"applicationUrl": "https://localhost:5001;http://localhost:5000"
25+
}
26+
}
27+
}

src/Tests/Tests.csproj

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<ImplicitUsings>true</ImplicitUsings>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
10+
<PackageReference Include="xunit" Version="2.4.1" />
11+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" />
12+
<PackageReference Include="coverlet.collector" Version="3.1.0" PrivateAssets="all" />
13+
<PackageReference Include="System.IO.Pipelines" Version="5.0.1" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<ProjectReference Include="..\WebSocketChannel\WebSocketChannel.csproj" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<Using Include="System.Buffers" />
22+
<Using Include="System.IO.Pipelines" />
23+
<Using Include="System.Threading.Channels" />
24+
<Using Include="System.Net.WebSockets" />
25+
<Using Include="System.Text" />
26+
<Using Include="Xunit" />
27+
<Using Include="Xunit.Abstractions" />
28+
</ItemGroup>
29+
30+
</Project>

0 commit comments

Comments
 (0)