程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 用C++寫的CGI程序

用C++寫的CGI程序

編輯:關於JAVA

經過前面的學習,大家應該能夠根據例子用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++仍然顯得非常方便。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved