LHZ.WebSocket 1.2.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package LHZ.WebSocket --version 1.2.0
                    
NuGet\Install-Package LHZ.WebSocket -Version 1.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="LHZ.WebSocket" Version="1.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="LHZ.WebSocket" Version="1.2.0" />
                    
Directory.Packages.props
<PackageReference Include="LHZ.WebSocket" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add LHZ.WebSocket --version 1.2.0
                    
#r "nuget: LHZ.WebSocket, 1.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package LHZ.WebSocket@1.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=LHZ.WebSocket&version=1.2.0
                    
Install as a Cake Addin
#tool nuget:?package=LHZ.WebSocket&version=1.2.0
                    
Install as a Cake Tool

LHZ.WebSocket

中文

A lightweight, zero-dependency WebSocket library for .NET, implementing RFC 6455 from the ground up using raw TcpListener / TcpClient. Supports both server-side and client-side WebSocket connections.

Features

  • Zero dependencies — pure .NET, no third-party libraries
  • RFC 6455 compliant — FIN, RSV1–3, all opcodes, masking, extended payload lengths
  • Server & Client — create WebSocket servers or connect as a client via CreateWebSocketClient
  • Fragmented messages — automatic reassembly of continuation frames (client → server)
  • Streaming send — split large payloads across multiple frames via CreateDataFrame(Stream)
  • Bounded channel — producer-consumer pattern for outgoing frames; no unbounded queues
  • Event-driven — OnMessageReceived, OnBytesReceived, OnCloseReceived, OnClientClose, OnPingReceived, OnPongReceived
  • Multi-targeting — net5.0, net6.0, net8.0, net9.0, net10.0 with nullable reference types enabled
  • Handshake timeout — configurable timeout to reject slow HTTP upgrade requests
  • xUnit tested — comprehensive unit tests for frame parsing, close messages, HTTP headers, server, and client

Quick Start

1. Add LHZ.WebSocket Package

Package Manager
Install-Package LHZ.WebSocket -version 1.2.0
.NET CLI
dotnet add package LHZ.WebSocket --Version 1.2.0
Package Reference
<PackageReference Include="LHZ.WebSocket" Version="1.2.0" />

Upgrading from 1.1.x? Some misspelled public names were corrected, so you will get compile errors until you rename the call sites:

Old New
ClientStatus.Opend ClientStatus.Opened
OnCloseRecived OnCloseReceived
OnPingRecived OnPingReceived
OnPongRecived OnPongReceived
DataFrame.DataDataFrameFlag DataFrame.DataFrameFlag

Two parameter names were corrected as well, which only matters if you pass them by name: CreateWebSocketClient's timeOUt is now timeOut, and CreateDataFrame's dataDataFrameLength is now dataFrameLength.

Also note that a Pong is now sent automatically on every incoming Ping, so a handler that replied by hand can be removed.

2. Create and start a server

using LHZ.WebSocket;
using LHZ.WebSocket.Core;
using LHZ.WebSocket.Http;
using LHZ.WebSocket.Interfaces;

// Listen on port 5000, all interfaces
var server = new WebSocketServer(5000);

server.OnUpgradeRequest += (HttpContext context) =>
{
    // Optional: inspect headers, add custom response headers, or deny the upgrade
    // context.Response?.Headers.Add("X-Custom", "value");

    var client = context.HttpUpgrade();    // completes the 101 handshake

    client.OnMessageReceived += (IWebSocketClient sender, string message) =>
    {
        Console.WriteLine($"Received: {message}");
        sender.SendMessage($"Echo: {message}");
    };

    client.OnBytesReceived += (IWebSocketClient sender, byte[] data) =>
    {
        Console.WriteLine($"Received {data.Length} bytes");
    };

    client.OnCloseReceived += (IWebSocketClient sender, CloseMessage msg) =>
    {
        Console.WriteLine($"Client closed: {msg.CloseCode} — {msg.Message}");
        sender.Close();
    };
};

server.Start();
Console.WriteLine($"Server started, {server.ClientNums} clients connected");
Console.ReadLine();
server.Stop();

3. Connect as a client

using LHZ.WebSocket;
using LHZ.WebSocket.Core;
using LHZ.WebSocket.Interfaces;

// Connect to a WebSocket server
var client = WebSocketClient.CreateWebSocketClient("ws://localhost:5000/");

client.OnMessageReceived += (IWebSocketClient sender, string message) =>
{
    Console.WriteLine($"Received: {message}");
};

client.OnCloseReceived += (IWebSocketClient sender, CloseMessage message) =>
{
    Console.WriteLine($"Connection closed: {message.CloseCode}");
    sender.Close();
};

client.Open();
client.SendMessage("Hello World!");

4. Bind to a specific IP

var server = new WebSocketServer(IPAddress.Loopback, 5000);

