惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

云风的 BLOG
云风的 BLOG
M
MIT News - Artificial intelligence
博客园 - Franky
J
Java Code Geeks
V
Visual Studio Blog
G
Google Developers Blog
罗磊的独立博客
MongoDB | Blog
MongoDB | Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Recent Announcements
Recent Announcements
Last Week in AI
Last Week in AI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Stack Overflow Blog
Stack Overflow Blog
博客园 - 司徒正美
The GitHub Blog
The GitHub Blog
腾讯CDC
阮一峰的网络日志
阮一峰的网络日志
V
V2EX
博客园 - 【当耐特】
IT之家
IT之家
I
InfoQ
U
Unit 42
C
Check Point Blog
Martin Fowler
Martin Fowler

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Another simple TCP chat in C#? Why not
Luke Matt · 2026-05-17 · via DEV Community

Does this low-latency TCP runtime:

  • let me build something in one evening instead of two days?
  • assume writing the code will take the whole day, but we finish the topic in 2 hours?
  • solve website refactoring problems without trying multiple approaches?

None of these things 🙂

Because obviously it's not an AI agent.
I had to start like this because posts without mentioning AI are kind of niche these days 😉


So what will be in the post?

Well - I created a library for client-server communication with, let's say, a bit of a mixed style. Something like actor-style, message-driven with RPC over TCP transport.

It was created mainly out of the need for simplicity and as little code as possible, but with an API as high-level as possible.

When using simple socket wrappers before, you usually have one receiving endpoint at your disposal, which is generally nice, but what next?

Once the message is ready, we have it in the form of bytes and now you can deal with it yourself...

Some libraries don't even frame the message, so assembling the data from the stream is additionally passed on to us.

Having worked with many client-server projects, I had a rough framework of what I expected.

Over time, the concept changed, even very often, but ultimately the library gives me what I expect (of course, for today 😄).


Moving on to the main topic - our simple chat

I set myself a challenge - console chat with as little code as possible.

I didn't fully achieve that 😄 but I'm still satisfied because this minimalist code offers quite good performance on the server side using simple techniques.

Project assumptions

  • global chat
  • each user automatically connects to the server and joins the chat

Without further ado, let's get to the best part: the code.


Server side

[EAttr(ChannelId = ChatSync)]
public static class MainChat
{
    [EAttrChannel(ChannelType = EChannelType.Share, ChannelTasks = 1)]
    public const ushort ChatSync = 1;

    [EAttrPool(MaxPoolObjs = 1000)]
    public const ushort MsgPool = 1;

    static List<byte> _message = [0, 0, 0, 0];
    static readonly EArrayBufferWriter _bufferMsg = new(5000);
    static readonly List<MyUserServer> _users = [];

    static long RegisterUser(MyUserServer user)
    {
        if (!_users.Contains(user))
        {
            _users.Add(user);
            return user.UserId;
        }
        return 0;
    }

