Home > Engineering > Infrastructure > bare-for-pear > avsc-rpc > Wire Protocol
avsc-rpc Wire Protocol
Handshake, framing, and protocol negotiation — how bytes are structured on the wire.
Handshake
Every new connection (stateful) or every message (stateless) begins with a handshake. The handshake determines whether client and server share a protocol.
Handshake Request
{
clientHash: bytes(16), — MD5 of client protocol
clientProtocol: null|string, — full protocol (if needed)
serverHash: bytes(16), — expected server protocol hash
meta: null|map<bytes> — optional metadata
}
Handshake Response
{
match: enum { BOTH, CLIENT, NONE },
serverProtocol: null|string, — full protocol (if mismatch)
serverHash: null|bytes(16), — server's actual hash
meta: null|map<bytes> — optional metadata
}
Resolution
- BOTH — client and server protocols match. Proceed directly.
- CLIENT — server recognises client’s protocol but client doesn’t know server’s. Server sends its protocol. Client caches it.
- NONE — neither recognises the other. Both protocols exchanged. Both cache.
After resolution, client and server create an Adapter — a resolver that maps between the two protocol versions using Avro’s type resolution. Subsequent messages use the adapter.
This is schema evolution applied to the protocol level. A client with an older protocol version can communicate with a newer server if the types are compatible.
Framing
Two framing formats are available for binary transports:
Standard Frame Encoding
Avro specification framing. Messages are split into frames, each prefixed with its byte length.
[frameLength: 4 bytes, big-endian] [payload: N bytes]
[frameLength: 4 bytes, big-endian] [payload: N bytes]
...
[0x00 0x00 0x00 0x00] — zero-length frame terminates
Used by FrameEncoder and FrameDecoder.
Netty Encoding
Java Netty-compatible framing. Default for stateful binary channels. Interoperates with JVM Avro RPC implementations.
[messageID: 4 bytes] [frameCount: 4 bytes]
[frameLength: 4 bytes] [payload: N bytes]
[frameLength: 4 bytes] [payload: N bytes]
...
The message ID enables multiplexing — responses are
matched to requests by ID. Used by NettyEncoder and
NettyDecoder.
Stream Classes
All four are streamx.Transform instances:
const { streams } = require('avsc-rpc')
// Standard
const enc = new streams.FrameEncoder()
const dec = new streams.FrameDecoder()
// Netty
const enc = new streams.NettyEncoder()
const dec = new streams.NettyDecoder()
Message Wire Format
A single RPC message on the wire:
Request
[handshake request (first message only, stateful)]
[message metadata: map<bytes>]
[method name: string]
[request body: Avro-encoded by message.requestType]
Response
[handshake response (first message only, stateful)]
[message metadata: map<bytes>]
[boolean: is-error flag]
[body: Avro-encoded by message.responseType or errorType]
The metadata map in each message is distinct from the handshake metadata. It carries per-message context — the wire-level equivalent of mycelium’s context entries.
Multiplexing
Stateful channels multiplex messages over a single connection using message IDs (4-byte prefix in Netty encoding). Multiple concurrent requests share one TCP socket.
The Registry class tracks pending callbacks by ID. When a response arrives, the ID matches it to the original request’s callback.
// Internal — not normally used directly
const registry = new Registry()
const id = registry.add(callback)
// ... later ...
registry.get(id)(err, response)
Protocol Discovery
Discover a remote server’s protocol without knowing it in advance:
const { discoverProtocol } = require('avsc-rpc')
discoverProtocol(transport, (err, protocol) => {
// protocol is the server's Avro protocol definition
const service = Service.forProtocol(protocol)
const client = service.createClient()
client.createChannel(transport)
})
Sends a handshake with an intentionally wrong hash. The server responds with its full protocol. The client now knows what the server speaks.