返回介绍

15.2.1 一个简单的服务器和客户机程序

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

这个例子将以最简单的方式运用套接字对服务器和客户机进行操作。服务器的全部工作就是等候建立一个连接,然后用那个连接产生的 Socket 创建一个 InputStream 以及一个 OutputStream。在这之后,它从 InputStream 读入的所有东西都会反馈给 OutputStream,直到接收到行中止(END)为止,最后关闭连接。

客户机连接与服务器的连接,然后创建一个 OutputStream。文本行通过 OutputStream 发送。客户机也会创建一个 InputStream,用它收听服务器说些什么(本例只不过是反馈回来的同样的字句)。

服务器与客户机(程序)都使用同样的端口号,而且客户机利用本地主机地址连接位于同一台机器中的服务器(程序),所以不必在一个物理性的网络里完成测试(在某些配置环境中,可能需要同真正的网络建立连接,否则程序不能工作——尽管实际并不通过那个网络通信)。

下面是服务器程序:

//: JabberServer.java
// Very simple server that just
// echoes whatever the client sends.
import java.io.*;
import java.net.*;

public class JabberServer {  
  // Choose a port outside of the range 1-1024:
  public static final int PORT = 8080;
  public static void main(String[] args) 
      throws IOException {
    ServerSocket s = new ServerSocket(PORT);
    System.out.println("Started: " + s);
    try {
      // Blocks until a connection occurs:
      Socket socket = s.accept();
      try {
        System.out.println(
          "Connection accepted: "+ socket);
        BufferedReader in = 
          new BufferedReader(
            new InputStreamReader(
              socket.getInputStream()));
        // Output is automatically flushed
        // by PrintWriter:
        PrintWriter out = 
          new PrintWriter(
            new BufferedWriter(
              new OutputStreamWriter(
                socket.getOutputStream())),true);
        while (true) {  
          String str = in.readLine();
          if (str.equals("END")) break;
          System.out.println("Echoing: " + str);
          out.println(str);
        }
      // Always close the two sockets...
      } finally {
        System.out.println("closing...");
        socket.close();
      }
    } finally {
      s.close();
    }
  } 
} ///:~

可以看到,ServerSocket 需要的只是一个端口编号,不需要 IP 地址(因为它就在这台机器上运行)。调用 accept() 时,方法会暂时陷入停顿状态(堵塞),直到某个客户尝试同它建立连接。换言之,尽管它在那里等候连接,但其他进程仍能正常运行(参考第 14 章)。建好一个连接以后,accept() 就会返回一个 Socket 对象,它是那个连接的代表。

清除套接字的责任在这里得到了很艺术的处理。假如 ServerSocket 构建器失败,则程序简单地退出(注意必须保证 ServerSocket 的构建器在失败之后不会留下任何打开的网络套接字)。针对这种情况,main() 会“掷”出一个 IOException 违例,所以不必使用一个 try 块。若 ServerSocket 构建器成功执行,则其他所有方法调用都必须到一个 try-finally 代码块里寻求保护,以确保无论块以什么方式留下,ServerSocket 都能正确地关闭。

同样的道理也适用于由 accept() 返回的 Socket。若 accept() 失败,那么我们必须保证 Socket 不再存在或者含有任何资源,以便不必清除它们。但假若执行成功,则后续的语句必须进入一个 try-finally 块内,以保障在它们失败的情况下,Socket 仍能得到正确的清除。由于套接字使用了重要的非内存资源,所以在这里必须特别谨慎,必须自己动手将它们清除(Java 中没有提供“破坏器”来帮助我们做这件事情)。

无论 ServerSocket 还是由 accept() 产生的 Socket 都打印到 System.out 里。这意味着它们的 toString 方法会得到自动调用。这样便产生了:

ServerSocket[addr=0.0.0.0,PORT=0,localport=8080]
Socket[addr=127.0.0.1,PORT=1077,localport=8080]

大家不久就会看到它们如何与客户程序做的事情配合。

程序的下一部分看来似乎仅仅是打开文件,以便读取和写入,只是 InputStream 和 OutputStream 是从 Socket 对象创建的。利用两个“转换器”类 InputStreamReader 和 OutputStreamWriter,InputStream 和 OutputStream 对象已经分别转换成为 Java 1.1 的 Reader 和 Writer 对象。也可以直接使用 Java1.0 的 InputStream 和 OutputStream 类,但对输出来说,使用 Writer 方式具有明显的优势。这一优势是通过 PrintWriter 表现出来的,它有一个过载的构建器,能获取第二个参数——一个布尔值标志,指向是否在每一次 println() 结束的时候自动刷新输出(但不适用于 print() 语句)。每次写入了输出内容后(写进 out),它的缓冲区必须刷新,使信息能正式通过网络传递出去。对目前这个例子来说,刷新显得尤为重要,因为客户和服务器在采取下一步操作之前都要等待一行文本内容的到达。若刷新没有发生,那么信息不会进入网络,除非缓冲区满(溢出),这会为本例带来许多问题。

编写网络应用程序时,需要特别注意自动刷新机制的使用。每次刷新缓冲区时,必须创建和发出一个数据包(数据封)。就目前的情况来说,这正是我们所希望的,因为假如包内包含了还没有发出的文本行,服务器和客户机之间的相互“握手”就会停止。换句话说,一行的末尾就是一条消息的末尾。但在其他许多情况下,消息并不是用行分隔的,所以不如不用自动刷新机制,而用内建的缓冲区判决机制来决定何时发送一个数据包。这样一来,我们可以发出较大的数据包,而且处理进程也能加快。

