用 C 写成的现代 Web 应用
Corey Prophitt 上线了一款完全用 C 写、通过 CGI 提供服务的商业 Web 产品,数据库用 SQLite,整个应用就是一个二进制文件。以下是 BCHS 技术栈替他省下的部分,以及他不得不自己动手写的部分。
中文
复制
1994 年,想做一个动态网站,你得写一个 C 程序,丢进 cgi-bin 目录,让 Apache 在每个请求上 fork 一次。三十二年后,我用同样的技术上线了一款商业 Web 产品。
从外面看,marketyour.games 很现代。它是一个渐进增强的 Web 产品,有用户账户、面向内容创作者的搜索面板、用来整理内容的文件夹,支付走 Stripe,事务邮件走 Postmark。
但里面一点也不现代。以下是我对在 2026 年用纯 C 写一个真实产品、并用 CGI 提供服务的一些想法。
基础
CGI 是 Web 服务器和你的应用之间的一个接口。没有框架或库需要依赖。但你确实需要一套扎实的基础来往上搭。
我喜欢的基础叫 BCHS 栈,也就是 OpenBSD、C、httpd 和 SQLite。
没有框架、模板语言、数据库服务器,也没有容器。整个工作流很直接:
- 你写一个 C 程序,读取 CGI 环境变量。
- 你把响应直接写到 stdout,把日志消息写到 stderr。
- 需要数据库连接,就用 SQLite 的 C API 直接打开数据库文件。
- 你构建一个静态链接的二进制文件,丢进 cgi-bin 目录。
由于每个请求都会启动一个新进程,部署新版本就跟替换旧文件一样简单。不用重启服务器,也没有停机。新请求自然会用上新的二进制文件。
OpenBSD 自带合理的默认配置,还给了你 pledge 和 unveil 这类工具,让你能真正把一个 C 程序锁住。httpd 默认从 chroot 里提供服务,所以你的 CGI 二进制文件运行在沙箱中,被限制在文件系统属于它自己的那个角落里。我还依赖另外三个库:
- libcurl,用于向 Stripe 和 Postmark 发起对外 Web 请求。
- libsodium,用于密码哈希、cookie 加密和 HMAC 校验。
- libcjson,用于解析 Stripe 和 Postmark 返回的 JSON。
真正的构建依赖只有 libcurl 和 libsodium。
libcjson 和 SQLite 都以 vendor 形式随项目一起构建。之所以能这么做,是因为 libcjson 只有一个 C 文件,没有任何外部依赖;SQLite 则专门为此提供了官方的 amalgamation(单文件源码)。
为了简化部署,所有资源在构建时都被打包进二进制文件。最终得到的构建几乎没有可变部件。一次干净构建只需几秒,完整重建不到半秒。
优点
基础,而非框架。
CGI 不是又一个要学的框架。1.1 规范于 2004 年发布,此后从未改动。没有需要记的 API。整个接口就是几个基本的 HTTP 概念:
- 请求头。
- 请求方法。
- 查询字符串。
- 请求体数据。
懂 HTTP,就懂 CGI。它与语言和平台无关。学一次,受用终身。
关注点分离清晰。
现代 Web 应用通常在前面放一个 nginx 之类的 Web 服务器,再反向代理到用 Ruby、Python、Go 或 Rust 写的应用服务器。我一直觉得这有点奇怪:实际上你有两个 Web 服务器,其中一个还兼管业务逻辑。CGI 直接把第二个服务器从图里去掉。你有一个 nginx(或 httpd)这样的 Web 服务器,由它直接调用你的业务逻辑。谁负责什么,一目了然。
调试也因此更简单。你的应用就是一个普通程序,可以直接在 shell 里运行:把请求管道给它,观察 stderr,用 gdb 单步调试,等等。CGI 很像 serverless 架构:一个 supervisor 收到请求,调用你的代码,递给它一袋环境变量,然后收集结果。就像现代的 serverless 应用,只是没有厂商(也没有意外账单)。
供应链风险极低。
随着自主 agent 的兴起和针对软件包仓库的每周攻击,依赖极少这件事让人安心。
大多数现代技术栈都自带包管理器和配套生态。方便是方便,但依赖图会膨胀得离谱。一两个直接依赖就能拖进来几百个间接依赖,每一个的维护者你都没听说过,发布凭证还靠一个 2015 年就在用的密码保护着。
我的供应链是一个 C 编译器和三个库。想攻破它,得先攻破 libcurl……那是整个互联网都盯着的项目,而不是某个每周下载十一次、维护者 2019 年就跑路的包。
工具简单。
我骨子里是个极简主义者。我用的是零插件的经典 Vim。也就是说没有 LSP,没有自动补全,没有代码格式化工具,也没有 AI。
复杂的技术栈需要复杂的工具:需要语言服务器来穿过一层层抽象。但这套栈足够简单,一个普通编辑器就够了。整个应用就是一个装满 C 文件的目录加一个 Makefile。
打开文件,读一遍,你就知道它在干什么。
好工具近在咫尺。
真需要工具的时候,它们并不远。Sanitizers、valgrind、gdb、ctags、cscope……这些都是成熟(好吧,是上了年纪)的工具,经过几十年打磨,在任何能编译 C 的系统上装个包就能用。
成熟的不仅是工具本身。集成已经原生内建在许多编辑器里。比如 vim 自带对 ctags、cscope 的支持,还有一个图形化的 gdb 前端(见 https://marketyour.games)。
用 C 写 HTML 没那么糟。
我一开始不太愿意把整个前端都用 C 写,但实际做下来远没有想象中那么痛苦。
我太多次:
- 跟 vim 较劲,就为了让 Go 的 html/template 语法高亮。
- 跟 helix 较劲,想让它识别 Go 模板又不破坏 HTML 检测。
- 跟 rustrover 较劲,想让它认出一个用 askama 的项目里的 jinja 模板。
用 C 就意味着编辑器不需要去半吊子地识别第二种语言,没有混合语法导致高亮崩掉,也不用折腾插件让两种语言配合。不好看,但能干活。
SQLite 是一股清流。
做 Web 应用,我一向默认选 Postgres。它是个好数据库,但带着运维负担:要照看一个服务器进程,还要频繁升级新版本。SQLite 这些都没有。没有进程,没有配置,没有依赖。应用直接打开数据库文件。运维负担几乎为零。
不好的地方
CGI 只是一个接口,所以开箱能用的东西很少。结果就是,下面这些事你都得自己写工具来处理:
- Cookie 的解析与生成。
- 查询参数的解析与校验。
- 表单数据的解析与校验。
- 输出到页面时对输入做转义。
我在一个头文件(cgi.h)里自己撸了一套,但我不确定这是不是正确的做法。kcgi 这类库是现成的,可一旦采用,就意味着要接受它的 API 和安全模型。我希望能有一组共享工具,开发者拿来就能用(就像 stb 系列库那样)。
最后一点想法
CGI 很老,但没被遗忘。
我们在 2000 年代初放弃它的那些理由,放到今天大多站不住脚。进程 fork 早就不像当年那么贵了,现代内核在这方面做了大量优化。
就算 fork 的开销真成了问题,升级到 FastCGI 也足够简单。
用 C 写现代 Web 应用完全可行——marketyour.games 就是一个例子。整个开发体验相当愉快,那种感觉是现代技术栈给不了的。我还会再用这套组合。