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.
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.mjsgap-closers (9/9). Seedocs/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
:9090admin 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
/socketsand/roomsutility methods, and ack-timeout cooperation are libuv-only. Thethreadsandepollbackends 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.
- 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/sidquery parsing and custom endpoint paths (e.g./custom-io/). - Cookies:
Set-Cookie: io=<sid>; Path=/; HttpOnly; SameSite=Laxon handshake, tunable viaEIO_COOKIE*. - CORS:
Access-Control-Allow-Originecho +OPTIONS204 preflight, driven byEIO_CORS_ORIGIN(*default /self/ allowlist) andEIO_CORS_CREDENTIALS. - JSONP polling:
j=<idx>served astext/javascript___eio[<idx>](<json-escaped payload>);for legacy browsers. - EIO version gating:
EIO != "4"rejected with HTTP400, cleanly refusing Socket.IO v2/v3 (EIO=3) clients.
- 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_TOKENgate — mismatch yields a Socket.IOCONNECT_ERROR. - Connection-state recovery (libuv only): opt-in (
EIO_RECOVERY=1); stable session id recovered on reconnect withinEIO_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 /socketsandGET /roomssnapshots.
- Graceful shutdown:
SIGTERM/SIGINTwith 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
Originheaders outsideEIO_ORIGIN_ALLOWLIST; reject non-http(s) schemes. - Container & ops: multi-stage
Dockerfile, plus systemd unit and nginx / Caddy reference configs incontrib/.
libuv(default) — cross-platform event loop via C-FFI (transport_libuv.odin+libuv_binding.odin); singleuv_runthread, non-blocking accept/read,uv_timer-driven heartbeat / idle-reap / write-flush. Directposix.writeon the socket fd (EAGAIN frames buffered and retried).threads— thread-per-connection, blockingcore:net.epoll— hand-rolledcore:sysevent loop (epoll_loop.odin/kqueue_loop.odin); preserved on branchepoll-kqueue-native.
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.odinbehind a 16-shard FNV-1a registry (engineio_sessions,socketio_connections,socketio_rooms); per-FDConnectionstate (connection_table.odin) is shared by all backends.
See docs/ARCHITECTURE.md and
CLAUDE.md for the full layering and hard constraints.
| 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/). |
- Odin compiler on your PATH (
odin versionto verify). - libuv —
brew install libuv(macOS) orapt-get install libuv1-dev(Linux).
# Canonical build (same command CI and Docker use) → ./engineio_server
./scripts/build.sh
# Run (protocol on :8080, admin on :9090; default transport: libuv)
./engineio_serverQuick 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 textAll configuration is via EIO_* environment variables (parsed in
config.odin). Unknown EIO_TRANSPORT values fall back to
libuv.
| 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. |
| 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). |
| 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). |
| 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. |
| 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 RPScap the default 10k load test. Raise or disable them for load tests:EIO_MAX_CONNECTIONS=0 EIO_RATE_LIMIT_PER_IP_RPS=1000000.
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 loopThe 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.
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:devThe Dockerfile is multi-stage: an official Odin builder image, then a
debian:bookworm-slim runtime (libuv ships as a shared library).
- systemd unit:
contrib/odin-engineio.service - nginx reference:
contrib/nginx.conf - Caddy reference:
contrib/Caddyfile
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.
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:advanced2Unit tests (engineio_codec.odin, socketio_layer.odin) run with
odin test . -custom-attribute:test.
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| 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. |