2016年8月30日 星期二

IT 考古: 什麼是 CGI?

某天有位朋友跟小弟討論起 PHP,因為他的裝置運算能力有限,無法負擔解譯 PHP script 的成本,於是他想出一個方法,把 *.php 丟給 Browser 去解譯,我跟他這樣講不行,Browser 只能解譯 HTML & JavaScript(當然,如果裝了什麼特殊 plug-in 有可能,但這裡指一般情況)。

接著我就跟他解釋說,反正你都用 C 寫了,就用 C 寫個 CGI 程式給 HTTP Server 呼叫就好,他一臉迷惑的問我「什麼是 CGI?」

後面我也忘了討論的結果了。但自以為很熟習 CGI 這老古董的我,卻是在接觸到 OpenWrt uhttpd 後才算對 CGI 有了深刻的理解。

傳統 CGI

因為接下來的討論都是以 OpenWrt uhttpd 當作範例,而 OpenWrt 是 Linux-based,小弟就不多做解釋,當作各位看官已經有 Linux 背景了。

如下圖所示,當 HTTP Server 接到一個 request,而 HTTP Server 發現要求的資源並不是 HTML/JavaScript,就會按副檔名檢查設定是否有對應的解譯器存在,有的話就 fork 一個 child process 用對應的解譯器(例如 PHP)去解譯該資源(例如下圖中的 login.php),然後把解譯的結果從 stdout 讀出,傳回給 Browser。



那 HTTP Server 要怎麼把 reqeust 傳給 script 呢?以 uhttpd 的作法就是用環境變數與 stdin 傳給 script,環境變數自然不用多言,而 stdin 則有以 anonymous pipe 替換這種最古老的招數,stdout 也是如法炮製。



那 stdin 要怎麼知道該讀多少?HTTP request header 裡有一欄 Content-Length 代表內容的長度,而這個欄位放在環境變數裡,所以只要用環境變數取得的長度讀取就保證讀好讀滿。下圖為 wireshark 掃描聯合新聞網的結果:



而 stdout 是把 script output 原封不動的照搬給 Browser?依照小弟對 uhttpd 的實驗,他還是會過濾 script 輸出的內容,在輸出 HTML/JavaScript 前至少要輸出一行 "Content-Type: text/html\r\n\r\n",否則 Browser 會收到「Bad Request」,status code(如 404 Not Found)則是有需要才填。

寫到這裡可以看到,寫 php 跟寫 uhttpd CGI 大同小異,差別只在於 php 把 GET 與 POST 預先幫你包裝成 $_GET, $_POST 陣列,而你自己寫 CGI(尤其是用 C 語言寫)則是要自己做一堆苦工(比方分析 query string)。

CGI 的繼承者們


從前面可以看到傳統 CGI 的最大缺點在於 fork,如果你用過 LinkIt Smart 7688 跑 Python CGI script,光是回傳一個簡單的 hello 字串就要 5 秒鐘。就算使用運上能力強上 n 倍的 x86,這樣做在高訪問量時也肯定 gg。

所以現代 HTTP Server,無一例外的全部都改為 event driven + async IO(M$除外),node.js 更是直白的強迫你用這種方式思考應用。

在 Programming in Lua,作者認為 script 的關鍵屬性之一就是「解譯器為程式庫的一部分」,作為程式庫呼叫(哪怕是從 script 本身也行),也就是可以在不結束 process 的情況下重複解譯不同的 script。所以我們可以依據 CPU 的核心數與記憶體的大小,預先配置一定數量的 Process Pool(或 Thread Pool),然後當 request 來臨時從中取出閒置的 process/thread,這就是 FastCGI!

您有什麼看法呢?歡迎留言

2 則留言:

  1. 你好,新手想請教CGI與JSON,二者有關連嗎

    回覆刪除
    回覆
    1. 你把http想成卡車,JSON就是上面裝貨物的箱子,XML也是一樣

      刪除