    [EAttr(PoolId = MsgPool, MaxParamSize = 4096)]
    static void PushMsg(MyUserServer user, List<byte> msg)
    {
        _message.RemoveRange(4, _message.Count - 4);
        BinaryPrimitives.WriteInt32LittleEndian(CollectionsMarshal.AsSpan(_message), user.UserId);
        _message.AddRange(msg);

        if (user.ESerial.Serialize(_bufferMsg, _message) < 1)
            return;

        var payload = _bufferMsg.WrittenSpan;
        for (int i = 0; i < _users.Count; i++)
        {
            var u = _users[i];
            if (!u.SendSerialized("PushMsg", payload) && u.Status == ESocketServerStatus.Dead)
            {
                _users.RemoveAt(i);
                i--;
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

As you noticed, we work on a static class (we don't need to create an instance if we only have one global room).

We also have two receiving methods.

When does a method become a receiver?

It is when it has the first argument inheriting from the EUserServer class.

This is a kind of contract - a method with: EUserServer and one optional parameter (message) becomes an endpoint.

In our example, the first parameter is the MyUserServer class, which looks like this:

public class MyUserServer : EUserServer
{
    static int _idValue;
    public int UserId { get; private set; }

    public MyUserServer(ESocketResourceServer ers) : base(ers) { }

    protected override void OnConnected()
    {
        UserId = Interlocked.Increment(ref _idValue);
    }
}

Enter fullscreen mode Exit fullscreen mode

Technically, we don't even really need the MyUserServer class here, but it helps assign a simple id to users after they connect.

Back to our main code.

The first thing that catches the eye is that these objects in MainChat:

static List<byte> _message = [0, 0, 0, 0];
static readonly EArrayBufferWriter _bufferMsg = new(5000);
static readonly List<MyUserServer> _users = [];

Enter fullscreen mode Exit fullscreen mode

are not thread-safe ... and in a sense this is true.

However, we use the attribute: ChatSync, which synchronizes all sockets to one thread thanks to the EChannelType.Share option and one task executing ChannelTasks = 1.

To illustrate: if we had EChannelType.Private set, each socket would call our access points on its own thread, breaking thread safety 😅

Fortunately, we can work with objects synchronously, allowing us to implement optimization techniques.

And there are several of them:

  • to reduce the pressure on GC, we use a reusable List<byte> msg object, which becomes reusable by setting PoolId = MsgPool in the attribute
  • we serialize once on one buffer and send the same bytes to everyone
  • the library uses SendSerialized, i.e. mixed sync/async sending, so that slow clients do not block the loop and thus do not create backpressure
  • disconnected clients are removed inline in a loop

In fact, that's all on the server side - the main logic fits in 45 lines.

And here, if anyone is still curious how we initialize the server:

var serv = new ETCPServer<MyUserServer>(new ERSA(PrivatePemKey, PrivatePemKeyToSign));
serv.Start(EAddress.Get());

Enter fullscreen mode Exit fullscreen mode

... yes, encryption is required.
I found no reason not to require it.


Client side

The client part is much simpler.

We don't need to worry too much about performance because, as a rule, each client has its own logical machine.

var client = new EUserClient(new ERSA(PublicPemKey, PublicPemKeyToSign));
if (await client.Connect(EAddress.Get()) == 0)
{
    long id = await client.SendWithResponse("RegisterUser");
    if (id > 0)
    {
        Console.WriteLine($"You have joined the chat room as User({id})");
        while (true)
        {
            var message = Console.ReadLine();

            var bytesMsg = Encoding.UTF8.GetBytes(message ?? "").ToList();
            if (bytesMsg.Count > 4000 || bytesMsg.Count < 1)
            {
                Console.WriteLine("Message length is out of range");
                continue;
            }

            if (!await client.Send("PushMsg", bytesMsg))
            {
                Console.WriteLine("Failed to send message");
                continue;
            }
        }
    }
    else
    {
        Console.WriteLine("Failed to join the room");
        Environment.Exit(0);
    }
}
else
{
    Console.WriteLine("Failed to connect to the server");
    Environment.Exit(0);
}

Enter fullscreen mode Exit fullscreen mode

In the usual way, we connect to the server, join the global room and can immediately write.

To make sure we joined successfully, we use SendWithResponse to guarantee a response.

Below is a simple class that receives messages from the server:

public static class MainChat
{
    [EAttr(PoolId = 1, MaxParamSize = 4096)]
    static void PushMsg(EUserClient user, List<byte> msg)
    {
        if (msg.Count <= 4)
            return;

        var spanMsg = CollectionsMarshal.AsSpan(msg);

        int userId = BinaryPrimitives.ReadInt32LittleEndian(spanMsg[..4]);
        string message = Encoding.UTF8.GetString(spanMsg[4..]);
        Console.WriteLine($"User({userId}): {message}");
    }
}

Enter fullscreen mode Exit fullscreen mode

That's all, really.

We could have tried to reduce another dozen lines, but I don't think it's worth it. We would have to make too many compromises.

Even now, while typing in the console, we can get a message from the server appended to ours 😄

But you can add a solution to this problem yourself.


Repo

Link to my repo with complete example:

Github: EnjoySockets

There is also a small benchmark comparing 3 libraries to show roughly where EnjoySockets ranks in terms of performance.

There are plans for an even more extensive example, showing almost all aspects of the library, but that's for another post.


P.S. This library isn't AI, but it should speed up your work 😉

Thanks for reading and just enjoy it 😄