每个请求都会生成一个全新的进程。部署新版本只需替换一个文件,这意味着无需重启服务器,也没有停机时间。新的请求会直接使用新的二进制文件。
OpenBSD 自带合理的默认配置,并提供了 pledge 和 unveil 等真正能加固 C 程序安全的工具。Httpd 默认在 chroot 环境中运行,因此你的 CGI 二进制程序被沙箱隔离在文件系统中属于自己的角落里。
libcurl 处理所有发往 Stripe 和 Postmark 的出站请求。
libsodium 负责随机标识符生成、密码哈希以及 Stripe webhook 的 HMAC 验证。
libcjson 负责 Stripe 和 Postmark 的 JSON 解析。
其中只有两个是真正的构建依赖:libcurl 和 libsodium。
- libcjson 是单个 C 文件,没有外部依赖。- SQLite 提供官方的 amalgamation(单 C 文件)构建版本。
两者都以 vendored(内嵌源码)的方式作为项目的一部分进行构建。
最终得到的构建几乎没有可动的部件。全新构建只需几秒钟,增量重建在半秒内即可完成。
我没有采用传统方式提供静态资源,而是把 httpd 的所有流量都路由到 CGI 二进制程序。每个资源都被编译进可执行文件,直接从内存中提供。
CGI 不是一套需要学习的框架。1.1 规范就是……没有需要内化的 API。整个接口只是一些基础的 HTTP 概念:
- 请求头。- 请求方法。- 查询字符串。- 请求体数据。
如果你懂 HTTP,你就已经懂 CGI 了。
它与语言和平台无关。学一次,受用终身。
一个现代 Web 应用通常会在前面放一个 nginx 之类的 Web 服务器,然后反向代理到用 Ruby、Python、Go 或 Rust 等其他语言编写的应用服务器。这一直让我觉得有点奇怪:你实际上有两个 Web 服务器,而其中一个还要负责你的业务逻辑。
CGI 让你只拥有一个直接调用业务逻辑的 Web 服务器。谁做什么一目了然。
它也让调试更简单。你的应用就是一个普通程序,可以在 shell 里运行:用管道传入一个请求,观察 stderr,或者用 gdb 单步调试。
从某种意义上说,CGI 类似于 serverless 架构:一个管理程序按请求调用你的代码,交给它一堆环境变量,然后收集结果。CGI 就是没有厂商(以及意外账单)的 serverless。
随着自主 agent 的兴起和软件包仓库每周都在遭受攻击,依赖极少让人感到安心。
大多数现代技术栈都配有包管理器和与之配套的生态系统。这很方便,但依赖图会急剧膨胀。一两个直接依赖可能引入数百个间接依赖,每个都由你从未听说过的人维护,其发布凭证靠一个从 2015 年就开始重复使用的密码保护。
我的供应链就是一个 C 编译器和三个库。要攻破它就得攻破 libcurl……一个被整个互联网密切关注的项目,而不是一个每周只有 11 次下载、维护者 2019 年就已离开的软件包。
我骨子里是个极简主义者。我用零插件的经典 Vim。这意味着没有 LSP,没有自动补全,没有代码格式化工具,也没有 AI。
复杂的技术栈需要复杂的工具:它们需要语言服务器来穿透层层抽象。但这个技术栈足够简单,一个普通编辑器就足够了。整个应用就是一个装满 C 文件的目录加上一个 Makefile。
打开一个文件,读一读,你就知道它是干什么的。
当你确实需要工具时,它们也近在咫尺。Sanitizers、valgrind、gdb、ctags、cscope……这些都是成熟(好吧,可以说是“高龄”)的工具,经过数十年的打磨,在任何能编译 C 的系统上安装一个软件包就能使用。
成熟度不止于工具本身:许多编辑器都内置了集成。Vim 原生支持 ctags 和 cscope,还带有图形化的 gdb 前端(参见 :h Termdebug)。
我曾犹豫是否要用 C 编写整个前端,但实际远没有我想象的那么痛苦。
- 折腾 vim 让它高亮 Go 的 html/template 语法。
- 折腾 helix 让它识别 Go 模板而不破坏 html 检测。
- 折腾 rustrover 让它识别使用 askama 的项目中的 jinja 模板。
使用 C 意味着编辑器不需要半吊子地识别第二种语言,没有高亮失灵的混合语法,也不需要为了让两种语言配合而折腾一堆插件。
直接写 C 让人耳目一新。它并不漂亮,但 Tailwind 也不漂亮。