Skip to content

Latest commit

 

History

50 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SSE

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.

Features

  • 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.h with generic void* 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

Architecture

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

Requirements

  • Linux x86-64
  • GCC (C23 standard)
  • CMake >= 3.14
  • just (optional, for task runner)

Building

Release build

just release-build
# or manually:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build

Debug build

just debug-build
# or manually:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build

API Usage

The 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.

Quick start

#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;
}

API reference

Configuration

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;

Callbacks

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.

Functions

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.

SSEEvent fields

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;

Example: counter with connect/disconnect logging

#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;
}

Client connection

# Using the included script
./scripts/sse_client.sh

# Or with curl
curl -N http://localhost:8080/events

Example output

id:1
event:tick
data:count:1

id:2
event:tick
data:count:2

id:3
event:tick
data:count:3

Testing

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_tests

Test structure

tests/
├── 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

SSE Protocol

This implementation follows the W3C Server-Sent Events specification.

Specification coverage

Event stream format

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

HTTP response

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

Reconnection

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()

Connection management

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

Encoding

Feature Status Notes
UTF-8 event stream Assumed No explicit BOM handling or encoding validation

Event format

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 multiple data: lines.
  • event: — Event type name (optional, defaults to message).
  • id: — Event ID for reconnection (optional).
  • retry: — Client reconnection interval in milliseconds (optional).
  • Lines starting with : are comments (used for keepalive).

Response headers

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.

Development

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

Formatting

just fmt

Clean

just clean

License

MIT — Copyright (c) 2026 Hakkadaikon

About

Server Side Events

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages