普贤教程 › 进阶教程
写一个 Web 接口
PuXian 写 Web 接口有两种方式:自己起一个监听进程,或把 .px 文件交给 Ma(网关)托管。生产上主流是后者——Ma 负责 TLS、限流、静态文件,px 只写业务。
模型:一次请求 = 一次进程调用
Ma 的 px_routes 把某个路径绑定到一个 .px 文件。请求进来时,Ma 把请求信息放进环境变量、把请求体写进标准输入,然后执行脚本;脚本往标准输出写「状态行 + 头 + 空行 + 体」,Ma 原样转给客户端。
text
客户端 → Ma(TLS / 限流 / 路由)→ 子进程 app.px
读 env + /dev/stdin
写 /dev/stdout一个最小接口
px
# hello.px def main(): var name = "世界" var qs = env("QUERY_STRING") if qs != null and len(qs) > 0: name = qs print("Status: 200 OK") print("Content-Type: application/json; charset=utf-8") print("") print(json_stringify({"ok": true, "hello": name}))
规则就三条:
- 第一行必须是
Status: <码> <原因>; - 之后每行一个响应头,键值与冒号;
- 一个空行之后是响应体。
环境变量
| 变量 | 内容 |
|---|---|
REQUEST_METHOD | GET / POST … |
QUERY_STRING | 问号后面的原始串(未解码) |
HTTP_COOKIE | Cookie 头原文 |
HTTP_ORIGIN / HTTP_HOST / HTTP_X_FORWARDED_* | 对应请求头:大写、横线变下划线、前缀 HTTP_ |
REMOTE_ADDR | 对端地址(脚本拿不到 socket,由 Ma 注入) |
读环境变量的内建函数是 env(k):变量不存在时返回 null,所以先判空再转字符串。包一层带默认值的helper:
px
def get_env(k, dflt = ""): var v = env(k) if v == null: return dflt return str(v)
别把 helper 命名为 env
def env(...) 会遮蔽内建 env,函数体里再调 env(k) 就成了调用自己——无限递归,直接段错误(Segmentation fault (core dumped)),而且报错信息里没有任何提示。给自定义函数避开内建名:get_env、read_env 都行。读请求体
px
def read_body(): var r = read_file_opt("/dev/stdin") if r.is_err(): return "" return r.ok()
不要用 read_file 读请求体它遇到 NUL 字节会截断。上传文件、gzip 体、任何二进制都可能含 NUL,一律用
read_file_opt(或底层 open + read)。返回 JSON
px
def reason(st): if st == 200: return "OK" if st == 400: return "Bad Request" if st == 401: return "Unauthorized" if st == 404: return "Not Found" if st == 405: return "Method Not Allowed" if st == 429: return "Too Many Requests" if st == 500: return "Internal Server Error" return "OK" def json_out(status, obj): print("Status: " + str(status) + " " + reason(status)) print("Content-Type: application/json; charset=utf-8") print("") print(json_stringify(obj))
不要用 print 打日志在 px_routes 里
print 写的是标准输出——也就是响应体本身。处理中间打一行日志,客户端收到的就是 [send-code] 已发送…\nStatus: 200 OK… 这种坏响应(线上真实踩过)。日志走 stderr:px
def log(msg): var f = open("/dev/stderr", "w") if f == null: return write(f, msg + "\n") close(f)
一个带校验的完整例子
px
def get_env(k, dflt = ""): var v = env(k) if v == null: return dflt return str(v) def reason(st): if st == 200: return "OK" if st == 400: return "Bad Request" if st == 405: return "Method Not Allowed" return "OK" def json_out(status, obj): print("Status: " + str(status) + " " + reason(status)) print("Content-Type: application/json; charset=utf-8") print("") print(json_stringify(obj)) def main(): if get_env("REQUEST_METHOD") != "POST": json_out(405, {"ok": false, "error": "只接受 POST"}) return var raw = "" var r = read_file_opt("/dev/stdin") if r.is_ok(): raw = r.ok() var parsed = json_parse_opt(raw) if parsed.is_err(): json_out(400, {"ok": false, "error": "请求体不是合法 JSON"}) return var body = parsed.ok() if not body.has("email"): json_out(400, {"ok": false, "error": "缺少 email"}) return var email = trim(body["email"]) if not contains(email, "@"): json_out(400, {"ok": false, "error": "邮箱格式不正确"}) return json_out(200, {"ok": true, "email": email})
本地跑一遍(不装 Ma,用环境变量 + 管道模拟):
sh
printf '%s' '{"email":"a@b.com"}' | REQUEST_METHOD=POST px run app.pxtext
Status: 200 OK Content-Type: application/json; charset=utf-8 {"ok":true,"email":"a@b.com"}
换成空体或错误方法,会如实返回 400 / 405:
sh
printf '' | REQUEST_METHOD=GET px run app.px
text
Status: 405 Method Not Allowed Content-Type: application/json; charset=utf-8 {"ok":false,"error":"只接受 POST"}
用 cat -A 看输出它能显示
\r、行尾 $ 等不可见字符。响应格式错一个字节,Ma 就会把整段当坏响应——这是排查最快的手段。接入 Ma
在站点配置里加 px_routes,把路径映射到脚本,设好模式与超时:
json
"px_routes": [ {"path": "/api/hello", "exact": true, "script": "hello.px", "methods": ["GET"], "mode": "fast", "timeout_ms": 5000} ]
| mode | 行为 |
|---|---|
auto | 按耗时自适应;首次请求走解释轨(慢) |
fast | 首次请求即编译并缓存,之后直跑产物 |
compiled | 只用编译产物 |
脚本目录必须自包含Ma 编译时只把「脚本所在目录」打包进沙箱。跨目录
import "../lib/x.px" 会失败:旧版本静默跳过、运行期报未定义变量(每次请求回落解释轨,1 秒多一次);0.22.4 之后会在编译期直接拒绝并打印是哪个文件哪一行。公共库复制进本目录,用同目录相对路径引用。性能预期
| 场景 | 量级 |
|---|---|
| 编译产物 + 简单 JSON | 10–50 ms |
| 编译产物 + 一次 MySQL 查询 | 30–80 ms |
| 解释轨 + 连接数据库 | 400–1500 ms |
| 编译产物 + 同步发一封信 | 2–3 秒(要用队列异步化) |
慢请求要异步化耗时以秒计的操作(发邮件、调第三方、生成大文件)不要放在请求里等结果:先落库、立刻返回,交给常驻进程处理。