https://github.com/hayabusa-cloud/framer
Portable message framing for Go over stream transports
https://github.com/hayabusa-cloud/framer
framing golang io
Last synced: 7 days ago
JSON representation
Portable message framing for Go over stream transports
- Host: GitHub
- URL: https://github.com/hayabusa-cloud/framer
- Owner: hayabusa-cloud
- License: mit
- Created: 2025-12-13T07:09:09.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2025-12-19T13:18:50.000Z (about 1 month ago)
- Last Synced: 2025-12-21T17:35:35.995Z (about 1 month ago)
- Topics: framing, golang, io
- Language: Go
- Homepage: https://code.hybscloud.com/
- Size: 138 KB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.es.md
- License: LICENSE
Awesome Lists containing this project
README
# framer — límites de mensaje sobre E/S de flujo
[](https://pkg.go.dev/code.hybscloud.com/framer)
[](https://goreportcard.com/report/github.com/hayabusa-cloud/framer)
[](https://codecov.io/gh/hayabusa-cloud/framer)
[](LICENSE)
**Idiomas / Languages:** [English](README.md) | [简体中文](README.zh-CN.md) | [日本語](README.ja.md) | Español | [Français](README.fr.md)
Framing de mensajes portable para Go. Conserva “un mensaje por `Read`/`Write`” sobre transportes tipo stream.
Alcance: `framer` resuelve la preservación de límites de mensaje en transportes de flujo.
## En resumen
- Resuelve problemas de límites de mensaje en flujos de bytes (TCP, Unix stream, pipes).
- Pass-through en transportes que ya preservan límites (UDP, Unix datagram, WebSocket, SCTP).
- Formato wire portable; orden de bytes configurable.
## Por qué
Muchos transportes son flujos de bytes (TCP, Unix stream, pipes). Un solo `Read` puede devolver una fracción de un mensaje de aplicación, o varios mensajes concatenados. `framer` restaura los límites: en modo stream, un `Read` devuelve exactamente un payload de mensaje y un `Write` emite exactamente un mensaje enmarcado.
## Adaptación de protocolo
- `BinaryStream` (transportes stream: TCP, TLS-over-TCP, Unix stream, pipes): agrega un prefijo de longitud; lee/escribe mensajes completos.
- `SeqPacket` (p. ej., SCTP, WebSocket): pass-through; el transporte ya preserva límites.
- `Datagram` (p. ej., UDP, Unix datagram): pass-through; el transporte ya preserva límites.
Selecciona al construir vía `WithProtocol(...)` (hay variantes de lectura/escritura) o usa los helpers de transporte (ver Options).
## Wire format
Prefijo de longitud compacto de tamaño variable, seguido por bytes de payload. El orden de bytes para la longitud extendida es configurable: `WithByteOrder`, o por dirección `WithReadByteOrder` / `WithWriteByteOrder`.
## Formato de datos del frame
El esquema de framing de `framer` es intencionalmente compacto:
- Byte de cabecera `H0` + bytes opcionales de longitud extendida.
- Sea `L` la longitud del payload en bytes.
- Si `0 ≤ L ≤ 253` (`0x00..0xFD`): `H0 = L`. Sin bytes extra.
- Si `254 ≤ L ≤ 65535` (`0x0000..0xFFFF`): `H0 = 0xFE` y los siguientes 2 bytes codifican `L` como entero sin signo de 16 bits en el orden configurado.
- Si `65536 ≤ L ≤ 2^56-1`: `H0 = 0xFF` y los siguientes 7 bytes llevan los 56 bits bajos de `L` en el orden configurado.
- Big-endian: bytes `[1..7]` son los 56 bits bajos de `L` en big-endian.
- Little-endian: bytes `[1..7]` son los 56 bits bajos de `L` en little-endian.
Límites y errores:
- La longitud máxima de payload soportada es `2^56-1`; valores mayores producen `framer.ErrTooLong`.
- Con un límite de lectura (`WithReadLimit`), longitudes mayores fallan con `framer.ErrTooLong`.
## Inicio rápido
Instala con `go get`:
```shell
go get code.hybscloud.com/framer
```
```go
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)` — elige `BinaryStream`, `SeqPacket` o `Datagram` (hay variantes de lectura/escritura).
- Orden de bytes: `WithByteOrder`, o `WithReadByteOrder` / `WithWriteByteOrder`.
- `WithReadLimit(n int)` — limita el tamaño máximo del payload al leer.
- `WithRetryDelay(d time.Duration)` — política de would-block; helpers: `WithNonblock()` / `WithBlock()`.
Helpers de transporte (presets):
- `WithReadTCP` / `WithWriteTCP` (`BinaryStream`, BigEndian en orden de red)
- `WithReadUDP` / `WithWriteUDP` (`Datagram`, BigEndian)
- `WithReadWebSocket` / `WithWriteWebSocket` (`SeqPacket`, BigEndian)
- `WithReadSCTP` / `WithWriteSCTP` (`SeqPacket`, BigEndian)
- `WithReadUnix` / `WithWriteUnix` (`BinaryStream`, BigEndian)
- `WithReadUnixPacket` / `WithWriteUnixPacket` (`Datagram`, BigEndian)
- `WithReadLocal` / `WithWriteLocal` (`BinaryStream`, orden nativo)
Más: GoDoc https://pkg.go.dev/code.hybscloud.com/framer
## Contrato de semántica (Semantics Contract)
### Taxonomía de errores
| Error | Significado | Acción del llamador |
|-------|-------------|---------------------|
| `nil` | Operación completada con éxito | Continúa; `n` refleja el progreso total |
| `io.EOF` | Fin de stream (no hay más mensajes) | Deja de leer; terminación normal |
| `io.ErrUnexpectedEOF` | El stream terminó a mitad de mensaje (header o payload incompleto) | Trátalo como fatal; posible corrupción o desconexión |
| `io.ErrShortBuffer` | Buffer destino demasiado pequeño para el payload | Reintenta con un buffer más grande |
| `io.ErrShortWrite` | El destino aceptó menos bytes que los provistos | Reintenta o trátalo como fatal según el contexto |
| `io.ErrNoProgress` | El Reader subyacente no avanzó (`n==0, err==nil`) con buffer no vacío | Trátalo como fatal; indica un `io.Reader` roto |
| `framer.ErrWouldBlock` | No es posible avanzar ahora sin esperar | Reintenta más tarde (tras poll/event); `n` puede ser >0 |
| `framer.ErrMore` | Hubo progreso; seguirán más completions del mismo op | Procesa y vuelve a llamar |
| `framer.ErrTooLong` | El mensaje excede límites o el máximo del wire format | Rechaza; posiblemente fatal |
| `framer.ErrInvalidArgument` | Reader/Writer nil o configuración inválida | Corrige la configuración |
### Tablas de resultados
**`Reader.Read(p []byte) (n int, err error)`** — modo BinaryStream
| Condición | n | err |
|----------|---|-----|
| Mensaje completo entregado | payload length | `nil` |
| `len(p) < payload length` | 0 | `io.ErrShortBuffer` |
| Payload excede ReadLimit | 0 | `ErrTooLong` |
| El subyacente devuelve would-block | bytes leídos hasta ahora | `ErrWouldBlock` |
| El subyacente devuelve more | bytes leídos hasta ahora | `ErrMore` |
| EOF en el límite de mensaje | 0 | `io.EOF` |
| EOF a mitad de header o payload | bytes leídos | `io.ErrUnexpectedEOF` |
**`Writer.Write(p []byte) (n int, err error)`** — modo BinaryStream
| Condición | n | err |
|----------|---|-----|
| Mensaje enmarcado completo emitido | `len(p)` | `nil` |
| Payload excede el máximo (2^56-1) | 0 | `ErrTooLong` |
| El subyacente devuelve would-block | bytes de payload escritos | `ErrWouldBlock` |
| El subyacente devuelve more | bytes de payload escritos | `ErrMore` |
**`Reader.WriteTo(dst io.Writer) (n int64, err error)`**
| Condición | n | err |
|----------|---|-----|
| Transferencia hasta EOF | bytes totales de payload | `nil` |
| Reader subyacente devuelve would-block | bytes de payload escritos | `ErrWouldBlock` |
| Reader subyacente devuelve more | bytes de payload escritos | `ErrMore` |
| `dst` devuelve would-block | bytes de payload escritos | `ErrWouldBlock` |
| Mensaje excede el buffer interno (64KiB por defecto) | bytes hasta ahora | `ErrTooLong` |
| Stream terminó a mitad de mensaje | bytes hasta ahora | `io.ErrUnexpectedEOF` |
**`Writer.ReadFrom(src io.Reader) (n int64, err error)`**
| Condición | n | err |
|----------|---|-----|
| Chunks codificados hasta src EOF | bytes totales de payload | `nil` |
| `src` devuelve would-block | bytes de payload escritos | `ErrWouldBlock` |
| `src` devuelve more | bytes de payload escritos | `ErrMore` |
| Writer subyacente devuelve would-block | bytes de payload escritos | `ErrWouldBlock` |
**`Forwarder.ForwardOnce() (n int, err error)`**
| Condición | n | err |
|----------|---|-----|
| Un mensaje reenviado completamente | bytes de payload (fase de escritura) | `nil` |
| Fuente packet devuelve `(n>0, io.EOF)` | bytes de payload (fase de escritura) | `nil` (la próxima llamada devuelve `io.EOF`) |
| No hay más mensajes | 0 | `io.EOF` |
| Would-block en fase de lectura | bytes leídos en esta llamada | `ErrWouldBlock` |
| Would-block en fase de escritura | bytes escritos en esta llamada | `ErrWouldBlock` |
| Mensaje excede el buffer interno | 0 | `io.ErrShortBuffer` |
| Mensaje excede ReadLimit | 0 | `ErrTooLong` |
| Stream terminó a mitad de mensaje | bytes hasta ahora | `io.ErrUnexpectedEOF` |
### Clasificación de operaciones
| Operación | Comportamiento de límites | Caso de uso |
|----------|----------------------------|------------|
| `Reader.Read` | **Preserva límites**: 1 llamada = 1 mensaje | Procesamiento por mensaje |
| `Writer.Write` | **Preserva límites**: 1 llamada = 1 mensaje | Envío por mensaje |
| `Reader.WriteTo` | **Chunking**: stream de bytes de payload (no wire format) | Transferencia eficiente; NO preserva límites |
| `Writer.ReadFrom` | **Chunking**: cada chunk de `src` se vuelve un mensaje | Codificación eficiente; NO preserva límites aguas arriba |
| `Forwarder.ForwardOnce` | **Relay con límites**: decodifica uno, re-encodifica uno | Proxy con preservación de límites |
### Política de bloqueo
Por defecto, framer es **no bloqueante** (`WithNonblock()`): devuelve `ErrWouldBlock` inmediatamente.
- `WithBlock()` — hace yield (`runtime.Gosched`) y reintenta ante would-block
- `WithRetryDelay(d)` — duerme `d` y reintenta ante would-block
- `RetryDelay` negativo (por defecto) — devuelve `ErrWouldBlock` inmediatamente
Ningún método oculta bloqueo a menos que se configure explícitamente.
## Fast paths
`framer` implementa fast paths del stdlib para interoperar con motores tipo `io.Copy` y con `iox.CopyPolicy`:
- `(*Reader).WriteTo(io.Writer)` — transfiere eficientemente payloads a `dst`.
- Stream (`BinaryStream`): procesa un mensaje por vez y escribe solo bytes de payload. Si `ReadLimit == 0`, usa un tope conservador (64KiB); mensajes más grandes devuelven `framer.ErrTooLong`.
- Packet (`SeqPacket`/`Datagram`): pass-through.
- `framer.ErrWouldBlock` y `framer.ErrMore` se propagan sin cambios, con el conteo reflejando bytes escritos.
- `(*Writer).ReadFrom(io.Reader)` — chunk-to-message: cada chunk leído de `src` se codifica como un mensaje y se escribe vía `w.Write`.
- Es eficiente pero no preserva límites de mensaje de `src`.
- En protocolos con límites preservados, se comporta como pass-through.
- `framer.ErrWouldBlock` y `framer.ErrMore` se propagan sin cambios.
Recomendación: en bucles no bloqueantes, prefiere `iox.CopyPolicy` con política de reintentos (p. ej., `PolicyRetry`) para manejar explícitamente `ErrWouldBlock` / `ErrMore`.
**Nota sobre recuperación de escrituras parciales:** Al usar `iox.Copy` con destinos no bloqueantes, pueden ocurrir escrituras parciales. Si la fuente no implementa `io.Seeker`, `iox.Copy` devuelve `iox.ErrNoSeeker` para evitar pérdida silenciosa de datos. Para fuentes no buscables (p. ej., sockets de red), usa `iox.CopyPolicy` con `PolicyRetry` para errores semánticos del lado de escritura, asegurando que todos los bytes leídos se escriban antes de retornar.
## Reenvío
- Proxy a nivel wire (motores de bytes): usa `iox.CopyPolicy` y fast paths estándar (`WriterTo`/`ReaderFrom`). Maximiza throughput cuando no necesitas preservar límites de nivel superior.
- Relay por mensaje (preserva límites): usa `framer.NewForwarder(dst, src, ...)` y llama `ForwardOnce()` en tu poll loop. Decodifica exactamente un mensaje desde `src` y lo re-encodifica como exactamente un mensaje hacia `dst`.
- Semántica no bloqueante: `ForwardOnce` devuelve `(n>0, framer.ErrWouldBlock|framer.ErrMore)` cuando hubo progreso parcial; reintenta con la misma instancia.
- Límites: `io.ErrShortBuffer` si el buffer interno es insuficiente; `framer.ErrTooLong` si excede `WithReadLimit`.
- Cero asignaciones en steady-state tras la construcción; el buffer interno se reutiliza.
## Licencia
MIT — ver `LICENSE`.