framer

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 16, 2025 License: MIT Imports: 7 Imported by: 0

README

framer — message boundaries over stream I/O

Go Reference Go Report Card Coverage Status

Languages: English | 简体中文 | 日本語 | Español | Français

Portable message framing for Go. Preserve one-message-per-Read/Write over stream transports.

Scope: framer solves message boundary preservation across stream transports.

At a glance

  • Solve message boundary problems for byte streams (TCP, Unix stream, pipes).
  • Pass-through on boundary-preserving transports (UDP, Unix datagram, WebSocket, SCTP).
  • Portable wire format; configurable byte order.

Why

Many transports are byte streams (TCP, Unix stream, pipes). A single Read may return a partial application message, or several messages concatenated. framer restores message boundaries: in stream mode, one Read returns exactly one message payload, and one Write emits exactly one framed message.

Protocol adaptation

  • BinaryStream (stream transports: TCP, TLS-over-TCP, Unix stream, pipes): adds a length prefix; reads/writes whole messages.
  • SeqPacket (e.g., SCTP, WebSocket): pass-through; the transport already preserves boundaries.
  • Datagram (e.g., UDP, Unix datagram): pass-through; boundary already preserved.

Select at construction time via WithProtocol(...) (read/write variants exist) or via transport helpers (see Options).

Wire format

Compact variable-length length prefix, followed by payload bytes. Byte order for the extended length is configurable: WithByteOrder, or per-direction WithReadByteOrder / WithWriteByteOrder.

Frame data format

The framing scheme used by framer is intentionally compact:

  • Header byte H0 + optional extended length bytes.
  • Let L be the payload length in bytes.
    • If 0 ≤ L ≤ 253 (0x00..0xFD): H0 = L. No extra length bytes.
    • If 254 ≤ L ≤ 65535 (0x0000..0xFFFF): H0 = 0xFE and the next 2 bytes encode L as an unsigned 16‑bit integer in the configured byte order.
    • If 65536 ≤ L ≤ 2^56-1: H0 = 0xFF and the next 7 bytes carry L as a 56‑bit integer, laid out in the configured byte order.
      • Big‑endian: bytes [1..7] are the big‑endian lower 56 bits of L.
      • Little‑endian: bytes [1..7] are the little‑endian lower 56 bits of L.

Limits and errors:

  • The maximum supported payload length is 2^56-1; larger values result in framer.ErrTooLong.
  • When a read‑side limit is configured (WithReadLimit), lengths exceeding the limit fail with framer.ErrTooLong.

Quick start

Install with go get:

go get code.hybscloud.com/framer
c1, c2 := net.Pipe()
defer c1.Close()
defer c2.Close()

w := framer.NewWriter(c1, framer.WithWriteTCP())
r := framer.NewReader(c2, framer.WithReadTCP())

go func() { _, _ = w.Write([]byte("hello")) }()

buf := make([]byte, 64)
n, err := r.Read(buf)
if err != nil {
    panic(err)
}
fmt.Printf("got: %q\n", buf[:n])

Options

  • WithProtocol(proto Protocol) — choose BinaryStream, SeqPacket, or Datagram (read/write variants available).
  • Byte order: WithByteOrder, or WithReadByteOrder / WithWriteByteOrder.
  • WithReadLimit(n int) — cap maximum message payload size when reading.
  • WithRetryDelay(d time.Duration) — configure would-block policy; helpers: WithNonblock() / WithBlock().

Transport helpers (presets):

  • WithReadTCP / WithWriteTCP (BinaryStream, network‑order BigEndian)
  • WithReadUDP / WithWriteUDP (Datagram, BigEndian)
  • WithReadWebSocket / WithWriteWebSocket (SeqPacket, BigEndian)
  • WithReadSCTP / WithWriteSCTP (SeqPacket, BigEndian)
  • WithReadUnix / WithWriteUnix (BinaryStream, BigEndian)
  • WithReadUnixPacket / WithWriteUnixPacket (Datagram, BigEndian)
  • WithReadLocal / WithWriteLocal (BinaryStream, native byte order)

Everything else: see GoDoc: https://pkg.go.dev/code.hybscloud.com/framer

Semantics Contract

