diff --git a/.gitignore b/.gitignore index 74233b8..44207f7 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ coverage/ # Dependency cache deps/ vendor/ + +# Blog drafts (not part of the repo) +.blog/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6444ee4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM alpine:3.21 AS builder + +# Install Zig 0.15.2 and build dependencies. +RUN apk add --no-cache curl xz && \ + curl -L https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz | \ + tar -xJ -C /opt && \ + mv /opt/zig-x86_64-linux-0.15.2 /opt/zig +ENV PATH="/opt/zig:${PATH}" + +WORKDIR /src +COPY build.zig build.zig.zon ./ +COPY src/ src/ +COPY schemas/ schemas/ + +RUN zig build -Doptimize=ReleaseSafe -Dtarget=x86_64-linux + +# --- Runtime stage --- +FROM alpine:3.21 + +RUN addgroup -S protomq && adduser -S protomq -G protomq + +WORKDIR /opt/protomq + +COPY --from=builder /src/zig-out/bin/protomq-server ./bin/protomq-server +COPY --from=builder /src/zig-out/bin/protomq-cli ./bin/protomq-cli +COPY schemas/ ./schemas/ + +USER protomq + +EXPOSE 1883 + +ENTRYPOINT ["./bin/protomq-server"] diff --git a/FEATURES.md b/FEATURES.md new file mode 100644 index 0000000..7d7f2c5 --- /dev/null +++ b/FEATURES.md @@ -0,0 +1,69 @@ +# Features + +This document covers ProtoMQ's features in more depth than the README. If you're looking for deployment guides and configuration, check [FAQ.md](FAQ.md). + +--- + +## Service Discovery + +ProtoMQ includes a built-in Service Discovery mechanism. Clients can discover available topics and their associated Protobuf schemas — including the full `.proto` source code — by querying the `$SYS/discovery/request` topic. + +This lets new clients bootstrap themselves in a single round-trip without any out-of-band configuration or pre-shared schema files. + +**Using the CLI:** + +```bash +protomq-cli discover --proto-dir schemas +``` + +Under the hood, the client subscribes to `$SYS/discovery/request` and receives a `ServiceDiscoveryResponse` message containing every registered topic-schema mapping. The response includes the raw `.proto` source so clients can dynamically configure their own decoding logic. + +--- + +## Admin Server + +An optional HTTP server for runtime schema management and telemetry. Disabled by default — when the build flag is off, the HTTP code is **completely stripped from the binary** (zero overhead, not just disabled). + +### Enabling it + +```bash +zig build -Dadmin_server=true run-server +``` + +### Build comparison + +| Build | Memory baseline | Admin API | +|---|---|---| +| `zig build` | ~2.6 MB | ✗ | +| `zig build -Dadmin_server=true` | ~4.0 MB | ✓ | + +The Admin Server runs cooperatively on the same event loop as the MQTT broker — enabling it does **not** degrade per-message MQTT performance. + +### Endpoints + +All endpoints require `Authorization: Bearer ` (defaults to `admin_secret`, configurable via the `ADMIN_TOKEN` environment variable). + +| Method | Path | Description | +|---|---|---| +| `GET` | `/metrics` | Active connections, message throughput, loaded schemas | +| `GET` | `/api/v1/schemas` | Current topic-to-schema mappings | +| `POST` | `/api/v1/schemas` | Register a new `.proto` schema and map it to a topic at runtime | + +### Dynamic schema registration + +With the Admin Server enabled, you can register schemas at runtime without restarting the broker: + +```bash +curl -X POST http://127.0.0.1:8080/api/v1/schemas \ + -H "Authorization: Bearer ${ADMIN_TOKEN:-admin_secret}" \ + -H "Content-Type: application/json" \ + -d '{ + "topic": "telemetry/gps", + "message_type": "GpsCoordinate", + "proto_file_content": "syntax = \"proto3\";\nmessage GpsCoordinate { float lat = 1; float lon = 2; }" + }' +``` + +The schema is parsed in-process and persisted to disk as `schemas/.proto`. The mapping is live immediately — no restart needed. + +> **Security note**: The Admin Server binds to `127.0.0.1:8080` only. If you need remote access, use an SSH tunnel or reverse proxy. diff --git a/README.md b/README.md index f1fad78..96b485e 100644 --- a/README.md +++ b/README.md @@ -2,100 +2,123 @@

ProtoMQ

- ProtoMQ Mascot + ProtoMQ Mascot
- Type-safe, bandwidth-efficient MQTT for the rest of us. + MQTT's simplicity. Protobuf's efficiency. Zig's bare-metal performance.
- Stop sending bloated JSON over the wire. + Built for IoT and edge computing. +

+ +

+ Quick Start • + Why ProtoMQ • + Performance • + Features • + FAQ

--- -- MQTT v3.1.1 packet parsing (CONNECT, PUBLISH, SUBSCRIBE, etc.) -- Thread-safe Topic Broker with wildcard support (`+`, `#`) -- Custom Protobuf Engine with runtime `.proto` schema parsing -- Topic-based Protobuf schema routing -- Service Discovery & Schema Registry -- CLI with automatic JSON-to-Protobuf encoding -- Structured diagnostic output for Protobuf payloads -### Building +ProtoMQ is an MQTT broker that enforces **Protobuf schemas at the broker level**. All messages on the wire are Protobuf — the broker validates incoming payloads against registered `.proto` schemas and rejects anything that doesn't conform. The bundled CLI can accept JSON and encode it to Protobuf client-side for convenience. -One have to have Zig 0.15.2 or later installed. Please download it from [here](https://ziglang.org/download/). +

+ ProtoMQ terminal demo +

-```bash -# Build server and client -zig build +- **Schema-enforced messaging** — `.proto` files define the contract. Malformed payloads get rejected *before* they reach subscribers. +- **Custom Protobuf engine** — written from scratch in Zig. Runtime `.proto` parsing, zero external dependencies. +- **Wildcard topic routing** — full MQTT `+` and `#` wildcard support via a trie-based topic broker. +- **Service Discovery** — clients query `$SYS/discovery/request` to discover topics and download schemas on the fly. No pre-shared `.proto` files needed. +- **Optional Admin HTTP API** — register new schemas and topic mappings at runtime, monitor connections and throughput. Disabled by default, zero overhead when off. See [FEATURES.md](FEATURES.md) for details. +- **Runs in 2.6 MB** — the entire broker with 100 active connections fits in under 3 MB of memory. -# Build and run server -zig build run-server +--- -# Build and run client -zig build run-client +### Why ProtoMQ -# Run tests -zig build test +If you've worked with IoT sensor fleets, you've probably been through this: you start with JSON over MQTT because it's easy to debug, every language has a parser, and `mosquitto_sub` lets you eyeball what's going on. It works fine... until you start caring about bandwidth. -# Run all integration tests -./tests/run_all.sh -``` +A 12-field sensor reading weighs around 310 bytes in JSON. The same data in Protobuf: 82 bytes. On a cellular-connected device pushing telemetry every 5 seconds, that gap adds up to roughly 1.6 MB/day per device — multiply by a few thousand devices and the data bill starts hurting. -### Limitations +

+ JSON vs Protobuf payload size comparison +

-For the initial release, we support: -- QoS 0 only (at most once delivery) -- No persistent sessions -- No retained messages -- Single-node deployment +But switching to Protobuf usually means code generation per language, keeping stubs in sync across firmware versions, and losing the ability to just read your payloads. ProtoMQ takes a different approach: the broker owns the `.proto` schemas and validates every message against them. The CLI can accept JSON and encode it to Protobuf before publishing, so you get a human-friendly workflow without sacrificing wire efficiency. -### Service Discovery +| | Plain MQTT + JSON | ProtoMQ | +|---|---|---| +| Schema enforcement | None — anything goes | Validated at every `PUBLISH` | +| Payload format | JSON (~170 bytes, 8 fields) | Protobuf (~48 bytes) | +| Client bootstrap | Pre-shared docs | Built-in Service Discovery | +| Code generation | Required per language | CLI encodes JSON → Protobuf for you | -ProtoMQ includes a built-in Service Discovery mechanism. Clients can discover available topics and their associated Protobuf schemas (including the full source code) by querying the `$SYS/discovery/request` topic. +--- -**Using the CLI for discovery:** -```bash -# Verify schemas are loaded and available -protomq-cli discover --proto-dir schemas -``` -This allows clients to "bootstrap" themselves without needing pre-shared `.proto` files. +### Under the Hood -### Admin Server +ProtoMQ is not a wrapper around an existing broker — it's a ground-up implementation. Here's what makes it tick: -ProtoMQ includes an optional HTTP Admin Server for runtime observability and dynamic schema management without polluting the core MQTT hot-paths. +- **`epoll` / `kqueue` event loop** — single-threaded, no abstraction layer. The network layer talks directly to the OS kernel I/O primitives. On Linux that's `epoll`, on macOS `kqueue`. No libuv, no tokio, no hidden threads. +- **One allocator, full control** — every allocation goes through Zig's `std.mem.Allocator`. No GC, no hidden heap churn, no runtime. You can trace every byte the broker touches. +- **Zero third-party dependencies** — the MQTT parser, TCP connection handler, Protobuf wire format encoder, `.proto` file parser — all written in Zig using only the standard library. `build.zig.zon` has an empty `dependencies` block. +- **Runtime schema registry** — `.proto` files are parsed at startup and mapped to MQTT topics. With the Admin Server enabled, you can register new schemas and mappings at runtime over HTTP without restarting the broker. +- **Comptime-generated lookup tables** — the MQTT packet parser uses Zig's `comptime` to build dispatch tables at compile time. No branching, no hash maps — just array indexing. +- **Cross-compilation** — `zig build -Dtarget=aarch64-linux` produces a Raspberry Pi binary from a Mac. One command, no toolchain headaches. -- **Dynamic Schema Registration**: Register `.proto` files at runtime via `POST /api/v1/schemas`. -- **Telemetry**: Monitor active connections, message throughput, and schemas via `GET /metrics`. -- **Zero Overhead Footprint**: The Admin Server is disabled by default to preserve the absolute minimum memory footprint for embedded devices. It is strictly conditionally compiled via the `zig build -Dadmin_server=true` flag. Enabling it moderately increases the initial static memory baseline (e.g., from ~2.6 MB to ~4.0 MB) by safely running a parallel HTTP listener, but it executes cooperatively on the same event loop ensuring zero degradation to per-message MQTT performance. When the flag is deactivated, it incurs **zero overhead footprint**. +--- -### Performance Results +### Quick Start -ProtoMQ delivers high performance across both high-end and edge hardware: +**Docker:** -| Scenario | Apple M2 Pro | Raspberry Pi 5 | -|----------|--------------|----------------| -| Latency (p99, 100 clients) | 0.44 ms | 0.13 ms | -| Concurrent clients | 10,000 | 10,000 | -| Sustained throughput | 9k msg/s | 9k msg/s | -| Message throughput (small) | 208k msg/s | 147k msg/s | -| Memory (100 clients) | 2.6 MB | 2.5 MB | +```bash +docker compose up +``` -Handles 100,000 connection cycles with zero memory leaks and sub-millisecond latency. +The server starts on port `1883` with the schemas from `schemas/`. Connect with any MQTT client. -For detailed methodology and full results, see [ProtoMQ Benchmarking Suite](benchmarks/README.md). +**From source** (requires [Zig 0.15.2+](https://ziglang.org/download/)): -### Contributing +```bash +git clone https://github.com/electricalgorithm/protomq.git +cd protomq +zig build run-server +``` -This is currently a learning/development project. Contributions will be welcome after the MVP is complete. +```bash +# In another terminal — publish (CLI encodes JSON to Protobuf for you) +zig build run-client -- publish --topic sensors/temp \ + --json '{"device_id":"s-042","temperature":22.5,"humidity":61,"timestamp":1706140800}' -### License +# In another terminal — subscribe +zig build run-client -- subscribe --topic "sensors/#" +``` -The project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +--- -### Resources +### Performance -- [Zig Documentation](https://ziglang.org/documentation/master/) -- [MQTT v3.1.1 Specification](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/mqtt-v3.1.1.html) -- [Protocol Buffers](https://protobuf.dev/) +ProtoMQ handles **208,000 messages/second** on an Apple M2 Pro and **147,000 msg/s** on a Raspberry Pi 5 — with sub-millisecond p99 latency and no memory leaks across 100,000 connection cycles. + +| Scenario | Apple M2 Pro | Raspberry Pi 5 | +|----------|--------------|----------------| +| **p99 latency** (100 clients) | 0.44 ms | 0.13 ms | +| **Throughput** (10-byte msgs) | 208k msg/s | 147k msg/s | +| **Throughput** (64 KB msgs) | 39k msg/s | 27k msg/s | +| **Sustained load** (10 min) | 8,981 msg/s | 9,012 msg/s | +| **Memory** (100 connections) | 2.6 MB | 2.5 MB | +| **Connection churn** (100k cycles) | 1,496 conn/s | 1,548 conn/s | +| **Memory leaks** | 0 MB | 0 MB | + +All benchmarks run on loopback, `ReleaseSafe` mode, Zig 0.15.2. Methodology and raw results: [`benchmarks/`](benchmarks/README.md). --- -**Note**: This project is under active development. The API and architecture may change significantly. +### Current Limitations + +QoS 0 only (at most once delivery), no persistent sessions, no retained messages, single-node deployment. These are scope decisions for the initial release — multi-node and QoS 1/2 are on the roadmap. + +### Contributing + +Contributions are welcome. If you're interested in MQTT internals, Protobuf wire format, or systems programming in Zig, there's plenty to dig into. See [FEATURES.md](FEATURES.md) for the full feature set and [FAQ.md](FAQ.md) for deployment and configuration guides. diff --git a/assets/payload_comparison.svg b/assets/payload_comparison.svg new file mode 100644 index 0000000..5490b6f --- /dev/null +++ b/assets/payload_comparison.svg @@ -0,0 +1,71 @@ + + + + + + + + + Payload Size: JSON vs Protobuf + Identical sensor telemetry data — fewer bytes = less bandwidth, lower latency + + + SensorData + 4 fields · device_id, temp, humidity, ts + + + JSON + + 114 bytes + + + Protobuf + + 40 bytes + ▼ 65% smaller + + + SensorTelemetry + 8 fields · device_id, temp, humidity, pressure, battery, lat, lon, ts + + + JSON + + 170 bytes + + + Protobuf + + 48 bytes + ▼ 72% smaller + + + FleetStatus + 12 fields · id, coords, engine metrics, diagnostics, timestamp + + + JSON + + 310 bytes + + + Protobuf + + 82 bytes + ▼ 74% smaller + + + + JSON (text) + + Protobuf (binary) + diff --git a/assets/terminal_demo.svg b/assets/terminal_demo.svg new file mode 100644 index 0000000..440f278 --- /dev/null +++ b/assets/terminal_demo.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + protomq — zsh — 120×30 + + + + zig build run-server -- --proto-dir schemas + + + [info] + ProtoMQ v0.1.0 starting on 0.0.0.0:1883 + [info] + Loaded schema: iot.sensor.SensorData (4 fields, 40 bytes max) + [info] + Mapped topic "sensors/#" → SensorData + [info] + Event loop ready (kqueue backend, 1000 max connections) + + + ─────────────────────────────────── terminal 2 ────────────────────────────────── + + + # Publish a JSON payload — ProtoMQ encodes it to Protobuf automatically + + protomq-cli publish --topic sensors/temp \ + --json '{"device_id":"s-042","temperature":22.5,"humidity":61,"timestamp":1706140800}' + + + [protobuf] + Encoded 114-byte JSON → 28-byte Protobuf (75% reduction) + [publish] + sensors/temp → 1 subscriber(s), 28 bytes delivered + + + ─────────────────────────────────── terminal 3 ────────────────────────────────── + + + # Subscribe and see decoded messages in real time + + protomq-cli subscribe --topic "sensors/#" + + + [sensors/temp] + SensorData { + device_id: "s-042" + temperature: 22.5 + humidity: 61 + timestamp: 1706140800 + } + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0c3cc09 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + protomq: + build: . + ports: + - "1883:1883" + volumes: + - ./schemas:/opt/protomq/schemas:ro + restart: unless-stopped