Skip to content

brianpht/netcode-java

Repository files navigation

netcode-java

A deterministic, allocation-conscious, lock-free Java implementation of the netcode 1.02 UDP connection protocol by Glenn Fiedler / Mas Bandwidth LLC, with an integrated reliable packet acknowledgement layer.

Targets low-latency, high-throughput workloads: game servers, real-time analytics, trading infrastructure, messaging pipelines, and telemetry systems.


Overview

netcode is a UDP-based, encrypted, connection-oriented client/server protocol. It provides:

  • Authentication via signed connect tokens issued by a trusted web backend (out-of-band, over HTTPS).
  • Encryption of all packets (except the initial connection request) using ChaCha20-Poly1305 IETF.
  • Replay attack prevention via a per-client 256-entry sliding-window sequence buffer.
  • DDoS amplification protection: servers never send more data than they receive during the handshake.
  • Byte-level compatibility with the C reference implementation (netcode.c).

The integrated reliable layer (optional, enabled per endpoint) adds:

  • Packet acknowledgement - compressed ack field piggybacked on every outgoing payload; the sender learns which packets the remote side received.
  • Transparent fragmentation and reassembly - payloads above a configurable threshold are split into fragments and reassembled on the remote side with zero application-level changes.
  • RTT, packet-loss, and bandwidth estimates - exponential moving averages updated once per agent tick.
  • Zero heap allocation in steady state - all sliding-window buffers, fragment reassembly slots, and scratch space are pre-allocated at construction.

reliable does NOT retransmit. It provides the acknowledgement signal so the application layer can decide whether to retransmit application-level data.

The serialize layer provides a deterministic, allocation-free bit-packing codec:

  • BitWriter / BitReader - little-endian bit-packed I/O into Agrona DirectBuffer; values are packed to the minimum number of bits required by the declared [min, max] range.
  • WriteStream / ReadStream / MeasureStream - typed stream wrappers; MeasureStream computes a conservative upper-bound buffer size before any allocation.
  • Serializable - interface for symmetric write / read / measure implementations; deterministic and allocation-free by contract.

The channel layer sits above the reliable layer and provides yojimbo-style reliable-ordered message channels:

  • ReliableOrderedChannel - reliable, ordered delivery of regular messages and large block messages; aggressive resend until packet ack; zero heap allocation in steady state.
  • Connection - multiplexes one or more channels over a single ReliableEndpoint; assembles per-tick outgoing packets and dispatches received payloads to each channel.

Architecture

flowchart TD
    Backend["Web Backend\n(ConnectTokenGenerator)"]
    App["Application Logic"]
    Channel["Connection + ReliableOrderedChannel\n(reliable-ordered messages, block msgs)\n[optional]"]
    Reliable["ReliableEndpoint\n(ack, fragment, telemetry)\n[optional per connection]"]
    Serialize["serialize layer\n(BitWriter/BitReader, WriteStream/ReadStream)"]
    Client["NetcodeClient\n(ClientAgent)"]
    Server["NetcodeServer\n(ServerAgent)"]
    Transport["UdpTransport\n(DatagramChannel, non-blocking)"]
    Simulator["NetworkSimulator\n(test only)"]

    Backend -- "connect token (2048 bytes, HTTPS)" --> App
    App -- "sendMessage / receiveMessage" --> Channel
    Channel -- "bit-packed payload" --> Serialize
    Channel -- "sendPacket / receivePacket" --> Reliable
    Reliable -- "transmitHandler (framed bytes)" --> Client
    Reliable -- "transmitHandler (framed bytes)" --> Server
    Client -- "CONNECTION_REQUEST / RESPONSE / PAYLOAD" --> Transport
    Server -- "CONNECTION_CHALLENGE / KEEP_ALIVE / PAYLOAD" --> Transport
    Transport -- "UDP datagrams" --> Server
    Simulator -. "in-process loopback (tests)" .-> Client
    Simulator -. "in-process loopback (tests)" .-> Server
Loading

Each component owns a single Agrona Agent event loop pinned to an isolated CPU core. There is no shared mutable state between components; all inter-thread data flows through pre-allocated ring buffers.

The serialize layer provides deterministic bit-packing I/O for channel message payloads. The channel/connection layer sits above reliable: a Connection multiplexes one or more ReliableOrderedChannel instances over a single ReliableEndpoint. The reliable layer sits above the netcode encryption boundary: the reliable header is prepended to the plaintext payload before netcode encrypts the outer CONNECTION_PAYLOAD envelope. Two Java endpoints will always agree on the wire format; no C interop is required for the reliable or channel layers.


Protocol Flow

sequenceDiagram
    participant B as Web Backend
    participant C as Client
    participant S as Server

    B->>C: connect token (2048 bytes, HTTPS)
    C->>S: CONNECTION_REQUEST (1078 bytes, unencrypted)
    S->>S: decrypt private token, validate, add encryption mapping
    S->>C: CONNECTION_CHALLENGE (encrypted, 308 bytes)
    C->>S: CONNECTION_RESPONSE (encrypted, 308 bytes)
    S->>S: decrypt challenge token, assign client slot
    S->>C: CONNECTION_KEEP_ALIVE (encrypted, 8 bytes)
    note over C,S: Steady state - reliable layer active when reliableEnabled = true
    C->>S: CONNECTION_PAYLOAD [reliable header + app data, encrypted]
    S->>C: CONNECTION_PAYLOAD [reliable header + app data, encrypted]
    S->>C: CONNECTION_PAYLOAD [ack bits piggybacked on every packet]
Loading

Client State Machine

stateDiagram-v2
    [*] --> DISCONNECTED

    DISCONNECTED --> SENDING_CONNECTION_REQUEST : connect(token)
    DISCONNECTED --> INVALID_CONNECT_TOKEN : bad token

    SENDING_CONNECTION_REQUEST --> SENDING_CONNECTION_RESPONSE : recv CHALLENGE
    SENDING_CONNECTION_REQUEST --> CONNECTION_REQUEST_TIMED_OUT : timeout
    SENDING_CONNECTION_REQUEST --> CONNECTION_DENIED : recv DENIED
    SENDING_CONNECTION_REQUEST --> CONNECT_TOKEN_EXPIRED : token expired
    SENDING_CONNECTION_REQUEST --> SENDING_CONNECTION_REQUEST : try next server addr

    SENDING_CONNECTION_RESPONSE --> CONNECTED : recv KEEP_ALIVE
    SENDING_CONNECTION_RESPONSE --> CONNECTION_RESPONSE_TIMED_OUT : timeout
    SENDING_CONNECTION_RESPONSE --> CONNECTION_DENIED : recv DENIED
    SENDING_CONNECTION_RESPONSE --> CONNECT_TOKEN_EXPIRED : token expired

    CONNECTED --> DISCONNECTED : recv DISCONNECT or app disconnect
    CONNECTED --> CONNECTION_TIMED_OUT : no packet for timeout_seconds
Loading

Package Layout

