This documentation is part of the "Projects with Books" initiative at zenOSmosis.
The source code for this project is available on GitHub.
Binary Framing Protocol
Loading…
Binary Framing Protocol
Relevant source files
Purpose and Scope
The binary framing protocol defines the lowest-level data structure for all communication in muxio. This protocol operates below the RPC layer, providing a schemaless, ordered, chunked byte transport mechanism.
Core Responsibilities:
- Define binary frame structure (7-byte header + variable payload)
- Encode structured data into frames via
FrameMuxStreamEncoder - Decode byte streams into
DecodedFramestructures viaFrameMuxStreamDecoder - Support frame types (
Data,End,Cancel) for stream lifecycle management - Enable payload chunking with configurable
max_chunk_size
The framing protocol is transport-agnostic and makes no assumptions about serialization formats. It operates purely on raw bytes. Higher-level concerns like RPC headers, method IDs, and serialization are handled by layers above this protocol.
Related Pages:
- Stream multiplexing and per-stream decoders: #3.2
- RPC protocol structures (RpcHeader, RpcRequest, RpcResponse): #3.4
- RPC session management and stream ID allocation: #3.2
Sources: src/rpc/rpc_internals/rpc_session.rs:15-24 DRAFT.md:11-21
Architecture Overview
The framing protocol sits between raw transport (WebSocket, TCP) and the RPC session layer. It provides discrete message boundaries over continuous byte streams.
Component Diagram: Frame Processing Pipeline
graph TB
RawBytes["Raw Byte Stream\nWebSocket/TCP"]
FrameEncoder["FrameMuxStreamEncoder\nencode frames"]
FrameDecoder["FrameMuxStreamDecoder\nparse frames"]
DecodedFrame["DecodedFrame\nstruct"]
RpcSession["RpcSession\nmultiplexer"]
RpcStreamEncoder["RpcStreamEncoder\nper-stream encoder"]
RpcSession -->|allocate stream_id| RpcStreamEncoder
RpcStreamEncoder -->|emit frames| FrameEncoder
FrameEncoder -->|write_bytes| RawBytes
RawBytes -->|read_bytes| FrameDecoder
FrameDecoder -->|yield| DecodedFrame
DecodedFrame -->|route by stream_id| RpcSession
Key Classes:
FrameMuxStreamEncoder: Encodes frames into bytes (referenced indirectly viaRpcStreamEncoder)FrameMuxStreamDecoder: Parses incoming bytes intoDecodedFramestructuresDecodedFrame: Represents a parsed frame withstream_id,kind, andpayloadRpcSession: Manages frame multiplexing and per-stream decoder lifecycle
Sources: src/rpc/rpc_internals/rpc_session.rs:20-33 src/rpc/rpc_internals/rpc_session.rs:52-60
Frame Structure
Each binary frame consists of a fixed-size header followed by a variable-length payload. The frame format is designed for efficient parsing with minimal copying.
Binary Layout
| Field | Offset | Size | Type | Description |
|---|---|---|---|---|
stream_id | 0 | 4 bytes | u32 (LE) | Logical stream identifier for multiplexing |
kind | 4 | 1 byte | u8 enum | FrameKind: Data=0, End=1, Cancel=2 |
payload_length | 5 | 2 bytes | u16 (LE) | Payload byte count (0-65535) |
payload | 7 | payload_length | [u8] | Raw payload bytes |
Total Frame Size: 7 bytes (header) + payload_length
graph LR
subgraph "Frame Header - 7 Bytes"
StreamID["stream_id\n4 bytes\nu32 LE"]
Kind["kind\n1 byte\nu8"]
PayloadLen["payload_length\n2 bytes\nu16 LE"]
end
subgraph "Frame Payload"
Payload["payload\n0-65535 bytes\n[u8]"]
end
StreamID --> Kind
Kind --> PayloadLen
PayloadLen --> Payload
All multi-byte integers use little-endian encoding. The u16 payload length field limits individual frames to 65,535 bytes, enforcing bounded memory consumption per frame.
Frame Header Diagram
Sources: src/rpc/rpc_internals/rpc_stream_decoder.rs:3-6 (frame structure constants)
stateDiagram-v2
[*] --> AwaitingFirstFrame
AwaitingFirstFrame --> AcceptingData: FrameKind::Data
AcceptingData --> AcceptingData: FrameKind::Data
AcceptingData --> StreamClosed: FrameKind::End
AcceptingData --> StreamAborted: FrameKind::Cancel
AwaitingFirstFrame --> StreamAborted: FrameKind::Cancel
StreamClosed --> [*]
StreamAborted --> [*]
Frame Types
The FrameKind enum (from crate::frame) defines frame semantics. Each frame’s kind field determines how the decoder processes it.
Frame Lifecycle State Machine
FrameKind Values
| Enum Variant | Wire Value | Purpose | Effect |
|---|---|---|---|
FrameKind::Data | Implementation-defined | Payload chunk | Accumulate or emit payload bytes |
FrameKind::End | Implementation-defined | Normal termination | Finalize stream, emit RpcStreamEvent::End |
FrameKind::Cancel | Implementation-defined | Abnormal abort | Discard state, emit error or remove decoder |
Frame Type Semantics
- Data frames: Carry payload bytes. A stream may consist of 1 to N data frames depending on chunking.
- End frames: Signal successful completion. Payload length is typically 0. Decoder emits final event and removes stream state.
- Cancel frames: Signal early termination. Decoder removes stream from
rpc_stream_decodersmap and may emit error event.
Sources: src/rpc/rpc_internals/rpc_session.rs:98-100 (decoder cleanup), src/rpc/rpc_internals/rpc_stream_decoder.rs:156-166 (End/Cancel handling)
Frame Encoding
The encoding process transforms logical data into binary frames suitable for transmission. The core encoder is FrameMuxStreamEncoder, though specific encoding details are handled by stream-level encoders like RpcStreamEncoder.
Frame Encoding Sequence
sequenceDiagram
participant RpcSession
participant RpcStreamEncoder
participant on_emit
participant Transport
RpcSession->>RpcSession: allocate stream_id
RpcSession->>RpcStreamEncoder: new(stream_id, max_chunk_size, header, on_emit)
RpcStreamEncoder->>RpcStreamEncoder: encode RpcHeader into first frame
RpcStreamEncoder->>on_emit: emit(Frame{stream_id, Data, header_bytes})
on_emit->>Transport: write_bytes()
loop "For each payload chunk"
RpcStreamEncoder->>RpcStreamEncoder: chunk payload by max_chunk_size
RpcStreamEncoder->>on_emit: emit(Frame{stream_id, Data, chunk})
on_emit->>Transport: write_bytes()
end
RpcStreamEncoder->>on_emit: emit(Frame{stream_id, End, []})
on_emit->>Transport: write_bytes()
Encoding Process
- Stream ID Allocation:
RpcSession::init_request()allocates a uniquestream_idviaincrement_u32_id() - Encoder Creation: Creates
RpcStreamEncoderwithstream_id,max_chunk_size, andon_emitcallback - Frame Emission: Encoder calls
on_emitfor each frame. Callback receives raw frame bytes for transport writing. - Chunking: If payload exceeds
max_chunk_size, encoder emits multipleDataframes with samestream_id - Finalization: Final
Endframe signals completion
The callback-based on_emit pattern enables non-async, runtime-agnostic operation. Callers provide their own I/O strategy.
Sources: src/rpc/rpc_internals/rpc_session.rs:35-50 (init_request method)
graph TB
read_bytes["read_bytes(&[u8])"]
FrameMuxStreamDecoder["FrameMuxStreamDecoder\nstateful parser"]
DecodedFrame["DecodedFrame\nResult iterator"]
RpcSession["RpcSession"]
rpc_stream_decoders["rpc_stream_decoders\nHashMap<u32, RpcStreamDecoder>"]
RpcStreamDecoder["RpcStreamDecoder\nper-stream state"]
read_bytes -->|input bytes| FrameMuxStreamDecoder
FrameMuxStreamDecoder -->|yield| DecodedFrame
RpcSession -->|iterate frames| DecodedFrame
RpcSession -->|route by stream_id| rpc_stream_decoders
rpc_stream_decoders -->|or_default| RpcStreamDecoder
RpcStreamDecoder -->|decode_rpc_frame| RpcStreamEvent
Frame Decoding
The decoding process parses incoming byte streams into structured DecodedFrame objects. The FrameMuxStreamDecoder maintains parsing state across multiple read_bytes calls to handle partial frame reception.
Decoding Architecture
Decoding Sequence
Decoder State Management
Per-Connection State (RpcSession):
frame_mux_stream_decoder: FrameMuxStreamDecoder- parses frame boundaries from byte streamrpc_stream_decoders: HashMap<u32, RpcStreamDecoder>- mapsstream_idto per-stream state
Per-Stream State (RpcStreamDecoder):
state: RpcDecoderState-AwaitHeader,AwaitPayload, orDoneheader: Option<Arc<RpcHeader>>- parsed RPC header from first framebuffer: Vec<u8>- accumulates bytes across framesrpc_request_id: Option<u32>- extracted from headerrpc_method_id: Option<u64>- extracted from header
Lifecycle:
- Decoders created on-demand via
entry(stream_id).or_default() - Removed on
Endframe: src/rpc/rpc_internals/rpc_session.rs:73-75 - Removed on
Cancelframe: src/rpc/rpc_internals/rpc_session.rs:98-100 - Removed on decode error: src/rpc/rpc_internals/rpc_session.rs82
Sources: src/rpc/rpc_internals/rpc_session.rs:20-33 src/rpc/rpc_internals/rpc_stream_decoder.rs:11-42
graph LR
Payload["Payload: 100 KB"]
Encoder["RpcStreamEncoder\nmax_chunk_size=16384"]
F1["Frame\nstream_id=42\nkind=Data\npayload=16KB"]
F2["Frame\nstream_id=42\nkind=Data\npayload=16KB"]
F3["Frame\nstream_id=42\nkind=Data\npayload=16KB"]
FN["Frame\nstream_id=42\nkind=Data\npayload=..."]
FEnd["Frame\nstream_id=42\nkind=End\npayload=0"]
Payload --> Encoder
Encoder --> F1
Encoder --> F2
Encoder --> F3
Encoder --> FN
Encoder --> FEnd
Chunk Management
Large messages are automatically split into multiple frames to avoid memory exhaustion and enable incremental processing. The chunking mechanism operates transparently at the frame level.
Chunking Strategy
Chunk Size Configuration
max_chunk_size controls payload bytes per frame. System-defined constant DEFAULT_MAX_CHUNK_SIZE provides default.
| Size Range | Latency | Overhead | Use Case |
|---|---|---|---|
| 4-8 KB | Lower | Higher | Interactive/real-time |
| 16-32 KB | Balanced | Moderate | General purpose |
| 64 KB | Higher | Lower | Bulk transfer |
Maximum frame payload is 65,535 bytes (u16::MAX) per frame structure. Practical values are typically 16-32 KB to balance latency and efficiency.
Reassembly Process
Frames with matching stream_id are processed by the same RpcStreamDecoder:
| Frame Type | Decoder Action | State Transition |
|---|---|---|
| First Data (with RPC header) | Parse header, emit RpcStreamEvent::Header | AwaitHeader → AwaitPayload |
| Subsequent Data | Emit RpcStreamEvent::PayloadChunk | Remain in AwaitPayload |
| End | Emit RpcStreamEvent::End, remove decoder | AwaitPayload → removed from map |
| Cancel | Remove decoder, optionally emit error | Any state → removed from map |
Sources: src/rpc/rpc_internals/rpc_stream_decoder.rs:59-146 (decoder state machine), src/rpc/rpc_internals/rpc_session.rs:68-100 (decoder lifecycle)
RPC Header Layer
The framing protocol is payload-agnostic, but RPC usage places an RpcHeader structure in the first frame’s payload. This is an RPC-level concern, not a frame-level concern.
RPC Header Binary Structure
The first Data frame for an RPC stream contains a serialized RpcHeader:
| Field | Offset | Size | Type | Description |
|---|---|---|---|---|
rpc_msg_type | 0 | 1 byte | u8 | RpcMessageType: Call=0, Response=1 |
rpc_request_id | 1 | 4 bytes | u32 LE | Request correlation ID |
rpc_method_id | 5 | 8 bytes | u64 LE | xxhash of method name |
rpc_metadata_length | 13 | 2 bytes | u16 LE | Byte count of metadata |
rpc_metadata_bytes | 15 | variable | [u8] | Serialized parameters or result |
Total: 15 bytes + rpc_metadata_length
RPC Header Constants
Constants from src/constants.rs define the layout:
| Constant | Value | Purpose |
|---|---|---|
RPC_FRAME_FRAME_HEADER_SIZE | 15 | Minimum RPC header size |
RPC_FRAME_MSG_TYPE_OFFSET | 0 | Offset to rpc_msg_type |
RPC_FRAME_ID_OFFSET | 1 | Offset to rpc_request_id |
RPC_FRAME_METHOD_ID_OFFSET | 5 | Offset to rpc_method_id |
RPC_FRAME_METADATA_LENGTH_OFFSET | 13 | Offset to metadata length |
RPC_FRAME_METADATA_LENGTH_SIZE | 2 | Size of metadata length field |
RpcStreamDecoder uses these constants to parse the header from the first frame’s payload.
Sources: src/rpc/rpc_internals/rpc_stream_decoder.rs:2-8 (constant imports), src/rpc/rpc_internals/rpc_stream_decoder.rs:64-125 (parsing logic)
Error Handling
Frame-level errors are represented by FrameDecodeError and FrameEncodeError enumerations. The decoder converts corrupted or invalid frames into error events rather than panicking.
Frame Decode Errors
| Error Variant | Cause | Decoder Behavior |
|---|---|---|
FrameDecodeError::CorruptFrame | Invalid header, failed parsing | Remove stream_id from map, emit error event |
FrameDecodeError::ReadAfterCancel | Data frame after Cancel frame | Return error, stop processing stream |
| Incomplete frame (not an error) | Insufficient bytes for full frame | FrameMuxStreamDecoder buffers, awaits more data |
Error Event Propagation
Error isolation ensures corruption in one stream (stream_id=42) does not affect other streams (stream_id=43, etc.) on the same connection.
Sources: src/rpc/rpc_internals/rpc_session.rs:80-94 (error handling and cleanup), src/rpc/rpc_internals/rpc_stream_decoder.rs:70-72 (error detection)
sequenceDiagram
participant Client
participant Connection
participant Server
Note over Client,Server: Three concurrent calls, interleaved frames
Client->>Connection: Frame{stream_id=1, Data, RpcHeader{Add}}
Client->>Connection: Frame{stream_id=2, Data, RpcHeader{Multiply}}
Client->>Connection: Frame{stream_id=3, Data, RpcHeader{Echo}}
Connection->>Server: Frames arrive in send order
Note over Server: Server processes concurrently
Server->>Connection: Frame{stream_id=2, Data, result}
Server->>Connection: Frame{stream_id=2, End}
Server->>Connection: Frame{stream_id=1, Data, result}
Server->>Connection: Frame{stream_id=1, End}
Server->>Connection: Frame{stream_id=3, Data, result}
Server->>Connection: Frame{stream_id=3, End}
Connection->>Client: Responses complete out-of-order
Multiplexing Example
Multiple concurrent RPC calls use distinct stream_id values to interleave over a single connection:
Concurrent Streams Over Single Connection
Frame multiplexing eliminates head-of-line blocking. A slow operation on stream_id=1 does not delay responses for stream_id=2 or stream_id=3.
Sources: src/rpc/rpc_internals/rpc_session.rs:20-33 (stream ID allocation), src/rpc/rpc_internals/rpc_session.rs:61-77 (decoder routing)
Summary
The binary framing protocol provides a lightweight, efficient mechanism for transmitting structured data over byte streams. Key characteristics:
- Fixed-format frames: 7-byte header + variable payload (max 64 KB per frame)
- Stream identification:
stream_idfield enables multiplexing - Lifecycle management: Data, End, and Cancel frame types control stream state
- Chunking support: Large messages split automatically into multiple frames
- Stateful decoding: Handles partial frame reception across multiple reads
- Error isolation: Frame errors affect only the associated stream
This framing protocol forms the foundation for higher-level stream multiplexing (#3.2) and RPC protocol implementation (#3.4).