NeoSocket

Published on
•
5 min read
•
––– views
Demo

Automatically generated narration. Code examples are available in the article.

Why I built a networking framework

NeoSocket grew out of Jingi and my experiments with multiplayer scenes. Coming from web development, I wanted the organization I liked in Spring Boot and ASP.NET: handlers, services, dependency injection, and clear places for shared logic.

The challenge was to keep that structure useful for stateful real-time applications. Besides games and metaverse prototypes, I used the framework in Cypher, a chat app.

Connections, packets, and middleware

NeoSocket keeps connection state and routes packets to handlers. A middleware layer provides places to add authentication, authorization, and traffic controls. Those hooks let the application define its policies; they do not by themselves make a server immune to attacks.

Neo-Protocol identifies packets by number and name. MessagePack is the default serialization format, with Protobuf also considered for data exchange. Binary serialization and packet batching reduce repeated metadata and send overhead. The amount saved depends on the payload, so I do not treat the earlier 50–90% estimate as a universal result.

The framework supports request-response calls and one-way commands. The goal is to choose the communication pattern that fits the operation.

Handlers and services

A handler delegates application logic to a service. This example shows the shape of a chat handler:

[NeoHandler]
public class ChatHandler
{
    [Autowired] private ChatService _chatService { get; set; }
    [HandleMethod]
    public void HandleChat(MessageDTO packet)
    {
        _chatService.HandleChat(packet);
    }
}

The service can use the current connection context, publish a message to the room, and save it:

[Service]
public class ChatService
{
    [Autowired] private IHandleContext _context { get; set; }
    [Autowired] private IChatDB _chatDB { get; set; }
    [Autowired] private ChatManager _chatManager { get; set; }

    public void HandleChat(MessageDTO message)
    {
        //Send message to all user in chatroom
        var chatRoom = _chatManager.GetUser(_context.Sender.State.IdentityId).ChatRoom;
        chatRoom.HandleChat(message);

        //Save message to database
        _chatDB.AddMessage(new MessageEntity
        {
            MessageId = Guid.NewGuid().ToString(),
            RoomName = chatRoom.RoomName,
            UserName = message.UserName,
            Content = message.Content,
            CreatedAt = DateTime.Now
        });
    }
}

The handler model is intended to work across TCP, UDP, WebSocket, HTTP, and Unix sockets, while each transport still has its own behavior.

Packet definitions and clients

Packets carry IDs and names, with MessagePack keys defining their fields:

[NeoPacket]
public class MessageDTO : NeoPacket
{
    [Key(0)] public string UserName { get; set; }

    [Key(1)] public string Content { get; set; }

    [Key(2)] public DateTime CreatedAt { get; set; }
}

The TypeScript SDK provides a corresponding definition for web clients:

@DNeoPacket(0, 'MessageDTO')
export class MessageDTO extends NeoPacket {
  @DKey(0)
  public UserName: string = '';
  @DKey(1)
  public Content: string = '';
  @DKey(2)
  public CreatedAt: Date = new Date();
}

The server implementation started on .NET 7, with a .NET Standard 2.0 client for Unity. The TypeScript SDK lets web apps use the same packet-oriented approach. A stateful connection is useful for these workloads, but it is not necessary for every website.

Service lifetimes

The IoC container includes Scoped, Transient, Singleton, and a Parasitic lifetime that follows a NeoClient. Qualifiers help select an implementation when several are available.

[Service]
[Scope(ServiceLifeTime.Parasitic)]
public class ConnectService : IConnectService
{
    [Autowired] private ChatManager _chatManager { get; set; }
    [Autowired] private IHandleContext _context { get; set; }
    [Autowired] private IChatDB _chatDB { get; set; }
}

NeoSocket can run in a console application or as a background service within a .NET application.

Architecture and test notes

NeoSocket server architecture
NeoSocket middleware architecture

One early test reported up to 400,000 requests per second on a GCP VM with one CPU core and two threads, while other Docker services were also running. The chart below records that test. Request handling in this setup is not equivalent to full game logic or a production capacity guarantee.

Metrics from an early NeoSocket test

I also used a simulated scene with 1,000 players to explore dense updates:

A simulated metaverse scene with 1,000 players

Work still to do

The project includes Prometheus and Grafana middleware for metrics, and I started a React monitoring dashboard. Further work includes reducing convenient but overused singletons, improving concurrency and caching, and making the APIs easier to understand.

The main areas I studied were async programming, thread-safe collections, context switching, serialization, reflection, and service lifetimes. Message queues and distributed game servers are possible next uses, rather than completed products.

Stack

AreaTechnology
Server and clientsC#, .NET 7, .NET Standard 2.0, TypeScript
TransportsTCP, UDP, WebSocket, HTTP, Unix sockets
SerializationMessagePack
Connection utilitiesUPnP, UDP hole punching
MetricsPrometheus, Grafana
EnvironmentsWindows, macOS, Linux; Unity and web clients