# 15.6 Java與CGI的溝通
Java程序可向一個服務器發出一個CGI請求,這與HTML表單頁沒什么兩樣。而且和HTML頁一樣,這個請求既可以設為GET(下載),亦可設為POST(上傳)。除此以外,Java程序還可攔截CGI程序的輸出,所以不必依賴程序來格式化一個新頁,也不必在出錯的時候強迫用戶從一個頁回轉到另一個頁。事實上,程序的外觀可以做得跟以前的版本別無二致。
代碼也要簡單一些,畢竟用CGI也不是很難就能寫出來(前提是真正地理解它)。所以在這一節里,我們準備辦個CGI編程速成班。為解決常規問題,將用C++創建一些CGI工具,以便我們編寫一個能解決所有問題的CGI程序。這樣做的好處是移植能力特別強——即將看到的例子能在支持CGI的任何系統上運行,而且不存在防火墻的問題。
這個例子也闡示了如何在程序片(Applet)和CGI程序之間建立連接,以便將其方便地改編到自己的項目中。
## 15.6.1 CGI數據的編碼
在這個版本中,我們將收集名字和電子函件地址,并用下述形式將其保存到文件中:
```
First Last <email@domain.com>;
```
這對任何E-mail程序來說都是一種非常方便的格式。由于只需收集兩個字段,而且CGI為字段中的編碼采用了一種特殊的格式,所以這里沒有簡便的方法。如果自己動手編制一個原始的HTML頁,并加入下述代碼行,即可正確地理解這一點:
```
<Form method="GET" ACTION="/cgi-bin/Listmgr2.exe">
<P>Name: <INPUT TYPE = "text" NAME = "name"
VALUE = "" size = "40"></p>
<P>Email Address: <INPUT TYPE = "text"
NAME = "email" VALUE = "" size = "40"></p>
<p><input type = "submit" name = "submit" > </p>
</Form>
```
上述代碼創建了兩個數據輸入字段(區),名為`name`和`email`。另外還有一個`submit`(提交)按鈕,用于收集數據,并將其發給CGI程序。`Listmgr2.exe`是駐留在特殊程序目錄中的一個可執行文件。在我們的Web服務器上,該目錄一般都叫作`cgi-bin`(注釋③)。如果在那個目錄里找不到該程序,結果就無法出現。填好這個表單,然后按下提交按鈕,即可在瀏覽器的URL地址窗口里看到象下面這樣的內容:
```
http://www.myhome.com/cgi-bin/Listmgr2.exe?name=First+Last&email=email@domain.com&submit=Submit
```
③:在Windows32平臺下,可利用與Microsoft Office 97或其他產品配套提供的Microsoft Personal Web Server(微軟個人Web服務器)進行測試。這是進行試驗的最好方法,因為不必正式連入網絡,可在本地環境中完成測試(速度也非常快)。如果使用的是不同的平臺,或者沒有Office 97或者FrontPage 98那樣的產品,可到網上找一個免費的Web服務器供自己測試。
當然,上述URL實際顯示時是不會拆行的。從中可稍微看出如何對數據編碼并傳給CGI。至少有一件事情能夠肯定——空格是不允許的(因為它通常用于分隔命令行參數)。所有必需的空格都用“+”號替代,每個字段都包含了字段名(具體由HTML頁決定),后面跟隨一個`=`號以及正式的字段數據,最后用一個`&`結束。
到這時,大家也許會對`+`,`=`以及`&`的使用產生疑惑。假如必須在字段里使用這些字符,那么該如何聲明呢?例如,我們可能使用“John & MarshaSmith”這個名字,其中的`&`代表“And”。事實上,它會編碼成下面這個樣子:
```
John+%26+Marsha+Smith
```
也就是說,特殊字符會轉換成一個`%`,并在后面跟上它的十六進制ASCII編碼。
幸運的是,Java有一個工具來幫助我們進行這種編碼。這是`URLEncoder`類的一個靜態方法,名為`encode()`。可用下述程序來試驗這個方法:
```
//: EncodeDemo.java
// Demonstration of URLEncoder.encode()
import java.net.*;
public class EncodeDemo {
public static void main(String[] args) {
String s = "";
for(int i = 0; i < args.length; i++)
s += args[i] + " ";
s = URLEncoder.encode(s.trim());
System.out.println(s);
}
} ///:~
```
該程序將獲取一些命令行參數,把它們合并成一個由多個詞構成的字符串,各詞之間用空格分隔(最后一個空格用`String.trim()`剔除了)。隨后對它們進行編碼,并打印出來。
為調用一個CGI程序,程序片要做的全部事情就是從自己的字段或其他地方收集數據,將所有數據都編碼成正確的URL樣式,然后匯編到單獨一個字符串里。每個字段名后面都加上一個`=`符號,緊跟正式數據,再緊跟一個`&`。為構建完整的CGI命令,我們將這個字符串置于CGI程序的URL以及一個`?`后。這是調用所有CGI程序的標準方法。大家馬上就會看到,用一個程序片能夠很輕松地完成所有這些編碼與合并。
## 15.6.2 程序片
程序片實際要比`NameSender.java`簡單一些。這部分是由于很容易即可發出一個GET請求。此外,也不必等候回復信息。現在有兩個字段,而非一個,但大家會發現許多程序片都是熟悉的,請比較`NameSender.java`。
```
//: NameSender2.java
// An applet that sends an email address
// via a CGI GET, using Java 1.02.
import java.awt.*;
import java.applet.*;
import java.net.*;
import java.io.*;
public class NameSender2 extends Applet {
final String CGIProgram = "Listmgr2.exe";
Button send = new Button(
"Add email address to mailing list");
TextField name = new TextField(
"type your name here", 40),
email = new TextField(
"type your email address here", 40);
String str = new String();
Label l = new Label(), l2 = new Label();
int vcount = 0;
public void init() {
setLayout(new BorderLayout());
Panel p = new Panel();
p.setLayout(new GridLayout(3, 1));
p.add(name);
p.add(email);
p.add(send);
add("North", p);
Panel labels = new Panel();
labels.setLayout(new GridLayout(2, 1));
labels.add(l);
labels.add(l2);
add("Center", labels);
l.setText("Ready to send email address");
}
public boolean action (Event evt, Object arg) {
if(evt.target.equals(send)) {
l2.setText("");
// Check for errors in data:
if(name.getText().trim()
.indexOf(' ') == -1) {
l.setText(
"Please give first and last name");
l2.setText("");
return true;
}
str = email.getText().trim();
if(str.indexOf(' ') != -1) {
l.setText(
"Spaces not allowed in email name");
l2.setText("");
return true;
}
if(str.indexOf(',') != -1) {
l.setText(
"Commas not allowed in email name");
return true;
}
if(str.indexOf('@') == -1) {
l.setText("Email name must include '@'");
l2.setText("");
return true;
}
if(str.indexOf('@') == 0) {
l.setText(
"Name must preceed '@' in email name");
l2.setText("");
return true;
}
String end =
str.substring(str.indexOf('@'));
if(end.indexOf('.') == -1) {
l.setText("Portion after '@' must " +
"have an extension, such as '.com'");
l2.setText("");
return true;
}
// Build and encode the email data:
String emailData =
"name=" + URLEncoder.encode(
name.getText().trim()) +
"&email=" + URLEncoder.encode(
email.getText().trim().toLowerCase()) +
"&submit=Submit";
// Send the name using CGI's GET process:
try {
l.setText("Sending...");
URL u = new URL(
getDocumentBase(), "cgi-bin/" +
CGIProgram + "?" + emailData);
l.setText("Sent: " + email.getText());
send.setLabel("Re-send");
l2.setText(
"Waiting for reply " + ++vcount);
DataInputStream server =
new DataInputStream(u.openStream());
String line;
while((line = server.readLine()) != null)
l2.setText(line);
} catch(MalformedURLException e) {
l.setText("Bad URl");
} catch(IOException e) {
l.setText("IO Exception");
}
}
else return super.action(evt, arg);
return true;
}
} ///:~
```
CGI程序(不久即可看到)的名字是`Listmgr2.exe`。許多Web服務器都在Unix機器上運行(Linux也越來越受到青睞)。根據傳統,它們一般不為自己的可執行程序采用`.exe`擴展名。但在Unix操作系統中,可以把自己的程序稱呼為自己希望的任何東西。若使用的是`.exe`擴展名,程序毋需任何修改即可通過Unix和Win32的運行測試。
和往常一樣,程序片設置了自己的用戶界面(這次是兩個輸入字段,不是一個)。唯一顯著的區別是在`action()`方法內產生的。該方法的作用是對按鈕按下事件進行控制。名字檢查過以后,大家會發現下述代碼行:
```
String emailData =
"name=" + URLEncoder.encode(
name.getText().trim()) +
"&email=" + URLEncoder.encode(
email.getText().trim().toLowerCase()) +
"&submit=Submit";
// Send the name using CGI's GET process:
try {
l.setText("Sending...");
URL u = new URL(
getDocumentBase(), "cgi-bin/" +
CGIProgram + "?" + emailData);
l.setText("Sent: " + email.getText());
send.setLabel("Re-send");
l2.setText(
"Waiting for reply " + ++vcount);
DataInputStream server =
new DataInputStream(u.openStream());
String line;
while((line = server.readLine()) != null)
l2.setText(line);
// ...
```
`name`和`email`數據都是它們對應的文字框里提取出來,而且兩端多余的空格都用`trim()`剔去了。為了進入列表,`email`名字被強制換成小寫形式,以便能夠準確地對比(防止基于大小寫形式的錯誤判斷)。來自每個字段的數據都編碼為URL形式,隨后采用與HTML頁中一樣的方式匯編GET字符串(這樣一來,我們可將Java程序片與現有的任何CGI程序結合使用,以滿足常規的HTML GET請求)。
到這時,一些Java的魔力已經開始發揮作用了:如果想同任何URL連接,只需創建一個URL對象,并將地址傳遞給構造器即可。構造器會負責建立同服務器的連接(對Web服務器來說,所有連接行動都是根據作為URL使用的字符串來判斷的)。就目前這種情況來說,URL指向的是當前Web站點的`cgi-bin`目錄(當前Web站點的基礎地址是用`getDocumentBase()`設定的)。一旦Web服務器在URL中看到了一個`cgi-bin`,會接著希望在它后面跟隨了`cgi-bin`目錄內的某個程序的名字,那是我們要運行的目標程序。程序名后面是一個問號以及CGI程序會在`QUERY_STRING`環境變量中查找的一個參數字符串(馬上就要學到)。
我們發出任何形式的請求后,一般都會得到一個回應的HTML頁。但若使用Java的URL對象,我們可以攔截自CGI程序傳回的任何東西,只需從URL對象里取得一個`InputStream`(輸入數據流)即可。這是用URL對象的`openStream()`方法實現,它要封裝到一個`DataInputStream`里。隨后就可以讀取數據行,若`readLine()`返回一個null(空值),就表明CGI程序已結束了它的輸出。
我們即將看到的CGI程序返回的僅僅是一行,它是用于標志成功與否(以及失敗的具體原因)的一個字符串。這一行會被捕獲并置放第二個`Label`字段里,使用戶看到具體發生了什么事情。
(1) 從程序片里顯示一個Web頁
程序亦可將CGI程序的結果作為一個Web頁顯示出來,就象它們在普通HTML模式中運行那樣。可用下述代碼做到這一點:
```
getAppletContext().showDocument(u);
```
其中,`u`代表URL對象。這是將我們重新定向于另一個Web頁的一個簡單例子。那個頁湊巧是一個CGI程序的輸出,但可以非常方便地進入一個原始的HTML頁,所以可以構建這個程序片,令其產生一個由密碼保護的網關,通過它進入自己Web站點的特殊部分:
```
//: ShowHTML.java
import java.awt.*;
import java.applet.*;
import java.net.*;
import java.io.*;
public class ShowHTML extends Applet {
static final String CGIProgram = "MyCGIProgram";
Button send = new Button("Go");
Label l = new Label();
public void init() {
add(send);
add(l);
}
public boolean action (Event evt, Object arg) {
if(evt.target.equals(send)) {
try {
// This could be an HTML page instead of
// a CGI program. Notice that this CGI
// program doesn't use arguments, but
// you can add them in the usual way.
URL u = new URL(
getDocumentBase(),
"cgi-bin/" + CGIProgram);
// Display the output of the URL using
// the Web browser, as an ordinary page:
getAppletContext().showDocument(u);
} catch(Exception e) {
l.setText(e.toString());
}
}
else return super.action(evt, arg);
return true;
}
} ///:~
```
URL類的最大的特點就是有效地保護了我們的安全。可以同一個Web服務器建立連接,毋需知道幕后的任何東西。
## 15.6.3 用C++寫的CGI程序
經過前面的學習,大家應該能夠根據例子用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 = "Bruce@EckelObjects.com";
#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庫函數`getenv()`完成的。在`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++仍然顯得非常方便。
## 15.6.4 POST的概念
在許多應用程序中使用GET都沒有問題。但是,GET要求通過一個環境變量將自己的數據傳遞給CGI程序。但假如GET字符串過長,有些Web服務器可能用光自己的環境空間(若字符串長度超過200字符,就應開始關心這方面的問題)。CGI為此提供了一個解決方案:POST。通過POST,數據可以編碼,并按與GET相同的方法連結起來。但POST利用標準輸入將編碼過后的查詢字符串傳遞給CGI程序。我們要做的全部事情就是判斷查詢字符串的長度,而這個長度已在環境變量`CONTENT_LENGTH`中保存好了。一旦知道了長度,就可自由分配存儲空間,并從標準輸入中讀入指定數量的字符。
對一個用來控制POST的CGI程序,由`CGITools.h`提供的`Pair`和`CGI_vector`均可不加絲毫改變地使用。下面這段程序揭示了寫這樣的一個CGI程序有多么簡單。這個例子將采用“純”C++,所以`studio.h`庫被`iostream`(IO數據流)代替。對于`iostream`,我們可以使用兩個預先定義好的對象:`cin`,用于同標準輸入連接;以及`cout`,用于同標準輸出連接。有幾個辦法可從`cin`中讀入數據以及向`cout`中寫入。但下面這個程序準備采用標準方法:用`<<`將信息發給`cout`,并用一個成員函數(此時是`read()`)從`cin`中讀入數據:
```
//: POSTtest.cpp
// CGI_vector works as easily with POST as it
// does with GET. Written in "pure" C++.
#include <iostream.h>
#include "CGITools.h"
void main() {
cout << "Content-type: text/plain\n" << endl;
// For a CGI "POST," the server puts the length
// of the content string in the environment
// variable CONTENT_LENGTH:
char* clen = getenv("CONTENT_LENGTH");
if(clen == 0) {
cout << "Zero CONTENT_LENGTH" << endl;
return;
}
int len = atoi(clen);
char* query_str = new char[len + 1];
cin.read(query_str, len);
query_str[len] = '\0';
CGI_vector query(query_str);
// Test: dump all names and values
for(int i = 0; i < query.size(); i++)
cout << "query[" << i << "].name() = [" <<
query[i].name() << "], " <<
"query[" << i << "].value() = [" <<
query[i].value() << "]" << endl;
delete query_str; // Release storage
} ///:~
```
`getenv()`函數返回指向一個字符串的指針,那個字符串指示著內容的長度。若指針為零,表明`CONTENT_LENGTH`環境變量尚未設置,所以肯定某個地方出了問題。否則就必須用ANSI C庫函數`atoi()`將字符串轉換成一個整數。這個長度將與`new`一起運用,分配足夠的存儲空間,以便容納查詢字符串(另加它的空中止符)。隨后為`cin()`調用`read()`。`read()`函數需要取得指向目標緩沖區的一個指針以及要讀入的字節數。隨后用空字符(`null`)中止`query_str`,指出已經抵達字符串的末尾,這就叫作“空中止”。
到這個時候,我們得到的查詢字符串與GET查詢字符串已經沒有什么區別,所以把它傳遞給用于`CGI_vector`的構造器。隨后便和前例一樣,我們可以自由·內不同的字段。
為測試這個程序,必須把它編譯到主機Web服務器的`cgi-bin`目錄下。然后就可以寫一個簡單的HTML頁進行測試,就象下面這樣:
```
<HTML>
<HEAD>
<META CONTENT="text/html">
<TITLE>A test of standard HTML POST</TITLE>
</HEAD>
Test, uses standard html POST
<Form method="POST" ACTION="/cgi-bin/POSTtest">
<P>Field1: <INPUT TYPE = "text" NAME = "Field1"
VALUE = "" size = "40"></p>
<P>Field2: <INPUT TYPE = "text" NAME = "Field2"
VALUE = "" size = "40"></p>
<P>Field3: <INPUT TYPE = "text" NAME = "Field3"
VALUE = "" size = "40"></p>
<P>Field4: <INPUT TYPE = "text" NAME = "Field4"
VALUE = "" size = "40"></p>
<P>Field5: <INPUT TYPE = "text" NAME = "Field5"
VALUE = "" size = "40"></p>
<P>Field6: <INPUT TYPE = "text" NAME = "Field6"
VALUE = "" size = "40"></p>
<p><input type = "submit" name = "submit" > </p>
</Form>
</HTML>
```
填好這個表單并提交出去以后,會得到一個簡單的文本頁,其中包含了解析出來的結果。從中可知道CGI程序是否在正常工作。
當然,用一個程序片來提交數據顯得更有趣一些。然而,POST數據的提交屬于一個不同的過程。在用常規方式調用了CGI程序以后,必須另行建立與服務器的一個連接,以便將查詢字符串反饋給它。服務器隨后會進行一番處理,再通過標準輸入將查詢字符串反饋回CGI程序。
為建立與服務器的一個直接連接,必須取得自己創建的URL,然后調用`openConnection()`創建一個`URLConnection`。但是,由于`URLConnection`一般不允許我們把數據發給它,所以必須很可笑地調用`setDoOutput(true`)函數,同時調用的還包括`setDoInput(true)`以及`setAllowUserInteraction(false)`——注釋⑥。最后,可調用`getOutputStream()`來創建一個`OutputStream`(輸出數據流),并把它封裝到一個`DataOutputStream`里,以便能按傳統方式同它通信。下面列出的便是一個用于完成上述工作的程序片,必須在從它的各個字段里收集了數據之后再執行它:
```
//: POSTtest.java
// An applet that sends its data via a CGI POST
import java.awt.*;
import java.applet.*;
import java.net.*;
import java.io.*;
public class POSTtest extends Applet {
final static int SIZE = 10;
Button submit = new Button("Submit");
TextField[] t = new TextField[SIZE];
String query = "";
Label l = new Label();
TextArea ta = new TextArea(15, 60);
public void init() {
Panel p = new Panel();
p.setLayout(new GridLayout(t.length + 2, 2));
for(int i = 0; i < t.length; i++) {
p.add(new Label(
"Field " + i + " ", Label.RIGHT));
p.add(t[i] = new TextField(30));
}
p.add(l);
p.add(submit);
add("North", p);
add("South", ta);
}
public boolean action (Event evt, Object arg) {
if(evt.target.equals(submit)) {
query = "";
ta.setText("");
// Encode the query from the field data:
for(int i = 0; i < t.length; i++)
query += "Field" + i + "=" +
URLEncoder.encode(
t[i].getText().trim()) +
"&";
query += "submit=Submit";
// Send the name using CGI's POST process:
try {
URL u = new URL(
getDocumentBase(), "cgi-bin/POSTtest");
URLConnection urlc = u.openConnection();
urlc.setDoOutput(true);
urlc.setDoInput(true);
urlc.setAllowUserInteraction(false);
DataOutputStream server =
new DataOutputStream(
urlc.getOutputStream());
// Send the data
server.writeBytes(query);
server.close();
// Read and display the response. You
// cannot use
// getAppletContext().showDocument(u);
// to display the results as a Web page!
DataInputStream in =
new DataInputStream(
urlc.getInputStream());
String s;
while((s = in.readLine()) != null) {
ta.appendText(s + "\n");
}
in.close();
}
catch (Exception e) {
l.setText(e.toString());
}
}
else return super.action(evt, arg);
return true;
}
} ///:~
```
⑥:我不得不說自己并沒有真正理解這兒都發生了什么事情,這些概念都是從Elliotte Rusty Harold編著的《Java Network Programming》里得來的,該書由O'Reilly于1997年出版。他在書中提到了Java連網函數庫中出現的許多令人迷惑的Bug。所以一旦涉足這些領域,事情就不是編寫代碼,然后讓它自己運行那么簡單。一定要警惕潛在的陷阱!
信息發送到服務器后,我們調用`getInputStream()`,并把返回值封裝到一個`DataInputStream`里,以便自己能讀取結果。要注意的一件事情是結果以文本行的形式顯示在一個`TextArea`(文本區域)中。為什么不簡單地使用`getAppletContext().showDocument(u)`呢?事實上,這正是那些陷阱中的一個。上述代碼可以很好地工作,但假如試圖換用`showDocument()`,幾乎一切都會停止運行。也就是說,`showDocument()`確實可以運行,但從`POSTtest`得到的返回結果是`Zero CONTENT_LENGTH`(內容長度為零)。所以不知道為什么原因,`showDocument()`阻止了POST查詢向CGI程序的傳遞。我很難判斷這到底是一個在以后版本里會修復的Bug,還是由于我的理解不夠(我看過的書對此講得都很模糊)。但無論在哪種情況下,只要能堅持在文本區域里觀看自CGI程序返回的內容,上述程序片運行時就沒有問題。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物