Skip to content

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 客户端。

函数一览 ​

拼音英文签名说明
jiantinglisten(host zifuchuan, port z64, opt 选项 = {}) 服务 kecuo开始监听
jieshouaccept(s &服务) 请求 kecuo等待并读入一个完整请求
huiyingrespond(r &请求, status z64, headers [zifuchuan], body zifuchuan) zhenjia kecuo回文本响应
huiying_zijierespond_bytes(r &请求, status z64, headers [zifuchuan], body [w8]) zhenjia kecuo回字节响应
qingqiu_touburequest_header(r &请求, name zifuchuan) kexuan(zifuchuan)查请求头
chaxun_canshuquery_param(r &请求, name zifuchuan) kexuan(zifuchuan)查询参数
guanbi_fuwuclose_server(s &服务) zhenjia kecuo关闭监听

jianting 监听 ​

  • host 传 "127.0.0.1" 只有本机能访问;传空字符串 "" 监听所有地址,其他机器也能访问
  • 端口被占用、端口越界、没有权限时失败

两个选项用来保护服务端自己:

字段默认值说明
max_body_bytes10485760(10 MB)请求体上限,超过的请求自动回 413
read_timeout_ms10000(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 的请求
  • 路由表、中间件:分支请在主循环里自己写

cpro 目前处于测试阶段