mc.http — Lua HTTP 绑定
概述
mc.http 是 mchttp C++ 库的 Lua 绑定,通过 require('mc.http') 加载。它提供与 C++ 对齐的 HTTP 服务端/客户端能力,同时适配 Lua 协程模型。
C++ 层的完整语义(管线机制、路由匹配、body handler 等)见 mchttp.md。本文档聚焦 Lua 侧的用法差异和 API 清单。
核心概念
Lua 侧的差异
| 特性 | 说明 |
|---|---|
| table 自动转换 | request/response 可直接用 Lua table 构造,字段自动转换 |
| handler 返回值灵活 | handler/middleware 可返回立即 response table/userdata,也可返回 mc.future |
| yieldable 自动切换 | send 在协程内自动 yield,在协程外阻塞(期间驱动消息循环) |
| body 统一为 io_buffer | 请求体、响应体、stream chunk 均为 mc.http.io_buffer userdata |
| executor 自动继承 | server/client/session 自动绑定当前 VM 的 executor,Lua 无需手动配 |
响应 Content-Type 与 body 格式
框架不会自动推断 body 格式,也不会自动设 Content-Type。 response 的 body 是原始字节,框架原样发送。
| 需求 | Lua 做法 |
|---|---|
| 返回 JSON | rsp:set_json(mc.dict{...}) —— 自动序列化并设 Content-Type: application/json |
| 返回纯文本 | rsp.headers['content-type'] = 'text/plain' + rsp.body = 'hello' |
| 返回已有 JSON 字符串 | 手动设 headers['content-type'] = 'application/json' + body = json_string |
| 无 body | 只设 status,不设 body |
注意:response table 里的 body 字段只接受 string 或 io_buffer——传 Lua table 会报错(不会自动 JSON 序列化)。要发 JSON 用 :set_json() 或先 mc.json.encode()。
服务端管线
与 C++ 一致的五角色管线:router 匹配 → middleware 链 → body handler → terminal handler → 响应。概念说明见 C++ 文档·核心概念。
快速上手
最小服务端
local http = require('mc.http')
local r = http.router()
r:route('GET', '/ping', function(req)
-- 一步构造成功 JSON 响应(自动设 Content-Type: application/json)
return http.ok_json(mc.dict{ ok = true })
end)
local srv = http.server{ port = 0 } -- port=0 系统分配
srv:on_accept(function(sess)
sess:start(r)
end)
srv:start()
local port = srv:local_port()
客户端请求
local http = require('mc.http')
local client = http.client{ base_url = 'http://127.0.0.1:' .. port }
-- 同步(协程外阻塞,协程内 yield)
local rsp = client:send{ method = 'GET', target = '/ping' }
print(rsp.status) -- 200
print(rsp:as_json().ok) -- true
-- 同步(send_sync,带超时,不 yield)
local rsp2 = client:send_sync{ method = 'GET', target = '/ping', timeout = 3000 }
-- 异步(返回 mc.future)
client:send_async{ method = 'GET', target = '/ping' }:await()
:next(function(r) print(r.status) end)
:await()
API 详细说明
加载模块:local http = require('mc.http')
模块函数
| 函数 | 说明 |
|---|---|
http.server(opts) |
创建 server 对象;opts 见 server_options |
http.client(opts) |
创建 client 对象;opts 见 client_options |
http.router(opts) |
创建 router 对象;opts 可含 case_insensitive=true |
http.request(table) |
构造 request userdata(也可直接传 table 给 handler 返回) |
http.response(table) |
构造 response userdata |
http.multipart{ fields = {...} } |
构造 multipart(客户端上传);mp:apply_to(req) 编码后 client:send(req) |
http.file_source(path[, chunk_bytes]) |
创建文件流式响应源 |
http.callback_source(cb) |
创建动态流式响应源;cb 每次返回 string/io_buffer/nil 或 mc.future |
http.raw_body(opts) |
创建 raw body handler |
http.json_body(opts) |
创建 JSON body handler |
http.none_body() |
创建 none body handler |
http.multipart_body(opts) |
创建 multipart body handler |
http.parse_cookie(header) |
解析 Cookie 请求头为 table(值支持 DQUOTE 包裹) |
http.serialize_cookie(table) |
生成 Set-Cookie 头值;字段:name/value/path/domain/max_age/same_site/httponly/secure |
http.delete_cookie(name[, path]) |
生成删除 cookie 的 Set-Cookie 值(Max-Age=0) |
http.serve_static(router, url_prefix, dir[, opts]) |
注册静态文件目录路由(Range 自动支持,路径穿越防护);opts: content_type、chunk_bytes |
-- use 实现的会话认证(协议进框架、业务进 use)
r:use('GET', '/me', function(req, next)
local cookies = http.parse_cookie(req.headers['cookie'] or '')
local who = cookies.session and sessions[cookies.session] -- 应用层会话表
if not who then return http.unauthorized() end
local attrs = req.attrs
attrs.identity = who
req.attrs = attrs -- 整体写回
return next(req)
end)
-- 登录时发 cookie:
headers = { ['set-cookie'] = http.serialize_cookie{ name = 'session', value = sid, httponly = true } }
server 对象
| 方法 | 参数 | 说明 |
|---|---|---|
on_accept(cb) |
function(session) |
注册连接回调;cb 里调用 session:start(router) |
start() |
无 | 开始 accept |
stop() |
无 | 停止并关闭全部 session |
local_port() |
无 | 实际监听端口 |
server_options(table 字段)
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
port |
number | 8080 |
监听端口;0 系统分配 |
max_body_bytes |
number | 128 MiB |
请求体上限 |
max_auto_drain_bytes |
number | 1 MiB |
短路排空上限 |
max_target_bytes |
number | 8192 |
request-target 长度上限 |
idle_timeout_ms |
number | 30000 |
keep-alive 空闲超时(毫秒) |
upload_dir |
string | 空 | multipart 文件落盘目录 |
gzip |
bool | false |
响应自动 gzip 压缩(Accept-Encoding 协商;跳过流式/小响应/不可压缩类型) |
gzip_min_bytes |
number | 1024 |
响应体小于此值不压缩 |
gzip_level |
number | -1 |
压缩级别 -1=默认 0-9 |
error_handler |
function | 无 | handler 异常时的响应转换回调:function(ctx),ctx = {status=建议状态码, message=异常文本, error=结构化错误表或 nil},返回 response/响应表;未返回时回落默认错误响应 |
io_executor |
pool | 默认全局池 | 自建 I/O 线程池(mc.runtime.new_pool) |
tls |
table | 空 | {cert_file=, key_file=, verify_peer=, ca_file=} |
http2 |
table | 关闭 | HTTP/2 配置,见下方 http2 子表 |
client 对象
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
send(req) |
request table/userdata | response | 异步请求的同步等待(yieldable:协程内 yield,协程外阻塞) |
send_async(req) |
request table/userdata | mc.future<response> |
异步请求(future 语义,与 C++ client::send 对齐) |
send_sync(req) |
request table/userdata | response | 同步请求(带超时,不 yield) |
connect_ws(url, handler) |
url(ws:///wss://)、function(ws, err) |
— | WebSocket 客户端握手:成功回调 (ws)(与服务端同类型),失败回调 (nil, exception) |
stream(req, cb) |
request、function(line) |
mc.future |
SSE 逐行回调 |
超时优先级:send/send_sync 的 timeout 参数 > req.timeout > client_options 的 timeout。
client_options(table 字段)
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
base_url |
string | 空 | 基础 URL |
timeout |
number | 30000 |
请求超时(毫秒) |
max_body_bytes |
number | 256 MiB |
响应体上限 |
follow_redirects |
bool | false |
3xx 自动跟随(301/302/303 改 GET,307/308 保留) |
max_redirects |
number | 10 |
最大重定向次数(防环) |
reuse_connections |
bool | true |
是否启用连接复用 |
max_idle_connections_per_host |
number | 4 |
每主机最大空闲连接 |
idle_timeout |
number | 30000 |
空闲连接超时(毫秒) |
io_executor |
pool | 默认全局池 | 自建 I/O 线程池 |
tls |
table | 空 | {cert_file=, key_file=, verify_peer=, ca_file=} |
http2 |
table | 关闭 | HTTP/2 配置,见下方 http2 子表 |
http2 子表(server_options / client_options 通用)
与 C++ mc::http::h2_options 一一对应。客户端启用后:https 走 ALPN 协商(对端不支持自动回落 h1),http 明文走 h2c prior-knowledge(不回落);同主机请求复用一条 h2 连接多路复用。SSE stream() 与 WebSocket 不走 h2。
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enabled |
bool | false |
启用 HTTP/2 |
max_concurrent_streams |
number | 64 |
单连接并发流上限 |
initial_window_bytes |
number | 262144 |
流级初始接收窗口 |
max_frame_bytes |
number | 16384 |
单帧上限(16KiB~16MiB-1) |
max_header_list_bytes |
number | 16384 |
解压后头表累计上限(防 HPACK 炸弹) |
hpack_table_bytes |
number | 4096 |
HPACK 动态表容量 |
rst_burst |
number | 100 |
RST_STREAM 令牌桶容量(rapid reset 防护;0 = 不限制,服务端参数) |
rst_rate_per_sec |
number | 20 |
RST_STREAM 令牌恢复速率(每秒,服务端参数) |
local srv = http.server{ port = 0, http2 = { enabled = true } }
local client = http.client{
base_url = 'http://127.0.0.1:' .. srv:local_port(),
http2 = { enabled = true },
}
-- 走 h2 的请求,服务端 handler 中 req.attrs.http_version == '2'
multipart 客户端上传
构造 http.multipart 后调 apply_to(req) 编码(设 method=POST + content-type + body),再用 send/send_sync 发送:
local req = http.request{ target = '/upload' }
local mp = http.multipart{ fields = {
{ name = 'file', filename = 'a.bin', content = file_io_buffer },
{ name = 'note', content = 'hello', filename = '' }, -- filename 空表示普通字段
} }
mp:apply_to(req) -- req 必须为 userdata(http.request 构造)
local rsp = client:send(req) -- 或 client:send_sync(req)
print(rsp.status)
router 对象
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
route(method, pattern, handler[, opts]) |
method、pattern、handler 函数、可选 options table | — | 注册路由(add 是等价别名,与 C++ router::add 对齐) |
remove(method, pattern) |
method、pattern | — | 删除路由(不影响 use 挂载) |
use(method, pattern, middleware) |
method(nil 或 '*' = 所有方法)、pattern、middleware 函数 |
句柄 id | 按模式挂载 middleware(定制层) |
unuse(id) |
句柄 id | — | 移除 use 挂载 |
ws(pattern, handler) |
pattern、function(ws) |
— | 注册 WebSocket 升级路由(method 隐含 GET) |
clear() |
无 | — | 清空全部路由与 use 挂载 |
route 的 handler 函数
r:route('GET', '/path', function(req)
-- 返回 response table 或 userdata
return { status = 200, body = 'ok' }
-- 或返回 mc.future
return mc.resolve({ status = 200, body = 'ok' })
end)
pattern 段语法(route 与 use 共用)
| 段语法 | 匹配 | 捕获 |
|---|---|---|
静态段 Systems |
逐字节精确匹配 | — |
{name} |
任意单个非空段 | req.params.name(string) |
{name:int} |
非负十进制整数(超出 int64 范围视为不命中) | req.params.name(number) |
* |
恰好一个段 | — |
** |
剩余 0 个或更多段(仅允许末尾) | — |
优先级:静态段 > {name:int} > {name} > * > **;同构按注册顺序。{id:int} 与 {id} 共存实现类型分发:/Systems/42 走 int 版、/Systems/bob 走 str 版。同 method+pattern 重复 route 覆盖(捕获键随新 pattern)。
use/unuse 定制层
use 是独立于路由表的按模式 middleware 挂载——对已发布路由加前后处理或短路替换,不改动已注册路由:
-- 认证:宽前缀挂载;写 req.attrs 传下游,已鉴权跳过重复鉴权
r:use('GET', '/redfish/**', function(req, next)
local attrs = req.attrs
if attrs.auth_identity then return next(req) end
if not check_auth(req) then return http.unauthorized() end -- 短路
attrs.auth_identity = 'admin'
req.attrs = attrs -- 整体写回(与 headers 一致)
return next(req)
end)
-- 后处理:await next 后改写响应
r:use('GET', '/redfish/v1/Systems/**', function(req, next)
return next(req):next(function(rsp)
rsp:set_header('x-vendor', 'acme')
return rsp
end)
end)
r:unuse(id) -- 用返回的句柄移除
- use pattern 与 route 同语义:精确匹配 +
**扩展(/redfish只命中自身,/redfish/**覆盖子树);参数段只做匹配约束 - use 链仅在路由命中后执行;404/405 直接短路,不执行 use(认证不会被未注册 URL 消耗)
- 执行顺序:router 级 → use(挂载顺序)→ route 级
useopts → handler - remove 路由不带走 use;use 生命周期只受 unuse 控制
route 的 opts table
| 字段 | 说明 |
|---|---|
middleware |
route 级 middleware 函数数组;每个 function(req, next)(与 router:use 定制层区分) |
body_handler |
body handler 函数 function(req, stream, next) 或 http.json_body() 等工厂返回值 |
router_options(构造时)
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
case_insensitive |
bool | false |
静态路径段大小写不敏感 |
local r = http.router{ case_insensitive = true }
middleware 函数
local function auth_middleware(req, next)
if not req.headers['authorization'] then
return { status = 401, body = 'unauthorized' } -- 短路
end
return next(req):next(function(rsp) -- 包装下游响应
rsp:set_header('x-auth', 'ok')
return rsp
end)
end
r:route('GET', '/secure', handler, { use = { auth_middleware } })
自定义 body handler
local function custom_body(req, stream, next)
local total = 0
local function pull()
return stream:read():next(function(chunk)
if chunk == nil then
return next(req) -- 读完,进入 terminal
end
total = total + chunk:length()
return pull() -- 继续读
end)
end
return pull()
end
r:route('POST', '/upload', handler, { body_handler = custom_body })
websocket 对象
WebSocket 升级路由 r:ws(pattern, handler) 注册;握手成功后 handler 收到 ws 对象:
r:ws('/ws/events', function(ws)
ws:on_message(function(w, msg)
w:send('echo: ' .. msg) -- 文本帧(返回 mc.future)
end)
ws:on_binary(function(w, data) -- 二进制帧(data 为 io_buffer)
w:send_binary(data)
end)
ws:on_close(function()
print('ws closed')
end)
ws:send('connected') -- 主动推送
end)
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
send(text) |
string | mc.future | 发送文本帧(异步;并发排队串行写出) |
send_binary(data) |
io_buffer/string | mc.future | 发送二进制帧 |
close([code[, reason]]) |
关闭码(默认 1000)、原因 | mc.future | 主动关闭 |
on_message(cb) |
function(ws, msg) |
— | 文本消息回调 |
on_binary(cb) |
function(ws, data) |
— | 二进制消息回调 |
on_close(cb) |
function() |
— | 连接关闭回调(仅一次) |
is_open() |
无 | bool | 连接是否打开 |
要点:回调签名带 ws 参数(框架传临时副本,回调内可 send);ping/pong 框架自动响应;ws 路由但非 Upgrade 请求返回 426,未注册路径返回 404。
request 对象
字段(可读可写)
| 字段 | 类型 | 说明 |
|---|---|---|
method |
string | HTTP 方法 |
target |
string | 请求目标(含 query) |
headers |
dict | 请求头(key 小写) |
body |
io_buffer | 请求体 |
params |
dict | 路径参数 |
query |
dict | 查询参数 |
attrs |
dict | 横切数据 + multipart 解析产物:attrs.files(数组,1 基)+ attrs.form(dict)。读为深转 table;写需取副本改后整体赋值 req.attrs = attrs(与 headers 一致)。middleware 可写入自定义键(如 auth_identity)传下游 |
timeout |
number/nil | 请求超时(毫秒);优先级高于 client_options 的 timeout;nil 表示不覆盖 |
方法
| 方法 | 返回值 | 说明 |
|---|---|---|
json() |
variant | 解析 body 为 JSON |
json_object() |
dict | 等价于 json().as_dict() |
attrs.files 数组元素(multipart 落盘文件)
| 字段 | 类型 | 说明 |
|---|---|---|
name |
string | form 字段名 |
filename |
string | 客户端原始文件名 |
path |
string | 落盘路径 |
size |
number | 文件字节数 |
req.attrs.form 是 dict(name → string),存 multipart 非文件字段。
response 对象
字段(可读可写)
| 字段 | 类型 | 说明 |
|---|---|---|
status |
number | HTTP 状态码(默认 200) |
headers |
dict | 响应头;Content-Type 需自己设(框架不自动推断) |
body |
io_buffer/string | 响应体(原始字节;只接受 string 或 io_buffer,传 table 会报错) |
stream |
file_source/callback_source | 设为流式源时 chunked 分块写 |
方法
| 方法 | 参数 | 说明 |
|---|---|---|
set_json(variant) |
mc.variant/dict | 设置 JSON body + application/json |
set_text(text) |
string | 设置 body + text/plain |
set_html(html) |
string | 设置 body + text/html |
set_body(body, type) |
string、string | 通用:设置 body + 指定 Content-Type |
as_json() |
variant | 解析 body 为 JSON |
as_object() |
dict | 等价于 as_json().as_dict() |
set_header(name, value) |
string、string | 设置响应头 |
get_header(name) |
string/nil | 读取响应头 |
remove_header(name) |
string | 删除响应头 |
快速成功工厂(模块级函数)
-- 一步构造成功响应
return http.ok_json(mc.dict{ ok = true }) -- 200 + application/json
return http.ok_text('hello') -- 200 + text/plain
return http.ok('data: hi\n\n', 'text/event-stream') -- 200 + 自定义 Content-Type
快速错误工厂(模块级函数)
handler 里常见的错误返回不需要手拼 status + body,用模块级工厂一步构造:
| 函数 | 说明 |
|---|---|
http.error(status[, msg]) |
通用错误工厂;status 为数字或 http.status.* 常量 |
http.error(status, error_name, args) |
标准错误 + 参数:error_name 查 error_engine 取 format,填 args 表;未注册抛异常 |
http.error(status, error_name, message) |
自定义错误:直接给错误名 + 格式化好的描述文本 |
http.not_found([msg]) |
404 |
http.bad_request([msg]) |
400 |
http.unauthorized([msg]) |
401 |
http.forbidden([msg]) |
403 |
http.method_not_allowed([msg]) |
405 |
http.payload_too_large([msg]) |
413 |
http.internal_server_error([msg]) |
500 |
http.service_unavailable([msg]) |
503 |
r:route('GET', '/users/{id}', function(req)
if not find_user(req.params.id) then
return http.not_found('user not found')
end
-- ...
end)
-- 标准错误 + 参数(error_engine 已注册的格式模板)
mc.errors.register('MyApp.ResourceNotFound', 'The resource {path} was not found')
r:route('GET', '/files/{path}', function(req)
return http.error(http.status.not_found, 'MyApp.ResourceNotFound', { path = req.params.path })
end)
默认错误体为 mc.error 的 encode JSON(含 name/format/params 等结构化字段),不再是 {"error":{"code":...,"message":"..."}} 格式。
status 常量表
避免硬编码状态码数字:
local rsp = http.response{ status = http.status.not_found }
-- http.status.ok = 200, http.status.bad_request = 400, ...
response table 构造
-- 直接用 table 构造(自动转换)
return { status = 200, body = 'hello', headers = { ['content-type'] = 'text/plain' } }
-- content_type 简写(等价于 headers['Content-Type'],写在 headers 之后以简写为准)
return { status = 200, body = '<b>hi</b>', content_type = 'text/html' }
-- 流式响应
return { status = 200, stream = http.file_source('/path/to/file') }
-- 动态流式(SSE)
return {
status = 200,
headers = { ['content-type'] = 'text/event-stream' },
stream = http.callback_source(function()
local chunk = produce_next_chunk() -- 返回 string 或 nil(结束)
return chunk
end),
}
io_buffer 对象
请求体、响应体、stream chunk 统一为 mc.http.io_buffer userdata。
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
length() / #__buf |
无 | number | 全链总字节数 |
data() / tostring(b) |
无 | string | 复制为 Lua string |
read(offset[, len]) |
number、可选 number | string | 读取指定区间的字节副本 |
equals(other) |
io_buffer/string | bool | 字节级比较 |
is_chained() |
无 | bool | 是否为多节点链 |
body handler 工厂选项
http.raw_body(opts)
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
max_bytes |
number | 0(不限) |
body 字节上限 |
http.json_body(opts)
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
max_bytes |
number | 0 |
body 字节上限 |
require_json_content_type |
bool | true |
是否要求 JSON Content-Type |
validate_json |
bool | true |
是否预校验 JSON |
http.multipart_body(opts)
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
upload_dir |
string | 空 | 文件落盘目录 |
max_body_bytes |
number | 0 |
整个 body 上限 |
max_file_bytes |
number | 0 |
单文件上限 |
max_form_field_bytes |
number | 1 MiB |
非文件字段上限 |
max_parts |
number | 128 |
part 数量上限 |
file_write_batch_bytes |
number | 256 KiB |
文件落盘批量写阈值 |
max_preamble_bytes |
number | 64 KiB |
preamble 字节上限 |
max_part_header_bytes |
number | 64 KiB |
单 part header 上限 |
suffix_whitelist |
string 数组 | 空 | 允许的文件后缀 |
on_field |
function(field) |
空 | per-part 策略回调 |
multipart on_field 回调
http.multipart_body{
upload_dir = '/var/tmp/uploads',
suffix_whitelist = { '.bin', '.hpm' },
on_field = function(field)
-- field: { name=, filename=, content_type=, headers=, is_file= }
if field.is_file then
return { action = 'accept', stored_filename = 'stored.bin' }
end
return { action = 'skip' }
-- return mc.resolve{ action = 'accept' } -- 也可返回 future
end,
}
| decision 字段 | 说明 |
|---|---|
action |
'accept'/'skip'/'reject' |
stored_filename |
accept 时自定义落盘文件名 |
status |
reject 时的状态码(默认 400) |
body |
reject 时的响应体 |
request_stream 对象(body handler 内)
自定义 body handler 收到的第二个参数。
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
read() |
无 | mc.future<io_buffer/nil> |
读下一块;nil 表示 EOF |
多 actor session 交接
on_accept 收到的 session 可以跨 actor 传递:
local worker = mc.actor.spawn([[
local http = require('mc.http')
return {
on_connect = function(sess)
local r = http.router()
r:route('GET', '/work', function() return { status = 200 } end)
sess:start(r) -- 在 worker strand 上接管
end,
}
]])
srv:on_accept(function(sess)
worker:send('on_connect', sess) -- session 经 variant 桥接投递到 worker
end)
最佳实践
✅ handler 返回 response 时用 table 构造最简洁;需要复用或修改时用 http.response() 构造 userdata。
✅ send 在协程内会自动 yield,在协程外会阻塞——两种场景都能用,无需区分调用方式;不需要 yield 的同步场景用 send_sync。
✅ 自建 I/O 线程池时,server/client userdata 会自动保留 pool 所有权(无需另存 pool 句柄防 GC)。
✅ middleware 用 next(req):next(function(rsp) ... end) 包装下游响应——这是修改响应头、追加 tracing 的惯用模式。
❌ 不要在 middleware 里调 next 两次——第二次会得到 rejected future。
❌ 不要把 Lua handler 函数注册到 router 后又跨 actor 共享 router——Lua handler 是 VM 私有的。多 actor 场景每个 worker 各建一份 router。
❌ 不要用 #body 假定它是完整 HTTP body 长度——链式 body 的 #__buf 是全链总长,用 data() 取完整内容。