px普贤教程PuXian
普贤教程 › 进阶教程

写一个 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}))

规则就三条:

  1. 第一行必须是 Status: <码> <原因>;
  2. 之后每行一个响应头,键值与冒号;
  3. 一个空行之后是响应体。

环境变量

变量内容
REQUEST_METHODGET / POST …
QUERY_STRING问号后面的原始串(未解码)
HTTP_COOKIECookie 头原文
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 命名为 envdef 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.px
text
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 之后会在编译期直接拒绝并打印是哪个文件哪一行。公共库复制进本目录,用同目录相对路径引用。

性能预期

场景量级
编译产物 + 简单 JSON10–50 ms
编译产物 + 一次 MySQL 查询30–80 ms
解释轨 + 连接数据库400–1500 ms
编译产物 + 同步发一封信2–3 秒(要用队列异步化)
慢请求要异步化耗时以秒计的操作(发邮件、调第三方、生成大文件)不要放在请求里等结果:先落库、立刻返回,交给常驻进程处理。