ESC
科技 1 分钟阅读

用 C 语言构建现代 Web 应用

一位开发者分享用纯 C 语言配合 OpenBSD httpd 与 CGI 构建现代 Web 应用的实践:请求各自独立进程、部署无停机,依赖极简(仅 libcurl 与 libsodium),静态资源编译进可执行文件由内存提供。文章还探讨了供应链安全、极简主义开发以及 CGI 与 serverless 架构的相似性。

来源:Hacker News

每个请求都会生成一个全新的进程。部署新版本只需替换一个文件,这意味着无需重启服务器,也没有停机时间。新的请求会直接使用新的二进制文件。

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 也不漂亮。