- 写在前面的话
- 引言
- 第 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.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 地址,程序会检查列表中的名字,判断是否存在那个地址。若不存在,就将其加入,并报告操作成功。但假如名字已在列表里了,就需要指出这一点,避免重复加入。大家不必担心自己不能完全理解下列代码的含义。它仅仅是一个演示程序,告诉你如何用其他语言写一个程序,并从 Java 中调用它。在这里具体采用何种语言并不重要,只要能够从标准输入中读取数据,并能写入标准输出即可。
//: Listmgr.c // Used by NameCollector.java to manage // the email list file on the server #include <stdio.h> #include <stdlib.h> #include <string.h> #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)) { printf("Already in list: %s", buf); fflush(stdout); } else { fseek(list, 0, SEEK_END); fprintf(list, "%s\n", buf); fflush(list); printf("%s added to list", buf); fflush(stdout); } } } ///:~
该程序假设 C 编译器能接受'//'样式注释(许多编译器都能,亦可换用一个 C++编译器来编译这个程序)。如果你的编译器不能接受,则简单地将那些注释删掉即可。
文件中的第一个函数检查我们作为第二个参数(指向一个 char 的指针)传递给它的名字是否已在文件中。在这儿,我们将文件作为一个 FILE 指针传递,它指向一个已打开的文件(文件是在 main() 中打开的)。函数 fseek() 在文件中遍历;我们在这儿用它移至文件开头。fgets() 从文件 list 中读入一行内容,并将其置入缓冲区 lbuf——不会超过规定的缓冲区长度 BSIZE。所有这些工作都在一个 while 循环中进行,所以文件中的每一行都会读入。接下来,用 strchr() 找到新行字符,以便将其删掉。最后,用 strcmp() 比较我们传递给函数的名字与文件中的当前行。若找到一致的内容,strcmp() 会返回 0。函数随后会退出,并返回一个 1,指出该名字已经在文件里了(注意这个函数找到相符内容后会立即返回,不会把时间浪费在检查列表剩余内容的上面)。如果找遍列表都没有发现相符的内容,则函数返回 0。
在 main() 中,我们用 fopen() 打开文件。第一个参数是文件名,第二个是打开文件的方式;a+表示“追加”,以及“打开”(或“创建”,假若文件尚不存在),以便到文件的末尾进行更新。fopen() 函数返回的是一个 FILE 指针;若为 0,表示打开操作失败。此时需要用 perror() 打印一条出错提示消息,并用 exit() 中止程序运行。
如果文件成功打开,程序就会进入一个无限循环。调用 gets(buf) 的函数会从标准输入中取出一行(记住标准输入会与 Java 程序连接到一起),并将其置入缓冲区 buf 中。缓冲区的内容随后会简单地传递给 alreadyInList() 函数,如内容已在列表中,printf() 就会将那条消息发给标准输出(Java 程序正在监视它)。fflush() 用于对输出缓冲区进行刷新。
如果名字不在列表中,就用 fseek() 移到列表末尾,并用 fprintf() 将名字“打印”到列表末尾。随后,用 printf() 指出名字已成功加入列表(同样需要刷新标准输出),无限循环返回,继续等候一个新名字的进入。
记住一般不能先在自己的计算机上编译此程序,再把编译好的内容上载到 Web 服务器,因为那台机器使用的可能是不同类的处理器和操作系统。例如,我的 Web 服务器安装的是 Intel 的 CPU,但操作系统是 Linux,所以必须先下载源码,再用远程命令(通过 telnet)指挥 Linux 自带的 C 编译器,令其在服务器端编译好程序。
2. Java 程序
这个程序先启动上述的 C 程序,再建立必要的连接,以便同它“交谈”。随后,它创建一个数据报套接字,用它“监视”或者“侦听”来自程序片的数据报包。
//: NameCollector.java // Extracts email names from datagrams and stores // them inside a file, using Java 1.02. import java.net.*; import java.io.*; import java.util.*; public class NameCollector { final static int COLLECTOR_PORT = 8080; final static int BUFFER_SIZE = 1000; byte[] buf = new byte[BUFFER_SIZE]; DatagramPacket dp = new DatagramPacket(buf, buf.length); // Can listen & send on the same socket: DatagramSocket socket; Process listmgr; PrintStream nameList; DataInputStream addResult; public NameCollector() { try { listmgr = Runtime.getRuntime().exec("listmgr.exe"); nameList = new PrintStream( new BufferedOutputStream( listmgr.getOutputStream())); addResult = new DataInputStream( new BufferedInputStream( listmgr.getInputStream())); } catch(IOException e) { System.err.println( "Cannot start listmgr.exe"); System.exit(1); } try { socket = new DatagramSocket(COLLECTOR_PORT); System.out.println( "NameCollector Server started"); while(true) { // Block until a datagram appears: socket.receive(dp); String rcvd = new String(dp.getData(), 0, 0, dp.getLength()); // Send to listmgr.exe standard input: nameList.println(rcvd.trim()); nameList.flush(); byte[] resultBuf = new byte[BUFFER_SIZE]; int byteCount = addResult.read(resultBuf); if(byteCount != -1) { String result = new String(resultBuf, 0).trim(); // Extract the address and port from // the received datagram to find out // where to send the reply: InetAddress senderAddress = dp.getAddress(); int senderPort = dp.getPort(); byte[] echoBuf = new byte[BUFFER_SIZE]; result.getBytes( 0, byteCount, echoBuf, 0); DatagramPacket echo = new DatagramPacket( echoBuf, echoBuf.length, senderAddress, senderPort); socket.send(echo); } else System.out.println( "Unexpected lack of result from " + "listmgr.exe"); } } catch(SocketException e) { System.err.println("Can't open socket"); System.exit(1); } catch(IOException e) { System.err.println("Communication error"); e.printStackTrace(); } } public static void main(String[] args) { new NameCollector(); } } ///:~
NameCollector 中的第一个定义应该是大家所熟悉的:选定端口,创建一个数据报包,然后创建指向一个 DatagramSocket 的句柄。接下来的三个定义负责与 C 程序的连接:一个 Process 对象是 C 程序由 Java 程序启动之后返回的,而且那个 Process 对象产生了 InputStream 和 OutputStream,分别代表 C 程序的标准输出和标准输入。和 Java IO 一样,它们理所当然地需要“封装”起来,所以我们最后得到的是一个 PrintStream 和 DataInputStream。
这个程序的所有工作都是在构建器内进行的。为启动 C 程序,需要取得当前的 Runtime 对象。我们用它调用 exec(),再由后者返回 Process 对象。在 Process 对象中,大家可看到通过一简单的调用即可生成数据流:getOutputStream() 和 getInputStream()。从这个时候开始,我们需要考虑的全部事情就是将数据传给数据流 nameList,并从 addResult 中取得结果。
和往常一样,我们将 DatagramSocket 同一个端口连接到一起。在无限 while 循环中,程序会调用 receive()——除非一个数据报到来,否则 receive() 会一起处于“堵塞”状态。数据报出现以后,它的内容会提取到 String rcvd 里。我们首先将该字串两头的空格剔除(trim),再将其发给 C 程序。如下所示:
nameList.println(rcvd.trim());
之所以能这样编码,是因为 Java 的 exec() 允许我们访问任何可执行模块,只要它能从标准输入中读,并能向标准输出中写。还有另一些方式可与非 Java 代码“交谈”,这将在附录 A 中讨论。
从 C 程序中捕获结果就显得稍微麻烦一些。我们必须调用 read(),并提供一个缓冲区,以便保存结果。read() 的返回值是来自 C 程序的字节数。若这个值为-1,意味着某个地方出现了问题。否则,我们就将 resultBuf(结果缓冲区)转换成一个字串,然后同样清除多余的空格。随后,这个字串会象往常一样进入一个 DatagramPacket,并传回当初发出请求的那个同样的地址。注意发送方的地址也是我们接收到的 DatagramPacket 的一部分。
记住尽管 C 程序必须在 Web 服务器上编译,但 Java 程序的编译场所可以是任意的。这是由于不管使用的是什么硬件平台和操作系统,编译得到的字节码都是一样的。就就是 Java 的“跨平台”兼容能力。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论