kevherro/tinyws
Tiny WebSocket in Zig
tinyws is a minimal, high-performance WebSocket library for Zig 0.15.1, optimized for trading systems.
Performance:
Platform: Linux, macOS, BSD (Windows not supported)
// In your build.zig.zon
.dependencies = .{
.tinyws = .{
.url = "https://github.com/kevherro/tinyws/archive/refs/tags/[tag].tar.gz",
.hash = "[hash]",
},
},
const std = @import("std");
const ws = @import("tinyws");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var server = try ws.Server(Handler).init(allocator, .{
.port = 9224,
});
try server.listen(.{});
}
const Handler = struct {
conn: *ws.Conn,
pub fn init(h: *ws.Handshake, conn: *ws.Conn, ctx: anytype) !Handler {
_ = h; _ = ctx;
return .{ .conn = conn };
}
pub fn clientMessage(self: *Handler, data: []const u8) !void {
try self.conn.write(data); // echo
}
};
var client = try ws.Client.init(allocator, .{
.host = "market-data.broker.com",
.port = 443,
.tls = true,
.mask = false, // Skip masking for server-to-server
.tcp_nodelay = true, // Minimize latency
});
defer client.deinit();
try client.handshake("/stream", .{
.headers = "Authorization: Bearer YOUR_TOKEN",
});
// Send order
try client.writeText("BUY:AAPL:100:175.50");
// Read confirmation
const msg = try client.read();
defer client.done(msg);
Your handler must implement:
pub fn init(h: *Handshake, conn: *Conn, ctx: anytype) !Handler
pub fn clientMessage(self: *Handler, data: []const u8) !void
Optional methods:
pub fn afterInit(self: *Handler) !void
pub fn close(self: *Handler) void
pub fn clientPing(self: *Handler, data: []const u8) !void
pub fn clientPong(self: *Handler, data: []const u8) !void
pub fn init(h: *ws.Handshake, conn: *ws.Conn, ctx: anytype) !Handler {
// Access request headers
const auth = h.headers.get("authorization") orelse return error.Unauthorized;
// Set response headers
h.res_headers.add("x-server", "tinyws");
return .{ .conn = conn };
}
// Basic handler
clientMessage(self: *Handler, data: []const u8) !void
// With message type
clientMessage(self: *Handler, data: []const u8, tpe: ws.MessageTextType) !void
// With allocator (freed after method returns)
clientMessage(self: *Handler, allocator: Allocator, data: []const u8) !void
conn.write(data: []const u8) // Send text
conn.writeBin(data: []const u8) // Send binary
conn.close(.{.code = 1000}) // Close with code
// Initialize
var client = try ws.Client.init(allocator, config);
defer client.deinit();
// Connect
try client.handshake("/path", .{
.timeout_ms = 5000,
.headers = "Custom: Headers",
});
// Write (note: []u8 not []const u8 for in-place masking)
try client.writeText(data);
try client.writeBin(data);
// Read
const msg = try client.read();
defer client.done(msg);
switch (msg.type) {
.text, .binary => process(msg.data),
.close => try client.close(.{}),
else => {},
}
const Handler = struct {
client: *ws.Client,
pub fn serverMessage(self: *Handler, data: []u8) !void {
// Handle messages
}
pub fn close(self: *Handler) void {
// Cleanup
}
};
const thread = try client.readLoopInNewThread(&handler);
.{
.port = 9224,
.address = "127.0.0.1",
.max_conn = 16384, // Per worker
.max_message_size = 65536,
.handshake = .{
.timeout = 10, // Seconds
.max_size = 1024,
.max_headers = 10, // 0 to skip parsing
},
.thread_pool = .{
.count = 4, // Worker threads
.backlog = 500,
},
.buffers = .{
.small_size = 2048,
.small_pool = null, // null = dedicated per connection
.large_pool = 8,
},
}
.{
.host = "localhost",
.port = 9224,
.tls = false,
.max_size = 65536,
.buffer_size = 4096,
.ca_bundle = null,
.mask = true, // false for server-to-server
.tcp_nodelay = false, // true for low latency
}
Use Case | Configuration | Rationale |
---|---|---|
Market Data Feed | mask=true, tcp_nodelay=false, buffer_size=4096 |
High throughput, exchange requires masking |
Order Execution | mask=false, tcp_nodelay=true, buffer_size=512 |
Ultra-low latency, small messages |
Internal Services | mask=false, tcp_nodelay=true, buffer_size=2048 |
Balanced performance |
make bench # Core performance metrics
make bench-echo # Round-trip latency test
Operation | Performance | Notes |
---|---|---|
Masking (256B) | 1.88 GB/s | Typical order size |
Masking (1KB) | 2.28 GB/s | Market data snapshot |
Frame Headers | < 1 ns | Essentially free |
Round-trip | 45 μs (p50) | With TCP_NODELAY |
For detailed performance analysis, see performance.md.
make test # Run all tests
make test F=mask # Filter by name
make quick # Fast test without trace
The library includes testing utilities:
const wt = ws.testing;
test "echo handler" {
var wtt = wt.init(.{});
defer wtt.deinit();
var handler = Handler{ .conn = &wtt.conn };
try handler.clientMessage("hello");
try wtt.expectMessage(.text, "hello");
}
server.stop()
from any thread