基于 Zig std.Io 接口的高性能、轻量级 HTTP 服务器框架,默认跑在 zio 异步运行时(io_uring / epoll / kqueue / IOCP + 协程)上,也可切换为 Zig 自带的 std.Io.Threaded 后端。4 层核心(协议 / 应用 / 路由 / 服务器)+ 一批单向依赖核心的 addon,支持请求级生命周期、radix-trie 路由、中间件管道、WebSocket、静态文件服务及内置 ORM。
架构要点:框架主体写在标准
std.Io接口上(路由/中间件/handler/会话等后端无关);与具体 异步运行时绑定的代码(监听/accept/信号/连接读写)各自集中在一个文件:http_server/zio_server.zig(zio 协程后端,默认)与http_server/std_server.zig(Zig 自带std.Io.Threaded后端)。 两者公开 API 完全对齐,入口换framework.runStd+framework.StdServer即完成切换,公共逻辑不改。
zig build run -Doptimize=ReleaseFast(src/main.zig 示例,完整中间件管道:ErrorRenderer → RequestId → Compress → Timing → SecurityHeaders → CORS)
oha -z 10s -c 200 http://127.0.0.1:9000/
Success rate: 100.00%
Requests/sec: 38206
P50: 0.64 ms
P90: 10.3 ms
P99: 30.6 ms
上表在本机(macOS,ReleaseFast)实测。三种 handler 模式的派发差异只差一次间接调用 (
.factory额外一次 create/destroy),在-c 200下GET /(fromFn)≈GET /api(initSingleton)≈ 3.8 万 req/s,可忽略不计。
zig build bench -Doptimize=ReleaseFast——内存内驱动 Router.dispatch 热路径(trie 匹配 + 3 层
中间件管道 + handler + 响应构建),排除网络/内核,度量纯框架开销(keep-alive 复用 arena):
static route (fromFn) ~8.1M ops/s ~123 ns/op
param route (/users/:id) ~7.5M ops/s ~133 ns/op
json response ~5.3M ops/s ~189 ns/op
404 (unmatched) ~8.0M ops/s ~125 ns/op
这是框架内部吞吐上限参考,不等于端到端 HTTP QPS(后者受 zio/内核/网络支配,见上表)。
| 模式 | 工厂函数 | 生命周期 | 每次请求分配 |
|---|---|---|---|
fromFn (纯函数) |
— | 无状态,全局共享 | 零 |
initSingleton (单例) |
— | main 创建并持有,框架不销毁 | 零 |
initFactory (请求级) |
init(allocator) !*T |
每请求 create/destroy 配对 | 1 次 create + 1 次 destroy |
- 跨平台统一异步 I/O — 跑在 zio 运行时上,Linux io_uring(自动 epoll 回退)/ macOS·BSD kqueue / Windows IOCP,一套 API 无平台分支
- 三种处理器模式 — 纯函数(零分配)、单例(零分配)、请求级(每请求创建/销毁)
- radix-trie 动态路由 —
/users/:id路径参数、通配符/static/*、路由分组(组级中间件)、HEAD 自动回退到 GET、405 带Allow、自定义 404 - 中间件管道 — 经典
process(ctx, res, next)模型(非 threadlocal),next.call后置逻辑、缓冲模式响应修改 - 应用级服务容器 —
Services.init/register注册进程级单例(SessionManager/Logger/ORM 等),server.setServices(&services)装配,handler 里用ctx.service(T)取回,脱离全局变量; 启动阶段注册完成后建议调services.seal()封箱,此后register返回error.ServicesSealed - 统一错误处理 —
AppError(状态码 + 消息)经ErrorRenderer渲染;无 ErrorRenderer 时框架自动兜底用 AppError 的状态码渲染(不再静默 500) - WebSocket — RFC 6455 握手 + 帧编解码 + 连接劫持(
wsUpgrade一步升级,可运行的服务端路由)、分片拼合、ping/pong(帧长上限防 DoS) - 静态文件服务 — 路径遍历防护、ETag/Last-Modified 条件请求、304、HEAD、目录 index.html、大文件流式 + gzip
- HTTP keep-alive — 连接循环 + 优雅关闭(zio.Signal 同时处理 SIGINT 与 SIGTERM);Connection 头按 token 列表解析
- 内置 ORM — JSON 文件持久化,编译期反射表结构,CRUD、查询、排序、分页
- 安全与扩展 — Auth(Bearer/Basic/API Key)、CORS(预检/Vary)、CSRF(双提交)、Security Headers、Session(Path=/、Secure 可配)、Multipart、响应压缩(q 值协商)、结构化日志、限流(Retry-After/X-RateLimit-*)
- 可替换后端 — 框架核心只依赖
std.Io;运行时绑定代码隔离在zio_server.zig/std_server.zig,入口两行切换运行时,不动核心(两行必须成对换:runZio↔ZioServer,runStd↔StdServer,错配会在启动时直接给出配对提示)
- Zig:
0.18.0-dev.253+16d31ceed(与仓库 vendored 的 zio 副本vendor/zio配套, zio 后端为默认/首选工具链;zio 上游 0.19.0 尚未适配 0.18-dev 的 std.Io 变更, 故 vendor 并打补丁,见vendor/zio/README.md)。换 zig 快照前务必先跑zig build+examples/zig build验证。 std 后端(runStd/StdServer)不依赖 zio,纯std.Io即可运行。 - zio: 默认后端所需的异步运行时依赖,以路径依赖形式 vendored 在
vendor/zio;std_server.zig后端只用 Zig 自带的std.Io.Threaded,不依赖 zio
# 开发模式(框架自带示例)
zig build run
# 发布模式
zig build run -Doptimize=ReleaseFast
# 运行测试
zig build test
# 运行性能微基准(框架内部热路径)
zig build bench -Doptimize=ReleaseFast
# 运行综合示例
cd examples && zig build run把 http_framework 加入 build.zig.zon 依赖后,入口只做两件事:用 framework.runZio
启动 zio 运行时(入口不直接依赖 zio),在 appMain(io, allocator) 里做 http_framework 初始化:
const std = @import("std");
const framework = @import("http_framework");
pub fn main(init: std.process.Init) !void {
// 启动 zio 运行时,在其协程上下文里跑 appMain。io = zio.Runtime.io()(一个 std.Io 值)。
try framework.runZio(init.gpa, appMain);
}
fn appMain(io: std.Io, allocator: std.mem.Allocator) !void {
// 1. 分层配置(network / http / body / pool)
const config = framework.Config{
.network = .{ .port = 9000 },
.http = .{ .server_name = "my-app" },
.body = .{ .size_limit = 10 * 1024 * 1024 },
};
// 2. 路由 + handler
var router = try framework.Router.init(allocator);
defer router.deinit();
try router.route(.GET, "/", framework.Handler.fromFn(helloHandler));
try router.route(.GET, "/users/:id", framework.Handler.fromFn(userHandler));
router.notFoundHandler(framework.Handler.fromFn(notFoundHandler));
// 3. 组装并运行(内部阻塞 accept + zio.Signal 处理 SIGINT/SIGTERM 优雅关闭)
var server = try framework.ZioServer.init(allocator, io, config, &router);
defer server.deinit();
try server.setup();
try server.run();
}
fn helloHandler(_: *framework.Context, res: *framework.Response) !void {
try res.json(.{ .greeting = "Hello, World!" });
}
fn userHandler(ctx: *framework.Context, res: *framework.Response) !void {
const id = ctx.param("id") orelse {
try ctx.failWith(framework.AppError.badRequest("missing :id"));
return;
};
try res.json(.{ .user_id = id });
}
fn notFoundHandler(_: *framework.Context, res: *framework.Response) !void {
_ = res.statusCode(.not_found);
try res.json(.{ .error_code = "not_found", .message = "no route matched" });
}入口不再需要手写 allocator/
std.Io.Threaded/installSignalHandlers——runZio封装了 运行时启动,信号关机内置在server.run()(等 SIGINT 或 SIGTERM → 取消 accept → drain 在途连接)。
配置启动期校验:
server.setup()会调Config.validate()——取值越界(端口/超时/ buffer 为 0 等)逐条报原因后拒绝启动;“能设置、不报错,但不改变行为”的死开关 (http.access_log_enabled(要访问日志请注册LoggingHook/LoggingMiddleware)) 默认只打 warn,http.strict_config=true时升级为启动失败(CI 推荐)。
切换运行时后端:入口改两行即可 ——
framework.runZio(gpa, appMain)→framework.runStd(gpa, appMain),framework.ZioServer→framework.StdServer,其余组装代码不动。runStd基于 Zig 自带的std.Io.Threaded:每条在途连接约占用一个 OS 线程(zio 是协程), 建议把network.max_connections调到与真实并发相当而不是照搬 zio 后端的几千;另 Windows 上 std 的 net_read/net_write 尚未接入重叠 I/O,per-operation 读写超时不可用(退化为纯阻塞,setup()会打告警),POSIX 上与 zio 版语义对齐。
接线方式参考 examples/build.zig:b.dependency("http_framework", .{...}).module("http_module")。
Config 支持「comptime 默认值 + 运行时覆盖」两级。部署时改端口/地址不用重新编译:
var server_cfg: framework.Config = .{}; // env 只在 main 里可得,自行中转给 appMain
pub fn main(init: std.process.Init) !void {
// APP_PORT=8080 APP_ADDRESS=127.0.0.1 APP_SIZE_LIMIT=67108864 ……
server_cfg = try framework.Config.fromEnv(init.arena.allocator(), init.environ_map, "APP_");
// …… framework.runZio(allocator, appMain);
}- 键名 =
prefix+ 大写字段名(取叶子字段,不带层级前缀):APP_PORT、APP_ADDRESS、APP_SERVER_NAME、APP_SIZE_LIMIT、APP_MAX_CONNECTIONS、APP_READ_TIMEOUT_NS…… 覆盖 network/http/body/pool 全部叶子字段;未设置的保持默认值。 - 0.16+ 环境变量非全局:把
main(init: std.process.Init)里的init.environ_map传进来。 allocator推荐进程级 arena(address/server_name 等字符串值由它持有)。- bool 接受
1/true/yes/on(大小写不敏感);非法值报错并在启动日志里指出是哪个键。
应用自己的配置字段:框架 Config 不预留万能槽,用组合 + 同一个加载器承接 (回应 issue:配置入口分裂):
const AppConfig = struct {
server: framework.Config = .{}, // 框架配置
data_dir: ?[]const u8 = null, // 应用自有字段
db_path: []const u8 = "./db.sqlite",
};
var app = AppConfig{};
try framework.applyEnv(AppConfig, &app, arena, init.environ_map, "APP_");
// APP_PORT=8080 → app.server.network.port;APP_DB_PATH=/srv/db.sqlite → app.db_pathapplyEnv 支持整数 / bool / []const u8 / ?[]const u8 / 嵌套 struct,其余字段类型
comptime 报错。
需要配置文件而非 env 时不用新增框架入口:Config 是纯数据类型,
std.json.parseFromSlice(framework.Config, gpa, text, .{ .ignore_unknown_fields = true })
可直接吃 JSON;文件位置/格式/热重载策略归应用决策。
| 模式 | 工厂函数 | 生命周期 | 分配开销 | 适用场景 |
|---|---|---|---|---|
| 纯函数 | fromFn |
无状态,全局共享 | 零 | 简单的请求处理、404 |
| 单例 | initSingleton(T, ptr) |
main 创建并持有,框架不销毁(只存指针) | 仅启动时一次 | 全局配置、计数器、静态文件服务 |
| 请求级 | initFactory(T, alloc) |
每次请求 create/destroy | 每次 1 次 create + 1 次 destroy | 请求隔离状态、上下文数据 |
initFactory的 deinit 规则:deinit()只释放实例内部字段(如allocator.free), 不要调allocator.destroy(self)——框架在deinit()之后会统一销毁实例内存。
try router.route(.GET, "/", framework.Handler.fromFn(struct {
fn handler(_: *framework.Context, res: *framework.Response) !void {
try res.text("Hello");
}
}.handler));const CounterHandler = struct {
count: u32 = 0,
pub fn handle(self: *CounterHandler, _: *framework.Context, res: *framework.Response) !void {
self.count += 1;
try res.json(.{ .requests = self.count });
}
};
var counter = CounterHandler{};
try router.route(.GET, "/count", framework.Handler.initSingleton(CounterHandler, &counter));const UserHandler = struct {
pub fn init(allocator: std.mem.Allocator) !*UserHandler {
const self = try allocator.create(UserHandler);
self.* = .{};
return self;
}
pub fn handle(self: *UserHandler, ctx: *framework.Context, res: *framework.Response) !void {
_ = self;
try res.json(.{ .path = ctx.request.path });
}
pub fn deinit(self: *UserHandler) void {
// 只释放内部资源;实例内存由框架销毁
_ = self;
}
};
const handler = try framework.Handler.initFactory(UserHandler, allocator);
defer handler.deinit();
try router.route(.GET, "/users/:id", handler);router.route(.GET, "/users/:id", handler); // 静态 / 动态路由
router.route(.GET, "/static/*", handler); // 通配符
router.use(middleware); // 全局中间件(先注册 = 外层 = 先执行)
router.notFoundHandler(handler); // 自定义 404(仍走全局中间件管道)ctx.param("id"); // 路径参数 /users/:id → "42"
ctx.query("page"); // Query 参数 ?page=1 → "1"(原始,未解码)
try ctx.queryDecoded("q"); // Query 参数(`+`→空格、`%XX`→字节,用 ctx.arena)
try ctx.formDecoded("name", 1<<20); // 读 body 后取表单字段并解码(urlencoded)
ctx.header("Content-Type"); // 请求头
ctx.request.getCookie("sid"); // Cookie
ctx.request.getHeader("X-Id"); // 请求头(Request 不可变)
ctx.readBody(ctx.arena, limit); // 请求体(首次读入后缓存;支持 Content-Length 与 chunked)
// allocator 应传 ctx.arena:缓存由 RequestState 持有,
// 传非 arena 分配器需自行回收,配 parseJson 时更是必须(见下)
ctx.service(T); // 取应用级服务单例(SessionManager/Logger 等)
ctx.arena; // 请求级 arena(请求结束自动回收,无需 free)
ctx.failWith(app_err); // 抛 AppError(ErrorRenderer 或框架兜底渲染)
ctx.getUserData(T); // 取中间件通讯槽(按类型索引)
ctx.setUserData(T, ptr); // 设中间件通讯槽JSON 反序列化是 addon,用自由函数:
const LoginRequest = struct { username: []const u8, password: []const u8 };
const body = framework.parseJson(LoginRequest, ctx.arena, ctx.readBody(ctx.arena, 1 << 20) catch {
try ctx.failWith(framework.AppError.badRequest("failed to read body"));
return;
}) catch {
try ctx.failWith(framework.AppError.badRequest("invalid JSON body"));
return;
};无 body 方法携带请求体(GET/HEAD/DELETE/TRACE/OPTIONS +
Content-Length: n,n>0): 按 nginx 式宽容处理——收下但忽略,handler 看到的body恒为.none,框架在响应 发完后从连接上排空这 n 个字节,keep-alive 正常继续(兼容 Elasticsearch 风格的 DELETE+body)。两个例外:携带Transfer-Encoding(chunked 无法预知长度,是走私面) 仍直接 400;Content-Length超过 64KB 时不排空(防带宽放大),响应带Connection: close后关连接。
res.json(.{ .key = "value" }); // JSON
res.text("plain text"); // 纯文本
res.html("<h1>Hello</h1>"); // HTML
res.raw(bytes, "application/octet-stream"); // 原始字节 + 指定 Content-Type
res.statusCode(.created).json(.{ ... }); // 链式调用
res.header("X-Custom", "value"); // 自定义头
res.setCookie("token", "abc123"); // Cookie
res.setCookieFull(.{ .name = "sid", .value = "x", .max_age = 0 }); // 完整 Cookie 控制
res.redirect("/new-path", false); // 重定向(false=302, true=301)
res.redirectStatus("/new-path", .see_other); // 指定状态码(301/302/303/307/308)
res.stream(buffer, .{ .content_type = "text/plain" }); // 流式响应中间件实现 process(ctx, res, next) !void,用 Middleware.init 包装后挂到 router.use()。
可以 next.call 之前做前置逻辑、之后做后置逻辑:
所有权约束(重要):
Middleware.init只有在T声明了deinit时才自动注册销毁 钩子;一旦router.use()/Pipeline.add()注册成功,实例所有权即移交框架——router.deinit()/Pipeline.deinit()通过deinitAll统一释放(按值去重, 同一实例只会deinit一次)。注册后的实例不要再手动调T.deinit(): 两边都会释放同一份资源 → double-free(实测症状如 debug 下panic: incorrect alignment+ 栈指向 destroy 钩子二次触发)。 收尾顺序统一交给 main:server.deinit→router.deinit→ 业务资源。 类型没有deinit的中间件不受此约束,但建议照此约定,避免日后补上deinit时埋下隐蔽的双重释放。examples/src/main.zig中的中间件均为正例。
const TimingMiddleware = struct {
pub fn process(self: *@This(), ctx: *framework.Context, res: *framework.Response, next: framework.Next) !void {
_ = self;
res.setBuffered(); // 缓冲模式:next() 之后还能改响应
const start = std.Io.Timestamp.now(ctx.io, .awake).nanoseconds;
next.call(ctx, res) catch |err| return err;
const elapsed = std.Io.Timestamp.now(ctx.io, .awake).nanoseconds - start;
_ = try res.header("X-Response-Time-ns", std.fmt.allocPrint(ctx.arena, "{d}", .{elapsed}) catch "?");
}
};
var timing = TimingMiddleware{};
try router.use(framework.Middleware.init(TimingMiddleware, &timing));框架内置中间件(都经 router.use(...) 全局挂载):
// 错误渲染(放最外层,兜底所有 handler 抛出的错误,渲染 AppError)
var error_renderer = framework.ErrorRenderer{};
try router.use(framework.Middleware.init(framework.ErrorRenderer, &error_renderer));
// 请求 ID(X-Request-Id 头)
var rid = framework.RequestIdMiddleware{};
try router.use(framework.Middleware.init(framework.RequestIdMiddleware, &rid));
// 响应压缩(Accept-Encoding: gzip/deflate,默认 >=1KB 才压缩)
var compress = framework.CompressMiddleware{ .config = .{} };
try router.use(framework.Middleware.init(framework.CompressMiddleware, &compress));
// 安全响应头
var security = framework.SecurityHeaders{ .config = .{} };
try router.use(framework.Middleware.init(framework.SecurityHeaders, &security));
// CORS(预检 OPTIONS 自动处理)
var cors = framework.CorsMiddleware{ .config = .{} };
try router.use(framework.Middleware.init(framework.CorsMiddleware, &cors));
// 限流(窗口内超过上限返回 429 + Retry-After;window_seconds=0 / max_requests=0
// 是启动期 error.InvalidConfig——ReleaseFast 下也不会静默失效)
var rate = try framework.RateLimiter.init(allocator, io, .{
.window_seconds = 60,
.max_requests = 100,
.per_ip = false, // per_ip=true 需在 Config.body 开 trust_proxy_headers
});
try router.use(framework.Middleware.init(framework.RateLimiter, &rate));
// 鉴权(Bearer / Basic / API Key / 自定义)
var auth = framework.AuthMiddleware{ .config = .{
.bearer_token = "my-secret-token",
.basic_username = "admin",
.basic_password = "secret123",
} };
try router.use(framework.Middleware.init(framework.AuthMiddleware, &auth));
// 校验通过后,handler 用 ctx.getUserData(framework.AuthInfo) 取身份信息
//(strategy / token / username / api_key / roles)session / DB 类登录态用 resolver(带状态身份解析器)。custom_auth 是无捕获的裸函数
指针(fn (*Context) bool),接不了 SessionManager/DB,也区分不了 401/403;resolver 把
状态、身份、错误语义都补齐:
const SessionStore = struct {
// 返回 null = 未登录(框架发 401);返回 error = 故障(ErrorRenderer 渲染 500,
// 不会被误渲染成 401);返回 Identity = 认证成功。
fn resolve(self: *SessionStore, ctx: *framework.Context) !?framework.Identity {
const sid = ctx.request.getCookie("sid") orelse return null;
const sess = self.lookup(sid) orelse return null;
return .{ .user_id = sess.user_id, .roles = &.{"user"}, .extra = sess };
}
};
var store = SessionStore{ ... };
var auth = framework.AuthMiddleware{ .config = .{
.resolver = .{ .self = &store, .resolve = SessionStore.resolve },
.required_roles = &.{"admin"}, // 角色不命中 → 403;留空则只验登录
} };
try router.use(framework.Middleware.init(framework.AuthMiddleware, &auth));
// 命中后 handler:ctx.getUserData(framework.Identity).?.user_id / .extra
// 同时 framework.AuthInfo 会被填上 strategy=.identity 与 roles。resolver 优先于 custom/bearer/basic/api_key 尝试;返回 null 时继续走其余已启用策略。
AppError = (HTTP 状态码, 消息),ErrorRenderer(管道最外层)把 handler 抛出的错误转成响应:
// handler 里直接用(failWith 只有 1 个参数,返回 error.AppError,配合 try 自动 return):
try ctx.failWith(framework.AppError.notFound("user not found")); // 404
try ctx.failWith(framework.AppError.unauthorized("bad token")); // 401
try ctx.failWith(framework.AppError.forbidden("no access")); // 403
try ctx.failWith(framework.AppError.badRequest("bad input")); // 400
try ctx.failWith(framework.AppError.conflict("already exists")); // 409
try ctx.failWith(framework.AppError.tooManyRequests("slow down")); // 429
// failWith 只是把 AppError 存进 user_data 槽并返回 error.AppError,
// 渲染由 ErrorRenderer 完成;没有 ErrorRenderer 时框架兜底用 AppError 的状态码渲染基于 Cookie 的内存 Session 存储(std.Io.Mutex 保护):
var sessions = framework.SessionManager.init(allocator, io, .{
.cookie_name = "sid",
.session_timeout_sec = 3600,
});
defer sessions.deinit();
const session_id = try sessions.getOrCreate(ctx, res); // 无 cookie 就新建并写 Set-Cookie
try sessions.setData(session_id, "username", body.username);
// 读单个键:getValue(session_id, key, allocator),返回独立拷贝(用 ctx.arena 即可随请求回收)
const username = (try sessions.getValue(session_id, "username", ctx.arena)) orelse "anonymous";
// 需要遍历全部键值时没有 getData:框架刻意不越过锁把内部 map 交出来,按 key 取值
// 登出:res.setCookieFull(.{ .name = "sid", .value = "deleted", .max_age = 0 })RFC 6455 实现(帧编解码 + 握手 + 连接级读写 + 连接劫持)。framework.wsUpgrade
一步完成握手校验 + 注册劫持回调:handler 直接 return,ConnectionRunner 在 dispatch
结束后写 101 握手响应、构造 WebSocket 并调用你的回调接管连接。
// 劫持上下文:必须是**长期稳定**的地址(进程级变量 / 服务容器),
// 因为它在 handler 返回之后、hijack 回调里才被解引用。
// 指针形态由编译器校验(可变单项指针 *T,F-20);生命周期仍需自己保证。
var ws_conns: usize = 0;
fn wsRoute(ctx: *framework.Context, res: *framework.Response) !void {
const upgraded = framework.wsUpgrade(ctx, res, &ws_conns, onWs) catch {
try ctx.failWith(framework.AppError.badRequest("upgrade failed"));
return;
};
if (!upgraded) return; // 非法升级请求(wsUpgrade 已写好 426/错误响应)
// 升级成功:直接 return,后续由 onWs 回调接管连接。
}
// 连接回调:拿到已建好的 *WebSocket,跑 receive/send 循环,返回即关连接。
// 尾参类型由 `&ws_conns` 编译期推断为 *usize,不再手写 @ptrCast。
fn onWs(ws: *framework.WebSocket, conns: *usize) anyerror!void {
conns.* += 1;
while (true) {
var msg = ws.receive() catch |err| {
if (err == error.ConnectionClosed or err == error.EndOfStream) return;
return err;
};
defer msg.deinit();
try ws.sendText(msg.payload); // echo(分片拼合、自动回 pong、帧长上限防 DoS)
}
}注册路由:try router.route(.GET, "/ws", framework.Handler.fromFn(wsRoute));
(可运行的完整 echo 示例见 examples/src/main.zig 的 /ws 路由,传模块级
&ws_echo_conns 作为稳定 hijack_ctx;examples/src/admin.zig 的
wsNotificationsHandler 则传服务容器指针,两者都是正例。)
底层 API 也可单独使用:framework.wsComputeAcceptKey(计算 Accept)、
framework.WebSocket.initServer/initClient(连接级读写;initServer 需传真实
调度器 io —— send() 靠持锁保证推送任务与 auto-pong 不交错写坏帧)、
framework.wsEncodeFrame/wsDecodeFrame(单帧编解码)。
var static_server = framework.StaticFileServer.init(allocator, io, "./public", "/static");
try router.route(.GET, "/static/*", framework.Handler.initSingleton(framework.StaticFileServer, &static_server));支持 ETag / If-None-Match(*/列表/W/)、Last-Modified / If-Modified-Since、304、
HEAD(只发头)、目录自动 index.html、大文件流式 + gzip、MIME 大小写不敏感。
var logger = try framework.Logger.init(allocator, io, .{
.min_level = .info,
.format = .json,
.output = .file,
.file = .{ .path = "log/app.log", .max_size = 2 * 1024 * 1024, .max_backups = 1, .compress = true },
});
defer logger.deinit();
logger.info(ctx, "user created", &.{
framework.fstr("name", "alice"),
framework.fint("id", 42),
framework.ffloat("price", 9.9),
});
// 请求级结构化日志:注册 LoggingHook 到 server lifecycle
var log_hook = framework.LoggingHook{ .logger = &logger };
const hooks = [_]framework.Hook{ framework.Hook.init(&log_hook) };
server.setLifecycle(.{ .hooks = &hooks });基于 JSON 文件持久化,编译期反射自动推导表结构(id 字段自动成为主键并自增):
const User = struct {
id: u64 = 0,
name: []const u8,
email: []const u8,
};
const UserModel = framework.orm.Model(User, "users");
const UserStore = UserModel.Store;
const store = try UserStore.open(allocator, io, "./data");
defer store.close() catch {};
const id = try store.insert(.{ .id = 0, .name = "alice", .email = "alice@example.com" });
try store.flush(); // 改动只在内存,需显式 flush() 写回 JSON 文件
// 读系列方法都要先传 allocator:行内字符串字段会被深拷贝进它
const rows = try store.all(allocator); // 全部行
defer store.freeRows(allocator, rows); // 用同一个 allocator 释放
const user = try store.findById(allocator, id); // 按主键查找,找不到为 null
defer if (user) |u| store.freeRow(allocator, u);
const updated = try store.updateById(id, .{ .id = id, .name = "a2", .email = "a2@x.com" });
const deleted = try store.deleteById(id);条件查询用 framework.orm.Query(T):
var qb = framework.orm.Query(User).init(allocator);
defer qb.deinit();
_ = qb.where(.Eq, "name", .{ .string = "alice" }).orderBy("id", .Asc).limit(10).offset(0);
const matches = try store.findAll(allocator, &qb); // 读方法统一先传 allocator
defer store.freeRows(allocator, matches);
const one = try store.findOne(allocator, &qb); // ?T,同样先传 allocator
const total = try store.count(&qb); // count 不返回行,无需 allocator
const page = try store.paginate(allocator, 0, 20); // 分页(按 id 升序),page 从 0 开始
defer store.freeRows(allocator, page);
// 单字段等值查询的便捷方法,免去手搭 QueryBuilder:
const by_field = try store.findAllBy(allocator, "name", "alice"); // 按字段取所有匹配行
defer store.freeRows(allocator, by_field);
const n = try store.countAll(); // 全表行数,O(1)examples/ 目录是一个完整、可运行的服务器,覆盖框架绝大部分功能:
| 路由 | 说明 |
|---|---|
GET / /health /greet /echo /users/:id |
基础路由、路径参数、query 参数(解码) |
POST /form |
表单解码(urlencoded,+/%XX) |
POST /login POST /api/items |
JSON body 解析 |
POST /upload |
multipart 文件上传 |
GET /static/* |
静态文件(./public,ETag/304/HEAD) |
GET /compress |
响应压缩(~80KB → 464B) |
GET /redirect |
303 See Other 重定向 |
GET /errors/:kind |
AppError → 各 HTTP 状态码 |
POST /session/* GET /session/me |
会话登录/登出/读取 |
GET /admin/secret |
Bearer Token 鉴权(仅 /admin 前缀) |
GET /rate-limit |
限流(60s 窗口全局 30 次,超限 429) |
GET/POST /orm/users GET/PUT/DELETE /orm/users/:id |
ORM CRUD(持久化 ./data/users.json) |
cd examples
zig build # 构建
zig build run # 运行(端口 9000)
zig build test # 运行测试(含 WebSocket 内存往返测试)示例头部注释有完整的 curl 测试清单。
4 层核心各司其职,addon 单向依赖核心。依赖方向由 build.zig 模块边界在编译期强制。
http-framework/
├── build.zig # 模块图定义(每个 addon 独立注册为模块)
├── build.zig.zon
├── src/
│ ├── root.zig # 伞形聚合模块 http_framework(@import 一次拿全部能力)
│ ├── main.zig # 框架自带示例(完整中间件管道)
│ ├── http_protocol/ # ── 第 1 层:字节 ↔ 报文 ──────────────
│ │ ├── request.zig # Request 解析(不可变)
│ │ ├── response.zig # Response / Sink / Cookie
│ │ └── conn_loop.zig # keep-alive 连接状态机
│ ├── http_app/ # ── 第 2 层:生命周期 + 管道 ──────────
│ │ ├── context.zig # Context / RequestState / RequestConfig
│ │ ├── handler.zig # Handler(union(enum):func/singleton/factory)
│ │ ├── middleware.zig # Middleware / Next / DynPipeline
│ │ ├── error.zig # AppError / ErrorRenderer
│ │ ├── lifecycle.zig # Hook / Lifecycle(请求生命周期事件)
│ │ ├── config.zig # 分层配置 + RuntimeState + ServerStats
│ │ ├── request_id.zig # RequestIdMiddleware
│ │ └── arena.zig # Arenas(请求级 arena 池)
│ ├── http_router/ # ── 第 3 层:radix-trie 路由 ──────────
│ │ ├── router.zig # Router(route / use / notFound / dispatch)
│ │ └── trie.zig # Trie(:param 与 * 通配匹配)
│ ├── http_server/ # ── 第 4 层:组装 ──────────────────
│ │ ├── connection.zig # 后端无关:ConnectionRunner(纯 HTTP 引擎)
│ │ ├── zio_server.zig # zio 专属:监听/accept/背压/信号/连接读写/启动(唯一 import zio)
│ │ ├── std_server.zig # std 专属:基于 std.Io.Threaded 的平行后端(API 与 zio 版对齐)
│ │ └── integration_test.zig
│ ├── http_security/ # ── 依赖第 1、2 层的 addon ─────────────
│ │ ├── auth.zig # AuthMiddleware(bearer/basic/api_key/custom)
│ │ ├── cors.zig # CorsMiddleware
│ │ ├── csrf.zig # CsrfMiddleware
│ │ └── security_headers.zig
│ ├── http_session/ # 会话(Cookie + 内存存储 + Mutex)
│ ├── http_rate_limit/ # 限流(RateLimiter,429 + Retry-After)
│ ├── http_static/ # 静态文件服务(目录遍历防护)
│ ├── http_codec/ # JSON 解析(parseJson)
│ ├── http_multipart/ # multipart/form-data 解析
│ ├── http_compress/ # 响应压缩(gzip/deflate 流式)
│ ├── http_logging/ # 结构化日志(JSON/文本、文件轮转)
│ ├── http_orm/ # ORM(编译期反射 + JSON 持久化)
│ └── http_websocket/ # WebSocket(RFC 6455)
├── examples/ # 综合示例(见上文"示例")
└── README.md
不需要 session、模板、WebSocket 等项目,可以只依赖核心 4 层模块:
// build.zig
const http_framework = b.dependency("http_framework", .{}).module("http_framework");所有 addon 也都有独立模块名(http_security、http_session、http_rate_limit、
http_static、http_codec、http_multipart、http_compress、http_logging、
http_orm、http_websocket),按需 b.dependency(...).module("...") 导入即可。
核心层的 http_protocol 不依赖任何其它模块。
docs/API.md—— API 参考、设计约束与「硬约束与陷阱」清单docs/API_generated.md—— 全模块公开符号清单(zig run tools/gen_api.zig -- generate > docs/API_generated.md刷新)docs/ROADMAP.md—— 功能路线图:待办、已知缺口、明确不做什么examples/FRICTION.md—— 真实业务使用踩坑清单(路线图的主要输入)ANALYSIS.md—— 架构分析与改进报告(历史快照,不回改)
MIT