|
| 1 | +# framer — límites de mensaje sobre E/S de flujo |
| 2 | + |
| 3 | +[](https://pkg.go.dev/code.hybscloud.com/framer) |
| 4 | +[](https://goreportcard.com/report/github.com/hayabusa-cloud/framer) |
| 5 | +[](https://codecov.io/gh/hayabusa-cloud/framer) |
| 6 | +[](LICENSE) |
| 7 | + |
| 8 | +**Idiomas / Languages:** [English](README.md) | [简体中文](README.zh-CN.md) | [日本語](README.ja.md) | Español | [Français](README.fr.md) |
| 9 | + |
| 10 | +Framing de mensajes portable para Go. Conserva “un mensaje por `Read`/`Write`” sobre transportes tipo stream. |
| 11 | + |
| 12 | +Alcance: `framer` resuelve la preservación de límites de mensaje en transportes de flujo. |
| 13 | + |
| 14 | +## En resumen |
| 15 | + |
| 16 | +- Resuelve problemas de límites de mensaje en flujos de bytes (TCP, Unix stream, pipes). |
| 17 | +- Pass-through en transportes que ya preservan límites (UDP, Unix datagram, WebSocket, SCTP). |
| 18 | +- Formato wire portable; orden de bytes configurable. |
| 19 | + |
| 20 | +## Por qué |
| 21 | + |
| 22 | +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. |
| 23 | + |
| 24 | +## Adaptación de protocolo |
| 25 | + |
| 26 | +- `BinaryStream` (transportes stream: TCP, TLS-over-TCP, Unix stream, pipes): agrega un prefijo de longitud; lee/escribe mensajes completos. |
| 27 | +- `SeqPacket` (p. ej., SCTP, WebSocket): pass-through; el transporte ya preserva límites. |
| 28 | +- `Datagram` (p. ej., UDP, Unix datagram): pass-through; el transporte ya preserva límites. |
| 29 | + |
| 30 | +Selecciona al construir vía `WithProtocol(...)` (hay variantes de lectura/escritura) o usa los helpers de transporte (ver Options). |
| 31 | + |
| 32 | +## Wire format |
| 33 | + |
| 34 | +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`. |
| 35 | + |
| 36 | +## Formato de datos del frame |
| 37 | + |
| 38 | +El esquema de framing de `framer` es intencionalmente compacto: |
| 39 | + |
| 40 | +- Byte de cabecera `H0` + bytes opcionales de longitud extendida. |
| 41 | +- Sea `L` la longitud del payload en bytes. |
| 42 | + - Si `0 ≤ L ≤ 253` (`0x00..0xFD`): `H0 = L`. Sin bytes extra. |
| 43 | + - 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. |
| 44 | + - Si `65536 ≤ L ≤ 2^56-1`: `H0 = 0xFF` y los siguientes 7 bytes llevan los 56 bits bajos de `L` en el orden configurado. |
| 45 | + - Big-endian: bytes `[1..7]` son los 56 bits bajos de `L` en big-endian. |
| 46 | + - Little-endian: bytes `[1..7]` son los 56 bits bajos de `L` en little-endian. |
| 47 | + |
| 48 | +Límites y errores: |
| 49 | +- La longitud máxima de payload soportada es `2^56-1`; valores mayores producen `framer.ErrTooLong`. |
| 50 | +- Con un límite de lectura (`WithReadLimit`), longitudes mayores fallan con `framer.ErrTooLong`. |
| 51 | + |
| 52 | +## Inicio rápido |
| 53 | + |
| 54 | +Instala con `go get`: |
| 55 | +```shell |
| 56 | +go get code.hybscloud.com/framer |
| 57 | +``` |
| 58 | + |
| 59 | +```go |
| 60 | +c1, c2 := net.Pipe() |
| 61 | +defer c1.Close() |
| 62 | +defer c2.Close() |
| 63 | + |
| 64 | +w := framer.NewWriter(c1, framer.WithWriteTCP()) |
| 65 | +r := framer.NewReader(c2, framer.WithReadTCP()) |
| 66 | + |
| 67 | +go func() { _, _ = w.Write([]byte("hello")) }() |
| 68 | + |
| 69 | +buf := make([]byte, 64) |
| 70 | +n, err := r.Read(buf) |
| 71 | +if err != nil { |
| 72 | + panic(err) |
| 73 | +} |
| 74 | +fmt.Printf("got: %q\n", buf[:n]) |
| 75 | +``` |
| 76 | + |
| 77 | +## Options |
| 78 | + |
| 79 | +- `WithProtocol(proto Protocol)` — elige `BinaryStream`, `SeqPacket` o `Datagram` (hay variantes de lectura/escritura). |
| 80 | +- Orden de bytes: `WithByteOrder`, o `WithReadByteOrder` / `WithWriteByteOrder`. |
| 81 | +- `WithReadLimit(n int)` — limita el tamaño máximo del payload al leer. |
| 82 | +- `WithRetryDelay(d time.Duration)` — política de would-block; helpers: `WithNonblock()` / `WithBlock()`. |
| 83 | + |
| 84 | +Helpers de transporte (presets): |
| 85 | +- `WithReadTCP` / `WithWriteTCP` (`BinaryStream`, BigEndian en orden de red) |
| 86 | +- `WithReadUDP` / `WithWriteUDP` (`Datagram`, BigEndian) |
| 87 | +- `WithReadWebSocket` / `WithWriteWebSocket` (`SeqPacket`, BigEndian) |
| 88 | +- `WithReadSCTP` / `WithWriteSCTP` (`SeqPacket`, BigEndian) |
| 89 | +- `WithReadUnix` / `WithWriteUnix` (`BinaryStream`, BigEndian) |
| 90 | +- `WithReadUnixPacket` / `WithWriteUnixPacket` (`Datagram`, BigEndian) |
| 91 | +- `WithReadLocal` / `WithWriteLocal` (`BinaryStream`, orden nativo) |
| 92 | + |
| 93 | +Más: GoDoc https://pkg.go.dev/code.hybscloud.com/framer |
| 94 | + |
| 95 | +## Contrato de semántica (Semantics Contract) |
| 96 | + |
| 97 | +### Taxonomía de errores |
| 98 | + |
| 99 | +| Error | Significado | Acción del llamador | |
| 100 | +|-------|-------------|---------------------| |
| 101 | +| `nil` | Operación completada con éxito | Continúa; `n` refleja el progreso total | |
| 102 | +| `io.EOF` | Fin de stream (no hay más mensajes) | Deja de leer; terminación normal | |
| 103 | +| `io.ErrUnexpectedEOF` | El stream terminó a mitad de mensaje (header o payload incompleto) | Trátalo como fatal; posible corrupción o desconexión | |
| 104 | +| `io.ErrShortBuffer` | Buffer destino demasiado pequeño para el payload | Reintenta con un buffer más grande | |
| 105 | +| `io.ErrShortWrite` | El destino aceptó menos bytes que los provistos | Reintenta o trátalo como fatal según el contexto | |
| 106 | +| `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 | |
| 107 | +| `framer.ErrWouldBlock` | No es posible avanzar ahora sin esperar | Reintenta más tarde (tras poll/event); `n` puede ser >0 | |
| 108 | +| `framer.ErrMore` | Hubo progreso; seguirán más completions del mismo op | Procesa y vuelve a llamar | |
| 109 | +| `framer.ErrTooLong` | El mensaje excede límites o el máximo del wire format | Rechaza; posiblemente fatal | |
| 110 | +| `framer.ErrInvalidArgument` | Reader/Writer nil o configuración inválida | Corrige la configuración | |
| 111 | + |
| 112 | +### Tablas de resultados |
| 113 | + |
| 114 | +**`Reader.Read(p []byte) (n int, err error)`** — modo BinaryStream |
| 115 | + |
| 116 | +| Condición | n | err | |
| 117 | +|----------|---|-----| |
| 118 | +| Mensaje completo entregado | payload length | `nil` | |
| 119 | +| `len(p) < payload length` | 0 | `io.ErrShortBuffer` | |
| 120 | +| Payload excede ReadLimit | 0 | `ErrTooLong` | |
| 121 | +| El subyacente devuelve would-block | bytes leídos hasta ahora | `ErrWouldBlock` | |
| 122 | +| El subyacente devuelve more | bytes leídos hasta ahora | `ErrMore` | |
| 123 | +| EOF en el límite de mensaje | 0 | `io.EOF` | |
| 124 | +| EOF a mitad de header o payload | bytes leídos | `io.ErrUnexpectedEOF` | |
| 125 | + |
| 126 | +**`Writer.Write(p []byte) (n int, err error)`** — modo BinaryStream |
| 127 | + |
| 128 | +| Condición | n | err | |
| 129 | +|----------|---|-----| |
| 130 | +| Mensaje enmarcado completo emitido | `len(p)` | `nil` | |
| 131 | +| Payload excede el máximo (2^56-1) | 0 | `ErrTooLong` | |
| 132 | +| El subyacente devuelve would-block | bytes de payload escritos | `ErrWouldBlock` | |
| 133 | +| El subyacente devuelve more | bytes de payload escritos | `ErrMore` | |
| 134 | + |
| 135 | +**`Reader.WriteTo(dst io.Writer) (n int64, err error)`** |
| 136 | + |
| 137 | +| Condición | n | err | |
| 138 | +|----------|---|-----| |
| 139 | +| Transferencia hasta EOF | bytes totales de payload | `nil` | |
| 140 | +| Reader subyacente devuelve would-block | bytes de payload escritos | `ErrWouldBlock` | |
| 141 | +| Reader subyacente devuelve more | bytes de payload escritos | `ErrMore` | |
| 142 | +| `dst` devuelve would-block | bytes de payload escritos | `ErrWouldBlock` | |
| 143 | +| Mensaje excede el buffer interno (64KiB por defecto) | bytes hasta ahora | `ErrTooLong` | |
| 144 | +| Stream terminó a mitad de mensaje | bytes hasta ahora | `io.ErrUnexpectedEOF` | |
| 145 | + |
| 146 | +**`Writer.ReadFrom(src io.Reader) (n int64, err error)`** |
| 147 | + |
| 148 | +| Condición | n | err | |
| 149 | +|----------|---|-----| |
| 150 | +| Chunks codificados hasta src EOF | bytes totales de payload | `nil` | |
| 151 | +| `src` devuelve would-block | bytes de payload escritos | `ErrWouldBlock` | |
| 152 | +| `src` devuelve more | bytes de payload escritos | `ErrMore` | |
| 153 | +| Writer subyacente devuelve would-block | bytes de payload escritos | `ErrWouldBlock` | |
| 154 | + |
| 155 | +**`Forwarder.ForwardOnce() (n int, err error)`** |
| 156 | + |
| 157 | +| Condición | n | err | |
| 158 | +|----------|---|-----| |
| 159 | +| Un mensaje reenviado completamente | bytes de payload (fase de escritura) | `nil` | |
| 160 | +| Fuente packet devuelve `(n>0, io.EOF)` | bytes de payload (fase de escritura) | `nil` (la próxima llamada devuelve `io.EOF`) | |
| 161 | +| No hay más mensajes | 0 | `io.EOF` | |
| 162 | +| Would-block en fase de lectura | bytes leídos en esta llamada | `ErrWouldBlock` | |
| 163 | +| Would-block en fase de escritura | bytes escritos en esta llamada | `ErrWouldBlock` | |
| 164 | +| Mensaje excede el buffer interno | 0 | `io.ErrShortBuffer` | |
| 165 | +| Mensaje excede ReadLimit | 0 | `ErrTooLong` | |
| 166 | +| Stream terminó a mitad de mensaje | bytes hasta ahora | `io.ErrUnexpectedEOF` | |
| 167 | + |
| 168 | +### Clasificación de operaciones |
| 169 | + |
| 170 | +| Operación | Comportamiento de límites | Caso de uso | |
| 171 | +|----------|----------------------------|------------| |
| 172 | +| `Reader.Read` | **Preserva límites**: 1 llamada = 1 mensaje | Procesamiento por mensaje | |
| 173 | +| `Writer.Write` | **Preserva límites**: 1 llamada = 1 mensaje | Envío por mensaje | |
| 174 | +| `Reader.WriteTo` | **Chunking**: stream de bytes de payload (no wire format) | Transferencia eficiente; NO preserva límites | |
| 175 | +| `Writer.ReadFrom` | **Chunking**: cada chunk de `src` se vuelve un mensaje | Codificación eficiente; NO preserva límites aguas arriba | |
| 176 | +| `Forwarder.ForwardOnce` | **Relay con límites**: decodifica uno, re-encodifica uno | Proxy con preservación de límites | |
| 177 | + |
| 178 | +### Política de bloqueo |
| 179 | + |
| 180 | +Por defecto, framer es **no bloqueante** (`WithNonblock()`): devuelve `ErrWouldBlock` inmediatamente. |
| 181 | + |
| 182 | +- `WithBlock()` — hace yield (`runtime.Gosched`) y reintenta ante would-block |
| 183 | +- `WithRetryDelay(d)` — duerme `d` y reintenta ante would-block |
| 184 | +- `RetryDelay` negativo (por defecto) — devuelve `ErrWouldBlock` inmediatamente |
| 185 | + |
| 186 | +Ningún método oculta bloqueo a menos que se configure explícitamente. |
| 187 | + |
| 188 | +## Fast paths |
| 189 | + |
| 190 | +`framer` implementa fast paths del stdlib para interoperar con motores tipo `io.Copy` y con `iox.CopyPolicy`: |
| 191 | + |
| 192 | +- `(*Reader).WriteTo(io.Writer)` — transfiere eficientemente payloads a `dst`. |
| 193 | + - 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`. |
| 194 | + - Packet (`SeqPacket`/`Datagram`): pass-through. |
| 195 | + - `framer.ErrWouldBlock` y `framer.ErrMore` se propagan sin cambios, con el conteo reflejando bytes escritos. |
| 196 | + |
| 197 | +- `(*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`. |
| 198 | + - Es eficiente pero no preserva límites de mensaje de `src`. |
| 199 | + - En protocolos con límites preservados, se comporta como pass-through. |
| 200 | + - `framer.ErrWouldBlock` y `framer.ErrMore` se propagan sin cambios. |
| 201 | + |
| 202 | +Recomendación: en bucles no bloqueantes, prefiere `iox.CopyPolicy` con política de reintentos (p. ej., `PolicyRetry`) para manejar explícitamente `ErrWouldBlock` / `ErrMore`. |
| 203 | + |
| 204 | +## Reenvío |
| 205 | + |
| 206 | +- 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. |
| 207 | +- 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`. |
| 208 | + - Semántica no bloqueante: `ForwardOnce` devuelve `(n>0, framer.ErrWouldBlock|framer.ErrMore)` cuando hubo progreso parcial; reintenta con la misma instancia. |
| 209 | + - Límites: `io.ErrShortBuffer` si el buffer interno es insuficiente; `framer.ErrTooLong` si excede `WithReadLimit`. |
| 210 | + - Cero asignaciones en steady-state tras la construcción; el buffer interno se reutiliza. |
| 211 | + |
| 212 | +## Licencia |
| 213 | + |
| 214 | +MIT — ver `LICENSE`. |
0 commit comments