Error taxonomy
Error Meaning Caller action
nil Operation completed successfully Proceed; n reflects full progress
io.EOF End of stream (no more messages) Stop reading; normal termination
io.ErrUnexpectedEOF Stream ended mid-message (header or payload incomplete) Treat as fatal; data corruption or disconnect
io.ErrShortBuffer Destination buffer too small for message payload Retry with larger buffer
io.ErrShortWrite Destination accepted fewer bytes than provided Retry or treat as fatal per context
io.ErrNoProgress Underlying Reader made no progress (n==0, err==nil) on a non-empty buffer Treat as fatal; indicates a broken io.Reader implementation
framer.ErrWouldBlock No progress possible now without waiting Retry later (after poll/event); n may be >0
framer.ErrMore Progress made; more completions will follow Process result, then call again
framer.ErrTooLong Message exceeds limit or max wire format Reject message; possibly fatal
framer.ErrInvalidArgument Nil reader/writer or invalid config Fix configuration
Outcome tables

Reader.Read(p []byte) (n int, err error) — BinaryStream mode

Condition n err
Complete message delivered payload length nil
len(p) < payload length 0 io.ErrShortBuffer
Payload exceeds ReadLimit 0 ErrTooLong
Underlying returns would-block bytes read so far ErrWouldBlock
Underlying returns more bytes read so far ErrMore
EOF at message boundary 0 io.EOF
EOF mid-header or mid-payload bytes read io.ErrUnexpectedEOF

Writer.Write(p []byte) (n int, err error) — BinaryStream mode

Condition n err
Complete framed message emitted len(p) nil
Payload exceeds max (2^56-1) 0 ErrTooLong
Underlying returns would-block payload bytes written so far ErrWouldBlock
Underlying returns more payload bytes written so far ErrMore

Reader.WriteTo(dst io.Writer) (n int64, err error)

Condition n err
All messages transferred until EOF total payload bytes nil
Underlying reader returns would-block payload bytes written ErrWouldBlock
Underlying reader returns more payload bytes written ErrMore
dst returns would-block payload bytes written ErrWouldBlock
Message exceeds internal buffer (64KiB default) bytes so far ErrTooLong
Stream ended mid-message bytes so far io.ErrUnexpectedEOF

Writer.ReadFrom(src io.Reader) (n int64, err error)

Condition n err
All chunks encoded until src EOF total payload bytes nil
src returns would-block payload bytes written ErrWouldBlock
src returns more payload bytes written ErrMore
Underlying writer returns would-block payload bytes written ErrWouldBlock

Forwarder.ForwardOnce() (n int, err error)

Condition n err
One message fully forwarded payload bytes (write phase) nil
Packet source returns (n>0, io.EOF) payload bytes (write phase) nil (next call returns io.EOF)
No more messages 0 io.EOF
Read phase would-block bytes read this call ErrWouldBlock
Write phase would-block bytes written this call ErrWouldBlock
Message exceeds internal buffer 0 io.ErrShortBuffer
Message exceeds ReadLimit 0 ErrTooLong
Stream ended mid-message bytes so far io.ErrUnexpectedEOF
Operation classification
Operation Boundary behavior Use case
Reader.Read Message-preserving: one call = one message Application-level message processing
Writer.Write Message-preserving: one call = one framed message Application-level message sending
Reader.WriteTo Chunking: streams payload bytes (not wire format) Efficient bulk transfer; does NOT preserve boundaries
Writer.ReadFrom Chunking: each src chunk becomes one message Efficient bulk encoding; does NOT preserve upstream boundaries
Forwarder.ForwardOnce Message-preserving relay: decode one, re-encode one Message-aware proxying with boundary preservation
Blocking policy

By default, framer is non-blocking (WithNonblock()): ErrWouldBlock is returned immediately.

  • WithBlock() — yield (runtime.Gosched) and retry on would-block
  • WithRetryDelay(d) — sleep d and retry on would-block
  • Negative RetryDelay (default) — return ErrWouldBlock immediately

No method hides blocking unless explicitly configured.

Fast paths

