返回介绍

15.5.1 服务器应用

发布于 2024-10-15 23:56:33 字数 7492 浏览 0 评论 0 收藏 0

现在讨论一下服务器应用(程序)的问题,我把它叫作 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文