A lightweight Server-Sent Events (SSE) server written in C with zero libc dependency. All system calls are made directly via x86-64 Linux syscalls through hand-written assembly wrappers.
- Zero libc dependency — custom types, string operations, memory management, and I/O via raw syscalls
- SSE protocol stack — event serialization, connection management, multi-client broadcast, and Last-Event-ID replay
- epoll-based event loop — non-blocking I/O for handling multiple concurrent SSE connections
- Callback-based API — simple high-level interface via
sse/sse.hwith genericvoid*user data - Graceful shutdown — handles SIGINT/SIGTERM to clean up connections and file descriptors
- HTTP/2 support — binary framing, HPACK compression, stream management, transport abstraction (RFC 9113)
- TDD-tested — 162 tests (40+94 unit + 9+10 integration + 9 API) using Google Test
src/
├── sse/ # SSE protocol stack
│ ├── sse.h / sse.c # High-level server API (SSEServer, callbacks)
│ ├── sse_event.* # Event serialization (data/event/id/retry fields)
│ ├── sse_conn.* # Per-client connection management
│ └── sse_stream.* # Multi-client stream, broadcast, event queue
├── http/ # HTTP/1.1 request parser
├── http2/ # HTTP/2 protocol stack (RFC 9113)
│ ├── h2_frame.* # Binary framing layer (9-byte headers, frame builders)
│ ├── h2_hpack.* # HPACK header compression (RFC 7541, static table)
│ ├── h2_stream.* # Stream state machine, flow control
│ ├── h2_conn.* # Connection state, settings negotiation
│ └── h2_proto.* # Protocol detection (preface / h2c upgrade)
├── transport/ # Protocol abstraction layer (HTTP/1.1 ↔ HTTP/2)
│ └── transport.* # Unified send/recv interface for SSE layer
├── arch/ # x86-64 Linux syscall wrappers (socket, epoll, send, recv, etc.)
│ └── linux/x86_64/ # Assembly syscall implementations
├── util/ # Utilities (types, string, logging, memory, signals)
└── main.c # Sample SSE server
- Linux x86-64
- GCC (C23 standard)
- CMake >= 3.14
- just (optional, for task runner)
just release-build
# or manually:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build buildjust debug-build
# or manually:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build buildThe sse/sse.h header provides a high-level, callback-based API for building SSE servers. Include only this one header — it pulls in everything you need.
#include "sse/sse.h"
#include "util/allocator.h" /* _memcpy, _memset */
#include "util/string.h" /* _itoa, _strnlen */
static bool on_event(SSEEvent* event, void* user_data)
{
/* Fill in the event fields — return true to broadcast, false to skip */
_memcpy(event->data, "hello", 5);
_memcpy(event->event_type, "greeting", 8);
return true;
}
int main(void)
{
SSEServerConfig config = {
.port = 8080,
.backlog = 128,
.event_interval_ms = 2000 /* call on_event every 2 seconds */
};
SSEServer server;
sse_server_init(&server, &config);
sse_server_on_event(&server, on_event, NULL);
sse_server_run(&server); /* blocks until SIGINT/SIGTERM */
sse_server_cleanup(&server);
return 0;
}typedef struct {
uint16_t port; /* TCP port to listen on */
int32_t backlog; /* listen() backlog (0 defaults to 128) */
int32_t event_interval_ms; /* epoll timeout / event callback interval */
char cors_origin[256]; /* CORS origin ("*", "https://...", or "" to disable) */
} SSEServerConfig;| Callback type | Signature | Purpose |
|---|---|---|
SSEEventCallback |
bool (*)(SSEEvent* event, void* user_data) |
Called periodically. Fill event fields and return true to broadcast, false to skip. |
SSEConnectCallback |
void (*)(int32_t client_fd, void* user_data) |
Called when a new client connects (optional). |
SSEDisconnectCallback |
void (*)(int32_t client_fd, void* user_data) |
Called when a client disconnects (optional). |
All callbacks receive the same void* user_data pointer registered with sse_server_on_event, allowing any application state to be passed through.
| Function | Description |
|---|---|
sse_server_init(server, config) |
Initialize server: create socket, bind, listen, set up epoll, register signal handlers. |
sse_server_on_event(server, cb, user_data) |
Register the event callback with a user data pointer. |
sse_server_on_connect(server, cb) |
Register an optional connect callback. |
sse_server_on_disconnect(server, cb) |
Register an optional disconnect callback. |
sse_server_run(server) |
Run the event loop (blocking). Returns on SIGINT/SIGTERM or sse_server_stop(). |
sse_server_stop(server) |
Signal the event loop to stop. |
sse_server_cleanup(server) |
Close all connections and file descriptors. |
typedef struct {
char data[4096]; /* Event payload (required for broadcast) */
char event_type[64]; /* Event type name (optional, "message" if empty) */
char id[64]; /* Event ID for Last-Event-ID (optional) */
int32_t retry_ms; /* Reconnection interval hint (optional, -1 = unset) */
} SSEEvent;#include "sse/sse.h"
#include "util/allocator.h"
#include "util/string.h"
typedef struct {
uint32_t counter;
uint32_t clients;
} AppState;
static bool on_tick(SSEEvent* event, void* user_data)
{
AppState* state = (AppState*)user_data;
state->counter++;
char buf[16];
_memset(buf, 0, sizeof(buf));
_itoa(state->counter, buf, sizeof(buf));
size_t len = _strnlen(buf, sizeof(buf));
_memcpy(event->data, "count:", 6);
_memcpy(event->data + 6, buf, len);
_memcpy(event->event_type, "tick", 4);
_memcpy(event->id, buf, len);
return true;
}
static void on_connect(int32_t fd, void* user_data)
{
AppState* state = (AppState*)user_data;
state->clients++;
}
static void on_disconnect(int32_t fd, void* user_data)
{
AppState* state = (AppState*)user_data;
state->clients--;
}
int main(void)
{
SSEServerConfig config = {
.port = 8080,
.backlog = 128,
.event_interval_ms = 2000
};
SSEServer server;
static AppState state = {0, 0};
sse_server_init(&server, &config);
sse_server_on_event(&server, on_tick, &state);
sse_server_on_connect(&server, on_connect);
sse_server_on_disconnect(&server, on_disconnect);
sse_server_run(&server);
sse_server_cleanup(&server);
return 0;
}# Using the included script
./scripts/sse_client.sh
# Or with curl
curl -N http://localhost:8080/eventsid:1
event:tick
data:count:1
id:2
event:tick
data:count:2
id:3
event:tick
data:count:3
Tests require Google Test (fetched automatically via CMake FetchContent).
just test # Run all tests (unit + integration + api + h2)
just unit-test # Run unit tests only (40 tests)
just integration-test # Run integration tests only (9 tests)
just api-test # Run API tests only (9 tests)
just h2-test # Run HTTP/2 tests (94 unit + 10 integration)Or manually:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
./build/tests/unit_tests
./build/tests/integration_tests
./build/tests/api_teststests/
├── unit/
│ ├── test_sse_event.cpp # Event init, serialization, comment, response header, CORS
│ ├── test_sse_conn.cpp # Connection lifecycle, Last-Event-ID, send
│ ├── test_sse_stream.cpp # Stream management, broadcast, replay
│ ├── test_sse_server_api.cpp # High-level SSE server API, CORS config
│ ├── test_h2_frame.cpp # HTTP/2 frame encode/decode, builders, validation
│ ├── test_h2_hpack.cpp # HPACK static table, integer coding, header encode/decode
│ ├── test_h2_stream.cpp # HTTP/2 stream state machine, flow control, SSE detection
│ ├── test_h2_conn.cpp # HTTP/2 connection state, server preface, data frames
│ └── test_h2_proto.cpp # Protocol detection, h2c upgrade
└── integration/
├── test_sse_server.cpp # End-to-end SSE via socketpair, CORS headers
└── test_h2_server.cpp # HTTP/2 transport layer via socketpair
This implementation follows the W3C Server-Sent Events specification.
| Feature | Status | Notes |
|---|---|---|
data: field |
Implemented | sse_event_serialize() |
| Multi-line data splitting | Implemented | Splits on \n, emits separate data: per line |
event: field (named events) |
Implemented | Omitted when empty (defaults to message) |
id: field (event IDs) |
Implemented | Omitted when empty |
retry: field (reconnection interval) |
Implemented | Omitted when SSE_RETRY_UNSET (-1) |
Comment lines (: prefix) |
Implemented | sse_serialize_comment(), used for keepalive |
Blank line event terminator (\n\n) |
Implemented | Appended after each event |
| Feature | Status | Notes |
|---|---|---|
HTTP/1.1 200 OK status |
Implemented | sse_build_response_header() |
Content-Type: text/event-stream |
Implemented | |
Cache-Control: no-cache |
Implemented | |
Connection: keep-alive |
Implemented | |
CORS (Access-Control-Allow-Origin) |
Implemented | sse_build_response_header_with_cors(), sse_build_cors_preflight_response() |
| HTTP chunked transfer encoding | Not implemented | Uses persistent connection with direct sends |
| HTTP/2 binary framing | Implemented | h2_frame.* — 9-byte frame headers, DATA/HEADERS/SETTINGS/PING/GOAWAY/WINDOW_UPDATE/RST_STREAM |
| HTTP/2 HPACK compression | Implemented | h2_hpack.* — static table (61 entries), integer/Huffman coding (RFC 7541) |
| HTTP/2 stream management | Implemented | h2_stream.* — state machine, flow control, SSE request detection |
| HTTP/2 connection state | Implemented | h2_conn.* — server preface, settings negotiation, connection-level flow control |
| HTTP/2 protocol detection | Implemented | h2_proto.* — connection preface detection, h2c upgrade |
| HTTP/1.1 ↔ HTTP/2 abstraction | Implemented | transport.* — unified interface for SSE layer |
| Feature | Status | Notes |
|---|---|---|
Last-Event-ID header extraction |
Implemented | sse_conn_extract_last_event_id() |
| Event replay after reconnect | Implemented | Ring buffer of 128 recent events, sse_stream_replay_events() |
| Feature | Status | Notes |
|---|---|---|
| Multiple concurrent clients | Implemented | Up to 64 connections (epoll-based) |
| Client disconnect detection | Implemented | Via EPOLLRDHUP / EPOLLERR / EPOLLHUP |
| Graceful server shutdown | Implemented | SIGINT / SIGTERM / SIGHUP signal handling |
| Feature | Status | Notes |
|---|---|---|
| UTF-8 event stream | Assumed | No explicit BOM handling or encoding validation |
id:<event-id>\n
event:<event-type>\n
retry:<milliseconds>\n
data:<payload>\n
\n
data:— Event payload (required). Multi-line data is split into multipledata:lines.event:— Event type name (optional, defaults tomessage).id:— Event ID for reconnection (optional).retry:— Client reconnection interval in milliseconds (optional).- Lines starting with
:are comments (used for keepalive).
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Access-Control-Allow-Origin: *
When cors_origin is set to a specific origin (not *), Access-Control-Allow-Credentials: true is also included.
This project uses three Claude Code agent skills for development:
| Agent | Model | Role |
|---|---|---|
designer |
Opus | Protocol stack design from SSE specification |
tester |
Opus | TDD with Red-Green-Refactor (t-wada style) |
programmer |
Sonnet | Implementation from design / TDD Green phase |
just fmtjust cleanMIT — Copyright (c) 2026 Hakkadaikon