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 秒实验
- 把脚本改成读
POST的正文(设CONTENT_LENGTH并从标准输入读),跑一次看结果。 - 把脚本里那句加法改成一个空操作,看耗时会不会明显下降——为什么?
- 想一想:如果每秒有一千个请求,CGI 要起多少个进程?
常见误区
- 「CGI 是一种编程语言」——它是一个调用约定,任何语言都能写。
- 「CGI 慢是因为代码写得差」——慢在每请求起进程,不在代码。
- 「CGI 已经彻底没用了」——思路还在,FastCGI 就是它的直系后代。
自测三问(点开对答案)
1. CGI 程序从哪里取参数? —— GET 从环境变量 `QUERY_STRING`,POST 从标准输入,长度看 `CONTENT_LENGTH`。 2. CGI 程序的输出怎么变成 HTTP 响应? —— 前几行当首部,一个空行之后当正文。 3. CGI 最主要的性能问题是什么? —— 每个请求起一个新进程,初始化和销毁的开销远大于干活本身。既然每次重来太贵,那就别退出了。Servlet 的思路就是这一句。
- ☐ (本节完,下一节 10.3.3 因Java而普及的Servlet)