5. Run the tests

cd src
dotnet test

API Reference

WebSocketServer

Member Description
WebSocketServer(int port) Bind to all interfaces on the given port
WebSocketServer(IPAddress ip, int port) Bind to a specific IP and port
WebSocketServer(IPAddress ip, int port, int timeOut) Bind to a specific IP and port with a handshake timeout (seconds)
Start() Begin accepting connections
Stop() Disconnect all clients and stop listening
ClientNums Current number of connected clients
WebSocketClients Snapshot of connected clients (IEnumerable<IWebSocketClient>)
Status Current ServerStatus (Ready / Start / Closing / Closed)
OnUpgradeRequest Fired when an HTTP upgrade is received; call HttpUpgrade() to accept
OnClientConnected Fired after the WebSocket handshake completes (Action<IWebSocketClient>)
OnError Fired when the accept loop or a handshake fails (Action<Exception>); falls back to the console when unsubscribed

IWebSocketClient (interface)

Member Description
ID Unique Guid for this connection
Status Current ClientStatus (Connection / Opened / Close)
MaxMessageSize Largest reassembled incoming message in bytes (default 4 MiB; 0 disables the limit). A peer exceeding it is closed with 1009
SendMessage(string) Send a UTF-8 text frame. Blocks while the outgoing queue is full
SendMessageAsync(string, CancellationToken) Send a UTF-8 text frame without blocking
SendByte(byte[]) Send a binary frame. Blocks while the outgoing queue is full
SendByteAsync(byte[], CancellationToken) Send a binary frame without blocking
Ping(byte[]) / PingAsync(byte[], CancellationToken) Send a Ping frame
Pong(byte[]) / PongAsync(byte[], CancellationToken) Send a Pong frame. A Pong is sent automatically on every incoming Ping, so this is only needed for unsolicited Pongs
Open() Start the background send/receive loops
Close() Cancel tasks and dispose the TCP connection
Dispose() Alias for Close() (implements IDisposable)
OnMessageReceived EventHandler<IWebSocketClient, string> — complete text message
OnBytesReceived EventHandler<IWebSocketClient, byte[]> — complete binary message
OnCloseReceived EventHandler<IWebSocketClient, CloseMessage> — close frame received
OnPingReceived EventHandler<IWebSocketClient, byte[]> — Ping frame received
OnPongReceived EventHandler<IWebSocketClient, byte[]> — Pong frame received
OnClientClose Action<IWebSocketClient> — connection closed (local or remote)
OnError EventHandler<IWebSocketClient, Exception> — a background task failed; falls back to the console when unsubscribed

WebSocketClient

Member Description
CreateWebSocketClient(string url, HttpHeaders? headers) Static — creates a client connection to a WebSocket server. ws:// only; wss:// throws NotSupportedException
HttpContext The underlying HTTP context for this connection
Status Current ClientStatus
MaxMessageSize Largest reassembled incoming message in bytes (default 4 MiB)
SendMessage(string) / SendMessageAsync(string) Send a UTF-8 text frame
SendByte(byte[]) / SendByteAsync(byte[]) Send a binary frame
Ping(byte[]) / PingAsync(byte[]) Send a Ping frame
Pong(byte[]) / PongAsync(byte[]) Send a Pong frame
Open() Start background send/receive loops
Close() Cancel tasks and dispose the TCP connection
Dispose() Alias for Close()

IHttpContext (interface)

Member Description
Request Parsed HTTP request (method, URL, headers)
Response HTTP response object
Stream The underlying network stream
Status Current HttpContextStatus
HttpUpgrade(int capacity = 1024) Completes the WebSocket handshake and returns the WebSocketClient

HttpContextBase (abstract class)

Base class implementing IHttpContext. Provides HTTP request parsing, timeout handling, and the HttpUpgrade() handshake logic. The concrete HttpContext class adds TcpClient support.

HttpContext (sealed, extends HttpContextBase)

Member Description
Request Parsed HTTP request (method, URL, headers)
Response HTTP response object (nullable; populated for server-side upgrades)
TcpClient The underlying TCP connection
Stream The underlying network stream
Status Current HttpContextStatus (NotInitialized / Initialized / Upgraded / TimedOut / Rejected)
HttpUpgrade(int capacity = 1024) Computes Sec-WebSocket-Accept, writes 101 Switching Protocols, returns the WebSocketClient
WebSocketClient The upgraded client (populated after HttpUpgrade())
Dispose() Disposes the TCP client if no upgrade was performed

HttpRequest

Member Description
Method HTTP method (e.g. GET)
Url Request path (e.g. /chat)
HttpVersion HTTP version string (e.g. HTTP/1.1)
Headers Parsed request headers (System.Net.Http.Headers.HttpHeaders, case-insensitive keys)
WriteToStream(Stream) Writes the request line and headers to a stream (used for client-side handshake)

