友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
富士康小说网 返回本书目录 加入书签 我的书架 我的书签 TXT全本下载 『收藏到我的浏览器』

Java编程思想第4版[中文版](PDF格式)-第93部分

快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部! 如果本书没有阅读完,想下次继续接着阅读,可使用上方 "收藏到我的浏览器" 功能 和 "加入书签" 功能!



冲区内。可按第 10章介绍的方法对类进行格式化,就象对待其他任何流对象那样。  

对于Java 库的命名机制,ServerSocket (服务器套接字)的使用无疑是容易产生混淆的又一个例证。大家可 

能认为ServerSocket 最好叫作“ServerConnector”(服务器连接器),或者其他什么名字,只是不要在其 

中安插一个“Socket”。也可能以为ServerSocket 和 Socket 都应从一些通用的基础类继承。事实上,这两 

种类确实包含了几个通用的方法,但还不够资格把它们赋给一个通用的基础类。相反,ServerSocket 的主要 

任务是在那里耐心地等候其他机器同它连接,再返回一个实际的Socket。这正是“ServerSocket”这个命名 

不恰当的地方,因为它的目标不是真的成为一个Socket,而是在其他人同它连接的时候产生一个Socket 对 

象。  

然而,ServerSocket 确实会在主机上创建一个物理性的“服务器”或者侦听用的套接字。这个套接字会侦听 

进入的连接,然后利用 accept()方法返回一个“已建立”套接字(本地和远程端点均已定义)。容易混淆的 

地方是这两个套接字(侦听和已建立)都与相同的服务器套接字关联在一起。侦听套接字只能接收新的连接 

请求,不能接收实际的数据包。所以尽管 ServerSocket 对于编程并无太大的意义,但它确实是“物理性” 

的。  

创建一个ServerSocket 时,只需为其赋予一个端口编号。不必把一个 IP 地址分配它,因为它已经在自己代 

表的那台机器上了。但在创建一个 Socket 时,却必须同时赋予IP 地址以及要连接的端口编号(另一方面, 

从ServerSocket。accept()返回的 Socket 已经包含了所有这些信息)。  



15。2。1 一个简单的服务器和客户机程序  



这个例子将以最简单的方式运用套接字对服务器和客户机进行操作。服务器的全部工作就是等候建立一个连 

接,然后用那个连接产生的Socket 创建一个 InputStream 以及一个OutputStream。在这之后,它从 

InputStream读入的所有东西都会反馈给OutputStream,直到接收到行中止(END)为止,最后关闭连接。  

客户机连接与服务器的连接,然后创建一个OutputStream。文本行通过OutputStream 发送。客户机也会创 

建一个 InputStream,用它收听服务器说些什么(本例只不过是反馈回来的同样的字句)。  

服务器与客户机(程序)都使用同样的端口号,而且客户机利用本地主机地址连接位于同一台机器中的服务 



                                                               539 


…………………………………………………………Page 541……………………………………………………………

器(程序),所以不必在一个物理性的网络里完成测试(在某些配置环境中,可能需要同真正的网络建立连 

接,否则程序不能工作——尽管实际并不通过那个网络通信)。  

下面是服务器程序:  

  

//: JabberServer。java  

// Very simple server that just  

// echoes whatever the client sends。  

import java。io。*;  

import java。*;  

  

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地址(因为它就在这台机器上运行)。调用 



                                                                                          540 


…………………………………………………………Page 542……………………………………………………………

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。*;  

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  



                                                                             541 


…………………………………………………………Page 543……………………………………………………………

    // 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)上进行。现在,为了使数据能在客户及服务程序之间来回传送,每一端都需要知道把数据发到 

哪里。所以在同一个“已知”服务程序连接的时候,客户会发出一个“返回地址”,使服务器程序知道将自 



                                                                                          542 


…………………………………………………………Page 544……………………………………………………………

己的数据发到哪儿。我们在服务器端的示范输出中可以体会到这一情况:  

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块实现的。  

套接字建立了一个“专用”连接,它会一直持续到明确断开连接为止(专用连接也可能间接性地断开,前提 

是某一端或者中间的某条链路出现故障而崩溃)。这意味着参与连接的双方都被锁定在通信中,而且无论是 

否有数据传递,连接都会连续处于开放状态。从表面看,这似乎是一种合理的连网方式。然而,它也为网络 

带来了额外的开销。本章后面会介绍进行连网的另一种方式。采用那种方式,连接的建立只是暂时的。  



15。3 服务多个客户  



JabberServer 可以正常工作,但每次只能为一个客户程序提供服务。在典型的服务器中,我们希望同时能处 

理多个客户的请求。解决这个问题的关键就是多线程处理机制。而对于那些本身不支持多线程的语言,达到 

这个要求无疑是异常困难的。通过第 14 章的学习,大家已经知道Java 已对多线程的处理进行了尽可能的简 

化。由于Java 的线程处理方式非常直接,所以让服务器控制多名客户并不是件难事。  

最基本的方法是在服务器(程序)里创建单个 ServerSocket,并调用accept()来等候一个新连接。一旦 

accept()返回,我们就取得结果获得的 Socket,并用它新建一个线程,令其只为那个特定的客户服务。然后 

再调用 accept() ,等候下一次新的连接请求。  

对于下面这段服务器代码,大家可发现它与JabberServer。java 例子非常相似,只是为一个特定的客户提供 

服务的所有操作都已移入一个独立的线程类中:  

  

//: MultiJabberServer。java  

// A server that uses multithreading to handle   

// any number of clients。  

import java。io。*;  

import java。*;  

  

class ServeOneJabber extends Thread {  

  private Socket socket;  

  private BufferedReader in;  

  private PrintWriter out;  

  public ServeOneJabber(Socket s)   

      throws IOException {  

    socket = s;  

    in =   

      new BufferedReader(  

        new InputStreamReader(  

          socket。getInputStream()));  

    // Enable auto…flush:  



                                                                               543 


…………………………………………………………Page 545……………………………………………………………

    out =   

      new PrintWriter(  

        new BufferedWriter(  

          new OutputStreamWriter(  

            socket。getOutputStream())); true);  

    // If any of the above calls throw an   

    // exception; the caller is responsible for  

    // closing the socket。 Otherwise the thread  

    // will close it。  

    start(); // Calls run()  

  }  

  public void run() {  

    try {  

      while (true) {    

        String str = in。readLine();  

        if (str。equals(〃END〃)) break;  

        System。out。println(〃Echoing: 〃 + str);  

        out。println(str);  

      }  

      System。out。println(〃closing。。。〃);  

    } catch (IOException e) {  

    } finally {  

      try {  

        socket。close();  

      } catch(IOException e) {}  

    }  

  }  

}  

  

public class MultiJabberServer {    

  static final int PORT = 8080;  

  public static void main(String'' args)  

      throws IOException {  

    ServerSocket s = new ServerSocket(PORT);  

    System。out。println(〃Server Started〃);  

    try {  

      while(true) {  

        // Blocks until a connection occurs:  

        Socket socket = s。accept();  

        try {  

          new ServeOneJabber(socket);  

        } catch(IOException e) {  

          // If it fails; close the socket;  

          // otherwise the thread will close it:  

          socket。close();  

        }  

      }  

    } finally {  

      s。close();  

    }  

  }   



                                                                                             544 


…………………………………………………………Page 546………
返回目录 上一页 下一页 回到顶部 10 9
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!