/websocket-protocol-codegen
When to Use
- Design binary WebSocket protocol frames for inter-agent communication
- Code-generate message serialization/deserialization from a schema definition
- Implement keepalive, reconnect, and framing logic for custom agent transport
- Bandwidth optimization: pack multiple agent events into binary frames
Do NOT use for
- Simple JSON-over-WebSocket apps (use ws/Socket.IO directly)
- High-level Pub/Sub (use [[wamp-pubsub-patterns]])
RFC 6455 frame layout (reference)
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - -+
Opcodes: 0x0 continuation, 0x1 text, 0x2 binary, 0x8 close, 0x9 ping, 0xA pong
Binary protocol schema → codegen
// Protocol definition
interface AgentFrame {
version: 1 // 1 byte
opcode: FrameOp // 1 byte: 0x01=task, 0x02=ack, 0x03=heartbeat
agentId: string // 8 bytes (truncated SHA256)
sequence: number // 4 bytes uint32
payload: Buffer // variable
}
enum FrameOp { TASK = 0x01, ACK = 0x02, HEARTBEAT = 0x03 }
// Encoder
function encodeFrame(frame: AgentFrame): Buffer {
const idBytes = Buffer.from(frame.agentId.slice(0, 8).padEnd(8, '\0'))
const header = Buffer.allocUnsafe(14)
header.writeUInt8(frame.version, 0)
header.writeUInt8(frame.opcode, 1)
idBytes.copy(header, 2)
header.writeUInt32BE(frame.sequence, 10)
return Buffer.concat([header, frame.payload])
}
// Decoder
function decodeFrame(buf: Buffer): AgentFrame {
return {
version: buf.readUInt8(0),
opcode: buf.readUInt8(1) as FrameOp,
agentId: buf.slice(2, 10).toString('utf8').replace(/\0/g, ''),
sequence: buf.readUInt32BE(10),
payload: buf.slice(14),
}
}
WebSocket server with ping/pong keepalive
import { WebSocketServer } from 'ws'
const wss = new WebSocketServer({ port: 8080 })
const PING_INTERVAL = 30_000 // 30s
const PONG_TIMEOUT = 10_000 // close if no pong within 10s
wss.on('connection', (ws) => {
let alive = true
ws.on('pong', () => { alive = true })
const keepalive = setInterval(() => {
if (!alive) { ws.terminate(); return }
alive = false
ws.ping()
}, PING_INTERVAL)
ws.on('close', () => clearInterval(keepalive))
ws.on('message', (data: Buffer) => {
const frame = decodeFrame(data)
if (frame.opcode === FrameOp.TASK) {
// handle task
ws.send(encodeFrame({ ...frame, opcode: FrameOp.ACK, payload: Buffer.alloc(0) }))
}
})
})
Auto-generate protocol serializer from field schema
// Schema-driven codegen: fields → encode/decode functions
function generateSerializer(schema: { name: string; type: 'uint8'|'uint32'|'bytes8'|'string' }[]) {
let offset = 0
const encodeLines: string[] = []
const decodeLines: string[] = []
for (const field of schema) {
switch (field.type) {
case 'uint8':
encodeLines.push(`buf.writeUInt8(obj.${field.name}, ${offset})`)
decodeLines.push(`${field.name}: buf.readUInt8(${offset})`)
offset += 1; break
case 'uint32':
encodeLines.push(`buf.writeUInt32BE(obj.${field.name}, ${offset})`)
decodeLines.push(`${field.name}: buf.readUInt32BE(${offset})`)
offset += 4; break
case 'bytes8':
encodeLines.push(`Buffer.from(obj.${field.name}).copy(buf, ${offset})`)
decodeLines.push(`${field.name}: buf.slice(${offset}, ${offset + 8}).toString()`)
offset += 8; break
}
}
return {
encode: new Function('obj', `const buf = Buffer.allocUnsafe(${offset}); ${encodeLines.join('; ')}; return buf`),
decode: new Function('buf', `return { ${decodeLines.join(', ')} }`),
}
}
Anti-Fake-Pass Checklist
❌ ws.ping() without checking ws.readyState === OPEN → throws on closed socket
❌ Buffer.allocUnsafe without filling → uninitialized memory in frames (security issue)
❌ No ping/pong → zombie connections consume server memory indefinitely
❌ Binary frames with no version byte → impossible to add fields without breaking old clients
❌ Frame sequence number overflow at uint32 max → not handled → sequence resets silently
❌ String fields without length prefix → variable-length strings corrupt fixed-offset decoder