HttpResponse

Member Description
StatusCode HTTP status code (e.g. HttpStatusCode.SwitchingProtocols)
HttpVersion HTTP version string (e.g. HTTP/1.1)
Headers Response headers (System.Net.Http.Headers.HttpHeaders)

DataFrame

Member Description
CreateDataFrame(OpCode, bool FIN, byte[]? key, byte[] data) Build a single frame from a byte array
CreateDataFrame(OpCode, byte[]? key, Stream data, int maxLen) Split a stream into multiple frames
FIN / RSV1 / RSV2 / RSV3 Frame control flags
Opcode OpCode enum value
Masked Whether the payload is XOR-masked
MaskingKey 4-byte masking key (null if unmasked)
DataFrameHeader Serialized frame header (2–14 bytes)
Data Payload as ArraySegment<byte>

CloseMessage

Member Description
CloseCode RFC 6455 status code (e.g. Normal = 1000)
Message Optional human-readable reason

Delegates

EventHandler<TSender, TEventArgs> — Custom delegate defined in LHZ.WebSocket.Delegates

public delegate void EventHandler<in TSender, TEventArgs>(TSender sender, TEventArgs e);

Enums

OpCode — Continuation (0x0), Text (0x1), Binary (0x2), Close (0x8), Ping (0x9), Pong (0xA)

CloseCode — All RFC 6455 codes: Normal (1000), GoingAway (1001), ProtocolError (1002), …, TlsHandshake (1015)

ClientStatus — Connection, Opened, Close

ServerStatus — Ready, Start, Closing, Closed

HttpContextStatus — NotInitialized (0), Initialized (1), Upgraded (2), TimedOut (-2), Rejected (-1)

Architecture

sequenceDiagram
    participant Peer
    participant TcpListener
    participant WebSocketServer
    participant HttpContext
    participant WebSocketClient

    Peer->>TcpListener: TCP connect
    TcpListener->>WebSocketServer: AcceptTcpClientAsync()
    WebSocketServer->>HttpContext: Parse HTTP upgrade request
    WebSocketServer->>+Peer: OnUpgradeRequest (user code calls HttpUpgrade())
    HttpContext->>Peer: HTTP 101 + Sec-WebSocket-Accept
    HttpContext->>WebSocketClient: new WebSocketClient(httpContext)
    WebSocketClient->>WebSocketClient: Open() → StartReceiver() + StartSender()
    Peer->>WebSocketClient: Data frames
    WebSocketClient->>Peer: OnMessageReceived / OnBytesReceived / OnPingReceived / OnPongReceived
    WebSocketClient-->>Peer: SendMessage() / SendByte() / Ping() / Pong()

Internally, each WebSocketClient runs two background tasks:

  • Receiver — reads frames via DataFrameReader, reassembles fragmented messages, dispatches by opcode
  • Sender — dequeues frames from a bounded Channel<DataFrame>, writes header + payload to the network stream

Project Structure

LHZ.WebSocket/
├── src/
│   ├── LHZ.WebSocket/                    # Core library
│   │   ├── WebSocketServer.cs           # TCP listener & client lifecycle
│   │   ├── WebSocketClient.cs           # Per-connection send/receive (server & client)
│   │   ├── Core/
│   │   │   ├── CloseMessage.cs          # Close frame payload
│   │   │   ├── DataFrame.cs             # Frame builder / parser (RFC 6455 §5.2)
│   │   │   └── DataFrameReader.cs       # Frame reader from Stream
│   │   ├── Delegates/
│   │   │   └── EventHandler.cs          # Custom event delegate with `in` modifier
│   │   ├── Enums/
│   │   │   ├── ClientStatus.cs          # Client lifecycle states
│   │   │   ├── CloseCode.cs             # RFC 6455 close status codes
│   │   │   ├── HttpContextStatus.cs     # HTTP context lifecycle states
│   │   │   ├── OpCode.cs                # Frame opcodes
│   │   │   └── ServerStatus.cs          # Server lifecycle states
│   │   ├── Http/
│   │   │   ├── HttpContext.cs           # HTTP upgrade handshake (server & client)
│   │   │   ├── HttpContextBase.cs       # Abstract base with handshake & timeout logic
│   │   │   ├── HttpHeaders.cs           # Internal header collection
│   │   │   ├── HttpRequest.cs           # HTTP request-line & header parser/writer
│   │   │   └── HttpResponse.cs          # HTTP response builder & parser
│   │   └── Interfaces/
│   │       ├── IHttpContext.cs          # HTTP context interface
│   │       └── IWebSocketClient.cs      # WebSocket client interface
│   ├── LHZ.WebSocket.Test/              # xUnit test project
│   │   ├── WebSocketServerTests.cs
│   │   ├── WebSocketClientTests.cs
│   │   └── Core/ / Http/
│   └── LHZ.WebSocket.slnx              # Solution file
├── LICENSE
├── README.md
└── README.zh-CN.md

