o
odinpkg.dev
packages / app / odin-engineio

odin-engineio

60012e7app

A single-package Odin server implementing Engine.IO v4 + Socket.IO v4/v5 protocol layers

MIT · updated 2 weeks ago

odin-engineio

Languages / 語言: English · 繁體中文

A proof-of-concept, single-package Engine.IO v4 + Socket.IO v4/v5 server written in the Odin programming language. It validates that a real-time, event-driven socket server can be implemented natively in Odin and interoperates with the official @socket.io/client v4.

The server compiles to a single engineio_server binary. The default transport backend is libuv — the same cross-platform event loop Node.js uses (epoll on Linux, kqueue on macOS/BSD, IOCP on Windows) — wired in through Odin C-FFI. Thread-per-connection and a hand-rolled core:sys event loop are available as selectable alternatives.


Status

The PoC is complete and validated against the official client integration suites.

  • Coverage: on the default libuv backend, 30 / 30 tracked features PASS (0 out-of-scope, 0 intentionally-unsupported) across 24 executed scenarios — basic 6/6, advanced1 5/5, advanced2 4/4, plus the features.mjs gap-closers (9/9). See docs/COVERAGE_MATRIX.md (source of truth).
  • Protocol parity: Engine.IO v4 (polling + WebSocket), full Socket.IO v4 (namespaces, rooms, ack, volatile, binary, auth, recovery), and the previously-out-of-scope advanced features (cookies, CORS, JSONP, EIO version gating, ack-timeout, server utility methods) are all implemented.
  • Admin reliability: the :9090 admin endpoints are stable (a prior ECONNRESET that intermittently dropped the utility scenarios is resolved).

Backend-parity caveat: binary payloads, the auth hook, connection-state recovery, the /sockets and /rooms utility methods, and ack-timeout cooperation are libuv-only. The threads and epoll backends still pass basic 6/6, advanced1 5/5, and advanced2 4/4, but treat those advanced features as out-of-scope — not full feature parity with libuv. See Transport modes.


Key Features

Engine.IO Layer (v4)

  • Multi-transport: HTTP long-polling (XHR) and WebSocket.
  • Transport upgrades: race-free polling → WebSocket upgrade.
  • Heartbeat & liveness: server-initiated ping/pong with configurable timeout.
  • Query & path routing: EIO / transport / sid query parsing and custom endpoint paths (e.g. /custom-io/).
  • Cookies: Set-Cookie: io=<sid>; Path=/; HttpOnly; SameSite=Lax on handshake, tunable via EIO_COOKIE*.
  • CORS: Access-Control-Allow-Origin echo + OPTIONS 204 preflight, driven by EIO_CORS_ORIGIN (* default / self / allowlist) and EIO_CORS_CREDENTIALS.
  • JSONP polling: j=<idx> served as text/javascript ___eio[<idx>](<json-escaped payload>); for legacy browsers.
  • EIO version gating: EIO != "4" rejected with HTTP 400, cleanly refusing Socket.IO v2/v3 (EIO=3) clients.

Socket.IO Layer

  • Namespace routing: multi-namespace isolation (e.g. / vs /admin).
  • Room system: dynamic join/leave + targeted room broadcast.
  • Acknowledgement callbacks: in-memory request/response acks.
  • Volatile events: dropped (not replayed) while a client is offline.
  • Auto-reconnection: clean teardown; clients reconnect with new socket ids.
  • Binary payloads (libuv only): BINARY_EVENT (type 5) reassembled across WebSocket binary frames and echoed back.
  • Authorization hook (libuv only): EIO_AUTH_TOKEN gate — mismatch yields a Socket.IO CONNECT_ERROR.
  • Connection-state recovery (libuv only): opt-in (EIO_RECOVERY=1); stable session id recovered on reconnect within EIO_RECOVERY_MAX_DISCONNECT_MS (session-id continuity, not full offset replay).
  • ack-timeout cooperation (libuv only): honors client-driven socket.timeout(ms).emit(...); the server acks promptly.
  • Server utility methods (libuv only): admin listener exposes GET /sockets and GET /rooms snapshots.

Operational

  • Graceful shutdown: SIGTERM / SIGINT with configurable drain timeout.
  • Structured JSON logging: RFC3339Nano timestamps, level, msg, sid, ns, kv — jq-parseable.
  • Prometheus /metrics: counters, gauges, and a latency histogram.
  • Health endpoints: GET /healthz, GET /readyz (200 healthy / 503 draining).
  • Per-IP rate limiting: token bucket (refill EIO_RATE_LIMIT_PER_IP_RPS, burst 2× rate).
  • Origin allowlist: reject Origin headers outside EIO_ORIGIN_ALLOWLIST; reject non-http(s) schemes.
  • Container & ops: multi-stage Dockerfile, plus systemd unit and nginx / Caddy reference configs in contrib/.

