位元詩人 [網頁設計] 教學:用 Nginx 搭配 fcgiwrap 執行 CGI 程式

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

雖然現在 (西元 2020 年三月) 還會用 CGI (Common Gateway Interface) 寫網頁程式的程式設計者很少,CGI 程式對於小型網頁程式來說仍是方便簡單的選擇。本文介紹在 Nginx 上搭配 FCGI Wrap (fcgiwrap) 執行 CGI 程式的流程,給想寫 CGI 程式的讀者做個參考。

什麼是 CGI?

CGI 是一種界面的規格,而 CGI 程式是執行 CGI 界面的應用程式。CGI 界面用於網頁伺服器和外部程式之間的溝通。程式設計者只要撰寫符合 CGI 界面的應用程式,該程式就可以視為網頁程式來運作。由於 CGI 程式不需要圖形使用者界面,所以會以終端機程式來撰寫。

如果想深入了解 CGI 程式的撰寫方式,最正統的資料是 RFC 3875。由於許多程式語言都有撰寫 CGI 的模組,需要自行從頭實作 CGI 程式的機會不多。不過,不一定所有語言都有 CGI 模組,必要時還是可以參考一下該份文件,自己實作 CGI 程式。

撰寫 CGI 程式的程式語言

由於 CGI 界面是語言中立的,只要程式語言符合以下條件,就可用來撰寫 CGI 程式:

  • 該語言可製作命令列程式 (執行檔或命令稿皆可)
  • 該語言有標準輸入 (standard input) 和標準輸出 (standard output)
  • 該語言可接收環境變數 (environment variable)

由此可知,幾乎所有的主流程式語言都有可能拿來撰寫 CGI 程式。相對來說,CGI 模組只是讓撰寫 CGI 程式的過程變簡單,而不是必要條件。

傳統上會用 C 語言來撰寫 CGI 程式。但 C 處理字串的能力較弱,在 Perl 出現後程式設計者就改用 Perl 寫 CGI 程式。後來 PHP 出現,程式設計者就改以 PHP 撰寫網頁程式。在 PHP 問世後,程式設計者撰寫網頁的方式慢慢改變,撰寫 CGI 程式的機會就變少了。

撰寫第一個 CGI 程式

在本文中,我們使用 Pascal 來寫 CGI 程式,並且不使用任何 CGI 模組。Pascal 不是撰寫網頁程式的熱門語言,代表 CGI 程式不限定語言。

以編輯器建立 helloCGI.pas 文字檔案,並加入以下內容:

program main;

const
  newline = AnsiChar(#10);

begin
  (* Response header *)
  (* 200 is default HTTP status code. Hence, this line is optional. *)
  write('HTTP/1.1 200 OK' + newline);
  (* Content-Type is highly recommended for HTTP client. *)
  write('Content-Type: text/plain; charset=utf-8' + newline + newline);
  (* Two newlines is mandatory after HTTP response header. *)

  (* Response body *)
  writeLn('Hello World');
end.

如果讀者看不懂 Pascal 程式碼也無妨。重點是這段程式碼會在標準輸出吐出以下訊息:

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8

Hello World

簡單地說,只要符合 HTTP 回應 (HTTP response) 的格式即可,使用什麼語言並不是重點。

使用 Free Pascal 編譯此程式:

$ fpc -oindex.cgi helloCGI.pas

編譯出來的 index.cgi 即為 CGI 程式。

在開發時期快速執行 CGI 程式

在撰寫 CGI 程式的過程中,總是要有一個網頁伺服器來試跑該 CGI 程式。但安裝並配置正統的網頁伺服器比較麻煩,故我們介紹一個在開發時期快速執行 CGI 程式的方式。

本節所介紹的方式會用到 Python 3,請讀者自行在系統上安裝。

先移動工作目錄,並建立 cgi-bin 子目錄:

$ cd path/to/www
$ mkdir cgi-bin

cgi-bin 目錄是根據 Python 的 http.server 模組的預設 CGI 目錄而建立。

將先前建立好的 index.cgi 程式移動到 cgi-bin 目錄。

使用以下指令來建立一個臨時的網頁伺服器:

$ python -m http.server --cgi 8080

用瀏覽器拜訪 http://localhost:8080/cgi-bin/index.cgi 即可看到 Hello World 字串。

本節所使用的網頁伺服器只適合在開發時期使用,如果要上線的話,還是要用正規的伺服器。詳見下一節。

使用 Nginx 搭配 FastCGI 協定

現行的網頁伺服器中,最常見的是 Apache 和 Nginx。在這兩者之中,我們選擇 Nginx,因為 Nginx 輕量、效率好。而且 Nginx 是自由軟體,在主流 GNU/Linux 系統上均可使用。但 Nginx 本身不處理 CGI 程式,而是以代理伺服器的身份將網頁請求透過 FastCGI 協定傳給後端程式。

所以我們需要另外找一個 FastCGI 伺服軟體。常見的 FastCGI 軟體如下:

  • 支援 FastCGI 的網頁伺服器,像 Apache 或 lighttpd
  • PHP-FPM (限 PHP)
  • fcgiwrap

為了掛 CGI 程式跑兩個網頁伺服器似乎有點本末倒置,我們這裡就不用這個方式。PHP-FPM 只限 PHP 程式,當然也無法使用。所以我們會使用 fcgiwrap 這個輕量級軟體來跑 CGI 程式。

原本的 CGI 程式會在每次回應皆開新的行程 (process),對系統資源消耗過大。FastCGI 改善了這項議題。所以,如果是要實際上線的 CGI 程式,建議用 FastCGI 而非原本的 CGI 界面來跑。

將 CGI 程式放上雲端

為了簡化練習 CGI 程式的過程,不需要用實體主機。可以用低價虛擬主機來快速取得免安裝的 GNU/Linux 環境。或者是用 VirtualBox 等虛擬電腦軟體在本地端開 GNU/Linux 系統來練習。

有許多虛擬主機以小時來計費,月費率僅 5 美元至 10 美元左右。而且練習完就可以把虛擬主機砍掉,實際上的費用相當低廉。一些常見的供應商包括 DigitalOceanVultrLinode 等,讀者可自行尋找合適的方案。

我們會假定讀者用 Ubuntu 或 Debian,因為 Debian 系列的 GNU/Linux 系統套件多,省下自行編譯的時間和心力。

以 APT 安裝系統提供的 Nginx 和 fcgiwrap:

$ sudo apt install nginx fcgiwrap

我們假定網站的根目錄位於 /var/www ,將寫好的 CGI 程式移到該目錄:

$ sudo mv index.cgi /var/www

修改該 CGI 程式的所有者和群組,避免該 CGI 程式權限過大:

$ sudo chown www-data:www-data /var/www/index.cgi

nohup 在背景啟動 fcgiwrap 程式:

$ sudo nohup fcgiwrap -c 4 -s unix:/var/run/fcgiwrap.socket </dev/null &>/dev/null &

在這個範例中,我們開了四個線程。讀者可視自己系統實際的情形修改線程數。常見的設置方式是每一個 CPU 核心開一個線程。

同樣地,我們修改 fcgiwrap 程式的擁有者和群組:

$ sudo chown www-data:www-data /var/run/fcgiwrap.socket

ps 檢查 fcgiwrap 是否有確實運行:

$ ps aux | grep fcgi

如果 fcgiwrap 有運行,透過 ps 可觀察到類似以下訊息輸出:

root      8014  0.0  0.0  25200  1408 pts/0    S    12:29   0:00 fcgiwrap -c 4 -s unix:/var/run/fcgiwrap.socket
root      8015  0.0  0.0  25200   200 pts/0    S    12:29   0:00 fcgiwrap -c 4 -s unix:/var/run/fcgiwrap.socket
root      8016  0.0  0.0  25200   200 pts/0    S    12:29   0:00 fcgiwrap -c 4 -s unix:/var/run/fcgiwrap.socket
root      8017  0.0  0.0  25200   200 pts/0    S    12:29   0:00 fcgiwrap -c 4 -s unix:/var/run/fcgiwrap.socket
root      8018  0.0  0.0  25200   200 pts/0    S    12:29   0:00 fcgiwrap -c 4 -s unix:/var/run/fcgiwrap.socket

接著,我們來設置 Nginx 的伺服器設定檔:

server {
      listen 80;

      server_name example.com;
      root /var/www;

      location ~ \.cgi$ {
          gzip off;

          try_files $uri/index.cgi $uri.cgi $uri/ $uri = 404;
          index index.cgi;

          include fastcgi_params;
          fastcgi_pass  unix:/var/run/fcgiwrap.socket;
          fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
      }

      location / {
          try_files $uri/index.html $uri.html $uri/ $uri = 404;
      }
}

以這個設置來說,網站位於 example.com ,開標準的 (非加密) HTTP 埠 80。讀者可根據自己系統實際的情形修改。當然在 DNS 伺服器那裡也要做相對應的設置。如果手上沒有網域可用,可先以虛擬主機的 IP 來拜訪網站,反正這裡網站只是暫時性的。

典型的 CGI 範例是用 CGI 程式處理表單,但 CGI 程式本質上和 PHP 命令稿無異,也可用來直接輸出頁面。在這個範例中,我們假定直接以 CGI 程式輸出純文字文件。

指定 Nginx 搜尋 CGI 程式的順序:

try_files $uri/index.cgi $uri.cgi $uri/ $uri = 404;

以及指定預設的頁面:

index index.cgi;

使用 FastCGI 時,建議引入 FastCGI 的預設參數的設定檔:

include fastcgi_params;

透過這行指令,我們就不需要逐一輸出環境變數給 CGI 程式。

實際上,Nginx 不處理 CGI 程式,而會把請求導向 fcgiwrap

fastcgi_pass  unix:/var/run/fcgiwrap.socket;

除了標準參數外,我們額外傳入 SCRIPT_FILENAME 參數:

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

單純以這個例子來說,這行指令是不必要的。但我們刻意保留該行指令,給讀者參考。

設定完成後,重開 Nginx 讓設定生效:

$ sudo systemctl restart nginx

如果整個過程都正確執行,用瀏覽器拜訪該網站,就可以看到以純文字呈現的 Hello World 字串。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。