C#
Asynchronous Programming
Socket Programming
Software Design
Network Communication

Design of asynchronous socket classes in C

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Designing asynchronous socket classes in C# is less about wrapping Socket in async methods and more about deciding where responsibilities live. A reliable design usually separates connection lifecycle, message framing, send and receive loops, and application-level protocol handling instead of putting everything in one giant socket class.

That separation matters because socket code fails in messy ways under load: partial reads, disconnects, backpressure, cancellation, and concurrent send attempts all appear at once.

Separate Transport from Protocol Logic

A common mistake is to let the same class both manage the socket and understand the business protocol. Keep the transport layer focused on bytes and connection state, then let higher layers interpret messages.

A minimal asynchronous client wrapper might look like this:

csharp
1using System;
2using System.Net.Sockets;
3using System.Text;
4using System.Threading;
5using System.Threading.Tasks;
6
7public sealed class TcpClientConnection : IAsyncDisposable
8{
9    private readonly TcpClient _client = new();
10    private NetworkStream? _stream;
11
12    public async Task ConnectAsync(string host, int port, CancellationToken cancellationToken)
13    {
14        await _client.ConnectAsync(host, port, cancellationToken);
15        _stream = _client.GetStream();
16    }
17
18    public async Task SendAsync(string text, CancellationToken cancellationToken)
19    {
20        byte[] bytes = Encoding.UTF8.GetBytes(text + "\n");
21        await _stream!.WriteAsync(bytes, cancellationToken);
22    }
23
24    public async ValueTask DisposeAsync()
25    {
26        if (_stream is not null) await _stream.DisposeAsync();
27        _client.Dispose();
28    }
29}

This is intentionally small. It handles connection and send behavior, but it does not pretend to define the entire protocol surface.

Treat Reading as a Loop, Not a Single Call

Socket reads are incremental. A single ReadAsync may return a partial message, several messages, or zero bytes for a disconnect. That means the receive path should usually be a dedicated async loop that accumulates bytes and emits complete frames.

csharp
1public async Task ReceiveLinesAsync(Func<string, Task> onMessage, CancellationToken cancellationToken)
2{
3    byte[] buffer = new byte[1024];
4    var builder = new StringBuilder();
5
6    while (!cancellationToken.IsCancellationRequested)
7    {
8        int count = await _stream!.ReadAsync(buffer, cancellationToken);
9        if (count == 0) break;
10
11        builder.Append(Encoding.UTF8.GetString(buffer, 0, count));
12        string current = builder.ToString();
13
14        int newline;
15        while ((newline = current.IndexOf('\n')) >= 0)
16        {
17            string line = current[..newline].TrimEnd('\r');
18            await onMessage(line);
19            current = current[(newline + 1)..];
20        }
21
22        builder.Clear();
23        builder.Append(current);
24    }
25}

The message framing rule here is line-based, but the design point is broader: define framing explicitly.

Control Concurrency Around Sends

Many socket bugs come from concurrent writes interleaving unexpectedly. If multiple callers can send at once, serialize writes through a queue or a lock. Reads and writes can be concurrent, but multiple unrelated writers need coordination.

This is one reason many production designs expose a message queue or channel above the raw socket instead of letting every caller write directly.

Prefer Cancellation and Shutdown Paths Early

Good async socket classes accept CancellationToken and define what clean shutdown means. Waiting until late in the design usually produces abandoned tasks and awkward shutdown races.

A transport class should answer these questions clearly:

  • how do connects cancel
  • how do receive loops stop
  • how is disconnect signaled
  • what happens to pending sends during shutdown

Common Pitfalls

  • Combining transport, framing, and application protocol logic in one class.
  • Assuming one ReadAsync call equals one full message.
  • Letting multiple callers write concurrently with no coordination.
  • Omitting cancellation and graceful shutdown behavior from the design.
  • Treating async socket code as complete once the happy-path demo works.

Summary

  • A good asynchronous socket design separates transport responsibilities from protocol logic.
  • Read paths should be explicit loops with clear message framing.
  • Concurrent writes need coordination.
  • Cancellation and shutdown behavior should be designed up front.
  • Small focused classes are easier to test and reason about than one monolithic socket wrapper.

Course illustration
Course illustration

All Rights Reserved.