經過前面的學習,大家應該能夠根據例子用ANSI C為自己的服務器寫出CGI程序。之所以選用ANSI C,是因為它幾乎隨處可見,是最流行的C語言標准。當然,現在的C++也非常流行了,特別是采用GNU C++編譯器(g++)形式的那一些(注釋④)。可從網上許多地方免費下載g++,而且可選用幾乎所有平台的版本(通常與Linux那樣的操作系統配套提供,且已預先安裝好)。正如大家即將看到的那樣,從CGI程序可獲得面向對象程序設計的許多好處。
④:GNU的全稱是“Gnu's Not Unix”。這最早是由“自由軟件基金會”(FSF)負責開發的一個項目,致力於用一個免費的版本取代原有的Unix操作系統。現在的Linux似乎正在做前人沒有做到的事情。但GNU工具在Linux的開發中扮演了至關重要的角色。事實上,Linux的整套軟件包附帶了數量非常多的GNU組件。
為避免第一次就提出過多的新概念,這個程序並未打算成為一個“純”C++程序;有些代碼是用普通C寫成的——盡管還可選用C++的一些替用形式。但這並不是個突出的問題,因為該程序用C++制作最大的好處就是能夠創建類。在解析CGI信息的時候,由於我們最關心的是字段的“名稱/值”對,所以要用一個類(Pair)來代表單個名稱/值對;另一個類(CGI_vector)則將CGI字串自動解析到它會容納的Pair對象裡(作為一個vector),這樣即可在有空的時候把每個Pair(對)都取出來。
這個程序同時也非常有趣,因為它演示了C++與Java相比的許多優缺點。大家會看到一些相似的東西;比如class關鍵字。訪問控制使用的是完全相同的關鍵字public和private,但用法卻有所不同。它們控制的是一個塊,而非單個方法或字段(也就是說,如果指定private:,後續的每個定義都具有private屬性,直到我們再指定public:為止)。另外在創建一個類的時候,所有定義都自動默認為private。
在這兒使用C++的一個原因是要利用C++“標准模板庫”(STL)提供的便利。至少,STL包含了一個vector類。這是一個C++模板,可在編譯期間進行配置,令其只容納一種特定類型的對象(這裡是Pair對象)。和Java的Vector不同,如果我們試圖將除Pair對象之外的任何東西置入vector,C++的vector模板都會造成一個編譯期錯誤;而Java的Vector能夠照單全收。而且從vector裡取出什麼東西的時候,它會自動成為一個Pair對象,毋需進行造型處理。所以檢查在編譯期進行,這使程序顯得更為“健壯”。此外,程序的運行速度也可以加快,因為沒有必要進行運行期間的造型。vector也會過載operator[],所以可以利用非常方便的語法來提取Pair對象。vector模板將在CGI_vector創建時使用;在那時,大家就可以體會到如此簡短的一個定義居然蘊藏有那麼巨大的能量。
若提到缺點,就一定不要忘記Pair在下列代碼中定義時的復雜程度。與我們在Java代碼中看到的相比,Pair的方法定義要多得多。這是由於C++的程序員必須提前知道如何用副本構建器控制復制過程,而且要用過載的operator=完成賦值。正如第12章解釋的那樣,我們有時也要在Java中考慮同樣的事情。但在C++中,幾乎一刻都不能放松對這些問題的關注。
這個項目首先創建一個可以重復使用的部分,由C++頭文件中的Pair和CGI_vector構成。從技術角度看,確實不應把這些東西都塞到一個頭文件裡。但就目前的例子來說,這樣做不會造成任何方面的損害,而且更具有Java風格,所以大家閱讀理解代碼時要顯得輕松一些:
//: CGITools.h // Automatically extracts and decodes data // from CGI GETs and POSTs. Tested with GNU C++ // (available for most server machines). #include <string.h> #include <vector> // STL vector using namespace std; // A class to hold a single name-value pair from // a CGI query. CGI_vector holds Pair objects and // returns them from its operator[]. class Pair { char* nm; char* val; public: Pair() { nm = val = 0; } Pair(char* name, char* value) { // Creates new memory: nm = decodeURLString(name); val = decodeURLString(value); } const char* name() const { return nm; } const char* value() const { return val; } // Test for "emptiness" bool empty() const { return (nm == 0) || (val == 0); } // Automatic type conversion for boolean test: operator bool() const { return (nm != 0) && (val != 0); } // The following constructors & destructor are // necessary for bookkeeping in C++. // Copy-constructor: Pair(const Pair& p) { if(p.nm == 0 || p.val == 0) { nm = val = 0; } else { // Create storage & copy rhs values: nm = new char[strlen(p.nm) + 1]; strcpy(nm, p.nm); val = new char[strlen(p.val) + 1]; strcpy(val, p.val); } } // Assignment operator: Pair& operator=(const Pair& p) { // Clean up old lvalues: delete nm; delete val; if(p.nm == 0 || p.val == 0) { nm = val = 0; } else { // Create storage & copy rhs values: nm = new char[strlen(p.nm) + 1]; strcpy(nm, p.nm); val = new char[strlen(p.val) + 1]; strcpy(val, p.val); } return *this; } ~Pair() { // Destructor delete nm; // 0 value OK delete val; } // If you use this method outide this class, // you're responsible for calling 'delete' on // the pointer that's returned: static char* decodeURLString(const char* URLstr) { int len = strlen(URLstr); char* result = new char[len + 1]; memset(result, len + 1, 0); for(int i = 0, j = 0; i <= len; i++, j++) { if(URLstr[i] == '+') result[j] = ' '; else if(URLstr[i] == '%') { result[j] = translateHex(URLstr[i + 1]) * 16 + translateHex(URLstr[i + 2]); i += 2; // Move past hex code } else // An ordinary character result[j] = URLstr[i]; } return result; } // Translate a single hex character; used by // decodeURLString(): static char translateHex(char hex) { if(hex >= 'A') return (hex & 0xdf) - 'A' + 10; else return hex - '0'; } }; // Parses any CGI query and turns it // into an STL vector of Pair objects: class CGI_vector : public vector<Pair> { char* qry; const char* start; // Save starting position // Prevent assignment and copy-construction: void operator=(CGI_vector&); CGI_vector(CGI_vector&); public: // const fields must be initialized in the C++ // "Constructor initializer list": CGI_vector(char* query) : start(new char[strlen(query) + 1]) { qry = (char*)start; // Cast to non-const strcpy(qry, query); Pair p; while((p = nextPair()) != 0) push_back(p); } // Destructor: ~CGI_vector() { delete start; } private: // Produces name-value pairs from the query // string. Returns an empty Pair when there's // no more query string left: Pair nextPair() { char* name = qry; if(name == 0 || *name == '\0') return Pair(); // End, return null Pair char* value = strchr(name, '='); if(value == 0) return Pair(); // Error, return null Pair // Null-terminate name, move value to start // of its set of characters: *value = '\0'; value++; // Look for end of value, marked by '&': qry = strchr(value, '&'); if(qry == 0) qry = ""; // Last pair found else { *qry = '\0'; // Terminate value string qry++; // Move to next pair } return Pair(name, value); } }; ///:~
在#include語句後,可看到有一行是:
using namespace std;
C++中的“命名空間”(Namespace)解決了由Java的package負責的一個問題:將庫名隱藏起來。std命名空間引用的是標准C++庫,而vector就在這個庫中,所以這一行是必需的。
Pair類表面看異常簡單,只是容納了兩個(private)字符指針而已——一個用於名字,另一個用於值。默認構建器將這兩個指針簡單地設為零。這是由於在C++中,對象的內存不會自動置零。第二個構建器調用方法decodeURLString(),在新分配的堆內存中生成一個解碼過後的字串。這個內存區域必須由對象負責管理及清除,這與“破壞器”中見到的相同。name()和value()方法為相關的字段產生只讀指針。利用empty()方法,我們查詢Pair對象它的某個字段是否為空;返回的結果是一個bool——C++內建的基本布爾數據類型。operator bool()使用的是C++“運算符過載”的一種特殊形式。它允許我們控制自動類型轉換。如果有一個名為p的Pair對象,而且在一個本來希望是布爾結果的表達式中使用,比如if(p){//...,那麼編譯器能辨別出它有一個Pair,而且需要的是個布爾值,所以自動調用operator bool(),進行必要的轉換。
接下來的三個方法屬於常規編碼,在C++中創建類時必須用到它們。根據C++類采用的所謂“經典形式”,我們必須定義必要的“原始”構建器,以及一個副本構建器和賦值運算符——operator=(以及破壞器,用於清除內存)。之所以要作這樣的定義,是由於編譯器會“默默”地調用它們。在對象傳入、傳出一個函數的時候,需要調用副本構建器;而在分配對象時,需要調用賦值運算符。只有真正掌握了副本構建器和賦值運算符的工作原理,才能在C++裡寫出真正“健壯”的類,但這需要需要一個比較艱苦的過程(注釋⑤)。
⑤:我的《Thinking in C++》(Prentice-Hall,1995)用了一整章的地方來討論這個主題。若需更多的幫助,請務必看看那一章。
只要將一個對象按值傳入或傳出函數,就會自動調用副本構建器Pair(const Pair&)。也就是說,對於准備為其制作一個完整副本的那個對象,我們不准備在函數框架中傳遞它的地址。這並不是Java提供的一個選項,由於我們只能傳遞句柄,所以在Java裡沒有所謂的副本構建器(如果想制作一個本地副本,可以“克隆”那個對象——使用clone(),參見第12章)。類似地,如果在Java裡分配一個句柄,它會簡單地復制。但C++中的賦值意味著整個對象都會復制。在副本構建器中,我們創建新的存儲空間,並復制原始數據。但對於賦值運算符,我們必須在分配新存儲空間之前釋放老存儲空間。我們要見到的也許是C++類最復雜的一種情況,但那正是Java的支持者們論證Java比C++簡單得多的有力證據。在Java中,我們可以自由傳遞句柄,善後工作則由垃圾收集器負責,所以可以輕松許多。
但事情並沒有完。Pair類為nm和val使用的是char*,最復雜的情況主要是圍繞指針展開的。如果用較時髦的C++ string類來代替char*,事情就要變得簡單得多(當然,並不是所有編譯器都提供了對string的支持)。那麼,Pair的第一部分看起來就象下面這樣:
class Pair { string nm; string val; public: Pair() { } Pair(char* name, char* value) { nm = decodeURLString(name); val = decodeURLString(value); } const char* name() const { return nm.c_str(); } const char* value() const { return val.c_str(); } // Test for "emptiness" bool empty() const { return (nm.length() == 0) || (val.length() == 0); } // Automatic type conversion for boolean test: operator bool() const { return (nm.length() != 0) && (val.length() != 0); }
(此外,對這個類decodeURLString()會返回一個string,而不是一個char*)。我們不必定義副本構建器、operator=或者破壞器,因為編譯器已幫我們做了,而且做得非常好。但即使有些事情是自動進行的,C++程序員也必須了解副本構建以及賦值的細節。
Pair類剩下的部分由兩個方法構成:decodeURLString()以及一個“幫助器”方法translateHex()——將由decodeURLString()使用。注意translateHex()並不能防范用戶的惡意輸入,比如“%1H”。分配好足夠的存儲空間後(必須由破壞器釋放),decodeURLString()就會其中遍歷,將所有“+”都換成一個空格;將所有十六進制代碼(以一個“%”打頭)換成對應的字符。
CGI_vector用於解析和容納整個CGI GET命令。它是從STL vector裡繼承的,後者例示為容納Pair。C++中的繼承是用一個冒號表示,在Java中則要用extends。此外,繼承默認為private屬性,所以幾乎肯定需要用到public關鍵字,就象這樣做的那樣。大家也會發現CGI_vector有一個副本構建器以及一個operator=,但它們都聲明成private。這樣做是為了防止編譯器同步兩個函數(如果不自己聲明它們,兩者就會同步)。但這同時也禁止了客戶程序員按值或者通過賦值傳遞一個CGI_vector。
CGI_vector的工作是獲取QUERY_STRING,並把它解析成“名稱/值”對,這需要在Pair的幫助下完成。它首先將字串復制到本地分配的內存,並用常數指針start跟蹤起始地址(稍後會在破壞器中用於釋放內存)。隨後,它用自己的nextPair()方法將字串解析成原始的“名稱/值”對,各個對之間用一個“=”和“&”符號分隔。這些對由nextPair()傳遞給Pair構建器,所以nextPair()返回的是一個Pair對象。隨後用push_back()將該對象加入vector。nextPair()遍歷完整個QUERY_STRING後,會返回一個零值。
現在基本工具已定義好,它們可以簡單地在一個CGI程序中使用,就象下面這樣:
//: Listmgr2.cpp // CGI version of Listmgr.c in C++, which // extracts its input via the GET submission // from the associated applet. Also works as // an ordinary CGI program with HTML forms. #include <stdio.h> #include "CGITools.h" const char* dataFile = "list2.txt"; const char* notify = "[email protected]"; #undef DEBUG // Similar code as before, except that it looks // for the email name inside of '<>': int inList(FILE* list, const char* emailName) { const int BSIZE = 255; char lbuf[BSIZE]; char emname[BSIZE]; // Put the email name in '<>' so there's no // possibility of a match within another name: sprintf(emname, "<%s>", emailName); // Go to the beginning of the list: fseek(list, 0, SEEK_SET); // Read each line in the list: while(fgets(lbuf, BSIZE, list)) { // Strip off the newline: char * newline = strchr(lbuf, '\n'); if(newline != 0) *newline = '\0'; if(strstr(lbuf, emname) != 0) return 1; } return 0; } void main() { // You MUST print this out, otherwise the // server will not send the response: printf("Content-type: text/plain\n\n"); FILE* list = fopen(dataFile, "a+t"); if(list == 0) { printf("error: could not open database. "); printf("Notify %s", notify); return; } // For a CGI "GET," the server puts the data // in the environment variable QUERY_STRING: CGI_vector query(getenv("QUERY_STRING")); #if defined(DEBUG) // Test: dump all names and values for(int i = 0; i < query.size(); i++) { printf("query[%d].name() = [%s], ", i, query[i].name()); printf("query[%d].value() = [%s]\n", i, query[i].value()); } #endif(DEBUG) Pair name = query[0]; Pair email = query[1]; if(name.empty() || email.empty()) { printf("error: null name or email"); return; } if(inList(list, email.value())) { printf("Already in list: %s", email.value()); return; } // It's not in the list, add it: fseek(list, 0, SEEK_END); fprintf(list, "%s <%s>;\n", name.value(), email.value()); fflush(list); fclose(list); printf("%s <%s> added to list\n", name.value(), email.value()); } ///:~
alreadyInList()函數與前一個版本幾乎是完全相同的,只是它假定所有電子函件地址都在一個“<>”內。
在使用GET方法時(通過在FORM引導命令的METHOD標記內部設置,但這在這裡由數據發送的方式控制),Web服務器會收集位於“?”後面的所有信息,並把它們置入環境變量QUERY_STRING(查詢字串)裡。所以為了讀取那些信息,必須獲得QUERY_STRING的值,這是用標准的C庫函數getnv()完成的。在main()中,注意對QUERY_STRING的解析有多麼容易:只需把它傳遞給用於CGI_vector對象的構建器(名為query),剩下的所有工作都會自動進行。從這時開始,我們就可以從query中取出名稱和值,把它們當作數組看待(這是由於operator[]在vector裡已經過載了)。在調試代碼中,大家可看到這一切是如何運作的;調試代碼封裝在預處理器引導命令#if defined(DEBUG)和#endif(DEBUG)之間。
現在,我們迫切需要掌握一些與CGI有關的東西。CGI程序用兩個方式之一傳遞它們的輸入:在GET執行期間通過QUERY_STRING傳遞(目前用的這種方式),或者在POST期間通過標准輸入。但CGI程序通過標准輸出發送自己的輸出,這通常是用C程序的printf()命令實現的。那麼這個輸出到哪裡去了呢?它回到了Web服務器,由服務器決定該如何處理它。服務器作出決定的依據是content-type(內容類型)頭數據。這意味著假如content-type頭不是它看到的第一件東西,就不知道該如何處理收到的數據。因此,我們無論如何也要使所有CGI程序都從content-type頭開始輸出。
在目前這種情況下,我們希望服務器將所有信息都直接反饋回客戶程序(亦即我們的程序片,它們正在等候給自己的回復)。信息應該原封不動,所以content-type設為text/plain(純文本)。一旦服務器看到這個頭,就會將所有字串都直接發還給客戶。所以每個字串(三個用於出錯條件,一個用於成功的加入)都會返回程序片。
我們用相同的代碼添加電子函件名稱(用戶的姓名)。但在CGI腳本的情況下,並不存在無限循環——程序只是簡單地響應,然後就中斷。每次有一個CGI請求抵達時,程序都會啟動,對那個請求作出反應,然後自行關閉。所以CPU不可能陷入空等待的尴尬境地,只有啟動程序和打開文件時才存在性能上的隱患。Web服務器對CGI請求進行控制時,它的開銷會將這種隱患減輕到最低程度。
這種設計的另一個好處是由於Pair和CGI_vector都得到了定義,大多數工作都幫我們自動完成了,所以只需修改main()即可輕松創建自己的CGI程序。盡管小服務程序(Servlet)最終會變得越來越流行,但為了創建快速的CGI程序,C++仍然顯得非常方便。