framer implements stdlib copy fast paths to interoperate with io.Copy-style engines and iox.CopyPolicy:

  • (*Reader).WriteTo(io.Writer) — efficiently transfers framed message payloads to dst.

    • Stream (BinaryStream): processes one framed message at a time and writes only the payload bytes to dst. If ReadLimit == 0, an internal default cap (64KiB) is used; messages larger than this cap return framer.ErrTooLong.
    • Packet (SeqPacket/Datagram): pass-through (reads bytes, writes bytes).
    • Semantic errors framer.ErrWouldBlock and framer.ErrMore are propagated unchanged with the progress count reflecting bytes written.
  • (*Writer).ReadFrom(io.Reader) — chunk-to-message: each successful Read chunk from src is encoded as a single framed message.

    • This is efficient but does not preserve application message boundaries from src.
    • On boundary-preserving protocols it effectively behaves like pass-through.
    • Semantic errors framer.ErrWouldBlock and framer.ErrMore are propagated unchanged with progress counts.

Recommendation: prefer iox.CopyPolicy with a retry-aware policy (e.g., PolicyRetry) in non-blocking loops so ErrWouldBlock / ErrMore are handled explicitly.

Forwarding

  • Wire proxying (byte engines): use iox.CopyPolicy and standard io fast paths (WriterTo/ReaderFrom). This maximizes throughput when you don't need to preserve higher-level boundaries.
  • Message relay (preserve boundaries): use framer.NewForwarder(dst, src, ...) and call ForwardOnce() in your poll loop. It decodes exactly one framed message from src and re-encodes it as exactly one framed message to dst.
    • Non-blocking semantics: ForwardOnce returns (n>0, framer.ErrWouldBlock|framer.ErrMore) when partial progress happened; retry the same Forwarder instance later to complete.
    • Limits: io.ErrShortBuffer when the internal buffer is too small for the message; framer.ErrTooLong when a message exceeds the configured WithReadLimit.
    • Zero‑alloc steady state after construction; the internal scratch buffer is reused per message.

License

MIT — see LICENSE.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidArgument reports an invalid configuration or nil reader/writer.
	ErrInvalidArgument = errors.New("framer: invalid argument")

	// ErrTooLong reports that a frame length exceeds limits or the supported wire format.
	ErrTooLong = errors.New("framer: message too long")
)
View Source
var (
	// ErrWouldBlock means “no further progress without waiting”.
	//
	// It is an expected, non-failure control-flow signal for non-blocking I/O.
	// Any returned byte count (n) still represents real progress.
	//
	// Caller action: stop the current attempt and retry later (after readiness/event),
	// or configure RetryDelay to emulate cooperative blocking on top of a non-blocking transport.
	ErrWouldBlock = iox.ErrWouldBlock

	// ErrMore means “this completion is usable and more completions will follow”.
	//
	// It is not io.EOF and not “try later”. The operation remains active and additional
	// data/results are expected from the same ongoing operation.
	//
	// Caller action: process the returned bytes/result, then call again to obtain the next chunk.
	ErrMore = iox.ErrMore
)

These are provided as package-level aliases so callers can reference the semantic control-flow errors without importing iox directly.

Functions

func NewPipe

func NewPipe(opts ...Option) (reader io.Reader, writer io.Writer)

NewPipe returns a synchronous in-memory framing pipe.

func NewReadWriter

func NewReadWriter(r io.Reader, w io.Writer, opts ...Option) io.ReadWriter

NewReadWriter returns an io.ReadWriter that reads and writes framed messages.

func NewReader

func NewReader(r io.Reader, opts ...Option) io.Reader

NewReader returns an io.Reader that reads framed messages from r.

func NewWriter

func NewWriter(w io.Writer, opts ...Option) io.Writer

NewWriter returns an io.Writer that writes framed messages to w.

Types

type Forwarder

type Forwarder struct {
	// contains filtered or unexported fields
}

Forwarder relays framed messages from a source to a destination while preserving message boundaries.

Semantics (BinaryStream):

  • One call to ForwardOnce processes at most one logical message.
  • Two-phase state machine per message: 1) Read a whole framed message payload from src into an internal buffer (non-blocking; may return early with partial progress and ErrWouldBlock or ErrMore). 2) Write that same payload as exactly one framed message to dst (non-blocking; may return early with partial progress and ErrWouldBlock or ErrMore).
  • Returns (n, nil) when a whole message payload has been forwarded to dst.
  • Returns (n>0, ErrWouldBlock|ErrMore) when progress happened in the current phase (read or write) but the forwarding of this message is incomplete.
  • Message boundaries are preserved: the destination sees exactly the same payload bytes as the source, encoded as one framed message on stream transports.

