Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

GitHub

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 DecodedFrame structures via FrameMuxStreamDecoder
  • 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 via RpcStreamEncoder)
  • FrameMuxStreamDecoder: Parses incoming bytes into DecodedFrame structures
  • DecodedFrame: Represents a parsed frame with stream_id, kind, and payload
  • RpcSession: 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

FieldOffsetSizeTypeDescription
stream_id04 bytesu32 (LE)Logical stream identifier for multiplexing
kind41 byteu8 enumFrameKind: Data=0, End=1, Cancel=2
payload_length52 bytesu16 (LE)Payload byte count (0-65535)
payload7payload_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 VariantWire ValuePurposeEffect
FrameKind::DataImplementation-definedPayload chunkAccumulate or emit payload bytes
FrameKind::EndImplementation-definedNormal terminationFinalize stream, emit RpcStreamEvent::End
FrameKind::CancelImplementation-definedAbnormal abortDiscard 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_decoders map 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

  1. Stream ID Allocation: RpcSession::init_request() allocates a unique stream_id via increment_u32_id()
  2. Encoder Creation: Creates RpcStreamEncoder with stream_id, max_chunk_size, and on_emit callback
  3. Frame Emission: Encoder calls on_emit for each frame. Callback receives raw frame bytes for transport writing.
  4. Chunking: If payload exceeds max_chunk_size, encoder emits multiple Data frames with same stream_id
  5. Finalization: Final End frame 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 stream
  • rpc_stream_decoders: HashMap<u32, RpcStreamDecoder> - maps stream_id to per-stream state

Per-Stream State (RpcStreamDecoder):

  • state: RpcDecoderState - AwaitHeader, AwaitPayload, or Done
  • header: Option<Arc<RpcHeader>> - parsed RPC header from first frame
  • buffer: Vec<u8> - accumulates bytes across frames
  • rpc_request_id: Option<u32> - extracted from header
  • rpc_method_id: Option<u64> - extracted from header

Lifecycle:

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 RangeLatencyOverheadUse Case
4-8 KBLowerHigherInteractive/real-time
16-32 KBBalancedModerateGeneral purpose
64 KBHigherLowerBulk 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 TypeDecoder ActionState Transition
First Data (with RPC header)Parse header, emit RpcStreamEvent::HeaderAwaitHeaderAwaitPayload
Subsequent DataEmit RpcStreamEvent::PayloadChunkRemain in AwaitPayload
EndEmit RpcStreamEvent::End, remove decoderAwaitPayload → removed from map
CancelRemove decoder, optionally emit errorAny 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:

FieldOffsetSizeTypeDescription
rpc_msg_type01 byteu8RpcMessageType: Call=0, Response=1
rpc_request_id14 bytesu32 LERequest correlation ID
rpc_method_id58 bytesu64 LExxhash of method name
rpc_metadata_length132 bytesu16 LEByte count of metadata
rpc_metadata_bytes15variable[u8]Serialized parameters or result

Total: 15 bytes + rpc_metadata_length

RPC Header Constants

Constants from src/constants.rs define the layout:

ConstantValuePurpose
RPC_FRAME_FRAME_HEADER_SIZE15Minimum RPC header size
RPC_FRAME_MSG_TYPE_OFFSET0Offset to rpc_msg_type
RPC_FRAME_ID_OFFSET1Offset to rpc_request_id
RPC_FRAME_METHOD_ID_OFFSET5Offset to rpc_method_id
RPC_FRAME_METADATA_LENGTH_OFFSET13Offset to metadata length
RPC_FRAME_METADATA_LENGTH_SIZE2Size 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 VariantCauseDecoder Behavior
FrameDecodeError::CorruptFrameInvalid header, failed parsingRemove stream_id from map, emit error event
FrameDecodeError::ReadAfterCancelData frame after Cancel frameReturn error, stop processing stream
Incomplete frame (not an error)Insufficient bytes for full frameFrameMuxStreamDecoder 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_id field 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).