Hacker News 中文摘要

RSS订阅

用 C 语言开发现代 Web 应用 -- A Modern Web App in C

1994年,想要动态网站就得写个C语言(C)程序,放进cgi-bin目录让阿帕奇(Apache)按请求派生(fork)它。三十二年后的2026年,我用同样的技术交付了名为营销你的游戏(marketyour.games)的商业网络产品:它有用户账户、搜索仪表盘和文件夹,通过条纹(Stripe)付款、邮戳(Postmark)发事务性邮件,且不需JavaScript。除了制作和提供服务的方式,一切都很现代,而它由C语言编写、通过CGI服务。

正文

简介

如果你想在 1994 年做一个动态网站,你会写一个 C 程序,把它放进 cgi-bin 目录,让 Apache 按每次请求 fork 它。32 年后,我交付了一个商业 Web 产品,用的本质上还是同一套技术。

marketyour.games 的一切都很现代。它是一个有用户账户的 Web 产品,为内容创作者提供搜索仪表盘、用来整理这些创作者的文件夹,通过 Stripe 收款,并通过 Postmark 发送事务性邮件。它遵循渐进增强,且不需要任何 JavaScript。

除了它的构建方式和提供服务的方式,一切都是现代的。下面是我在 2026 年用 C 写真实产品并通过 CGI 提供服务的一点想法。

太长不看

CGI 在 2004 年的衰落是有道理的,但到了 2026 年,那些理由已经站不住脚了。

你可以用 C 写出现代 Web 应用。我就交付了一个。一个二进制文件、一个 SQLite 文件、三个依赖,整个东西都能装进你的脑子里。

它是如何实现的

技术栈是 BCHS:OpenBSD、C、httpd 和 SQLite。

没有框架。你写一个 C 程序,读取 CGI 环境变量并作出响应。

没有模板语言。你写的 C 直接输出 HTML。

没有独立的数据库服务器。你通过 SQLite 的 C API 打开数据库文件。

没有容器。你构建出一个静态链接的二进制文件,把它丢进一个文件夹。

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

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 规范发布于 2004 年,此后就没变过。

没有需要内化的 API。整个接口就是几个基础的 HTTP 概念:

  • 请求头。
  • 请求方法。
  • 查询字符串。
  • 请求体数据。

如果你懂 HTTP,你就已经懂 CGI 了。

它不绑定语言和平台。你学一次,这些知识就永远有效。

关注点分离清晰

现代 Web 应用通常会把 nginx 这类 Web 服务器放在前面,反向代理到一个用 Ruby、Python、Go 或 Rust 写的应用服务器。我一直觉得这有点奇怪:你本质上有了两个 Web 服务器,而其中一个还要负责你的业务逻辑。

CGI 只给你一个 Web 服务器,直接调用你的业务逻辑。谁做什么很清楚。

调试也更简单。你的应用就是一个普通的程序,可以从 shell 里运行:把请求传给它,看 stderr,用 gdb 单步调试。

从某种意义上说,CGI 类似于 serverless 架构:一个 supervisor 按请求调用你的代码,交给它一袋环境变量,然后收集结果。CGI 就是 serverless,只是没有云厂商(也没有意外账单)。

极小的供应链风险

随着自主代理的兴起,以及针对软件仓库的攻击每周都在发生,依赖很少让人感到安心。

大多数现代技术栈都带一个包管理器和相应生态。这很方便,但依赖图会膨胀。一两个直接依赖可能拉入几百个间接依赖,每个都由你从没听说过的人维护,发布凭证靠一个从 2015 年就开始复用的密码保护着。

我的供应链就是一个 C 编译器和三个库。要攻破它,就得攻破 libcurl……一个整个互联网都在盯着的项目,而不是一个每周下载十一次、所有者 2019 年就离开了的包。

工具简单

我骨子里是个极简主义者。我用零插件的经典 Vim。也就是说,没有 LSP、没有自动补全、没有代码格式化工具,也没有 AI。

复杂的技术栈需要复杂的工具:它们需要一个语言服务器来穿越层层抽象。但这个栈足够简单,一个普通编辑器就够了。整个应用就是一个目录的 C 文件和一个 Makefile。

打开一个文件,读它,你就知道它在做什么。

优秀的工具触手可及

当你确实需要工具时,它们也不远。Sanitizers、valgrind、gdb、ctags、cscope……这些都是成熟(好吧,是老牌)的工具,经过几十年的打磨,在任何能编译 C 的系统上都能通过包安装获得。

这种成熟度不止于工具本身:集成已经内建在许多编辑器里。Vim 原生支持 ctags、cscope,还带一个图形化 gdb 前端(见 :h Termdebug)。

用 C 写 HTML 和 CSS 没那么糟

我本来对用 C 写整个前端犹豫不决,但它并没有我想象的那么痛苦。

我有太多次:

  • 为了高亮 Go 的 html/template 语法而跟 vim 较劲。
  • 为了让 helix 检测 Go 模板而不破坏 html 检测而跟它较劲。
  • 为了一个用 askama 的项目,跟 rustrover 较劲,让它识别 jinja 模板。

用 C 意味着没有第二种语言让你的编辑器半检测,没有混合语法导致高亮崩溃,也不需要为了让两种语言协作而跳插件舞。

能只写 C 让人耳目一新。它算不上好看,但 Tailwind 也不好看。

SQLite 令人耳目一新

我一向默认给 Web 应用用 Postgres。它是个很棒的数据库,但带来一些运维负担:有一个服务器进程要照看,还要频繁升级到新版本。

SQLite 没有这些。没有进程,没有配置,没有依赖。你的应用直接打开数据库文件。运维负担极小。

我想改进的地方

CGI 是 Web 服务器和你的应用之间的接口。没有框架或库。这意味着开箱可用的东西非常少。

你需要自己写工具来处理这些事:

  • Cookie 的解析和生成。
  • 查询参数的解析与校验。
  • 表单数据的解析与校验。
  • 为显示而转义输入。

我在一个头文件(cgi.h)里自己实现了一套,但我不确定这是对的解决方案。像 kcgi 这样的库是存在的,但采用一个就意味着接受它的 API 和安全模型。

我希望看到一组开发者可以直接丢进去的共享工具(就像 stb 库那样)。

也许我会把 cgi.h 发布出来,作为开始。

结论

CGI 很老,但没有被遗忘。

我们在 2000 年代初抛弃它的大多数理由,今天都不再成立。进程 fork 不再像当年那么昂贵;现代内核对它做了大量优化。

而且如果 fork 的开销真成了问题,FastCGI 也是一个足够简单的升级方案。

用 C 写现代 Web 应用完全可行——marketyour.games 就是一个商业产品,跑在一个二进制文件上,使用的服务模型早在 2004 年就正式标准化了。

它的活动部件比我以前交付过的任何东西都少。我以后还会再选这个技术栈。

评论

讨论气氛以调侃和质疑开场,并很快分化为两条主轴:不少评论吐槽落地页与博客正文几乎看不清、像是 AI 生成的垃圾,并质疑用 C 手搓 Web 与 CGI 每请求新建进程是否值得;另一边则有较多开发者认可无框架、单文件、SQLite 直连的极简部署,并补充 PHP、Go、C++ 或自写服务器同样能达到类似效果,分歧集中在“CGI 与手写 C 到底是返璞归真还是低效复古”。

吐槽页面排版刺眼且怀疑博客由 AI 生成

  • 多位读者抱怨页面几乎只有标题能看清,正文低对比度让人“眼睛流血”,并把这种观感称为“现代”实则是疑似 AI 生成的垃圾内容;还有人觉得作者手写了 C 项目,却用 AI 来写博客文章,挺可惜。(认可度:较多人) > 手搓了 C 项目,却用 AI 写博客,挺可惜(@afavour)

认可无框架 CGI 在低流量下足够简单可用

  • 不少开发者认同文章列出的优点是真实合理的:每个请求新建进程、替换单个文件即可部署且无需重启、用 SQLite 的 C API 直连数据库、配合 pledge/chroot 降权等;CGI 很适合简单动态页或低流量场景,在 Apache、lighttpd、busybox 等环境里都有先例,也有人用 PHP 或无框架方式复刻类似体验。(认可度:较多人) > 文章提到的每一点都成立,尤其配合 pledge/chroot 降权(@ramses0)

认为回到 C/C++ 能规避供应链问题并让系统更可控

  • 一些开发者表示正是因为“没有供应链问题、能装进脑子里、能一直跑下去”而回到 C/C++;有人用 C++(仅 std 与 boost)写内部 API 服务器,觉得体验非常可靠且依赖地狱极少,也有人展示了用 C++ 写 WebAssembly 客户端与 C++ 服务端配合的项目。(认可度:较多人) > 用 C++(只用 std 和 boost)写 Web API 服务器体验非常愉快,可靠且几乎没有依赖地狱(@hollowonepl)