Semantics (SeqPacket/Datagram):

  • Treats one packet as one message unit per call. Reads one packet from src and writes one packet to dst.
  • Returns values and non-blocking semantics as above.

Limits and buffer sizing:

  • The internal payload buffer is allocated during construction based on read-side limit (WithReadLimit). If ReadLimit is zero, a conservative default (64KiB) is used. There are no heap allocations in the steady-state forwarding path.
  • If the current message exceeds the internal buffer capacity, ForwardOnce returns io.ErrShortBuffer. Callers can construct a new Forwarder with a larger ReadLimit to accommodate larger messages.
  • If the current message exceeds the configured ReadLimit, ForwardOnce returns ErrTooLong.

Retry rule:

  • On ErrWouldBlock or ErrMore, the caller must retry ForwardOnce on the SAME Forwarder instance to complete the in-flight message. Do not reuse a different instance because the in-flight state (read/write progress) is maintained internally.

func NewForwarder

func NewForwarder(dst io.Writer, src io.Reader, opts ...Option) *Forwarder

NewForwarder constructs a Forwarder that relays messages from src to dst. Options apply per direction (read/write) following the same rules as Reader/Writer.

func (*Forwarder) ForwardOnce

func (f *Forwarder) ForwardOnce() (n int, err error)

ForwardOnce forwards at most one message. See Forwarder docs for semantics.

Return value n reflects progress in the current phase:

  • During the read phase, n is the number of payload bytes read into the internal buffer in this call.
  • During the write phase, n is the number of payload bytes written to dst in this call.

type Option

type Option func(*Options)

func WithBlock

func WithBlock() Option

WithBlock enables cooperative blocking (yield-and-retry) on iox.ErrWouldBlock.

func WithByteOrder

func WithByteOrder(order binary.ByteOrder) Option

func WithNonblock

func WithNonblock() Option

WithNonblock forces non-blocking behavior (return iox.ErrWouldBlock immediately).

func WithProtocol

func WithProtocol(proto Protocol) Option

func WithReadByteOrder

func WithReadByteOrder(order binary.ByteOrder) Option

func WithReadLimit

func WithReadLimit(limit int) Option

func WithReadLocal

func WithReadLocal() Option

WithReadLocal configures the reader side for local (stream) transports: BinaryStream, native byte order.

func WithReadProtocol

func WithReadProtocol(proto Protocol) Option

func WithReadSCTP

func WithReadSCTP() Option

WithReadSCTP configures the reader side for SCTP: SeqPacket (boundaries preserved), BigEndian.

func WithReadTCP

func WithReadTCP() Option

WithReadTCP configures the reader side for TCP: BinaryStream with BigEndian length prefix.

func WithReadUDP

func WithReadUDP() Option

WithReadUDP configures the reader side for UDP: Datagram (pass-through), BigEndian default.

func WithReadUnix

func WithReadUnix() Option

WithReadUnix configures the reader side for Unix stream sockets: BinaryStream, BigEndian.

func WithReadUnixPacket

func WithReadUnixPacket() Option

WithReadUnixPacket configures the reader side for Unix datagram sockets: Datagram (pass-through), BigEndian.

func WithReadWebSocket

func WithReadWebSocket() Option

WithReadWebSocket configures the reader side for WebSocket: SeqPacket (boundaries preserved), BigEndian.

func WithRetryDelay

func WithRetryDelay(d time.Duration) Option

WithRetryDelay sets the retry/wait policy used when the underlying transport returns iox.ErrWouldBlock.

func WithWriteByteOrder

func WithWriteByteOrder(order binary.ByteOrder) Option

func WithWriteLocal

func WithWriteLocal() Option

WithWriteLocal configures the writer side for local (stream) transports: BinaryStream, native byte order.

func WithWriteProtocol

func WithWriteProtocol(proto Protocol) Option

func WithWriteSCTP

