本章导读
xlang 的 HTTP 服务器是嵌入式的——它运行在你的 xlang 进程内,不需要 Nginx 或 Apache 做前置代理。这让它非常适合:微服务、API 网关、开发工具后端、IoT 设备 Web 管理界面、以及 xServer 这类将 HTTP 服务器与脚本引擎合一的场景。
- 用
http.server.create创建并启动 HTTP 服务器 - 注册 GET/POST/PUT/任意方法路由,理解路径参数
{id} - 从
HttpRequest提取路径、查询、Cookie、请求头、请求体 - 用
HttpResponse返回文本、JSON、文件、重定向、Cookie、流式数据 - 解析 URL-encoded 表单和 multipart 文件上传
- 配置静态文件服务和 TLS/SNI
- 使用 App 隔离和虚拟主机托管多个应用
- 掌握 drain/stop/destroy 优雅停机流程
44.1 创建与启动服务器
一个最小的 HTTP 服务器只需四步:创建、注册路由、启动、停止。
// 路由处理函数
func route(point reqHandle, point respHandle) as bool
HttpRequest req = HttpRequest(reqHandle)
HttpResponse resp = HttpResponse(respHandle)
return resp.text("Hello from xlang!")
end
// 声明函数值(路由注册需要函数值类型)
func(point, point) as bool handler = route
// 创建服务器:端口 0 = 自动分配,绑定 127.0.0.1
point server = http.server.create(0, "127.0.0.1", 128, 1048576, 1048576, 100, 65536, 1)
// 注册路由
http.server.getRequest(server, "/hello", handler)
// 启动
assert http.server.start(server), "服务器启动失败"
int port = http.server.port(server)
println("服务器运行在 http://127.0.0.1:" + port.toString())
// 阻塞等待(生产环境中保持运行)
time.sleep(60000)
// 停止并销毁
http.server.stop(server)
http.server.destroy(server)
http.server.create 的参数:
| 参数 | 含义 | 示例值 |
|---|---|---|
| port | 监听端口(0 = 系统自动分配) | 8080 |
| host | 绑定地址 | "0.0.0.0" |
| backlog | 连接等待队列长度 | 128 |
| maxBody | 最大请求体字节 | 1048576 (1MB) |
| maxResponse | 最大响应体字节 | 1048576 |
| timeout | 连接超时(秒) | 100 |
| maxHeader | 最大请求头字节 | 65536 |
| threads | 工作线程数 | 1 |
44.2 路由注册
xlang HTTP 服务器支持多种路由注册方式:
// 按方法注册——Request 风格(推荐)
http.server.getRequest(server, "/users", handler) // 仅 GET
http.server.postRequest(server, "/users", handler) // 仅 POST
http.server.anyRequest(server, "/health", handler) // 任意方法
// 多方法路由——用 | 分隔
http.server.routeRequest(server, "PUT|PATCH|DELETE", "/users/{id}", handler)
// 路径参数——用 {name} 占位
http.server.getRequest(server, "/users/{id}", handler)
// 请求 /users/42 时,req.param("id") == "42"
路由匹配规则:
- 精确路径优先于参数路径:
/users/me优先于/users/{id} - 方法必须匹配:
getRequest只响应 GET anyRequest响应所有 HTTP 方法- 未匹配的路径返回 404(或触发自定义错误处理器)
Request 风格 func(point, point) as bool 是推荐方式,功能完整。Legacy 风格 func(string, string, string, string) as string 只接收 method/path/query/body 四个字符串,适合极简场景。
44.3 HttpRequest 请求解析
HttpRequest 封装了入站请求的全部信息:
func route(point reqHandle, point respHandle) as bool
HttpRequest req = HttpRequest(reqHandle)
HttpResponse resp = HttpResponse(respHandle)
// 基本信息
string method = req.method() // "GET" / "POST" / ...
string path = req.path() // "/users/42"
bool open = req.connectionOpen() // 连接是否仍然打开
// 路径参数(来自 {id} 占位符)
string userId = req.param("id") // "42"
string pName = req.paramName(0) // "id"
string pVal = req.paramValue(0) // "42"
// 查询参数(?name=xlang&page=1)
string name = req.queryValue("name") // "xlang"
string qName = req.queryName(0) // "name"
string qVal = req.queryValueAt(0) // "xlang"
// 请求头
string auth = req.header("Authorization")
string hName = req.headerName(0)
// Cookie
string sid = req.cookie("sid")
string cName = req.cookieName(0)
string cVal = req.cookieValueAt(0)
// 请求体
string body = req.body()
return resp.text("ok")
end
44.4 HttpResponse 响应构建
HttpResponse 提供丰富的响应构建方法:
func route(point reqHandle, point respHandle) as bool
HttpRequest req = HttpRequest(reqHandle)
HttpResponse resp = HttpResponse(respHandle)
// 设置状态码(默认 200)
resp.status(201, "Created")
// 设置响应头
resp.header("X-Server", "xlang")
resp.addHeader("Set-Cookie", "a=1") // 可重复添加同名头
// 返回文本
return resp.text("Hello World")
// 返回 JSON
// return resp.jsonText("{\"ok\":true}")
// 返回文件
// return resp.file("static/index.html")
// 返回文件的一部分(Range 请求)
// resp.status(206, "Partial Content")
// return resp.fileRange("video.mp4", 0, 1024)
// 重定向
// return resp.redirect("/new-location", 302)
// 设置 Cookie
// resp.cookie("sid", "abc123", "/", 3600, http.server.sameSiteLax, http.server.cookieHttpOnly)
// return resp.text("cookie set")
// 返回二进制数据
// Buffer data = ...
// return resp.body(data.data(), c_size(data.len()), "application/octet-stream")
end
响应方法速查:
| 方法 | 说明 |
|---|---|
text(s) | 返回 text/plain 文本 |
jsonText(s) | 返回 application/json |
file(path) | 返回文件内容(自动推断 MIME) |
fileRange(path, off, len) | 返回文件片段 |
redirect(url, code) | 302/301 重定向 |
body(ptr, size, mime) | 返回二进制数据 |
cookie(...) | 设置 Set-Cookie 头 |
start() / write() / finish() | 流式响应(见 44.7) |
reserveBody(n) / appendText(s) | 分块构建响应体 |
44.5 表单与文件上传
xlang 服务器自动解析两种表单格式:
// URL-encoded 表单(Content-Type: application/x-www-form-urlencoded)
if req.isFormUrlEncoded() then
string name = req.formValue("name") // 按名取值
string first = req.formName(0) // 第一个字段名
string val = req.formValueAt(0) // 第一个字段值
end
// Multipart 表单(Content-Type: multipart/form-data)
if req.isMultipartForm() then
string title = req.multipartValue("title") // 普通字段
string partName = req.multipartName(0) // 第 0 部分字段名
string text = req.multipartText(0) // 第 0 部分文本内容
if req.multipartIsFile(1) then // 第 1 部分是文件
string fileName = req.multipartFileName(1) // 原始文件名
Buffer data = req.multipartBytes(1) // 文件内容
println("上传文件: " + fileName + ", 大小: " + data.len().toString())
end
end
multipart 解析会将整个文件读入内存。对于大文件(视频、镜像),应使用流式上传(见 44.7),数据逐块到达,边收边写磁盘,内存占用恒定。
44.6 静态文件服务
将 URL 路径映射到磁盘目录:
// 基本静态服务:/public/* → ./static/ 目录
http.server.static(server, "/public", "./static")
// 请求 /public/css/style.css → 读取 ./static/css/style.css
// 高级静态服务:自定义默认文件、缓存、标志
int flags = http.server.allowDotfiles + http.server.allowBackslash
http.server.staticEx(
server,
"/assets", // URL 前缀
"./static", // 磁盘目录
"index.html", // 默认文件(访问 /assets/ 时返回)
"max-age=3600", // Cache-Control 头
65536, // 最大文件大小
flags // 标志位
)
MIME 类型由文件扩展名自动推断:
// 查询 MIME 类型
string mime = http.server.mimeByPath("data.json")
// → "application/json; charset=utf-8"
44.7 流式响应与流式上传
流式响应——适合 SSE、大文件下载、实时日志推送:
if req.path() == "/stream" then
resp.header("Content-Type", "text/event-stream")
if not resp.start() then
return false // 发送头失败
end
resp.write("data: hello\n\n")
resp.write("data: world\n\n")
return resp.finish()
end
流式上传——适合大文件接收,内存占用恒定:
// 三个回调:开始、数据块、结束
func uploadBegin(point reqHandle) as bool
HttpRequest req = HttpRequest(reqHandle)
string name = req.param("name")
return file.writeText("upload_" + name + ".tmp", "")
end
func uploadChunk(point reqHandle, point data, int size) as bool
HttpRequest req = HttpRequest(reqHandle)
string name = req.param("name")
string text = http.server.chunkText(data, size)
return file.appendText("upload_" + name + ".tmp", text)
end
func uploadEnd(point reqHandle, point respHandle) as bool
HttpRequest req = HttpRequest(reqHandle)
HttpResponse resp = HttpResponse(respHandle)
string name = req.param("name")
string content = file.readText("upload_" + name + ".tmp")
return resp.text("上传完成: " + name + ", 大小: " + content.len().toString())
end
// 声明函数值
func(point) as bool beginFn = uploadBegin
func(point, point, int) as bool chunkFn = uploadChunk
func(point, point) as bool endFn = uploadEnd
// 注册流式上传路由
http.server.postStreamRequest(server, "/upload/{name}", beginFn, chunkFn, endFn)
44.8 TLS 与 SNI 多证书
创建 HTTPS 服务器:
// 创建 TLS 服务器(指定证书和私钥)
point server = http.server.createTls("cert.pem", "key.pem", 443)
// SNI 多证书——同一端口为不同域名使用不同证书
http.server.tlsHost(server, "api.example.com", "api_cert.pem", "api_key.pem")
http.server.tlsHost(server, "web.example.com", "web_cert.pem", "web_key.pem")
// 移除 SNI 证书
http.server.removeTlsHost(server, "api.example.com")
SNI(Server Name Indication)允许一个 IP:端口 为多个域名提供不同的 TLS 证书。客户端在 TLS 握手时告知目标域名,服务器选择对应证书。这是现代 CDN 和虚拟主机的基础。
44.9 App 多应用与虚拟主机
一个服务器可以托管多个独立应用,彼此路由隔离:
// 创建应用
point app1 = http.server.app()
point app2 = http.server.app()
// 为 app1 注册路由
http.server.appGet(app1, "/", firstHandler)
http.server.appPost(app1, "/api/data", dataHandler)
http.server.appStatic(app1, "/public", "./app1_static")
// 为 app2 注册路由
http.server.appGet(app2, "/", secondHandler)
http.server.appError(app2, errorHandler)
// 将 app2 设为默认应用
http.server.setApp(server, app2)
// 热切换应用(零停机)
http.server.reloadApp(server, app1)
// 虚拟主机——按域名分发
point vhost = http.server.app()
http.server.appGet(vhost, "/", hostHandler)
http.server.host(server, "admin.example.com", vhost)
// 移除虚拟主机
http.server.removeHost(server, "admin.example.com")
// 销毁应用
http.server.destroyApp(app1)
http.server.destroyApp(app2)
http.server.destroyApp(vhost)
App 的典型用途:同一进程中运行管理后台 + 用户 API + 静态资源服务,彼此路由不干扰,且可以独立热更新。
44.10 生命周期与优雅停机
服务器的完整生命周期:
// 1. 创建
point server = http.server.create(8080, "0.0.0.0", 128, 1048576, 1048576, 100, 65536, 4)
// 2. 注册路由...
// 3. 启动
assert http.server.start(server)
// 4. 运行中——查询状态
int port = http.server.port(server)
int reqs = http.server.requestCount(server)
int conns = http.server.connectionCount(server)
// 5. 优雅停机
// drain:停止接受新连接,等待已有请求完成(最多等 timeout 毫秒)
bool drained = http.server.drain(server, 5000)
// stop:强制停止
http.server.stop(server)
// 6. 销毁(释放资源)
http.server.destroy(server)
停机顺序很重要:
drain(server, timeout):停止监听新连接,等待进行中的请求完成。适合滚动更新。stop(server):立即停止,不等待。适合紧急关闭。destroy(server):释放服务器占用的所有资源。必须在 stop 之后调用。
推荐顺序:① 从负载均衡器摘除本节点;② drain(server, 30000) 等待存量请求完成;③ stop(server);④ destroy(server);⑤ 进程退出。这保证了零请求丢失。
深入细节
处理函数返回 true 表示"响应已构建完毕,可以发送给客户端";返回 false 表示"处理失败,关闭连接"。注意:即使你调用了 resp.status(404),只要最后 return true,404 响应也会正常发送。return false 不会发送任何响应,直接断开连接。
通过 http.server.error(server, errorHandler) 注册全局错误处理器。当路由未匹配或内部错误时,错误处理器被调用,签名为 func(int, string, point, point) as bool——接收状态码、错误消息、请求和响应句柄。这让你可以返回自定义 404 页面或 JSON 错误格式。
调用 req.closeConnection() 会在响应发送后关闭 TCP 连接(发送 Connection: close 头)。适用场景:响应后不再需要连接(如某些安全端点)、检测到异常客户端、或实现简单的限流(迫使客户端重新握手)。
⚠️ 常见错误
路由注册需要函数值(func(point, point) as bool),不能直接传函数名。必须先 func(point, point) as bool handler = route,再传 handler。直接传函数名会编译报错。
resp.start() 发送头,resp.write() 写数据块,但如果忘记 resp.finish(),客户端会一直等待响应结束(或超时断开)。三者必须配套使用。
直接调用 destroy 而不先 stop,可能导致进行中的请求被强制中断、资源未正确释放。正确顺序:drain → stop → destroy。
默认情况下 http.server.static 会阻止 ../ 路径穿越。但如果你设置了 http.server.allowBackslash 标志,在 Windows 上可能引入反斜杠穿越风险。生产环境不要随意开启此标志。
本章小结
| 主题 | 核心要点 |
|---|---|
| 创建 | http.server.create(port, host, backlog, ...) 或 createTls 创建 HTTPS |
| 路由 | getRequest/postRequest/anyRequest/routeRequest;{id} 路径参数 |
| 请求 | method/path/param/queryValue/cookie/header/body/formValue/multipart* |
| 响应 | text/jsonText/file/fileRange/redirect/body/cookie/status/header |
| 表单 | isFormUrlEncoded + formValue;isMultipartForm + multipartValue/multipartBytes |
| 静态 | static(prefix, dir);staticEx 支持默认文件/缓存/标志 |
| 流式 | 响应:start/write/finish;上传:postStreamRequest 三回调 |
| TLS | createTls 创建;tlsHost 添加 SNI 证书;removeTlsHost 移除 |
| App | app() 创建隔离应用;setApp/reloadApp 切换;host() 虚拟主机 |
| 生命周期 | start → drain(优雅) → stop(强制) → destroy(释放) |
复习题
- http.server.create 的 port 参数传 0 有什么效果?什么场景下有用?
- getRequest 和 anyRequest 的区别是什么?routeRequest 的 "PUT|PATCH" 语法表示什么?
- 路径 /users/{id} 匹配请求 /users/42 时,如何获取 "42"?
- resp.text() 和 resp.jsonText() 设置的 Content-Type 有什么不同?
- isFormUrlEncoded 和 isMultipartForm 分别对应什么 Content-Type?
- 流式响应的三个步骤是什么?忘记 finish() 会怎样?
- SNI 解决什么问题?没有 SNI 时一个 IP 只能有一个 HTTPS 站点吗?
- reloadApp 和 setApp 的区别是什么?哪个支持零停机切换?
- drain(server, 5000) 的 5000 是什么含义?drain 返回 false 说明什么?
- 为什么路由注册需要函数值而不能直接传函数名?
编程练习
实现一个内存 TODO 列表 API:GET /todos 返回 JSON 数组;POST /todos 添加项(从 body 解析 JSON);PUT /todos/{id} 更新;DELETE /todos/{id} 删除。用 list 存储,json 模块序列化。启动后用 http.get/http.request 自测。
参考思路:定义 Todo class,用 list<Todo> 存储;路由中按 method+path 分发;json.stringify 序列化响应。
实现 POST /upload/{name} 流式上传端点:begin 回调创建文件,chunk 回调追加写入,end 回调返回文件大小。再实现 GET /files/{name} 用 resp.file 返回已上传的文件。测试:用 http.post 发送文本,再用 http.get 下载验证。
参考思路:postStreamRequest 注册三回调;file.writeText/appendText/readText 操作磁盘。
创建一个服务器,托管两个 App:默认 App 返回 "main site",虚拟主机 "admin.local" 返回 "admin panel"。再为默认 App 添加 /public 静态文件服务。用 http.get 配合 Host 头测试虚拟主机分发。
参考思路:http.server.app() 创建两个应用;http.server.host(server, "admin.local", vhost) 绑定域名。