Skip to content

AboubacarSow/webSocket-server

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

⏱️ Building a WebSocket Server from Scratch in .NET 9

A from-scratch dive into the WebSocket protocol — connection lifecycle, broadcasting, background persistence, and retry resilience — built in .NET 9 as a hands-on, project-based learning exercise.

.NET C# Protocol Status

A simple real-time communication example: a .NET 9 server that accepts WebSocket connections and a minimal web client for manual testing.

At a Glance

Language / Runtime C# on .NET 9
Protocol WebSocket (RFC 6455)
Persistence EF Core, via an in-memory queue + background job with retry
Client Vanilla HTML/CSS/JS test client (included)
Status Core protocol + persistence pipeline functional; hardening features on the roadmap

Table of Contents

What is a WebSocket, and how is it different from HTTP?

HTTP is request/response and stateless: the client opens a connection, sends one request, the server sends back one response, and (logically) the exchange is done. Every new piece of data means a new request. Even with keep-alive reusing the underlying TCP socket, the protocol itself is still one-shot question → answer, and only the client can initiate.

sequenceDiagram
    participant Client
    participant Server

    Client->>Server: HTTP GET /messages
    Server-->>Client: 200 OK (data)
    Note over Client,Server: Exchange complete, nothing pending

    Client->>Server: HTTP GET /messages (poll again later)
    Server-->>Client: 200 OK (data)
    Note over Client,Server: Server can never push on its own —
    Note over Client,Server: client has to keep asking
Loading

A WebSocket starts its life as a normal HTTP request that asks to be "upgraded" (Connection: Upgrade, Upgrade: websocket). If the server agrees, it replies 101 Switching Protocols, and from that point on the same TCP connection is kept open and reused as a raw, full-duplex channel. There's no more request/response pairing — either side can send a message at any time, with very little framing overhead per message.

sequenceDiagram
    participant Client
    participant Server

    Client->>Server: HTTP GET /ws (Upgrade: websocket)
    Server-->>Client: 101 Switching Protocols
    Note over Client,Server: Handshake done — connection stays open

    Server-->>Client: { type: "welcome", connectionId }
    Client->>Server: text message
    Server-->>Client: { type: "broadcast", ... } (to other clients)
    Server-->>Client: { type: "user_left", ... } (pushed anytime, unprompted)
    Client->>Server: text message
    Note over Client,Server: No new connection needed — both sides
    Note over Client,Server: can send whenever they want
Loading
HTTP WebSocket
Connection lifecycle New request/response cycle each time One persistent TCP connection, opened once via handshake
Who can initiate Client only Either side, anytime (full-duplex)
Per-message overhead Full headers every request Handshake once, then lightweight frames
Typical use case Fetching pages/APIs, request-driven data Real-time updates, chat, live broadcasts, notifications

This is exactly what this project's middleware does: it accepts an incoming HTTP request, performs the upgrade handshake, then hands the now-persistent connection off to WebSocketServerManager to track and broadcast over for as long as the client stays connected.

Architecture

Once a client is upgraded, two things happen on every incoming message: it's broadcast live to other connected clients, and it's queued for durable persistence in the background — so a slow or failing database write never blocks or drops a live connection.

flowchart LR
    subgraph Clients
        C1[Web Client A]
        C2[Web Client B]
    end

    C1 <-->|WebSocket| MW[WebSocket Middleware]
    C2 <-->|WebSocket| MW

    MW --> MGR[WebSocketServerManager]
    MGR -->|broadcast| C1
    MGR -->|broadcast| C2

    MW -->|enqueue MessageEvent| Q[(In-Memory Message Queue)]
    Q --> BG[MessageBackgroundJob]
    BG -->|persist, retry on failure| DB[(Database via EF Core)]

    style Clients fill:#1118270d,stroke:#888
Loading

Flow:

  1. A client connects → WebSocketMiddleware performs the upgrade handshake and assigns a connectionId.
  2. WebSocketServerManager tracks the connection and broadcasts incoming messages to everyone else.
  3. In parallel, the middleware enqueues the message onto an in-memory channel rather than writing to the database directly.
  4. MessageBackgroundJob dequeues messages and persists them via the repository layer, retrying with linear backoff (RetryHelper) on transient DB failures.
  5. On disconnect, remaining clients get a user_left notification with the updated connection count.

This separation — live broadcast on the fast path, durable persistence on a decoupled background path — is the core architectural idea of the project.

Project Structure

websocket/
├── server/                 # C# .NET server application
│   ├── Program.cs          # Application entry point
│   ├── Data/               # Persistence, entities, repositories
│   ├── Extensions/         # Service & middleware extension helpers
│   ├── Manager/             # WebSocket connection manager
│   ├── Middleware/         # WebSocket middleware
│   └── Properties/         # Project properties
├── clients/                # Client applications
│   └── web-client/         # Web-based client used for manual testing
│       ├── index.html      # Main HTML
│       ├── script.js       # Client logic
│       └── style.css       # Styling
└── README.md               # This file

