Java编程思想第4版[中文版](PDF格式)-第142部分
按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!
// clients; each of which sends datagrams。
import java。lang。Thread;
import java。*;
import java。io。*;
public class ChatterClient extends Thread {
// Can listen & send on the same socket:
private DatagramSocket s;
private InetAddress hostAddress;
private byte'' buf = new byte'1000';
private DatagramPacket dp =
new DatagramPacket(buf; buf。length);
private int id;
public ChatterClient(int identifier) {
id = identifier;
try {
// Auto…assign port number:
s = new DatagramSocket();
hostAddress =
InetAddress。getByName(〃localhost〃);
} catch(UnknownHostException e) {
System。err。println(〃Cannot find host〃);
System。exit(1);
} catch(SocketException e) {
System。err。println(〃Can't open socket〃);
e。printStackTrace();
System。exit(1);
}
System。out。println(〃ChatterClient starting〃);
}
public void run() {
try {
for(int i = 0; i 《 25; i++) {
String outMessage = 〃Client #〃 +
id + 〃; message #〃 + i;
550
…………………………………………………………Page 552……………………………………………………………
// Make and send a datagram:
s。send(Dgram。toDatagram(outMessage;
hostAddress;
ChatterServer。INPORT));
// Block until it echoes back:
s。receive(dp);
// Print out the echoed contents:
String rcvd = 〃Client #〃 + id +
〃; rcvd from 〃 +
dp。getAddress() + 〃; 〃 +
dp。getPort() + 〃: 〃 +
Dgram。toString(dp);
System。out。println(rcvd);
}
} catch(IOException e) {
e。printStackTrace();
System。exit(1);
}
}
public static void main(String'' args) {
for(int i = 0; i 《 10; i++)
new ChatterClient(i)。start();
}
} ///:~
ChatterClient 被创建成一个线程(Thread),所以可以用多个客户来“骚扰”服务器。从中可以看到,用
于接收的DatagramPacket 和用于 ChatterServer 的那个是相似的。在构建器中,创建DatagramPacket 时没
有附带任何参数(自变量),因为它不需要明确指出自己位于哪个特定编号的端口里。用于这个套接字的因
特网地址将成为“这台机器”(比如 localhost),而且会自动分配端口编号,这从输出结果即可看出。同
用于服务器的那个一样,这个 DatagramPacket 将同时用于发送和接收。
hostAddress 是我们想与之通信的那台机器的因特网地址。在程序中,如果需要创建一个准备传出去的
DatagramPacket,那么必须知道一个准确的因特网地址和端口号。可以肯定的是,主机必须位于一个已知的
地址和端口号上,使客户能启动与主机的“会话”。
每个线程都有自己独一无二的标识号(尽管自动分配给线程的端口号是也会提供一个唯一的标识符)。在
run()中,我们创建了一个String 消息,其中包含了线程的标识编号以及该线程准备发送的消息编号。我们
用这个字串创建一个数据报,发到主机上的指定地址;端口编号则直接从 ChatterServer 内的一个常数取
得。一旦消息发出,receive()就会暂时被“堵塞”起来,直到服务器回复了这条消息。与消息附在一起的所
有信息使我们知道回到这个特定线程的东西正是从始发消息中投递出去的。在这个例子中,尽管是一种“不
可靠”协议,但仍然能够检查数据报是否到去过了它们该去的地方(这在 localhost 和LAN 环境中是成立
的,但在非本地连接中却可能出现一些错误)。
运行该程序时,大家会发现每个线程都会结束。这意味着发送到服务器的每个数据报包都会回转,并反馈回
正确的接收者。如果不是这样,一个或更多的线程就会挂起并进入“堵塞”状态,直到它们的输入被显露出
来。
大家或许认为将文件从一台机器传到另一台的唯一正确方式是通过TCP 套接字,因为它们是“可靠”的。然
而,由于数据报的速度非常快,所以它才是一种更好的选择。我们只需将文件分割成多个数据报,并为每个
包编号。接收机器会取得这些数据包,并重新“组装”它们;一个“标题包”会告诉机器应该接收多少个
包,以及组装所需的另一些重要信息。如果一个包在半路“走丢”了,接收机器会返回一个数据报,告诉发
送者重传。
15。5 一个 Web 应用
现在让我们想想如何创建一个应用,令其在真实的 Web 环境中运行,它将把Java 的优势表现得淋漓尽致。这
个应用的一部分是在Web 服务器上运行的一个 Java 程序,另一部分则是一个“程序片”或“小应用程序”
551
…………………………………………………………Page 553……………………………………………………………
(Applet),从服务器下载至浏览器(即“客户”)。这个程序片从用户那里收集信息,并将其传回Web 服
务器上运行的应用程序。程序的任务非常简单:程序片会询问用户的 E…mail 地址,并在验证这个地址合格后
(没有包含空格,而且有一个@符号),将该 E…mail 发送给 Web 服务器。服务器上运行的程序则会捕获传回
的数据,检查一个包含了所有E…mail 地址的数据文件。如果那个地址已包含在文件里,则向浏览器反馈一条
消息,说明这一情况。该消息由程序片负责显示。若是一个新地址,则将其置入列表,并通知程序片已成功
添加了电子函件地址。
若采用传统方式来解决这个问题,我们要创建一个包含了文本字段及一个“提交”(Submit)按钮的HTML
页。用户可在文本字段里键入自己喜欢的任何内容,并毫无阻碍地提交给服务器(在客户端不进行任何检
查)。提交数据的同时,Web 页也会告诉服务器应对数据采取什么样的操作——知会“通用网关接口”
(CGI)程序,收到这些数据后立即运行服务器。这种CGI 程序通常是用 Perl 或C 写的(有时也用C++,但
要求服务器支持),而且必须能控制一切可能出现的情况。它首先会检查数据,判断是否采用了正确的格
式。若答案是否定的,则CGI 程序必须创建一个 HTML 页,对遇到的问题进行描述。这个页会转交给服务器,
再由服务器反馈回用户。用户看到出错提示后,必须再试一遍提交,直到通过为止。若数据正确,CGI 程序
会打开数据文件,要么把电子函件地址加入文件,要么指出该地址已在数据文件里了。无论哪种情况,都必
须格式化一个恰当的HTML 页,以便服务器返回给用户。
作为Java 程序员,上述解决问题的方法显得非常笨拙。而且很自然地,我们希望一切工作都用Java 完成。
首先,我们会用一个Java 程序片负责客户端的数据有效性校验,避免数据在服务器和客户之间传来传去,浪
费时间和带宽,同时减轻服务器额外构建HTML 页的负担。然后跳过Perl CGI 脚本,换成在服务器上运行一
个Java 应用。事实上,我们在这儿已完全跳过了 Web 服务器,仅仅需要从程序片到服务器上运行的 Java 应
用之间建立一个连接即可。
正如大家不久就会体验到的那样,尽管看起来非常简单,但实际上有一些意想不到的问题使局面显得稍微有
些复杂。用 Java 1。1 写程序片是最理想的,但实际上却经常行不通。到本书写作的时候,拥有 Java 1。1 能
力的浏览器仍为数不多,而且即使这类浏览器现在非常流行,仍需考虑照顾一下那些升级缓慢的人。所以从
安全的角度看,程序片代码最好只用Java 1。0 编写。基于这一前提,我们不能用 JAR 文件来合并(压缩)程
序片中的。class 文件。所以,我们应尽可能减少。class 文件的使用数量,以缩短下载时间。
好了,再来说说我用的 Web 服务器(写这个示范程序时用的就是它)。它确实支持Java,但仅限于 Java
1。0!所以服务器应用也必须用Java 1。0 编写。
15。5。1 服务器应用
现在讨论一下服务器应用(程序)的问题,我把它叫作NameCollecor (名字收集器)。假如多名用户同时尝
试提交他们的E…mail 地址,那么会发生什么情况呢?若 NameCollector 使用TCP/IP 套接字,那么必须运用
早先介绍的多线程机制来实现对多个客户的并发控制。但所有这些线程都试图把数据写到同一个文件里,其
中保存了所有E…mail 地址。这便要求我们设立一种锁定机制,保证多个线程不会同时访问那个文件。一个
“信号机”可在这里帮助我们达到目的,但或许还有一种更简单的方式。
如果我们换用数据报,就不必使用多线程了。用单个数据报即可“侦听”进入的所有数据报。一旦监视到有
进入的消息,程序就会进行适当的处理,并将答复数据作为一个数据报传回原先发出请求的那名接收者。若
数据报半路上丢失了,则用户会注意到没有答复数据传回,所以可以重新提交请求。
服务器应用收到一个数据报,并对它进行解读的时候,必须提取出其中的电子函件地址,并检查本机保存的
数据文件,看看里面是否已经包含了那个地址(如果没有,则添加之)。所以我们现在遇到了一个新的问
题。Java 1。0 似乎没有足够的能力来方便地处理包含了电子函件地址的文件(Java 1。1 则不然)。但是,用
C 轻易就可以解决这个问题。因此,我们在这儿有机会学习将一个非 Java 程序同 Java 程序连接的最简便方
式。程序使用的Runtime 对象包含了一个名为exec()的方法,它会独立机器上一个独立的程序,并返回一个
Process (进程)对象。我们可以取得一个OutputStream,它同这个单独程序的标准输入连接在一起;并取
得一个 InputStream,它则同标准输出连接到一起。要做的全部事情就是用任何语言写一个程序,只要它能
从标准输入中取得自己的输入数据,并将输出结果写入标准输出即可。如果有些问题不能用Java 简便与快速
地解决(或者想利用原有代码,不想改写),就可以考虑采用这种方法。亦可使用Java 的“固有方法”
(Native Method ),但那要求更多的技巧,大家可以参考一下附录A 。
1。 C 程序
这个非 Java 应用是用 C 写成,因为 Java 不适合作 CGI 编程;起码启动的时间不能让人满意。它的任务是管
理电子函件(E…mail )地址的一个列表。标准输入会接受一个E…mail 地址,程序会检查列表中的名字,判断
是否存在那个地址。若不存在,就将其加入,并报告操作成功。但假如名字已在列表里了,就需要指出这一
552
…………………………………………………………Page 554……………………………………………………………
点,避免重复加入。大家不必担心自己不能完全理解下列代码的含义。它仅仅是一个演示程序,告诉你如何
用其他语言写一个程序,并从 Java 中调用它。在这里具体采用何种语言并不重要,只要能够从标准输入中读
取数据,并能写入标准输出即可。
//: Listmgr。c
// Used by NameCollector。java to manage
// the email list file on the server
#include
#include
#include
#define BSIZE 250
int alreadyInList(FILE* list; char* name) {
char lbuf'BSIZE';
// 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(strcmp(lbuf; name) == 0)
return 1;
}
return 0;
}
int main() {
char buf'BSIZE';
FILE* list = fopen(〃emlist。txt〃; 〃a+t〃);
if(list == 0) {
perror(〃could not open emlist。txt〃);
exit(1);
}
while(1) {
gets(buf); /* From stdin */
if(alreadyInList(list; buf)) {