- 写在前面的话
- 引言
- 第 1 章 对象入门
- 第 2 章 一切都是对象
- 第 3 章 控制程序流程
- 第 4 章 初始化和清除
- 第 5 章 隐藏实施过程
- 第 6 章 类再生
- 第 7 章 多形性
- 第 8 章 对象的容纳
- 第 9 章 违例差错控制
- 第 10 章 Java IO 系统
- 第 11 章 运行期类型鉴定
- 第 12 章 传递和返回对象
- 第 十三 章 创建窗口和程序片
- 第 14 章 多线程
- 第 15 章 网络编程
- 第 16 章 设计范式
- 第 17 章 项目
- 附录 A 使用非 JAVA 代码
- 附录 B 对比 C++和 Java
- 附录 C Java 编程规则
- 附录 D 性能
- 附录 E 关于垃圾收集的一些话
- 附录 F 推荐读物
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 库函数 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++仍然显得非常方便。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论