引言
很多系统级语言做 HTTP 服务要引入框架,Zig 却把 HTTP 客户端与服务端内置在标准库 std.http 中。自 0.11 起 std.http.Server 提供了完整的 HTTP/1.1 服务端能力:请求解析、响应构建、Keep-Alive、管道化处理。相比 Go 的 net/http,Zig 版本更底层、更透明,适合构建 API 网关、微服务边车、开发服务器等场景。
本文从最小服务端讲起,覆盖请求生命周期、路由与中间件模式、静态文件服务、并发连接处理、与 async/io_uring 的异步结合,最后落到 TLS、超时与生产部署。
前置:/zig-async-network/(epoll/io_uring 事件循环)、/zig-error-handling/(错误联合)。
目录
- 1. std.http.Server 概览
- 2. 最小 HTTP 服务
- 3. 请求生命周期与路由
- 4. 响应构建:状态码、Header 与 Body
- 5. 静态文件服务
- 6. 并发连接处理:线程池模型
- 7. 异步模型:async 与 io_uring
- 8. TLS、超时与生产部署
- 9. 速查表
- 10. 一句话记忆
- 延伸阅读
1. std.http.Server 概览
std.http.Server 是一个监听在已有 socket 上的请求循环,核心流程:
listen(socket)
→ server.receive() 拿到一个新连接
→ server.accept() 解析出 Request
→ 处理请求
→ server.respond() 写回响应
→ 回到 receive()(Keep-Alive 复用连接)
关键类型:
| 类型 | 职责 |
|---|---|
std.http.Server | 管理连接与请求循环 |
std.http.Server.Request | 单个请求:method、target、headers、body |
std.http.Server.Response | 待写回的响应:status、headers、body |
std.http.Server.RespondOptions | 响应选项(transfer 编码、keep-alive) |
认知:
Server把 socket 读取、HTTP 解析、头部管理封装好,但并发模型完全交给你——可以用单线程、线程池或事件循环。这正是 Zig 的哲学:标准库给能力,不给束缚。
2. 最小 HTTP 服务
const std = @import("std");
const Server = std.http.Server;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// 绑定 0.0.0.0:8080
var server = Server.init(allocator, .{ .reuse_address = true });
defer server.deinit();
try server.listen(.{ .address = try std.net.Address.parseIp("0.0.0.0", 8080) });
std.debug.print("listening on 8080\n", .{});
while (true) {
var request = server.accept(.{ .allocator = allocator }) catch |err| {
std.debug.print("accept error: {s}\n", .{@errorName(err)});
continue;
};
// 处理
try request.respond("hello from zig", .{});
}
}
zig run src/http_min.zig
curl http://127.0.0.1:8080/ # → hello from zig
要点:accept 返回的 Request 自带读取器,处理完用 respond 写回;连接自动 Keep-Alive,直到对端关闭。
3. 请求生命周期与路由
一个请求的处理阶段:读目标 → 分发路由 → 处理 → 响应。用 request.method、request.target 判断:
const std = @import("std");
const Server = std.http.Server;
const Method = std.http.Method;
fn handle(allocator: std.mem.Allocator, request: *Server.Request) !void {
// 读取请求体(若有)
var body: [4096]u8 = undefined;
const read_n = try request.read(&body);
_ = read_n;
const path = request.target; // 例如 "/api/users?page=2"
const query = std.Uri.parse(path) catch null;
switch (request.method) {
.GET => try routeGet(request, query),
.POST => try routePost(request, body),
else => try request.respond("method not allowed", .{ .status = .method_not_allowed }),
}
}
路由拆分建议:用前缀匹配 + 精确匹配表,避免引入重型路由框架:
// 简单前缀路由
if (std.mem.startsWith(u8, path, "/api/")) {
try apiRoute(allocator, request, path);
} else if (std.mem.eql(u8, path, "/health")) {
try request.respond("ok", .{ .status = .ok });
} else {
try request.respond("not found", .{ .status = .not_found });
}
中间件模式:把「鉴权、日志、限流」包在外层,用 defer 保证清理:
fn withLogging(request: *Server.Request) !void {
const start = std.time.nanoTimestamp();
defer std.debug.print("{s} {s} {d}ms\n", .{
@tagName(request.method), request.target,
(std.time.nanoTimestamp() - start) / 1_000_000,
});
try handle(allocator, request);
}
4. 响应构建:状态码、Header 与 Body
respond 支持完整响应控制:
// 返回 JSON
const json = "{\"status\":\"ok\"}";
try request.respond(json, .{
.status = .ok,
.extra_headers = &.{
.{ .name = "content-type", .value = "application/json" },
.{ .name = "cache-control", .value = "no-store" },
},
});
// 分块 / 流式写回(大响应)
var response = try request.respondStreaming(.{
.status = .ok,
.transfer_encoding = .chunked,
});
try response.writeAll("part1");
try response.writeAll("part2");
try response.end();
常用状态码(std.http.Status 枚举):
| 常量 | 含义 |
|---|---|
.ok | 200 |
.created | 201 |
.no_content | 204 |
.bad_request | 400 |
.unauthorized | 401 |
.not_found | 404 |
.internal_server_error | 500 |
注意:
respond一个请求只能调用一次;多次响应会触发错误,需按请求-响应一对一的模型组织代码。
5. 静态文件服务
静态资源用 std.fs 读取并写回,注意路径安全(防目录穿越):
fn serveStatic(request: *Server.Request, root: []const u8) !void {
const rel = request.target;
// 防止 "../" 目录穿越
if (std.mem.indexOf(u8, rel, "..") != null) {
return request.respond("forbidden", .{ .status = .forbidden });
}
const full = try std.fs.path.join(allocator, &.{ root, rel });
defer allocator.free(full);
const file = std.fs.cwd().openFile(full, .{}) catch |err| switch (err) {
error.FileNotFound => return request.respond("not found", .{ .status = .not_found }),
else => return err,
};
defer file.close();
const size = try file.getEndPos();
var response = try request.respondStreaming(.{
.status = .ok,
.transfer_encoding = .{ .content_length = size },
});
try file.copyRangeAll(0, response.writer(), size, null);
try response.end();
}
静态服务注意:设置正确的 content-type、cache-control;生产建议让 Nginx/CDN 直接托管静态资源,Zig 服务只做 API。
6. 并发连接处理:线程池模型
默认单线程循环串行处理请求——一个慢请求会阻塞后面的连接。需要并发时用线程池:
const std = @import("std");
const Thread = std.Thread;
const WorkerPool = struct {
threads: []Thread,
server: *std.http.Server,
next: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
fn init(allocator: std.mem.Allocator, server: *std.http.Server, n: usize) !WorkerPool {
var pool = WorkerPool{ .threads = try allocator.alloc(Thread, n), .server = server };
for (0..n) |i| {
pool.threads[i] = try Thread.spawn(.{}, worker, .{&pool});
}
return pool;
}
fn worker(pool: *WorkerPool) void {
while (true) {
const id = pool.next.fetchAdd(1, .monotonic);
_ = id;
var request = pool.server.accept(.{ .allocator = gpa.allocator() }) catch |err| {
std.debug.print("accept: {s}\n", .{@errorName(err)});
continue;
};
handleRequest(&request) catch |err| {
std.debug.print("handle: {s}\n", .{@errorName(err)});
continue;
};
}
}
};
线程池设计要点:
□ 多线程共享一个 server,accept 是线程安全的(内部有锁)
□ 每请求的 allocator 要独立或线程安全(GPA 默认线程安全,Arena 需小心)
□ worker 数 = 核数或核数×2,别为高并发起上千线程
□ 注意共享状态的同步:用 Mutex/原子操作,别让请求处理触碰未同步数据
记忆:Zig 不帮你做并发模型,但 std.http.Server 的 accept 线程安全,线程池是官方推荐的高并发路径。
7. 异步模型:async 与 io_uring
追求更高并发、更低线程数,把 std.http.Server 接进事件循环。Zig 原生 async 函数可在任意时刻 suspend 让出 CPU:
// 伪代码:把 handle 包成 async 函数,事件循环驱动
fn asyncHandle(request: *Server.Request) !void {
// 可能阻塞的 IO 在内部 suspend
var response = try request.respondStreaming(.{ .status = .ok });
try response.writeAll("async hello");
try response.end();
}
// 事件循环(简化):io_uring / epoll 就绪时恢复对应协程
在 Linux 上接 io_uring:std.os.linux.io_uring 提供原生接口,把 socket 读写注册进 ring,请求完成时再恢复协程。这是「单线程扛万级并发」的路径,也是 Zig 相对多数语言的优势。
路径选择:
| 模型 | 并发能力 | 复杂度 | 适用 |
|---|---|---|---|
| 单线程循环 | 低(串行) | 最低 | 工具、开发服务器 |
| 线程池 | 中(受线程数) | 中 | 生产 API |
| async + epoll | 高 | 高 | 高并发服务 |
| async + io_uring | 极高 | 很高 | 极限吞吐 |
8. TLS、超时与生产部署
TLS:std.http 本身不带 TLS,需要外部实现。主流做法:
□ 前置 Nginx/Caddy 做 TLS 终止 → Zig 服务只收内部 HTTP(最简单)
□ 或用 zig 生态的 TLS 库(如 @import 到 BearSSL/mbedTLS)做原生终止
超时控制:防止慢连接拖垮资源,用 std.posix 设置 socket 超时:
const timeout = std.posix.timeval{ .tv_sec = 10, .tv_usec = 0 };
std.posix.setsockopt(sock, std.posix.SOL.SOCKET, std.posix.SO.RCVTIMEO, &timeout);
std.posix.setsockopt(sock, std.posix.SOL.SOCKET, std.posix.SO.SNDTIMEO, &timeout);
生产 checklist:
□ 置于反向代理之后(Nginx/Caddy),终止 TLS、做静态与限流
□ 请求体大小限制(读 body 时设上限,防内存耗尽)
□ 全局 allocator 用 GPA,错误路径确保释放
□ 优雅关闭:SIGTERM 时停止 accept、等待在途请求
□ 监控:记录每请求耗时/状态码/错误率
9. 速查表
| 需求 | 手段 |
|---|---|
| 最小服务 | Server.init + listen + accept 循环 |
| 路由 | 前缀匹配 + 精确匹配表 |
| JSON 响应 | respond(json, .{ .status = .ok }) + content-type |
| 大响应流式 | respondStreaming + writeAll |
| 静态文件 | std.fs 读取 + 路径穿越防护 |
| 并发 | 线程池共享 accept(线程安全) |
| 高并发异步 | async + io_uring/epoll |
| TLS | 反向代理终止(推荐)或嵌入 TLS 库 |
| 超时 | setsockopt SO.RCVTIMEO/SNDTIMEO |
10. 一句话记忆
Zig 用 std.http.Server 内建 HTTP:accept 线程安全可进线程池,io_uring 可单线程扛万级并发;TLS 交给 Nginx/Caddy 终止,Zig 专注 API 逻辑——并发模型自己选,这正是 Zig 的透明哲学。
延伸阅读
- /zig-async-network/ — epoll/io_uring 事件循环与异步 IO
- /zig-error-handling/ — 错误联合与请求处理容错
- /zig-memory-management/ — 每请求分配与释放的生命周期管理
- /zig-testing-quality/ — 用 testing.allocator 测试 HTTP 处理器
- [[zig]] — Zig 系统编程专题
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。