Transport backends

  • libuv (default) — cross-platform event loop via C-FFI (transport_libuv.odin + libuv_binding.odin); single uv_run thread, non-blocking accept/read, uv_timer-driven heartbeat / idle-reap / write-flush. Direct posix.write on the socket fd (EAGAIN frames buffered and retried).
  • threads — thread-per-connection, blocking core:net.
  • epoll — hand-rolled core:sys event loop (epoll_loop.odin / kqueue_loop.odin); preserved on branch epoll-kqueue-native.

Architecture

Single-package Odin (package main), layered bottom → top — each layer never bleeds into the next:

TCP socket
  └─ transport_libuv / transport / transport_epoll   (HTTP parse, WS RFC 6455, polling, event loop)
       └─ engineio_codec.odin                        (Engine.IO packet encode/decode, type digit 0–6)
            └─ socketio_layer.odin                   (Socket.IO framing: type digit 0–4, namespace, ack)
                 └─ application events               (hello / join / leave / broadcast-room)
  • The codec does no I/O; the Socket.IO layer never touches raw frames; the transport never parses Socket.IO payloads.
  • All shared state lives in socketio_layer.odin behind a 16-shard FNV-1a registry (engineio_sessions, socketio_connections, socketio_rooms); per-FD Connection state (connection_table.odin) is shared by all backends.

See docs/ARCHITECTURE.md and CLAUDE.md for the full layering and hard constraints.

Project structure

File Responsibility
main.odin Entry point, transport dispatch, admin server, signal handlers.
config.odin EIO_* env-driven configuration (see Configuration).
engineio_codec.odin Pure Engine.IO packet codec (Packet_Type enum, encode/decode).
socketio_layer.odin Socket.IO state & protocol; 16-shard registry; single write path.
connection_table.odin Per-FD Connection with bounded 64 KiB read/write buffers.
transport_libuv.odin + libuv_binding.odin Default libuv event-loop backend (+ binary-echo path).
transport.odin .threads backend (thread-per-connection, blocking core:net).
transport_epoll.odin + epoll_loop.odin / kqueue_loop.odin .epoll backend (hand-rolled event loop).
health.odin :9090 admin server: /healthz, /readyz, /metrics, /sockets, /rooms.
metrics.odin Prometheus metrics.
logging.odin Structured JSON log sink.
rate_limit.odin Per-IP token bucket.
shutdown.odin Graceful drain on SIGTERM/SIGINT.
atomic.odin Atomic helpers.
Directory Purpose
coverage-tests/ Canonical Node.js + socket.io-client v4 integration suite.
docs/ Architecture, protocol specs, coverage matrix, runbook.
frontend/ React + TypeScript + Vite demo client (Gomoku game).
contrib/ systemd unit, nginx / Caddy reference configs.
scripts/ build.sh — reproducible build.
tests/ Older manual scripts (superseded by coverage-tests/).

Getting Started

Prerequisites

  • Odin compiler on your PATH (odin version to verify).
  • libuvbrew install libuv (macOS) or apt-get install libuv1-dev (Linux).

Build & Run

# Canonical build (same command CI and Docker use) → ./engineio_server
./scripts/build.sh

# Run (protocol on :8080, admin on :9090; default transport: libuv)
./engineio_server

Quick smoke test:

curl -s http://127.0.0.1:9090/healthz   # 200 OK (503 while draining)
curl -s http://127.0.0.1:9090/metrics   # Prometheus text

Configuration

All configuration is via EIO_* environment variables (parsed in config.odin). Unknown EIO_TRANSPORT values fall back to libuv.

Server / transport

Env var Default Description
EIO_PORT 8080 Protocol (main) listen port.
EIO_ADMIN_PORT 9090 Admin / metrics listen port.
EIO_TRANSPORT libuv libuv | threads | epoll.
EIO_LOG_LEVEL info debug | info | warn | error.
EIO_VERSION git describe Version stamp reported on /healthz.

Heartbeat & limits

Env var Default Description
EIO_PING_INTERVAL 25000 Heartbeat ping interval (ms).
EIO_PING_TIMEOUT 20000 Pong deadline before the session is dropped (ms).
EIO_MAX_CONNECTIONS 1000 Max live connections (libuv counts FDs incl. pre-handshake; 0 disables).
EIO_RATE_LIMIT_PER_IP_RPS 100 Per-IP token-bucket refill (RPS); burst = 2× rate.
EIO_MAX_REQUEST_BYTES 16384 Max HTTP request size.
EIO_MAX_PACKET_BYTES 1048576 Max single packet size (1 MiB).
EIO_GRACEFUL_DRAIN_TIMEOUT_MS 30000 Drain budget on SIGTERM/SIGINT (ms).

Security

Env var Default Description
EIO_ORIGIN_ALLOWLIST (allow all) Comma-separated allowed Origin hosts; empty/* = allow all.
EIO_CORS_ORIGIN * * | self (same-origin gate) | comma allowlist.
EIO_CORS_CREDENTIALS false Emit Access-Control-Allow-Credentials and echo the request Origin.
EIO_AUTH_TOKEN (disabled) Socket.IO CONNECT auth token gate (libuv only).

Cookies

Env var Default Description
EIO_COOKIE 1 Emit Set-Cookie on handshake.
EIO_COOKIE_NAME io Cookie name.
EIO_COOKIE_PATH / Cookie path.
EIO_COOKIE_HTTPONLY true HttpOnly flag.
EIO_COOKIE_SAMESITE lax SameSite value.

Recovery

Env var Default Description
EIO_RECOVERY 0 Enable connection-state recovery (session-id continuity; libuv only).
EIO_RECOVERY_MAX_DISCONNECT_MS 120000 Recovery window after disconnect (ms).

Load testing: the default max_connections=1000 / rate=100 RPS cap the default 10k load test. Raise or disable them for load tests: EIO_MAX_CONNECTIONS=0 EIO_RATE_LIMIT_PER_IP_RPS=1000000.


Transport modes

Selected at runtime via EIO_TRANSPORT — the same binary supports all three:

Mode Concurrency Status Use
libuv (default) verified 10k conn on a single event loop (p99 ≈ 83 ms on dev host; 50k / 24h soak are roadmap) Stable; full 30/30 feature parity. Production (recommended).
threads up to ~10k connections Stable; basic + advanced1 + advanced2 parity, but the 10 advanced features are libuv-only. Legacy / comparison.
epoll 50k+ (unverified target) Stable; basic + advanced1 + advanced2 parity, but the 10 advanced features are libuv-only. Experimentation.
./engineio_server                       # default: libuv
EIO_TRANSPORT=threads ./engineio_server # thread-per-connection
EIO_TRANSPORT=epoll   ./engineio_server # hand-rolled epoll/kqueue loop

Admin & Metrics

The admin listener (default :9090, a separate blocking-threads server) exposes:

Endpoint Method Returns
/healthz GET 200 healthy / 503 while draining.
/readyz GET Alias for /healthz.
/metrics GET Prometheus text format.
/sockets GET {"sockets":[...],"count":N} snapshot.
/rooms GET {"rooms":[{"namespace":..,"room":..,"members":N}]} snapshot.

Key Prometheus metrics (GET /metrics): eio_connections_active (gauge), eio_connections_total, eio_packets_in_total{type}, eio_packets_out_total{type}, eio_bytes_in_total, eio_bytes_out_total, eio_packet_errors_total{kind}, eio_sessions_expired_total, eio_rate_limited_total, eio_packet_latency_seconds_bucket (histogram), eio_room_members{room}, eio_broadcast_total{room}. Full definitions and alert sketches are in docs/RUNBOOK.md.


Deployment

Docker

docker build -t odin-engineio:dev .
docker run --rm -p 8080:8080 -p 9090:9090 odin-engineio:dev
# override any EIO_* var at runtime:
docker run --rm -p 8080:8080 -p 9090:9090 -e EIO_MAX_CONNECTIONS=5000 odin-engineio:dev

The Dockerfile is multi-stage: an official Odin builder image, then a debian:bookworm-slim runtime (libuv ships as a shared library).

systemd / reverse proxy

The reverse proxy should terminate TLS, strip untrusted inbound headers, and forward WebSocket upgrade headers. Proxy gotchas (long-poll timeouts, Host vs Origin) are covered in docs/RUNBOOK.md.

Scope: single-host, in-memory, no clustering. State is lost on restart; horizontal scaling is explicitly out of scope. The 50k-connection / 24h soak / dedicated-hardware targets are roadmap, not verified.


Testing

Integration tests are Node.js, driven by the official socket.io-client v4 against a live server. Run the canonical suite from coverage-tests/:

# Terminal 1 — start the server
./scripts/build.sh && ./engineio_server

# Terminal 2 — run the suites
cd coverage-tests
npm install
npm test            # all suites, sequentially
npm run test:basic
npm run test:advanced1
npm run test:advanced2

Unit tests (engineio_codec.odin, socketio_layer.odin) run with odin test . -custom-attribute:test.


Frontend Demo

The frontend/ directory is an interactive React + TypeScript + Vite demo that connects to the running server:

  • Debug panel — real-time packet/connection console.
  • Gomoku (五子棋) — a multi-user board game demonstrating room join/leave, state sync, and room broadcast over Socket.IO.
cd frontend
pnpm install   # or: npm install
pnpm dev       # open the printed local URL

Documentation

Doc Contents
docs/COVERAGE_MATRIX.md Feature-by-feature coverage (source of truth).
docs/ARCHITECTURE.md Layering, data flow, transport backends.
docs/RUNBOOK.md Ops runbook: metrics, failure modes, drain, proxy gotchas.
docs/SPEC_ENGINEIO.md Engine.IO v4 protocol spec.
docs/SPEC_SOCKETIO.md Socket.IO v4 protocol spec.
docs/SPEC_CLIENT_INTEROP.md Client interop spec.
docs/ENGINEIO_SCOPE.md Project scope & roadmap.
CLAUDE.md Build/test/architecture guidance for contributors.
CHANGELOG.md Release history.