质疑首选 C 的收益:字符串处理痛苦、并发难写,且 Go/Rust 等更安全

  • 有人直说看不出作者为何一开始就选 C;C 里任何字符串处理都被认为是痛苦的事,用 pthreads 做并发也不好玩,虽可通过 fork/exec 共享只读代码段,但 .bss 与 .data 的初始化和拷贝仍会带来无法避免的启动开销,手写 C 服务器还很难避免 RCE 风险,让人担心 SQLite 备份频率。与此同时,文章提到的多数收益在 Go、Rust 等语言里也能拿到,而且还额外获得内存安全;一个关闭 CGO、内嵌模板的 Go 单二进制即可提供更好的工具链与部署体验,放进 scratch 容器还能实现完全隔离。(认可度:较多人) > C 里任何和所有的字符串处理都相当痛苦(@Scubabear68)

认为文章其实是在重造早期 PHP 的体验

  • 有人指出作者描述的好处正是早期 PHP 3/4 的特点:那时 PHP 是 C 库之上的薄层,快且简单,且恰逢 CGI 被弃用时解决了 CGI 的问题;如今用现代 PHP 无框架开发也能获得同样收益,还能像作者一样避开繁重的依赖。(认可度:少数人) > 作者描述的其实是老派 PHP 3,早期 PHP 本就是 C 库之上的薄层(@bijowo1676)

认为 CGI 是低效半成品,主张自写服务器或改用长驻进程

  • 有开发者强烈建议避开 CGI:它仍要依赖外部 httpd 转发,不能任意写 HTTP 头或决定如何服务客户端,热加载与服务器模块系统难以标准化,每次请求新建进程的开销巨大;更好的做法是把 Web 应用和服务器一起打包成单个二进制自己写全套,或者默认改用 FastCGI/长驻进程,仅在请求间清理状态以兼顾速度与安全。(认可度:较多人) > 建议大家无论如何都避开 CGI,它每请求新建进程的开销巨大,本身就是半成品(@rfgplk)

有价值的补充

  • FastCGI 的存在正是为了绕开“每请求新建进程”:一次 fork 大约要 8 毫秒;长驻进程会在请求之间尽量清理状态,现代 PHP 还能靠引擎内省决定清理范围,但数据库连接池等通常仍会保留。(@EGreg)
  • 若在程序已加载框架并完成配置初始化后再 fork,Linux/macOS 会通过写时复制共享已加载内存,仅对脏页拷贝;由于进程结束时由操作系统高效回收内存,这种做法能在跑着阻塞式数据库、文件与网络 IO 的 PHP 代码时,同时支撑十万级并发请求,而 Swoole/FrankenPHP 做不到。(@EGreg)
  • 他还把 PHP 转译成 C:用 Claude 将约 1000 个 PHP 函数转成原生实现,字符串用 rope 结构避免拼接,并把 JSON 编解码并行化,CPU 密集部分可获得约 10–50 倍加速。(@EGreg)
  • 冷知识:busybox 内置了 cgi-bin 支持;若把一个 bash 命令套上 CGI,就能看出“无框架”所需的代码量其实极小(参见 bash.cgi)。(@ramses0)
  • 把进程和 shell 环境本身当 REPL,命令只要支持 --json、--markdown 等输出类型,--cgi-bin 就只是再多了一种输出格式;保证命令只依赖入参、独立且可复现,就能同时运行在 firecracker/Lambda 式敌意环境和 shell、调试器、测试等舒适环境里。(@ramses0)
  • 历史纠正:90 年代最早被丢进 cgi-bin 的是 Perl 脚本,而不是 C 程序;若文章开头暗示 C 是当年主流,并不准确。(@hughw)
  • 早年案例:有人在 2000 年代初用 FORTRAN 77 写过 CGI 应用,运行于 HPUX 上的 Apache httpd。(@Quitschquat)
  • 部署提示:nginx 原生并不支持 CGI(lighttpd 才支持),需要 nginx-cgi 这类项目;Ruby 的 Rack 应用也可通过 Phusion Passenger 直接从 nginx 启动。(@Levitating)