Languages: English | 简体中文 | 日本語 | Español | Français
Portable message framing for Go. Preserve one-message-per-Read/Write over stream transports.
Scope: message boundary preservation for stream transports.
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.
- Message boundary preservation for byte streams (TCP, Unix stream, pipes).
- Pass-through on boundary-preserving transports (UDP, Unix datagram, WebSocket, SCTP).
- Portable wire format; configurable byte order.
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.- For
Reader.Read, packet modes are pass-through:WithReadLimitis checked after one receive, so an oversized packet may returnn > limitwithErrTooLong;nis the bytes copied from that packet. - Packet-output paths retry whole packets only after zero-progress
ErrWouldBlock/ErrMore; a fully accepted packet returned withErrWouldBlockorErrMoreis not replayed, and partial packet writes are reported asio.ErrShortWrite.
Select at construction time via WithProtocol(...) (read/write variants exist) or via transport helpers (see Options).
Compact variable-length length prefix, followed by payload bytes. Byte order for the extended length is configurable: WithByteOrder, or per-direction WithReadByteOrder / WithWriteByteOrder.
The framing scheme used by framer is intentionally compact:
- Header byte
H0+ optional extended length bytes. - Let
Lbe the payload length in bytes.- If
0 ≤ L ≤ 253(0x00..0xFD):H0 = L. No extra length bytes. - If
254 ≤ L ≤ 65535(0x0000..0xFFFF):H0 = 0xFEand the next 2 bytes encodeLas an unsigned 16‑bit integer in the configured byte order. - If
65536 ≤ L ≤ 2^56-1:H0 = 0xFFand the next 7 bytes carryLas a 56‑bit integer, laid out in the configured byte order.- Big‑endian: bytes
[1..7]are the big‑endian lower 56 bits ofL. - Little‑endian: bytes
[1..7]are the little‑endian lower 56 bits ofL.
- Big‑endian: bytes
- If
Limits and errors:
- The maximum supported payload length is
2^56-1; larger values result inframer.ErrTooLong. - When a read‑side limit is configured (
WithReadLimit), lengths exceeding the limit fail withframer.ErrTooLong.
Install with go get:
go get code.hybscloud.com/framerc1, 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])framer defaults to non-blocking mode. In an event-driven loop:
for {
n, err := r.Read(buf)
if n > 0 {
process(buf[:n])
}
if err != nil {
if err == framer.ErrWouldBlock {
// No data now; wait for readability (epoll, io_uring, etc.)
continue
}
if err == io.EOF {
break
}
log.Fatal(err)
}
}WithProtocol(proto Protocol): chooseBinaryStream,SeqPacket, orDatagram(read/write variants available).- Byte order:
WithByteOrder, orWithReadByteOrder/WithWriteByteOrder. WithReadLimit(n int): cap maximum message payload size when reading;Reader.Readenforces it post-read in packet modes and may returnn > limitwithErrTooLong.WithRetryDelay(d time.Duration): configure zero-progressErrWouldBlockpolicy; a negative value returnsErrWouldBlockimmediately, zero yields and retries, and a positive value sleeps fordbefore retrying. If an operation already transferred bytes, it returns the positive count withErrWouldBlockso the caller can process progress before retrying; related options: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
- Packet mode preserves transport boundaries and does not split packets.
Reader.ReadenforcesWithReadLimitafter one packet read; transfer helpers use one sentinel byte to reject oversized packets before forwarding bytes.- Packet-preserving destinations retry the whole packet only after zero-progress
ErrWouldBlock/ErrMore; a fully accepted packet returned withErrWouldBlockorErrMoreis not replayed, and partial packet writes are boundary failures reported asio.ErrShortWrite. Reader.WriteToto an arbitraryio.Writeris a byte-copy transfer with suffix resume. When the destination is aframer.Writer, it uses the destination algebra: packet writers retry the whole packet after zero progress, and stream writers retry the same in-flight frame.- If a packet source returns
(n > 0, err),Reader.WriteToemits the admitted packet before reportingerr; write-side suspension keeps that source signal pending across retry. - Progress counts are operation-indexed:
Reader.Readreports bytes copied intop,Reader.WriteToreports bytes written todst,Writer.ReadFromreports bytes read fromsrcand admitted to the writer state, andForwarder.ForwardOncereports progress in its current phase.
ErrWouldBlockis readiness suspension, not failure; aggregate helpers may return a positive count when earlier loop steps made progress before suspension.ErrMoremeans the same operation has more progress to deliver; it is notio.EOFand not readiness suspension. Process any returned progress, then call the same operation again.- Retry
Reader.Readafter partial stream progress on the sameReaderwith the same buffer. - Retry
Writer.Writeafter BinaryStream suspension on the sameWriterwith the same message length; BinaryStream header bytes are not included inn. In packet modes,n == len(p)withErrWouldBlockorErrMoremeans the packet was accepted, so do not replayp. - Retry
Reader.WriteToon the sameReaderand same destination,Writer.ReadFromon the sameWriter, andForwarder.ForwardOnceon the sameForwarder.
- Hot paths keep runtime checks minimal for steady-state throughput.
- Callers are responsible for valid option/buffer usage and operation-specific retry after
ErrWouldBlockorErrMore.
| 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 |
Same operation has more progress to deliver, distinct from EOF and readiness suspension | Process returned progress, then call the same operation again |
framer.ErrTooLong |
Message exceeds a configured limit, transfer cap, or wire-format bound | Reject message; possibly fatal |
framer.ErrInvalidArgument |
Nil reader/writer or invalid config | Fix configuration |
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 |
| Packet source exceeds ReadLimit before forwarding | bytes already written before that packet | ErrTooLong |
| 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 bytes read from src | nil |
| src returns would-block | bytes read from src before the signal | ErrWouldBlock |
| src returns more | bytes read from src before the signal | ErrMore |
| Underlying writer returns would-block | bytes read from src and admitted before suspension; 0 on pure write-side resume | ErrWouldBlock |
| Underlying writer returns more | bytes read from src and admitted before suspension; 0 on pure write-side resume | ErrMore |
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 |
| Source returns would-block | read bytes if no packet was emitted; packet-source n > 0 is emitted first and returns payload bytes (write phase) |
ErrWouldBlock |
| Source returns more | read bytes if no packet was emitted; packet-source n > 0 is emitted first and returns payload bytes (write phase) |
ErrMore |
| Write phase would-block | bytes written this call | ErrWouldBlock |
| Write phase more | bytes written this call | ErrMore |
| Stream message or required packet read capacity exceeds internal buffer | 0 | io.ErrShortBuffer |
| Packet exceeds ReadLimit/default packet transfer cap before forwarding | bytes read from the packet, not forwarded | ErrTooLong |
| Stream ended mid-message | bytes so far | io.ErrUnexpectedEOF |
| 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 |
Byte transfer to arbitrary writers; known framer destinations preserve packet/frame retry law |
Efficient bulk transfer with suffix resume |
Writer.ReadFrom |
Chunking: each src chunk becomes one message; packet output is whole-packet retry only after zero progress | Efficient bulk encoding; does NOT preserve upstream boundaries |
Forwarder.ForwardOnce |
Message-preserving relay: decode one, re-encode one | Message-aware proxying with boundary preservation |
By default, framer is non-blocking (WithNonblock()): ErrWouldBlock is returned immediately.
WithBlock()orWithRetryDelay(0): yield (runtime.Gosched) and retry zero-progress would-blockWithRetryDelay(d > 0): sleepdand retry zero-progress would-block- Negative
RetryDelay(default): return zero-progressErrWouldBlockimmediately - If a read or write already transferred bytes, framer returns the positive count with
ErrWouldBlock; process the progress and retry the same operation as documented above.
No method hides blocking unless explicitly configured.
framer uses code.hybscloud.com/iox control flow signals. ErrWouldBlock and ErrMore are aliases from iox, enabling direct integration with other iox-aware components (iofd, takt).
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 todst.- Stream (
BinaryStream): processes one framed message at a time and writes only the payload bytes todst. IfReadLimit == 0, an internal default cap (64KiB) is used; messages larger than this cap returnframer.ErrTooLong. - Packet (
SeqPacket/Datagram): pass-through byte transfer; sentinel-cap reads reject oversized packets before forwarding, andncounts bytes written todst. - Semantic write-side errors
framer.ErrWouldBlockandframer.ErrMoreare propagated unchanged with the progress count reflecting bytes written; packet-source errors returned with bytes are reported after the admitted packet is emitted.
- Stream (
-
(*Writer).ReadFrom(io.Reader): chunk-to-message; each successfulReadchunk fromsrcis 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.ErrWouldBlockandframer.ErrMoreare propagated unchanged;ncounts bytes read fromsrcand admitted to the writer state.
- This is efficient but does not preserve application message boundaries from
Recommendation: prefer iox.CopyPolicy with a retry-aware policy (e.g., PolicyRetry) in non-blocking loops so ErrWouldBlock / ErrMore are handled explicitly.
Zero-allocation steady state: After initial buffer allocation, Forwarder and WriteTo paths reuse internal buffers. No heap allocations occur per message in steady state.
Note on partial write recovery: When using iox.Copy with non-blocking destinations, partial writes may occur. If the source does not implement io.Seeker, iox.Copy returns iox.ErrNoSeeker to prevent silent data loss. For non-seekable sources (e.g., network sockets), use iox.CopyPolicy with PolicyRetry for write-side semantic errors to ensure all read bytes are written before returning.
- Wire proxying (byte engines): use
iox.CopyPolicyand standardiofast paths (WriterTo/ReaderFrom) when byte-level forwarding is acceptable and higher-level boundaries do not need preservation. - Message relay (preserve boundaries): use
framer.NewForwarder(dst, src, ...)and callForwardOnce()in your poll loop. It decodes exactly one framed message fromsrcand re-encodes it as exactly one framed message todst.- Non-blocking semantics: retry the same
Forwarderinstance afterframer.ErrWouldBlockorframer.ErrMore; packet-source(n > 0, err)is emitted before the source signal is reported, and write-side suspension keeps that source signal pending for the later retry on the sameForwarder. - Limits:
io.ErrShortBufferwhen the internal buffer is too small for a stream message or required packet read capacity;framer.ErrTooLongwhen a packet exceedsWithReadLimitor the default packet transfer cap before forwarding. - Zero‑alloc steady state after construction; the internal scratch buffer is reused per message.
- Non-blocking semantics: retry the same
Message relay example:
fwd := framer.NewForwarder(dst, src, framer.WithReadTCP(), framer.WithWriteTCP())
for {
_, err := fwd.ForwardOnce()
if err != nil {
if err == framer.ErrWouldBlock {
continue // wait for src readable or dst writable
}
if err == io.EOF {
break
}
log.Fatal(err)
}
}MIT — see LICENSE.
©2025 Hayabusa Cloud Co., Ltd.