注意和我们打开的几乎所有数据流一样,它们都要进行缓冲处理。本章末尾有一个练习,清楚展现了假如我们不对数据流进行缓冲,那么会得到什么样的后果(速度会变慢)。

无限 while 循环从 BufferedReader in 内读取文本行,并将信息写入 System.out,然后写入 PrintWriter.out。注意这可以是任何数据流,它们只是在表面上同网络连接。

客户程序发出包含了"END"的行后,程序会中止循环,并关闭 Socket。

下面是客户程序的源码:

//: JabberClient.java
// Very simple client that just sends
// lines to the server and reads lines
// that the server sends.
import java.net.*;
import java.io.*;

public class JabberClient {
  public static void main(String[] args) 
      throws IOException {
    // Passing null to getByName() produces the
    // special "Local Loopback" IP address, for
    // testing on one machine w/o a network:
    InetAddress addr = 
      InetAddress.getByName(null);
    // Alternatively, you can use 
    // the address or name:
    // InetAddress addr = 
    //    InetAddress.getByName("127.0.0.1");
    // InetAddress addr = 
    //    InetAddress.getByName("localhost");
    System.out.println("addr = " + addr);
    Socket socket = 
      new Socket(addr, JabberServer.PORT);
    // Guard everything in a try-finally to make
    // sure that the socket is closed:
    try {
      System.out.println("socket = " + socket);
      BufferedReader in =
        new BufferedReader(
          new InputStreamReader(
            socket.getInputStream()));
      // Output is automatically flushed
      // by PrintWriter:
      PrintWriter out =
        new PrintWriter(
          new BufferedWriter(
            new OutputStreamWriter(
              socket.getOutputStream())),true);
      for(int i = 0; i < 10; i ++) {
        out.println("howdy " + i);
        String str = in.readLine();
        System.out.println(str);
      }
      out.println("END");
    } finally {
      System.out.println("closing...");
      socket.close();
    }
  }
} ///:~

在 main() 中,大家可看到获得本地主机 IP 地址的 InetAddress 的三种途径:使用 null,使用 localhost,或者直接使用保留地址 127.0.0.1。当然,如果想通过网络同一台远程主机连接,也可以换用那台机器的 IP 地址。打印出 InetAddress addr 后(通过对 toString() 方法的自动调用),结果如下:

localhost/127.0.0.1

通过向 getByName() 传递一个 null,它会默认寻找 localhost,并生成特殊的保留地址 127.0.0.1。注意在名为 socket 的套接字创建时,同时使用了 InetAddress 以及端口号。打印这样的某个 Socket 对象时,为了真正理解它的含义,请记住一次独一无二的因特网连接是用下述四种数据标识的:clientHost(客户主机)、clientPortNumber(客户端口号)、serverHost(服务主机)以及 serverPortNumber(服务端口号)。服务程序启动后,会在本地主机(127.0.0.1)上建立为它分配的端口(8080)。一旦客户程序发出请求,机器上下一个可用的端口就会分配给它(这种情况下是 1077),这一行动也在与服务程序相同的机器(127.0.0.1)上进行。现在,为了使数据能在客户及服务程序之间来回传送,每一端都需要知道把数据发到哪里。所以在同一个“已知”服务程序连接的时候,客户会发出一个“返回地址”,使服务器程序知道将自己的数据发到哪儿。我们在服务器端的示范输出中可以体会到这一情况:

Socket[addr=127.0.0.1,port=1077,localport=8080]

这意味着服务器刚才已接受了来自 127.0.0.1 这台机器的端口 1077 的连接,同时监听自己的本地端口(8080)。而在客户端:

Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]

这意味着客户已用自己的本地端口 1077 与 127.0.0.1 机器上的端口 8080 建立了 连接。

大家会注意到每次重新启动客户程序的时候,本地端口的编号都会增加。这个编号从 1025(刚好在系统保留的 1-1024 之外)开始,并会一直增加下去,除非我们重启机器。若重新启动机器,端口号仍然会从 1025 开始增值(在 Unix 机器中,一旦超过保留的套按字范围,数字就会再次从最小的可用数字开始)。

创建好 Socket 对象后,将其转换成 BufferedReader 和 PrintWriter 的过程便与在服务器中相同(同样地,两种情况下都要从一个 Socket 开始)。在这里,客户通过发出字串"howdy",并在后面跟随一个数字,从而初始化通信。注意缓冲区必须再次刷新(这是自动发生的,通过传递给 PrintWriter 构建器的第二个参数)。若缓冲区没有刷新,那么整个会话(通信)都会被挂起,因为用于初始化的“howdy”永远不会发送出去(缓冲区不够满,不足以造成发送动作的自动进行)。从服务器返回的每一行都会写入 System.out,以验证一切都在正常运转。为中止会话,需要发出一个"END"。若客户程序简单地挂起,那么服务器会“掷”出一个违例。

大家在这里可以看到我们采用了同样的措施来确保由 Socket 代表的网络资源得到正确的清除,这是用一个 try-finally 块实现的。

套接字建立了一个“专用”连接,它会一直持续到明确断开连接为止(专用连接也可能间接性地断开,前提是某一端或者中间的某条链路出现故障而崩溃)。这意味着参与连接的双方都被锁定在通信中,而且无论是否有数据传递,连接都会连续处于开放状态。从表面看,这似乎是一种合理的连网方式。然而,它也为网络带来了额外的开销。本章后面会介绍进行连网的另一种方式。采用那种方式,连接的建立只是暂时的。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

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

发布评论

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