net.ztrust.netcode/
  Netcode.java                     - protocol-wide constants (all #define equivalents)
  NetcodeAddress.java              - IPv4/IPv6 + port value type; parse, format, compare

  codec/
    BufferWriter.java              - little-endian write primitives (Agrona MutableDirectBuffer)
    BufferReader.java              - little-endian read primitives (Agrona DirectBuffer)
    ConnectTokenCodec.java         - public connect token (2048 bytes) read/write
    ConnectTokenPrivateCodec.java  - private token section (1024 bytes) serialize/encrypt/decrypt
    ChallengeTokenCodec.java       - challenge token (300 bytes) serialize/encrypt/decrypt
    PacketCodec.java               - all 7 packet types write/read

  crypto/
    NetcodeCrypto.java             - AEAD wrappers: ChaCha20-Poly1305 and XChaCha20-Poly1305
    Chacha20Poly1305Engine.java    - pluggable cipher engine interface
    BcChacha20Poly1305Engine.java  - Bouncy Castle engine (baseline, handshake path)
    DirectChacha20Poly1305Engine.java - pure-Java, zero-allocation engine (steady-state hot path)
    JdkChacha20Poly1305Engine.java - JDK built-in engine (JCA; hardware intrinsics if available)
    ReplayProtection.java          - 256-entry sliding-window replay filter (long[])

  client/
    ClientState.java               - client state constants (mirrors NETCODE_CLIENT_STATE_*)
    ClientConfig.java              - configuration: transports, clocks, reliable flags, callbacks
    NetcodeClient.java             - full client state machine
    ClientAgent.java               - Agrona Agent wrapper; optional CPU affinity pinning

  server/
    ServerConfig.java              - configuration: protocol ID, private key, reliable flags, callbacks
    NetcodeServer.java             - full server state machine (up to 256 clients)
    ServerAgent.java               - Agrona Agent wrapper; optional CPU affinity pinning
    EncryptionManager.java         - address-to-key mapping (parallel primitive arrays)
    ConnectTokenEntryTable.java    - connect-token replay prevention (2048-entry table)
    PacketQueue.java               - per-client circular packet queue (power-of-two)

  transport/
    UdpTransport.java              - non-blocking UDP (NIO DatagramChannel)
    TransportOverride.java         - interface for in-process test transports

  simulator/
    NetworkSimulator.java          - configurable latency/jitter/loss/duplicate (test use only)

  util/
    ConnectTokenGenerator.java     - web-backend API: generate signed connect tokens
    EpochClock.java                - epoch-seconds clock interface
    NanoClock.java                 - nanosecond clock interface
    CachedEpochClock.java          - epoch clock updated once per agent tick
    CachedNanoClock.java           - nano clock updated once per agent tick
    AgentLauncher.java             - factory for starting AgentRunner on a named thread

net.ztrust.reliable/
  ReliableConstants.java           - all integer constants (port of reliable.c #defines)
  ReliableConfig.java              - immutable configuration snapshot (built via builder)
  ReliableConfigBuilder.java       - fluent builder; enforces power-of-two capacity fields
  TransmitPacketHandler.java       - @FunctionalInterface: called when a framed datagram is ready
  ProcessPacketHandler.java        - @FunctionalInterface: called when a payload is fully reassembled
  SequenceBuffer.java              - sliding-window buffer: uint16 key -> off-heap entry slab
  PacketHeaderCodec.java           - encode/decode compressed 3-9 byte packet header
  FragmentCodec.java               - encode/decode 5-byte fragment header
  ReliableEndpoint.java            - main endpoint: send, receive, update, acks, telemetry

net.ztrust.serialize/
  Serializable.java                - interface: symmetric write/read/measure; deterministic, allocation-free
  BitWriter.java                   - little-endian bit-packed writes into Agrona MutableDirectBuffer
  BitReader.java                   - little-endian bit-packed reads from Agrona DirectBuffer
  BitUtils.java                    - static helpers: bitsRequired(min, max), compressed float utilities
  WriteStream.java                 - typed write-direction stream wrapping BitWriter
  ReadStream.java                  - typed read-direction stream wrapping BitReader; error flag on malformed input
  MeasureStream.java               - estimation stream: counts bits without writing; gives safe buffer upper bound

net.ztrust.channel/
  ChannelConfig.java               - power-of-two buffer sizes, resend timers, block fragment config
  ChannelError.java                - error level constants: NONE, DESYNC, SEND_QUEUE_FULL, etc.
  ReliableOrderedChannel.java      - reliable-ordered message channel with block message support (yojimbo port)
  Connection.java                  - multiplexes channels over a ReliableEndpoint; serializes/deserializes per tick

net.ztrust.core/
  Main.java                        - entry point placeholder (server/client launcher)

Requirements

Requirement Version
JDK 21 LTS
Gradle (wrapper) 8.x (included)

Dependencies

Library Version Purpose
Agrona 1.21.2 Off-heap buffers, primitive collections, Agent/AgentRunner
LMAX Disruptor 4.0.0 Ring-buffer inter-thread messaging
Bouncy Castle 1.78.1 XChaCha20-Poly1305 and ChaCha20-Poly1305 (handshake path)
OpenHFT Java-Thread-Affinity 3.23.3 CPU core pinning for hot agent threads
HdrHistogram 2.2.2 Latency percentile telemetry
JMH 1.37 Micro-benchmarks
JUnit 5 5.10.0 Unit and integration tests

Build

# Build library
./gradlew build

# Run all unit tests
./gradlew test

# Run integration tests (real UDP loopback)
./gradlew integrationTest

# Run both
./gradlew test integrationTest

# Lint check
./gradlew checkstyleMain checkstyleTest

# Auto-fix formatting
./gradlew spotlessApply

# JMH benchmarks (full run)
./gradlew jmh

# JMH quick smoke-run (1 warmup, 1 measurement iteration)
./gradlew jmh -PquickBench

All CI checks must pass before committing:

./gradlew spotlessApply
./gradlew checkstyleMain checkstyleTest
./gradlew compileJava
./gradlew test integrationTest
./gradlew jmh -PquickBench

Quick Start

1. Generate a connect token (web backend)

ConnectTokenGenerator gen = new ConnectTokenGenerator();
byte[] token = new byte[Netcode.CONNECT_TOKEN_BYTES];

NetcodeAddress publicAddr   = NetcodeAddress.fromString("127.0.0.1:9000");
NetcodeAddress internalAddr = NetcodeAddress.fromString("127.0.0.1:9000");
byte[] privateKey = new byte[Netcode.KEY_BYTES];
NetcodeCrypto.generateKey(privateKey);  // done once at backend startup

gen.generate(
    new NetcodeAddress[]{ publicAddr },    // public server addresses shown to client
    new NetcodeAddress[]{ internalAddr },  // internal addresses encrypted in the token
    /* expireSeconds   */ 30,
    /* timeoutSeconds  */ 5,
    /* clientId        */ 42L,
    /* protocolId      */ 0xFEDCBA98L,
    privateKey,
    /* userData        */ null,
    /* createTimestamp */ System.currentTimeMillis() / 1000L,
    token, 0);
// send `token` to the client over HTTPS

2. Start a server

ServerConfig cfg = new ServerConfig();
cfg.protocolId = 0xFEDCBA98L;
System.arraycopy(privateKey, 0, cfg.privateKey, 0, Netcode.KEY_BYTES);
cfg.connectDisconnectCallback = (clientIndex, connected) ->
    System.out.println("client " + clientIndex + (connected ? " connected" : " disconnected"));

// Optional: enable the reliable ack + fragmentation layer
cfg.reliableEnabled = true;   // default: false
cfg.fragmentAbove   = 1024;   // bytes; payloads above this are fragmented (default: 1024)
cfg.maxFragments    = 16;     // max fragments per packet (default: 16)
cfg.ackCallback     = (clientIndex, sequence) -> {
    // called once per update tick for each newly acked outgoing sequence number
};

NetcodeServer server = new NetcodeServer(cfg);
NetcodeAddress bindAddr = NetcodeAddress.fromString("0.0.0.0:9000");
UdpTransport transport = new UdpTransport();
transport.open(bindAddr, Netcode.SERVER_SOCKET_SNDBUF, Netcode.SERVER_SOCKET_RCVBUF);
server.start(bindAddr, 64 /* maxClients */, transport, null /* ipv6 */);

// Wrap in an agent and launch on a dedicated core
ServerAgent agent = new ServerAgent("server", cfg, server, /* cpuCore */ 2);
AgentRunner runner = AgentLauncher.launch(agent, new BusySpinIdleStrategy(), Throwable::printStackTrace);

3. Connect a client

ClientConfig clientCfg = new ClientConfig();
UdpTransport clientTransport = new UdpTransport();
clientTransport.open(NetcodeAddress.fromString("0.0.0.0:0"),
    Netcode.CLIENT_SOCKET_SNDBUF, Netcode.CLIENT_SOCKET_RCVBUF);
clientCfg.transportIpv4 = clientTransport;

// Optional: enable the reliable layer (must match server setting)
clientCfg.reliableEnabled = true;
clientCfg.fragmentAbove   = 1024;
clientCfg.ackCallback     = sequence -> {
    // called once per update tick for each newly acked outgoing sequence number
};

NetcodeClient client = new NetcodeClient(clientCfg);
client.connect(token);  // token obtained from the web backend

ClientAgent clientAgent = new ClientAgent("client", clientCfg, client, /* cpuCore */ 3);
AgentRunner clientRunner = AgentLauncher.launch(clientAgent,
    new BusySpinIdleStrategy(), Throwable::printStackTrace);

4. Send and receive payloads

// Sending (server -> client slot 0)
byte[] payload = new byte[64];
// ... fill payload ...
server.sendPacket(0 /* clientIndex */, payload, 0 /* offset */, payload.length);

// Sending (client -> server)
client.sendPacket(payload, 0, payload.length);

// Receiving on the server
byte[] recv = new byte[Netcode.MAX_PAYLOAD_BYTES];
long[] seqOut = new long[1];
int len = server.receivePacket(0 /* clientIndex */, seqOut, recv, 0);
if (len > 0) { /* process recv[0..len-1], sequence in seqOut[0] */ }

// Receiving on the client
len = client.receivePacket(seqOut, recv, 0);
if (len > 0) { /* process recv[0..len-1] */ }

When reliableEnabled = true, the sequence number in seqOut[0] is the reliable sequence (uint16, wraps at 65535). The ack callback fires once per tick for each sequence the remote side confirmed receiving.


Crypto Engine Selection

NetcodeCrypto supports three pluggable ChaCha20-Poly1305 engines. Select the engine best suited to your latency budget:

Factory method Engine Allocation Notes
new NetcodeCrypto() Bouncy Castle (high-level) ~2 objects per call Baseline; acceptable for handshake paths only
NetcodeCrypto.withDirectEngine() Pure-Java (RFC 8439, pre-allocated) Zero Recommended for steady-state payload hot path
NetcodeCrypto.withJdkEngine() JDK JCA ChaCha20-Poly1305 ~1 IvParameterSpec per call Leverages hardware intrinsics where available

XChaCha20-Poly1305 (used for connect-token encryption) is implemented on top of any engine using HChaCha20 subkey derivation, matching libsodium crypto_aead_xchacha20poly1305_ietf exactly.


Performance Budget

Metric Target
Small message decode < 100 ns
Ring buffer publish < 80 ns
Primitive map lookup < 50 ns
reliable sendPacket (256-byte, unfragmented) < 150 ns
reliable receivePacket (256-byte) < 100 ns
reliable sendPacket (4096-byte, 4 fragments) < 500 ns total
reliable update() telemetry < 2 us (called at 60 Hz)
serialize: single field write+read < 10 ns
serialize: full object write+read (14 fields) < 100 ns
serialize: compressed-float write+read < 15 ns
serialize: MeasureStream full object < 50 ns
channel: sendMessage (regular, 256-byte) < 200 ns
channel: receiveMessage (regular, 256-byte) < 150 ns
End-to-end IPC p50 < 5 us
End-to-end IPC p99 < 15 us
End-to-end IPC p99.99 < 50 us
Allocation on hot path 0 bytes / event
GC pause during operational window 0 ms

Recommended GC strategy:

  • ZGC or Shenandoah - sub-ms pauses, default for most services.
  • Epsilon GC + fixed heap - zero-GC operational windows (e.g., trading sessions).
  • G1 - acceptable for non-critical, non-latency-sensitive components only.

Testing

src/test/java/            - unit tests (deterministic clocks, in-process simulator, no real network)
src/integrationTest/java/ - integration tests (real UDP loopback via DatagramChannel)
src/jmh/java/             - JMH micro-benchmarks (codec, crypto engines, server update loop, reliable)

Test highlights:

Test class Coverage
BufferRoundTripTest Little-endian read/write round-trip for all primitive widths
ConnectTokenCodecTest Full 2048-byte public token encode/decode
ConnectTokenPrivateCodecTest Private section (1024 bytes) with XChaCha20 encryption
ChallengeTokenCodecTest Challenge token (300 bytes) with ChaCha20 encryption
PacketCodecTest All 7 packet types encode/decode
NetcodeCryptoTest AEAD encrypt/decrypt, HChaCha20 test vectors
CryptoEnginesTest Cross-engine output equivalence (BC, Direct, JDK)
ReplayProtectionTest Sliding-window accept/reject sequences
EncryptionManagerTest Address-to-key mapping, expiry, eviction
ConnectTokenEntryTableTest Token replay prevention across clients
NetcodeServerTest Full server connect/disconnect/timeout lifecycle
NetcodeClientTest Client state-machine transitions
ClientServerTest End-to-end in-process client-server handshake and payload exchange
AgentSoakTest Steady-state soak with zero-allocation assertions
NetworkSimulatorTest Simulator latency/jitter/loss/duplicate
NetcodeAddressTest IPv4/IPv6 address parse, format, compare
ConnectTokenGeneratorTest Connect token generation and field validation
SequenceBufferTest Sliding-window insert/find/advance/generateAckBits, wrap-around
PacketHeaderCodecTest Compressed header encode/decode round-trip, all prefix combinations
ReliableEndpointTest Ack flow, fragment reassembly, packet-loss scenario, RTT/bandwidth
ReliableNetcodeIntegrationTest reliable layer wired into full client-server: small payload acks, large fragmented payload reassembly, reconnect state reset
BitPackerTest BitWriter/BitReader round-trip; ports test_bitpacker and test_endian from C++ serialize reference
BitUtilsTest bitsRequired, compressed-float round-trip, edge cases
MeasureStreamTest MeasureStream bit-count upper-bound invariant vs WriteStream
StreamRoundTripTest WriteStream/ReadStream symmetric round-trip for all primitive types
EntityStateExampleTest Demo: 20-entity FPS snapshot; raw vs bit-packed vs delta comparison
ReliableOrderedChannelTest Single/multiple messages, resend after withheld ack, in-order delivery, block message send/reassembly
ConnectionTest End-to-end round-trip for Connection with two in-process loopback endpoints; ack propagation
UdpTransportIntegrationTest Real UDP send/receive over loopback

JMH benchmarks:

Benchmark class Measures
BufferBenchmark Little-endian read/write throughput on off-heap buffer
TokenPacketCodecBenchmark Token and packet encode/decode throughput
CryptoEngineBenchmark Per-engine encrypt/decrypt latency at 8/256/1200-byte payloads
ClientBenchmark Client-side payload send round-trip via NetworkSimulator
ServerBenchmark Server update idle, receive-one-payload, connect-token/encryption ops
SequenceBufferBenchmark SequenceBuffer hot-path: find (hit/miss), testInsert, insert steady-state
PacketHeaderCodecBenchmark PacketHeaderCodec/FragmentCodec encode/decode best/worst case
ReliableEndpointBenchmark Isolated reliable sendPacket/receivePacket at 256-byte and 4096-byte (fragmented)
ReliableNetcodeBenchmark End-to-end reliable layer wired into full client-server: unfragmented and fragmented round-trip
BitPackerBenchmark Bit-packing layer: single-field write+read, full-object write+read, compressed-float, MeasureStream

Wire Compatibility

The Java implementation is byte-compatible with the C reference (netcode.c):

  • Same little-endian encoding for all integers.
  • Same packet prefix-byte encoding (packet_type | (num_seq_bytes << 4)).
  • Same nonce construction: [0x00000000 || sequence64] for ChaCha20; 24-byte random for XChaCha20.
  • Same associated data: version_info || protocol_id || prefix_byte.
  • Same connect-token byte layout.

The reliable layer operates entirely above the netcode encryption boundary. The reliable header is prepended to the plaintext payload before netcode encrypts the outer CONNECTION_PAYLOAD packet. Two Java endpoints using the same ReliableConfig always agree on the wire format; no C interop test is required.

Replay tests: record a session from the C reference, replay in Java, assert byte-identical output.


Architectural Decisions

See docs/decisions/ for Architecture Decision Records.


License

This project is a Java port of the netcode protocol reference implementation. See the original LICENCE for the upstream C source terms.

About

A deterministic, allocation-conscious, lock-free Java implementation of the Netcode

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages