Files
Joseph Doherty 539b2b7588 feat: add structured logging, Shouldly assertions, CPM, and project documentation
- Add Microsoft.Extensions.Logging + Serilog to NatsServer and NatsClient
- Convert all test assertions from xUnit Assert to Shouldly
- Add NSubstitute package for future mocking needs
- Introduce Central Package Management via Directory.Packages.props
- Add documentation_rules.md with style guide, generation/update rules, component map
- Generate 10 documentation files across 5 component folders (GettingStarted, Protocol, Subscriptions, Server, Configuration/Operations)
- Update CLAUDE.md with logging, testing, porting, agent model, CPM, and documentation guidance
2026-02-22 21:05:53 -05:00

5.8 KiB

Protocol Overview

NATS uses a line-oriented, text-based protocol over TCP. All commands are terminated by \r\n. This simplicity makes it easy to debug with raw TCP tools and keeps parsing overhead low.

Command Reference

All commands flow either from server to client (S→C) or client to server (C→S). PING and PONG travel in both directions as part of the keepalive mechanism.

Command Direction Format
INFO S→C INFO {json}\r\n
CONNECT C→S CONNECT {json}\r\n
PUB C→S PUB subject [reply] size\r\n[payload]\r\n
HPUB C→S HPUB subject [reply] hdr_size total_size\r\n[headers+payload]\r\n
SUB C→S SUB subject [queue] sid\r\n
UNSUB C→S UNSUB sid [max_msgs]\r\n
MSG S→C MSG subject sid [reply] size\r\n[payload]\r\n
HMSG S→C HMSG subject sid [reply] hdr_size total_size\r\n[headers+payload]\r\n
PING/PONG Both PING\r\n / PONG\r\n
+OK/-ERR S→C +OK\r\n / -ERR 'msg'\r\n

Arguments in brackets are optional. sid is the subscription ID string assigned by the client. Commands with a payload body (PUB, HPUB, MSG, HMSG) use a two-line structure: a control line with sizes, then the raw payload bytes, then a terminating \r\n.

Connection Handshake

The handshake is always server-initiated:

  1. Client opens TCP connection.
  2. Server immediately sends INFO {json}\r\n describing itself.
  3. Client sends CONNECT {json}\r\n with its options.
  4. Normal operation begins (PUB, SUB, MSG, PING/PONG, etc.).

If verbose is enabled in ClientOptions, the server sends +OK after each valid client command. If the server rejects the CONNECT (bad credentials, unsupported protocol version, etc.) it sends -ERR 'message'\r\n and closes the connection.

ServerInfo

The ServerInfo JSON payload is sent in the initial INFO message. The ClientId and ClientIp fields are omitted from JSON when not set.

public sealed class ServerInfo
{
    [JsonPropertyName("server_id")]
    public required string ServerId { get; set; }

    [JsonPropertyName("server_name")]
    public required string ServerName { get; set; }

    [JsonPropertyName("version")]
    public required string Version { get; set; }

    [JsonPropertyName("proto")]
    public int Proto { get; set; } = NatsProtocol.ProtoVersion;

    [JsonPropertyName("host")]
    public required string Host { get; set; }

    [JsonPropertyName("port")]
    public int Port { get; set; }

    [JsonPropertyName("headers")]
    public bool Headers { get; set; } = true;

    [JsonPropertyName("max_payload")]
    public int MaxPayload { get; set; } = NatsProtocol.MaxPayloadSize;

    [JsonPropertyName("client_id")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public ulong ClientId { get; set; }

    [JsonPropertyName("client_ip")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string? ClientIp { get; set; }
}

headers signals that the server supports HPUB/HMSG. max_payload advertises the largest message body the server accepts (default 1 MB). proto is the protocol version integer; the current value is 1.

ClientOptions

The ClientOptions JSON payload is sent by the client in the CONNECT command.

public sealed class ClientOptions
{
    [JsonPropertyName("verbose")]
    public bool Verbose { get; set; }

    [JsonPropertyName("pedantic")]
    public bool Pedantic { get; set; }

    [JsonPropertyName("echo")]
    public bool Echo { get; set; } = true;

    [JsonPropertyName("name")]
    public string? Name { get; set; }

    [JsonPropertyName("lang")]
    public string? Lang { get; set; }

    [JsonPropertyName("version")]
    public string? Version { get; set; }

    [JsonPropertyName("protocol")]
    public int Protocol { get; set; }

    [JsonPropertyName("headers")]
    public bool Headers { get; set; }

    [JsonPropertyName("no_responders")]
    public bool NoResponders { get; set; }
}

echo defaults to true, meaning a client receives its own published messages if it has a matching subscription. Setting echo to false suppresses that. no_responders causes the server to send a status message when a request has no subscribers, rather than letting the client time out.

Protocol Constants

NatsProtocol centralises limits and pre-encoded byte arrays to avoid repeated allocations in the hot path.

public static class NatsProtocol
{
    public const int MaxControlLineSize = 4096;
    public const int MaxPayloadSize = 1024 * 1024; // 1MB
    public const int DefaultPort = 4222;

    // Pre-encoded protocol fragments
    public static readonly byte[] CrLf = "\r\n"u8.ToArray();
    public static readonly byte[] PingBytes = "PING\r\n"u8.ToArray();
    public static readonly byte[] PongBytes = "PONG\r\n"u8.ToArray();
    public static readonly byte[] OkBytes = "+OK\r\n"u8.ToArray();
    public static readonly byte[] InfoPrefix = "INFO "u8.ToArray();
    public static readonly byte[] MsgPrefix = "MSG "u8.ToArray();
    public static readonly byte[] HmsgPrefix = "HMSG "u8.ToArray();
    public static readonly byte[] ErrPrefix = "-ERR "u8.ToArray();
}

MaxControlLineSize (4096 bytes) is the maximum length of a command line before the payload. Any control line that exceeds this limit causes the parser to throw ProtocolViolationException. MaxPayloadSize (1 MB) is the default limit enforced by the parser; it is configurable per server instance.

Go Reference

The Go implementation of protocol parsing is in golang/nats-server/server/parser.go. The .NET implementation follows the same command identification strategy and enforces the same control line and payload size limits.