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 级 use opts → 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() 取完整内容。