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.
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;
MeasureStreamcomputes a conservative upper-bound buffer size before any allocation. - Serializable - interface for symmetric
write/read/measureimplementations; 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.
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
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.
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]
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
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)
| Requirement | Version |
|---|---|
| JDK | 21 LTS |
| Gradle (wrapper) | 8.x (included) |
| 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 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 -PquickBenchAll CI checks must pass before committing:
./gradlew spotlessApply
./gradlew checkstyleMain checkstyleTest
./gradlew compileJava
./gradlew test integrationTest
./gradlew jmh -PquickBenchConnectTokenGenerator 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 HTTPSServerConfig 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);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);// 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.
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.
| 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.
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 |
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.
See docs/decisions/ for Architecture Decision Records.
This project is a Java port of the netcode protocol reference implementation. See the original LICENCE for the upstream C source terms.