HTTP 服务端(http)
单线程、阻塞式的 HTTP 服务端:监听端口,一次接收一个请求,写回响应。适合写内部工具和本机调试用的接口。
cpro
daoru "http"
rukou() {
changshi {
bl srv = http.jianting("127.0.0.1", 8080)
dayin("监听 127.0.0.1:8080")
dang (zheng) {
bl req = http.jieshou(&srv)
ruguo (req.path ==[zifuchuan] "/hello") {
http.huiying(&req, 200, ["Content-Type: text/plain; charset=utf-8"], "你好")
} fouze {
http.huiying(&req, 404, [], "not found")
}
}
} buhuo (e) {
dayin(e.xinxi)
}
}cpro
import "http"
rukou() {
try {
var srv = http.listen("127.0.0.1", 8080)
print("监听 127.0.0.1:8080")
while (true) {
var req = http.accept(&srv)
if (req.path ==[string] "/hello") {
http.respond(&req, 200, ["Content-Type: text/plain; charset=utf-8"], "你好")
} else {
http.respond(&req, 404, [], "not found")
}
}
} catch (e) {
print(e.msg)
}
}用 curl http://127.0.0.1:8080/hello 访问,会收到 你好。
写法就是一个循环:jieshou 等来一个请求,按 req.method、req.path 判断该做什么,再用 huiying 回一个响应。
说明
服务端和客户端同属 http 模块,所以只用服务端也要装 libcurl,见 HTTP 客户端。
函数一览
| 拼音 | 英文 | 签名 | 说明 |
|---|---|---|---|
jianting | listen | (host zifuchuan, port z64, opt 选项 = {}) 服务 kecuo | 开始监听 |
jieshou | accept | (s &服务) 请求 kecuo | 等待并读入一个完整请求 |
huiying | respond | (r &请求, status z64, headers [zifuchuan], body zifuchuan) zhenjia kecuo | 回文本响应 |
huiying_zijie | respond_bytes | (r &请求, status z64, headers [zifuchuan], body [w8]) zhenjia kecuo | 回字节响应 |
qingqiu_toubu | request_header | (r &请求, name zifuchuan) kexuan(zifuchuan) | 查请求头 |
chaxun_canshu | query_param | (r &请求, name zifuchuan) kexuan(zifuchuan) | 查询参数 |
guanbi_fuwu | close_server | (s &服务) zhenjia kecuo | 关闭监听 |
jianting 监听
- host 传
"127.0.0.1"只有本机能访问;传空字符串""监听所有地址,其他机器也能访问 - 端口被占用、端口越界、没有权限时失败
两个选项用来保护服务端自己:
| 字段 | 默认值 | 说明 |
|---|---|---|
max_body_bytes | 10485760(10 MB) | 请求体上限,超过的请求自动回 413 |
read_timeout_ms | 10000(10 秒) | 读一个请求的时间上限,超时自动断开 |
cpro
bl srv = http.jianting("", 8080, { max_body_bytes: 1048576 })cpro
var srv = http.listen("", 8080, { max_body_bytes: 1048576 })传 0 表示不限制,只建议本机调试时用。服务端是单线程的,一个只连接不发数据的客户端会让其他人一直排队,读超时就是防这个的。
jieshou 接收请求
等到一个完整的请求读进来才返回。格式不对、请求头太大、请求体超限这类客户端的问题,服务端会自动回 4xx / 5xx 并继续等下一个,不会交到你手上。
所以 jieshou 失败只有一种情况:监听本身出了问题,这时通常应该退出循环。
请求
字段可以直接读,字段名只有英文:
| 字段 | 说明 |
|---|---|
req.method | "GET"、"POST" 等,大写 |
req.path | "/users/42",已解码,不含问号后面的部分 |
req.query | 问号后面的原文,如 "page=2&size=10",没有时为 "" |
req.headers | [zifuchuan],每个元素一行 "Name: value" |
req.body | [w8],没有请求体时为空数组 |
cpro
bl ct = http.qingqiu_toubu(&req, "content-type") // 头名不区分大小写
bl page = http.chaxun_canshu(&req, "page") // 已解码
bl text = zifuchuan.cong_utf8(req.body) // 请求体转文本,可能失败cpro
var ct = http.request_header(&req, "content-type") // 头名不区分大小写
var page = http.query_param(&req, "page") // 已解码
var text = string.from_utf8(req.body) // 请求体转文本,可能失败qingqiu_toubu、chaxun_canshu 找不到时返回 kong。
请求里持有这次的连接,所以它是 nocopy 结构体:不能复制、不能放进数组,类型名也用不了。处理逻辑请写成接收字段、返回结果的函数,huiying 留在主循环里调用:
cpro
hs handle_login(body [w8]) zifuchuan kecuo {
bl text = sp_kecuo zifuchuan.cong_utf8(body)
// 校验、查库……
fanhui "{\"ok\":true}"
}
// 主循环里
ruguo (req.path ==[zifuchuan] "/login") {
changshi {
bl out = handle_login(req.body)
http.huiying(&req, 200, ["Content-Type: application/json"], out)
} buhuo (e) {
http.huiying(&req, 400, [], e.xinxi)
}
}cpro
fn handle_login(body [u8]) string fails {
var text = up_fails string.from_utf8(body)
// 校验、查库……
return "{\"ok\":true}"
}
// 主循环里
if (req.path ==[string] "/login") {
try {
var out = handle_login(req.body)
http.respond(&req, 200, ["Content-Type: application/json"], out)
} catch (e) {
http.respond(&req, 400, [], e.msg)
}
}huiying 回响应
cpro
http.huiying(&req, 200, ["Content-Type: application/json"], body)
http.huiying_zijie(&req, 200, ["Content-Type: image/png"], png)cpro
http.respond(&req, 200, ["Content-Type: application/json"], body)
http.respond_bytes(&req, 200, ["Content-Type: image/png"], png)- 状态行、
Content-Length、Connection: close会自动加上,不要自己写 - 状态码只能是 200 到 599
- 响应头里不能含换行符
- 回完之后连接立即关闭,不支持 keep-alive
每个请求必须回一次:
- 回两次:第二次报错,不会发出任何内容
- 忘了回:请求离开作用域时自动回
500,免得客户端一直等。这是兜底,buhuo里出错时也请记得回响应
客户端在你处理期间断开时,huiying 会报错(错误码 4 或 5),一般忽略继续即可。
完整示例
cpro
daoru "http"
hs greeting(name zifuchuan) zifuchuan {
fanhui "{\"greeting\":\"你好, " +[zifuchuan] name +[zifuchuan] "\"}"
}
rukou() {
changshi {
bl srv = http.jianting("127.0.0.1", 8080)
dayin("监听 127.0.0.1:8080")
dang (zheng) {
changshi {
bl req = http.jieshou(&srv)
bl json_head = ["Content-Type: application/json; charset=utf-8"]
ruguo (req.method ==[zifuchuan] "GET" && req.path ==[zifuchuan] "/hello") {
bl name = jiebao(http.chaxun_canshu(&req, "name"), "世界")
http.huiying(&req, 200, json_head, greeting(name))
} fouze ruguo (req.path ==[zifuchuan] "/hello") {
http.huiying(&req, 405, ["Allow: GET"], "")
} fouze {
http.huiying(&req, 404, json_head, "{\"error\":\"not found\"}")
}
} buhuo (e) {
ruguo (http.shi_yi_guanbi(e.daima)) {
jixu // 客户端中途断开,不影响服务
}
dayin("服务出错:", e.xinxi)
tiaochu
}
}
} buhuo (e) {
dayin("启动失败:", e.xinxi) // 通常是端口被占用
}
}cpro
import "http"
fn greeting(name string) string {
return "{\"greeting\":\"你好, " +[string] name +[string] "\"}"
}
rukou() {
try {
var srv = http.listen("127.0.0.1", 8080)
print("监听 127.0.0.1:8080")
while (true) {
try {
var req = http.accept(&srv)
var json_head = ["Content-Type: application/json; charset=utf-8"]
if (req.method ==[string] "GET" && req.path ==[string] "/hello") {
var name = unwrap(http.query_param(&req, "name"), "世界")
http.respond(&req, 200, json_head, greeting(name))
} else if (req.path ==[string] "/hello") {
http.respond(&req, 405, ["Allow: GET"], "")
} else {
http.respond(&req, 404, json_head, "{\"error\":\"not found\"}")
}
} catch (e) {
if (http.is_closed(e.code)) {
continue // 客户端中途断开,不影响服务
}
print("服务出错:", e.msg)
break
}
}
} catch (e) {
print("启动失败:", e.msg) // 通常是端口被占用
}
}curl "http://127.0.0.1:8080/hello?name=cpro" 会收到 {"greeting":"你好, cpro"}。
错误码
沿用 HTTP 客户端 的错误码,服务端用到的有:
| 错误码 | 含义 |
|---|---|
| 4、5 | 回响应时客户端已断开;或同一个请求回了两次 |
| 6 | 端口已被占用(shi_dizhi_zhanyong / is_addr_in_use) |
| 7 | 端口越界、主机名无效 |
| 8 | 没有权限(如监听 1024 以下端口) |
| 11 | 状态码不在 200–599,或响应头含换行 |
暂不支持
- 多线程:一次只处理一个请求,一个慢接口会让后面的请求排队
- keep-alive:每个请求一条连接
- HTTPS:只能监听明文 HTTP,需要 HTTPS 时请在前面放 nginx 等反向代理
- 分块传输的请求体:只支持带
Content-Length的请求 - 路由表、中间件:分支请在主循环里自己写
