# 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 推荐读物