Technology Stack

  • Backend: C# (.NET 9)
  • Frontend: HTML, CSS, JavaScript (vanilla)
  • Communication: WebSocket (RFC6455)

Getting Started

Prerequisites

  • .NET 9.0 SDK or later
  • A modern web browser

Run the server

  1. Open a terminal and change to the server directory:

    cd server
  2. Restore dependencies (optional if already restored):

    dotnet restore
  3. Run the server:

    dotnet run

By default the app is reachable on the local host URLs used by ASP.NET Core (you can also check Properties/launchSettings.json). The included web client defaults to ws://localhost:5000.

Use the web client

  1. Open clients/web-client/index.html in your browser.
  2. Click Connect.
  3. Use the UI to send messages, observe incoming broadcasts and connection events in the communication log.

Implemented Features

Connection handling

  • WebSocket middleware accepts upgrade requests and assigns a GUID connectionId to each client.
  • WebSocketServerManager tracks active connections and safely broadcasts messages to other clients.
  • Server sends an initial payload with the assigned connectionId and a welcome message on connect.
  • Close-reason support — clients and server include close reason and close status (if present) when a connection is closed.

Messaging

  • On message, server broadcasts a JSON payload to other connected clients: { "type": "broadcast", "connectionId": "<id>", "message": "...", "timestamp": "YYYY-MM-DD HH:mm:ss" }.
  • On disconnect, server notifies other clients: { "type": "user_left", "connectionId": "<id>", "totalConnections": N }.

Persistence pipeline

  • Message persistence — incoming messages are persisted to the database through the repository layer (Message entity, WebSocketDbContext, IMessageRepository / MessageRepository), laying the groundwork for history-on-connect and replay.
  • Background message queue — messages are queued onto an in-memory channel instead of being persisted directly on the WebSocket receive path, decoupling DB writes from the middleware and keeping client connections responsive even under temporary DB failures.
  • Background persistence job — MessageBackgroundJob continuously dequeues messages and persists them with linear backoff retry logic via RetryHelper (default: starts at 1 second, configurable max retries). Messages that exhaust retries are logged and dropped to avoid infinite loops.

Wiring

  • Services and middleware registered via ServiceCollectionExtensions.RegisterServices() and activated with UseWebSocketServer().

Message Formats

Click to expand full message shapes
Message Sent when Shape
Connection ID Immediately after accept { "connectionId": "<id>" }
Welcome Immediately after accept { "type": "welcome", "message": "..." }
Broadcast A client sends a text message { "type": "broadcast", "connectionId": "<id>", "message": "...", "timestamp": "..." }
User left A client disconnects { "type": "user_left", "connectionId": "<id>", "totalConnections": N }

Close events may also include a WebSocket close status and an optional close reason string. Clients should inspect the close info to present user-friendly messages or to drive reconnection logic.

Project Status

Functional: WebSocket connection handling, broadcasting, graceful disconnects, message persistence via background queue, and retry logic with linear backoff are fully implemented. The background service decouples database writes from the WebSocket middleware, ensuring that temporary database failures do not impact client connections. Manual testing via the bundled web client is supported.

Roadmap

  • Move persistence to a background queue — messages are dequeued from an in-memory channel by a background job, decoupling DB writes from the WebSocket middleware.
  • Add retry logic for DB failures — linear backoff retry strategy via RetryHelper for transient database failures when persisting messages.
  • Console client — a console client for testing/automation and performance benchmarking.
  • Message history on connect — load recent message history for clients when they connect so they can catch up.
  • Message ordering guarantee — ensure messages are delivered/processed in a well-defined order (server-side sequence numbers or persisted sequence metadata).
  • Backpressure handling — detect and react when clients or the server are overloaded (pause reads, drop/queue messages, slow producers).
  • Durable background queue — replace the in-memory queue with a persistent one (RabbitMQ, Kafka, or Azure Service Bus) so messages survive server restarts.
  • Per-connection rate limiting — protect the server from abusive clients with configurable message thresholds.
  • Authentication / authorization — JWT tokens or basic auth for secure client connections.

References & Learning Resources

This project was inspired and informed by the following materials:

  1. RFC 6455 – The WebSocket Protocol https://datatracker.ietf.org/doc/html/rfc6455

  2. Building Production-Ready WebSocket Servers in C# ASP.NET Core https://medium.com/@bhargavkoya56/building-production-ready-websocket-servers-in-c-asp-net-core-927b737f14cc

  3. Writing a WebSocket server (MDN Web Docs) https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_server

  4. ASP.NET Core WebSockets vs SignalR – Which should you use? (Full Course) — Les Jackson https://www.youtube.com/watch?v=ycVgXe6v1VQ

License

See LICENSE

About

An Implementation of a webSocket server using Asp.Net Core web empty template

Topics

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors