网页功能: 加入收藏 设为首页 网站搜索  
Java编程思想(19)
发表日期:2003-06-19作者:[] 出处:  

第15章 网络编程(下)

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++仍然显得非常方便。

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的构建器。随后便和前例一样,我们可以自由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程序返回的内容,上述程序片运行时就没有问题。

15.7 用JDBC连接数据库

据估算,将近一半的软件开发都要涉及客户(机)/服务器方面的操作。Java为自己保证的一项出色能力就是构建与平台无关的客户机/服务器数据库应用。在Java 1.1中,这一保证通过Java数据库连接(JDBC)实现了。

数据库最主要的一个问题就是各家公司之间的规格大战。确实存在一种“标准”数据库语言,即“结构查询语言”(SQL-92),但通常都必须确切知道自己要和哪家数据库公司打交道,否则极易出问题,尽管存在所谓的“标准”。JDBC是面向“与平台无关”设计的,所以在编程的时候不必关心自己要使用的是什么数据库产品。然而,从JDBC里仍有可能发出对某些数据库公司专用功能的调用,所以仍然不可任性妄为。

和Java中的许多API一样,JDBC也做到了尽量的简化。我们发出的方法调用对应于从数据库收集数据时想当然的做法:同数据库连接,创建一个语句并执行查询,然后处理结果集。

为实现这一“与平台无关”的特点,JDBC为我们提供了一个“驱动程序管理器”,它能动态维护数据库查询所需的所有驱动程序对象。所以假如要连接由三家公司开发的不同种类的数据库,就需要三个单独的驱动程序对象。驱动程序对象会在装载时由“驱动程序管理器”自动注册,并可用Class.forName()强行装载。

为打开一个数据库,必须创建一个“数据库URL”,它要指定下述三方面的内容:

(1) 用“jdbc”指出要使用JDBC。

(2) “子协议”:驱动程序的名字或者一种数据库连接机制的名称。由于JDBC的设计从ODBC吸收了许多灵感,所以可以选用的第一种子协议就是“jdbc-odbc桥”,它用“odbc”关键字即可指定。

(3) 数据库标识符:随使用的数据库驱动程序的不同而变化,但一般都提供了一个比较符合逻辑的名称,由数据库管理软件映射(对应)到保存了数据表的一个物理目录。为使自己的数据库标识符具有任何含义,必须用自己的数据库管理软件为自己喜欢的名字注册(注册的具体过程又随运行平台的不同而变化)。

所有这些信息都统一编译到一个字串里,即“数据库URL”。举个例子来说,若想通过ODBC子协议同一个标识为“people”的数据库连接,相应的数据库URL可设为:

String dbUrl = "jdbc:odbc:people"

如果通过一个网络连接,数据库URL也需要包含对远程机器进行标识的信息。

准备好同数据库连接后,可调用静态方法DriverManager.getConnection(),将数据库的URL以及进入那个数据库所需的用户名密码传递给它。得到的返回结果是一个Connection对象,利用它即可查询和操纵数据库。

下面这个例子将打开一个联络信息数据库,并根据命令行提供的参数查询一个人的姓(Last Name)。它只选择那些有E-mail地址的人的名字,然后列印出符合查询条件的所有人:

//: Lookup.java

// Looks up email addresses in a

// local database using JDBC

import java.sql.*;

public class Lookup {

 public static void main(String[] args) {

  String dbUrl = "jdbc:odbc:people";

  String user = "";

  String password = "";

  try {

   // Load the driver (registers itself)

   Class.forName(

    "sun.jdbc.odbc.JdbcOdbcDriver");

   Connection c = DriverManager.getConnection(

    dbUrl, user, password);

   Statement s = c.createStatement();

   // SQL code:

   ResultSet r =

    s.executeQuery(

     "SELECT FIRST, LAST, EMAIL " +

     "FROM people.csv people " +

     "WHERE " +

     "(LAST='" + args[0] + "') " +

     " AND (EMAIL Is Not Null) " +

     "ORDER BY FIRST");

   while(r.next()) {

    // Capitalization doesn't matter:

    System.out.println(

     r.getString("Last") + ", "

     + r.getString("fIRST")

     + ": " + r.getString("EMAIL") );

   }

   s.close(); // Also closes ResultSet

  } catch(Exception e) {

   e.printStackTrace();

  }

 }

} ///:~

可以看到,数据库URL的创建过程与我们前面讲述的完全一样。在该例中,数据库未设密码保护,所以用户名和密码都是空串。

用DriverManager.getConnection()建好连接后,接下来可根据结果Connection对象创建一个Statement(语句)对象,这是用createStatement()方法实现的。根据结果Statement,我们可调用executeQuery(),向其传递包含了SQL-92标准SQL语句的一个字串(不久就会看到如何自动创建这类语句,所以没必要在这里知道关于SQL更多的东西)。

executeQuery()方法会返回一个ResultSet(结果集)对象,它与继承器非常相似:next()方法将继承器移至语句中的下一条记录;如果已抵达结果集的末尾,则返回null。我们肯定能从executeQuery()返回一个ResultSet对象,即使查询结果是个空集(也就是说,不会产生一个违例)。注意在试图读取任何记录数据之前,都必须调用一次next()。若结果集为空,那么对next()的这个首次调用就会返回false。对于结果集中的每条记录,都可将字段名作为字串使用(当然还有其他方法),从而选择不同的字段。另外要注意的是字段名的大小写是无关紧要的——SQL数据库不在乎这个问题。为决定返回的类型,可调用getString(),getFloat()等等。到这个时候,我们已经用Java的原始格式得到了自己的数据库数据,接下去可用Java代码做自己想做的任何事情了。

15.7.1 让示例运行起来

就JDBC来说,代码本身是很容易理解的。最令人迷惑的部分是如何使它在自己特定的系统上运行起来。之所以会感到迷惑,是由于它要求我们掌握如何才能使JDBC驱动程序正确装载,以及如何用我们的数据库管理软件来设置一个数据库。

当然,具体的操作过程在不同的机器上也会有所区别。但这儿提供的在32位Windows环境下操作过程可有效帮助大家理解在其他平台上的操作。

1. 步骤1:寻找JDBC驱动程序

上述程序包含了下面这条语句:

Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");

这似乎暗示着一个目录结构,但大家不要被它蒙骗了。在我手上这个JDK 1.1安装版本中,根本不存在叫作JdbcOdbcDriver.class的一个文件。所以假如在看了这个例子后去寻找它,那么必然会徒劳而返。另一些人提供的例子使用的是一个假名字,如“myDriver.ClassName”,但人们从字面上得不到任何帮助。事实上,上述用于装载jdbc-odbc驱动程序(实际是与JDK 1.1配套提供的唯一驱动)的语句在联机文档的多处地方均有出现(特别是在一个标记为“JDBC-ODBC Bridge Driver”的页内)。若上面的装载语句不能工作,那么它的名字可能已随着Java新版本的发布而改变了;此时应到联机文档里寻找新的表述方式。

若装载语句出错,会在这个时候得到一个违例。为了检验驱动程序装载语句是不是能正常工作,请将该语句后面直到catch从句之间的代码暂时设为注释。如果程序运行时未出现违例,表明驱动程序的装载是正确的。

2. 步骤2:配置数据库

同样地,我们只限于在32位Windows环境中工作;您可能需要研究一下自己的操作系统,找出适合自己平台的配置方法。

首先打开控制面板。其中可能有两个图标都含有“ODBC”字样,必须选择那个“32位ODBC”,因为另一个是为了保持与16位软件的向后兼容而设置的,和JDBC混用没有任何结果。双击“32位ODBC”图标后,看到的应该是一个卡片式对话框,上面一排有多个卡片标签,其中包括“用户DSN”、“系统DSN”、“文件DSN”等等。其中,“DSN”代表“数据源名称”(Data Source Name)。它们都与JDBC-ODBC桥有关,但设置数据库时唯一重要的地方“系统DSN”。尽管如此,由于需要测试自己的配置以及创建查询,所以也需要在“文件DSN”中设置自己的数据库。这样便可让Microsoft Query工具(与Microsoft Office配套提供)正确地找到数据库。注意一些软件公司也设计了自己的查询工具。

最有趣的数据库是我们已经使用过的一个。标准ODBC支持多种文件格式,其中包括由不同公司专用的一些格式,如dBASE。然而,它也包括了简单的“逗号分隔ASCII”格式,它几乎是每种数据工具都能够生成的。就目前的例子来说,我只选择自己的“people”数据库。这是我多年来一直在维护的一个数据库,中间使用了各种联络管理工具。我把它导出成为一个逗号分隔的ASCII文件(一般有个.csv扩展名,用Outlook Express导出通信簿时亦可选用同样的文件格式)。在“文件DSN”区域,我按下“添加”按钮,选择用于控制逗号分隔ASCII文件的文本驱动程序(Microsoft Text Driver),然后撤消对“使用当前目录”的选择,以便导出数据文件时可以自行指定目录。

大家会注意到在进行这些工作的时候,并没有实际指定一个文件,只是一个目录。那是因为数据库通常是由某个目录下的一系列文件构成的(尽管也可能采用其他形式)。每个文件一般都包含了单个“数据表”,而且SQL语句可以产生从数据库中多个表摘取出来的结果(这叫作“联合”,或者join)只包含了单张表的数据库(就象目前这个)通常叫作“平面文件数据库”。对于大多数问题,如果已经超过了简单的数据存储与获取力所能及的范围,那么必须使用多个数据表。通过“联合”,从而获得希望的结果。我们把这些叫作“关系型”数据库。

3. 步骤3:测试配置

为了对配置进行测试,需用一种方式核实数据库是否可由查询它的一个程序“见到”。当然,可以简单地运行上述的JDBC示范程序,并加入下述语句:

Connection c = DriverManager.getConnection(

dbUrl, user, password);

若掷出一个违例,表明你的配置有误。

然而,此时很有必要使用一个自动化的查询生成工具。我使用的是与Microsoft Office配套提供的Microsoft Query,但你完全可以自行选择一个。查询工具必须知道数据库在什么地方,而Microsoft Query要求我进入ODBC Administrator的“文件DSN”卡片,并在那里新添一个条目。同样指定文本驱动程序以及保存数据库的目录。虽然可将这个条目命名为自己喜欢的任何东西,但最好还是使用与“系统DSN”中相同的名字。

做完这些工作后,再用查询工具创建一个新查询时,便会发现自己的数据库可以使用了。

4. 步骤4:建立自己的SQL查询

我用Microsoft Query创建的查询不仅指出目标数据库存在且次序良好,也会自动生成SQL代码,以便将其插入我自己的Java程序。我希望这个查询能够检查记录中是否存在与启动Java程序时在命令行键入的相同的“姓”(Last Name)。所以作为一个起点,我搜索自己的姓“Eckel”。另外,我希望只显示出有对应E-mail地址的那些名字。创建这个查询的步骤如下:

(1) 启动一个新查询,并使用查询向导(Query Wizard)。选择“people”数据库(等价于用适应的数据库URL打开数据库连接)。

(2) 选择数据库中的“people”表。从这张数据表中,选择FIRST,LAST和EMAIL列。

(3) 在“Filter Data”(过滤器数据库)下,选择LAST,并选择“equals”(等于),加上参数Eckel。点选“And”单选钮。

(4) 选择EMAIL,并选中“Is not Null”(不为空)。

(5) 在“Sort By”下,选择FIRST。

查询结果会向我们展示出是否能得到自己希望的东西。

现在可以按下SQL按钮。不需要我们任何方面的介入,正确的SQL代码会立即弹现出来,以便我们粘贴和复制。对于这个查询,相应的SQL代码如下:

SELECT people.FIRST, people.LAST, people.EMAIL

FROM people.csv people

WHERE (people.LAST='Eckel') AND

(people.EMAIL Is Not Null)

ORDER BY people.FIRST

若查询比较复杂,手工编码极易出错。但利用一个查询工具,就可以交互式地测试自己的查询,并自动获得正确的代码。事实上,亲手为这些事情编码是难以让人接受的。

5. 步骤5:在自己的查询中修改和粘贴

我们注意到上述代码与程序中使用的代码是有所区别的。那是由于查询工具对所有名字都进行了限定,即便涉及的仅有一个数据表(若真的涉及多个数据表,这种限定可避免来自不同表的同名数据列发生冲突)。由于这个查询只需要用到一个数据表,所以可考虑从大多数名字中删除“people”限定符,就象下面这样:

SELECT FIRST, LAST, EMAIL

FROM people.csv people

WHERE (LAST='Eckel') AND

(EMAIL Is Not Null)<

我来说两句】 【加入收藏】 【返加顶部】 【打印本页】 【关闭窗口
中搜索 Java编程思想(19)
本类热点文章
  Java读取文件中含有中文的解决办法
  Java读取文件中含有中文的解决办法
  简单加密/解密方法包装, 含encode(),de..
  EJB 3.0规范全新体验
  java简单的获取windows系统网卡mac地址
  让Java程序带着JRE一起上路
  抢先体验"野马"J2SE6.0
  Java连接各种数据库的实例
  Java连接各种数据库的实例
  JAVA的XML编程实例解析
  Java学习从入门到精通(附FAQ)
  新手必读:Java学习的捷径
最新分类信息我要发布 
最新招聘信息

关于我们 / 合作推广 / 给我留言 / 版权举报 / 意见建议 / 广告投放  
Copyright ©2003-2024 Lihuasoft.net webmaster(at)lihuasoft.net
网站编程QQ群   京ICP备05001064号 页面生成时间:0.0126