Requirements

License

MIT

Product Compatible and additional computed target framework versions.
.NET net5.0 is compatible.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net5.0

    • No dependencies.
  • net6.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on LHZ.WebSocket:

Package Downloads
LHZ.WebSocket.AspNetCore

A lightweight ASP.NET Core middleware wrapper for the LHZ.WebSocket library, enabling WebSocket upgrade handling and client tracking in ASP.NET Core applications.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.3.0 133 10/29/2026
1.2.0 192 9/28/2026
1.1.1 155 8/19/2026
1.0.2 189 8/2/2026
1.0.1 125 7/25/2026
1.0.0 126 7/24/2026

1.2.0:
- BREAKING: long-standing misspellings in the public API were corrected. Rename call sites as follows: ClientStatus.Opend -> ClientStatus.Opened, OnCloseRecived -> OnCloseReceived, OnPingRecived -> OnPingReceived, OnPongRecived -> OnPongReceived, DataFrame.DataDataFrameFlag -> DataFrame.DataFrameFlag. The old names are gone rather than deprecated, so this is a compile error you fix once, not a silent behaviour change
- BREAKING (named arguments only): two parameters were renamed - CreateWebSocketClient's timeOUt -> timeOut, and CreateDataFrame's dataDataFrameLength -> dataFrameLength. Positional callers are unaffected
- BREAKING: IWebSocketClient gained members (the async send methods, MaxMessageSize and OnError), so any third-party implementation of the interface must be updated
- Fix: frames larger than 65535 bytes wrote an uninitialized 64-bit length field, because the send path rents an uncleared buffer from ArrayPool. The peer read a bogus length and the connection desynchronized
- Fix: ApplyMask masked too few bytes for payload segments with a non-zero offset
- Fix: the sender could return the same pooled buffer twice, corrupting ArrayPool for the whole process
- Fix: an all-zero masking key was treated as "not masked", dropping the MASK bit
- Fix: OnClientConnected was declared and documented but never raised
- Fix: Close() was check-then-act, so concurrent callers could double-dispose the socket and raise OnClientClose more than once
- Fix: close codes 3000-4999 are accepted per RFC 6455 §7.4 instead of being treated as errors; reserved codes (1004, 1005, 1006, 1015) are now rejected and never echoed back
- Fix: the handshake timeout could dispose the stream while the 101 response was being written, and its timer outlived every connection
- Fix: the server could stay stuck in the Closing state, making Start() refuse to run again
- Fix: header serialization no longer comma-joins multiple values and no longer throws on a header with no values
- Fix: HTTP response status codes are parsed numerically, so unusual codes no longer throw
- Fix: CreateWebSocketClient no longer mutates the caller's header collection, disposes the socket when the handshake fails, and rejects wss:// explicitly instead of connecting in plaintext
- Fix: the client handshake key is drawn from a cryptographic RNG instead of Guid.NewGuid
- Add: protocol validation the implementation previously lacked - reserved bits must be zero, incoming frames must match the peer's masking role, control frames are capped at 125 bytes, text payloads must be valid UTF-8 (1007)
- Add: MaxMessageSize (default 4 MiB) caps reassembled messages, closing the unbounded-memory path a peer could open by never setting FIN
- Add: automatic Pong reply to Ping, as required by RFC 6455 §5.5.2
- Add: SendMessageAsync, SendByteAsync, PingAsync and PongAsync, so sending no longer has to block when the outgoing queue is full
- Add: OnError on both client and server, and WebSocketProtocolException, replacing Console.WriteLine as the only way to observe failures
- Add: WebSocketServer.Status
- Change: a protocol violation now sends a Close frame stating the reason before the connection is failed

1.1.1:
- Fix: handshake handling is now concurrent, a slow client no longer blocks the accept loop
- Fix: client-role connections now mask every outgoing frame (RFC 6455 §5.1), with a cryptographic masking key
- Fix: the client collection is now per server instance instead of static
- Fix: OnClientClose event subscription no longer leaks
- Fix: masking no longer mutates the caller's buffer
- Fix: client handshake can parse restricted response headers (e.g. Connection)
- Add: server-side handshake validation (Sec-WebSocket-Key / Sec-WebSocket-Version 13) with HTTP 400 rejection
- Add: automatic Close frame reply on receiving a Close frame
- Add: fragmentation validation (reject orphan Continuation frames, control frames are never merged into messages)
- Add: 10 interop tests (client-server round trip, masking, concurrent handshakes, handshake validation, close reply, fragmentation)