func WithWriteSCTP() Option

WithWriteSCTP configures the writer side for SCTP: SeqPacket (boundaries preserved), BigEndian.

func WithWriteTCP

func WithWriteTCP() Option

WithWriteTCP configures the writer side for TCP: BinaryStream with BigEndian length prefix.

func WithWriteUDP

func WithWriteUDP() Option

WithWriteUDP configures the writer side for UDP: Datagram (pass-through), BigEndian default.

func WithWriteUnix

func WithWriteUnix() Option

WithWriteUnix configures the writer side for Unix stream sockets: BinaryStream, BigEndian.

func WithWriteUnixPacket

func WithWriteUnixPacket() Option

WithWriteUnixPacket configures the writer side for Unix datagram sockets: Datagram (pass-through), BigEndian.

func WithWriteWebSocket

func WithWriteWebSocket() Option

WithWriteWebSocket configures the writer side for WebSocket: SeqPacket (boundaries preserved), BigEndian.

type Options

type Options struct {
	ReadByteOrder  binary.ByteOrder
	WriteByteOrder binary.ByteOrder
	ReadProto      Protocol
	WriteProto     Protocol

	// ReadLimit caps the maximum allowed payload size (bytes). Zero means no limit.
	ReadLimit int

	// RetryDelay controls how the framer handles iox.ErrWouldBlock from the underlying transport:
	//   - negative: nonblock, return ErrWouldBlock immediately
	//   - zero: yield (runtime.Gosched) and retry
	//   - positive: sleep for the duration and retry
	RetryDelay time.Duration
}

Options configures framing behavior.

type Protocol

type Protocol uint8

Protocol describes the expected message-boundary behavior of the underlying transport.

The framer logic adapts its algorithm based on this setting:

  • BinaryStream: boundaries are not preserved (e.g., TCP). Framer adds a length prefix.
  • SeqPacket / Datagram: boundaries are preserved. Framer is pass-through.
const (
	BinaryStream Protocol = 1
	SeqPacket    Protocol = 2
	Datagram     Protocol = 3
)

type ReadWriter

type ReadWriter struct {
	*Reader
	*Writer
}

ReadWriter groups Reader and Writer.

type Reader

type Reader struct {
	// contains filtered or unexported fields
}

Reader reads framed messages.

func (*Reader) Read

func (r *Reader) Read(p []byte) (int, error)

func (*Reader) WriteTo

func (r *Reader) WriteTo(dst io.Writer) (int64, error)

WriteTo implements io.WriterTo.

Semantics:

  • Stream (BinaryStream): transfers one framed message payload at a time from the underlying reader into dst. The payload bytes are written as-is; this method does not attempt to preserve or reconstruct framer wire format on the destination unless dst is itself a framer.Writer. It uses an internal reusable scratch buffer sized by the Reader's ReadLimit; when ReadLimit is zero, a conservative default cap is used (64KiB) and messages exceeding this cap result in ErrTooLong.
  • Packet (SeqPacket/Datagram): pass-through, reads bytes and writes them to dst.

Non-blocking semantics: if the underlying reader or writer returns iox.ErrWouldBlock or iox.ErrMore, WriteTo returns immediately with the progress count (bytes written) and the same semantic error. Short writes on dst are handled per io.Writer contract.

type Writer

type Writer struct {
	// contains filtered or unexported fields
}

Writer writes framed messages.

func (*Writer) ReadFrom

func (w *Writer) ReadFrom(src io.Reader) (int64, error)

ReadFrom implements io.ReaderFrom.

Semantics:

  • Chunk-to-message: each chunk read from src (a successful src.Read call) is encoded as a single framed message and written via w.Write. This is efficient but does not preserve upstream application message boundaries. For protocols that already preserve boundaries (SeqPacket/Datagram), this is effectively pass-through.

Non-blocking semantics: if src.Read or the underlying writer returns iox.ErrWouldBlock or iox.ErrMore, ReadFrom returns immediately with the progress count and the same error. No heap allocations in the steady-state path.

func (*Writer) Write

func (w *Writer) Write(p []byte) (int, error)

Directories

Path Synopsis
internal
bo
Package bo provides native byte order selection.
Package bo provides native byte order selection.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL