📘 第十章 构建Web内容的技术
📄 一句话概括

CGI 是「Web 服务器叫外部程序干活」的最早约定,规矩只有三条:参数从环境变量和标准输入进来, 程序把首部加正文打到标准输出。简单到任何语言都能写,代价是每个请求都要起一个新进程。

📝 本节和前面几节的关系

上一节那个「会算加法」的程序,服务器是怎么把它叫起来的?这一节看最早也最朴素的那个答案。

一、三条规矩

第一条:参数从哪进。 GET 的参数放在环境变量 QUERY_STRING 里,POST 的正文从标准输入读, 读多少由环境变量 CONTENT_LENGTH 说了算。请求方法在 REQUEST_METHOD 里。 第二条:结果从哪出。程序往标准输出打东西,前几行当 HTTP 首部,一个空行之后是正文。 就这么简单,不需要懂任何协议细节。 第三条:活完就退。程序运行结束、进程退出,服务器把它的输出收走发回去。 这一次请求和这个进程绑定,没有下一次。 正因为规矩这么少,CGI 程序可以用任何语言写:Perl、Python、C、Shell 都行,只要能读环境变量、 能读标准输入、能往标准输出写。这在九十年代是巨大的优势——那时候还没有统一的「服务端框架」。

二、每请求一个进程的账

Demo 真跑了一个 CGI 脚本:把环境变量设好,起一个子进程,收它的输出。 一次 510 毫秒,连跑 20 次平均 496 毫秒,最快 476、最慢 572。 近半秒里,那句加法大概只占几微秒。剩下的全花在「起进程、加载解释器、初始化、退出」这一套上。 所以 CGI 的问题不在慢,在每次都从头来一遍。 访问量一上去,服务器的时间几乎全用来创建和销毁进程了。 还有一个隐患:每个进程都要占一份内存。 并发一高,进程数一多,内存直接被吃光——这是九十年代末很多网站「一火就挂」的真实原因。

三、它今天还在哪儿

CGI 本身基本退出主流了,但它的思路到处都是:「服务器把请求交给一个外部程序, 收它的输出」这条线,一直延伸到今天的 FastCGI、WSGI、Rack、Servlet 容器。 FastCGI 就是给 CGI 打的第一个补丁:进程不退出,留着接下一个请求。 这一改,把「每请求一进程」变成「进程池」,开销立刻降下来。 所以学 CGI 不是为了用它,是为了看懂后面那些东西在改什么——它们改的几乎都是同一件事: 别每次都从头来。

四、动手 Demo:真跑一个 CGI 脚本

写一个二十行的脚本,按 CGI 的规矩读环境变量、算加法、往标准输出打首部和正文。 然后用子进程真跑一次,把它的输出按 CGI 协议拆开;最后连跑 20 次量平均耗时。

# 脚本这一侧(_cgi37.py)
q = os.environ.get('QUERY_STRING', '')
n = int(os.environ.get('CONTENT_LENGTH') or 0)
body = sys.stdin.read(n) if n else ''
print('Content-Type: text/plain; charset=utf-8')
print('')
print('%d + %d = %d' % (a, b, a + b))

实跑输出:

--------------------------------------------------------------------
CGI:真跑一个外部脚本,按 CGI 协议说话
--------------------------------------------------------------------
  脚本路径                   _cgi37.py
  传进去的环境变量           REQUEST_METHOD/QUERY_STRING/CONTENT_LENGTH
  它吐出的首部               Content-Type: text/plain; charset=utf-8
  空行之后是正文             3 + 4 = 7
  正文第二行                 由进程 10168 算出,方法是 GET…
  这一次耗时                 510 毫秒

  CGI 的规矩就这三条:参数从环境变量和标准输入进来,
  回来的东西前几行当 HTTP 首部、空行之后当正文。就这么简单,所以什么语言都能写。

  连跑 20 次                 总共 9912 毫秒
  每次平均                   496 毫秒
  最快/最慢                 476/572 毫秒

脚本吐出来的首部是 Content-Type: text/plain; charset=utf-8,空行之后是正文 3 + 4 = 7, 第二行还带了进程号和请求方法。 正文第二行那句「由进程 10168 算出」是个证据:每次请求的进程号都不一样。 CGI 就是一次性的,干完就退。 一次 510 毫秒,连跑 20 次平均 496 毫秒。 这个数字在当年更夸张——那时候的机器比现在慢一个数量级。 所以 CGI 的瓶颈非常清楚:不是算得慢,是每次都要重新把程序装进内存。 记住这一点,下一节那个「常驻」的方案就好理解了。

30 秒实验

  1. 把脚本改成读 POST 的正文(设 CONTENT_LENGTH 并从标准输入读),跑一次看结果。
  2. 把脚本里那句加法改成一个空操作,看耗时会不会明显下降——为什么?
  3. 想一想:如果每秒有一千个请求,CGI 要起多少个进程?

常见误区

  1. 「CGI 是一种编程语言」——它是一个调用约定,任何语言都能写。
  2. 「CGI 慢是因为代码写得差」——慢在每请求起进程,不在代码。
  3. 「CGI 已经彻底没用了」——思路还在,FastCGI 就是它的直系后代。
自测三问(点开对答案) 1. CGI 程序从哪里取参数? —— GET 从环境变量 `QUERY_STRING`,POST 从标准输入,长度看 `CONTENT_LENGTH`。 2. CGI 程序的输出怎么变成 HTTP 响应? —— 前几行当首部,一个空行之后当正文。 3. CGI 最主要的性能问题是什么? —— 每个请求起一个新进程,初始化和销毁的开销远大于干活本身。

既然每次重来太贵,那就别退出了。Servlet 的思路就是这一句。

相关笔记