1. 程式人生 > >三. ServerSocket 用法詳解(二) .

三. ServerSocket 用法詳解(二) .

 

 本篇文章觀點和例子來自 《Java網路程式設計精解》, 作者為孫衛琴, 出版社為電子工業出版社。

      在ThreadPool 類中定義了一個LinkedList 型別的 workQueue 成員變數, 它表示工作佇列, 用來存放執行緒池要執行的任務, 每個任務都是 Runnable 例項. ThreadPool 類的客戶程式(利用 ThreadPool 來執行任務的程式) 只要呼叫 ThreadPool 類的execute(Runnable task) 方法, 就能向執行緒池提交任務. 在 ThreadPool 類的 execute() 方法中, 先判斷執行緒池是否已經關閉. 如果執行緒池已經關閉, 就不再接受任務, 負責就把任務加入到工作佇列中, 並且呼醒正在等待任務的工作執行緒.

      在 ThreadPool 的構造方法中, 會建立並啟動若干工作執行緒, 工作執行緒的數目由構造方法的引數 poolSize 決定. WorkThread 類表示工作執行緒, 它是 ThreadPool 類的內部類. 工作執行緒從工作佇列中取出一個任務, 接著執行該任務, 然後再從工作佇列中取出下一個任務並執行它, 如此反覆.

      工作執行緒從工作佇列中取任務的操作是由 ThreadPool 類的 getTask() 方法實現的, 它的處理邏輯如下:

  • 如果佇列為空並且執行緒池已關閉, 那就返回 null, 表示已經沒有任務可以執行了;
  • 如果佇列為空並且執行緒池沒有關閉, 那就在此等待, 直到其他執行緒將其呼醒或者中斷;
  • 如果佇列中有任務, 就取出第一個任務並將其返回.

      執行緒池的 join() 和 close() 方法都可用來關閉執行緒池. join() 方法確保在關閉執行緒之前, 工作執行緒把佇列中的所有任務都執行完. 而 close() 方法則立即清空佇列, 並且中斷所有的工作執行緒.

      ThreadPool 類是 ThreadGroup 類的子類, ThreadGroup 類表示執行緒組, 它提供了一些管理執行緒組中執行緒的方法. 例如, interrupt() 方法相當於呼叫執行緒組中所有活著的執行緒的 interrupt() 方法. 執行緒池中的所有工作執行緒都加入到當前 ThreadPool 物件表示的執行緒組中. ThreadPool 類在 close() 方法中呼叫了interrupt() 方法:

 /** 關閉執行緒池 */
 public synchronized void close(){
  if(!isClosed){
   isClosed = true;
   workQueue.clear();    //清空工作佇列
   interrupt();                    //中斷所有的的工作執行緒, 該方法繼承自 ThreadGroup 類
  }
 }

     以上 interrupt() 方法用於中斷所有的工作執行緒. interrupt() 方法會對工作執行緒造成以下影響:

  • 如果此時一個工作執行緒正在ThreadPool 的 getTask() 方法中因為執行 wait() 方法而阻塞, 則會丟擲 InterruptedException;
  • 如果此時一個工作執行緒正在執行任務, 並且這個任務不會被阻塞, 那麼這個工作執行緒會正常執行完任務, 但是在執行下一輪 while(!isInterrupted()){.....} 迴圈時, 由於 isInterrupted() 方法返回 true, 因此退出 while 迴圈.

     ThreadPoolTester 類用於測試 ThreadPool 的用法.

     ThreadPoolTester 略..............

     利用執行緒池ThreadPool 來完成與客戶的通訊任務的 EchoServer 類

     EchoServer.java(利用執行緒池 ThreadPool 類)

package multithread2;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class EchoServer {
 private int port = 8000;
 private ServerSocket serverSocket;
 private ThreadPool threadPool;               //執行緒池
 private final int  POOL_SIZE = 4;            //單個CPU 時執行緒池中工作執行緒的數目
 
 public EchoServer() throws IOException{
  serverSocket = new ServerSocket(port);
  //建立執行緒池
  //Runtime 的 availablePocessors() 方法返回當前系統的CPU 的數目
  //系統的CPU 越多, 執行緒池中工作執行緒的數目也越多
  threadPool = new ThreadPool(Runtime.getRuntime().availableProcessors()* POOL_SIZE);
  System.out.println("伺服器啟動");   
 }
 
 public void service(){
  while(true){
   Socket socket = null;
   try{
    socket = serverSocket.accept();
    threadPool.execute(new Handler(socket));    //把與可以通訊的任務交給執行緒池       
   }catch(IOException e){
    e.printStackTrace();
   }
  }
 }
 
 /**
  * @param args
  * @throws IOException
  */
 public static void main(String[] args) throws IOException {
  new EchoServer().service();
 }
 
 /** 負責與單個客戶通訊的任務, 程式碼與 6.1 的例子相同*/
 class Handler implements Runnable{....} 

     在以上 EchoServer 的 service() 方法中, 每接收到一個客戶連線, 就向執行緒池 ThreadPool 提交一個與客戶通訊的任務. ThreadPool 把任務加入到工作佇列中, 工作執行緒會在適當的時候從佇列中取出這個任務並執行它.

6.3 使用 JDK 類庫提供的執行緒池

      java.util.concurrent 包提供現成的執行緒池的實現, 它比 6.2 節介紹的執行緒池更加健壯, 而且功能也更強大. 如圖3-4 所示是執行緒池的類框圖.

       JDK類庫中的執行緒池的類框圖

                              圖3-4 JDK 類庫中的執行緒池的類框圖

      Executor 介面表示執行緒池, 它的 execute(Runable task) 方法用來執行 Runable 型別的任務. Executor 的子介面 ExecutorService 中聲明瞭管理執行緒池的一些方法, 比如用於關閉執行緒池的 shutdown() 方法等. Executors 類中包含一些靜態方法, 他們負責生成各種型別的執行緒池 ExecutorService 例項, 入表 3-1 所示.

                表3-1 Executors 類生成的 ExecutorService 例項的靜態方法 

Executors類的靜態方法

建立的ExecutorService執行緒池的型別

newCachedThreadPool()

在有任務時才建立新執行緒,空閒執行緒被保留60秒

newFixedThreadPool(int nThreads)

執行緒池中包含固定數目的執行緒,空閒執行緒會一直保留。引數nThreads設定執行緒池中執行緒的數目

newSingleThreadExecutor()

執行緒池中只有一個工作執行緒,它依次執行每個任務

newScheduledThreadPool(int corePoolSize)

執行緒池能按時間計劃來執行任務,允許使用者設定計劃執行任務的時間。引數corePoolSize設定執行緒池中執行緒的最小數目。當任務較多時,執行緒池可能會建立更多的工作執行緒來執行任務

newSingleThreadScheduledExecutor()

執行緒池中只有一個工作執行緒,它能按時間計劃來執行任務

   

      以下是利用上述執行緒池來負責與客戶通訊任務的 EchoServer

      EchoServer.java( 使用 java.util.concurrent 包中的執行緒池類)

package multithread3;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


public class EchoServer {

 private int port = 8000;
 private ServerSocket serverSocket;
 private ExecutorService executoService;      //執行緒池
 private final int  POOL_SIZE = 4;            //單個CPU 時執行緒池中工作執行緒的數目
 
 public EchoServer() throws IOException{
  serverSocket = new ServerSocket(port);
  //建立執行緒池
  //Runtime 的 availablePocessors() 方法返回當前系統的CPU 的數目
  //系統的CPU 越多, 執行緒池中工作執行緒的數目也越多
  executoService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()* POOL_SIZE);
  System.out.println("伺服器啟動");   
 }
 
 public void service(){
  while(true){
   Socket socket = null;
   try{
    socket = serverSocket.accept();
    executoService.execute(new Handler(socket));    //把與可以通訊的任務交給執行緒池       
   }catch(IOException e){
    e.printStackTrace();
   }
  }
 }
 public static void main(String[] args) throws IOException {
  new EchoServer().service();

 }
 
 /** 負責與單個客戶通訊的任務, 程式碼與 6.1 的例子相同*/
 class Handler implements Runnable{..}

      在 EchoServer 的構造方法中, 呼叫 Executors.newFixedThreadPool() 建立了具有固定工作執行緒數目的執行緒池. 在EchoServer 的 service() 方法中, 通過呼叫 executorService.execute() 方法, 把與客戶通訊的任務交給了 ExecutorService 執行緒池來執行. 

6.4 使用執行緒池的注意事項

      雖然執行緒池能大大提高伺服器的併發效能, 但使用它也會存在一定風險. 與所有多執行緒應用程式用於, 用執行緒池構建的應用程式容易產生各種併發問題, 如對共享資源的競爭和死鎖. 此外, 如果執行緒池本身的實現不健壯, 或者沒有合理地使用執行緒池, 還容易導致與執行緒池有關的死鎖、系統資源不足和執行緒洩露等問題.

      1. 死鎖

      任何多執行緒應用程式都有死鎖風險. 造成死鎖的最簡單的情形是, 執行緒 A 持有物件 X 的鎖, 並且在等待物件 Y 的鎖, 而執行緒 B 持有物件Y 的鎖, 並且在等待物件 X 的鎖. 執行緒 A 和 執行緒 B 都不釋放自己持有的鎖, 並且等待對方的鎖, 這就導致兩個執行緒永遠等待下去, 死鎖就這樣產生了.

      雖然任何多執行緒程式都有死鎖的風險, 但執行緒池還會導致另外一種死鎖. 在這種情況下, 假定執行緒池中的所有工作執行緒都在執行各自任務時被阻塞, 他們都在等待某個任務 A 的執行結果. 而任務 A 依然在工作佇列中, 由於沒有空閒執行緒, 使得任務 A 一直不能被執行. 這使得執行緒池中的所有工作執行緒都永遠阻塞下去, 死鎖就這樣產生了.

      2. 系統資源不足

      如果執行緒池中的執行緒數目非常多, 這些執行緒會消耗包括記憶體和其他系統資源在內的大量資源, 從而嚴重影響系統性能.

      3. 併發錯誤

      執行緒池的工作佇列依靠 wait() 和 notify() 方法來使工作執行緒及時取得任務, 但這兩個方法都難於使用. 如果編碼不正確, 可能會丟失通知, 導致工作執行緒一直保持空閒狀態, 無視工作佇列中需要處理的任務. 因此使用這些方法時, 必須格外小心, 即便是專家也可能在這方面出錯. 最好使用現有的、 比較成熟的執行緒池. 例如, 直接使用java.util.concurrent 包中的執行緒池類.

      4. 執行緒洩露

      使用執行緒池的一個嚴重風險是執行緒洩露. 對於工作執行緒數目固定的執行緒池, 如果工作執行緒在執行任務時丟擲 RuntimeException 或 Error, 並且這些異常或錯誤沒有被捕獲, 那麼這個工作執行緒就會異常終止, 使得執行緒池永遠失去了一個工作執行緒. 如果所有的工作執行緒都異常終止,  執行緒池就最終變為空, 沒有任何可用的工作執行緒來處理任務.

      導致執行緒洩露的另一種情形是, 工作執行緒在執行一個任務時被阻塞, 如等待使用者的輸入資料, 但是由於使用者一直不輸入資料(可能是因為使用者走開了), 導致這個工作執行緒一直被阻塞. 這樣的工作執行緒名存實亡, 它實際上不執行任何任務了. 假如執行緒池中所有的工作執行緒都處於這樣的阻塞狀態, 那麼執行緒池就無法處理新加入的任務了.

      5. 任務過載

      當工作佇列中有大量排隊等待執行的任務時, 這些任務本身可能會消耗太多的系統資源而引起系統資源缺乏.

      綜上所述, 執行緒池可能會帶來種種風險, 為了儘可能避免他們, 使用執行緒池時需要遵循以下原則.

        ⑴ 如果任務 A 在執行過程中需要同步等待任務 B 的執行結果, 那麼任務 A 不適合加入到執行緒池的工作佇列中. 如果把像任務 A 一樣的需要等待其他任務執行結果的任務加入到工作佇列中, 可能會導致執行緒池的死鎖.

        ⑵ 如果執行某個任務時可能會阻塞,並且是長時間的阻塞, 則應該設定超時時間, 避免工作執行緒永久的阻塞下去而導致執行緒洩露. 在伺服器程式中, 當執行緒等待客戶連線, 或者等待客戶傳送的資料時, 都可能會阻塞. 可以通過以下方式設定超時時間:

  • 呼叫 ServerSocket 的 setSoTimeout(int milliseconds)方法, 設定等待客戶連線的超時時間, 參見 5.1 節(SO_TIMEOUT 選項);
  • 對於每個與客戶連線的 Socket, 呼叫該 Socket 的 setSoTimeou(int milliseconds) 方法, 設定等待客戶傳送資料的超時時間, 參見本書第二章的 5.3 節(SO_TIMEOUT 選項) .

        ⑶ 瞭解任務的特點, 分析任務是執行經常會阻塞的 I/O 操作, 還是執行一直不會阻塞的運算操作. 前者時斷時續地佔用 CPU , 而後者對 CPU 具有更高的利用率. 預計完成任務大概需要多長時間? 是短時間任務還是長時間任務?

        根據任務的特點, 對任務進行分類, 然後把不同型別的任務分別加入到不同執行緒池的工作佇列中, 這樣可以根據任務的特點, 分別調整每個執行緒池.

        ⑷ 調整執行緒池的大小. 執行緒池的最佳大小主要取決於系統的可用 CPU 的數目, 以及工作佇列中任務的特點. 假如在一個具有N個CPU 的系統上只有一個工作佇列, 並且其中全部是運算性質(不會阻塞) 的任務, 那麼當執行緒池具有 N 或 N+1 個工作執行緒時, 一般會獲得最大的 CPU 利用率.

        如果工作佇列中包含會執行 I/O 操作並常常阻塞的任務, 則要讓執行緒池的大小超過可用的CPU 的數目, 因為並不是所有工作執行緒都一直在工作. 選擇一個典型的任務, 然後估計在執行這個任務的過程中, 等待時間( WT ) 與實際佔用 CPU 進行運算的時間( ST ) 之間的比例 WT/ST. 對於一個具有 N 個 CPU 的系統, 需要設定大約 N * (1+WT/ST) 個執行緒來保證 CPU 得到充分利用.

        當然, CPU 利用率不是調整執行緒池大小過程中唯一要考慮的事項. 隨著執行緒池中工作執行緒數目的增長, 還會碰到記憶體或者其他資源的限制, 如套接字, 開啟的檔案控制代碼或資料庫連線數目等. 要保證多執行緒消耗的系統資源在系統的承載範圍之內.

        ⑸ 避免任務過載. 伺服器應根據系統的承載能力, 限制客戶併發連線的數目. 當客戶併發連線的數目超過了限制值, 伺服器可以拒絕連線請求, 並友好地告知客戶: 伺服器正忙, 請稍後再試.

七. 關閉伺服器

      前面介紹的 EchoServer 伺服器都無法關閉自身, 只有依靠作業系統來強行終止伺服器程式. 這種強行終止伺服器程式的方式儘管簡單方便, 但是會導致伺服器中正在執行的任務被突然中斷. 如果伺服器處理的任務不是非常重要, 允許隨時中斷, 則可以依靠作業系統來強行終止伺服器程式; 如果伺服器處理的任務非常重要, 不允許被突然中斷, 則應該由伺服器自身在恰當的時刻關閉自己.

      本節介紹的 EchoServer 伺服器就具有關閉自己的功能. 它除了在 8000 埠監聽普通客戶程式 EchoClient 的連線外, 還會在 8001 埠監聽管理程式 AdminClient 的連線. 當 EchoServer 伺服器在8001 埠接收到了 AdminClient 傳送的 "shutdown" 命令時, EchoServer 就會開始關閉伺服器, 它不會再接收任何新的 EchoClient 程序的連線請求, 對於那些已經接收但是還沒有處理的客戶連線, 則會丟棄與該客戶的通訊任務, 而不會把通訊任務加入到執行緒池的工作佇列中. 另外, EchoServer 會等到執行緒池把當前工作佇列中的所有任務執行完, 才結束程式.

      下面是具有關閉伺服器功能的 EchoServer 的原始碼, 其中關閉伺服器的任務是由 shutdown-thread 執行緒來負責的.

      EchoServer.java (具有關閉伺服器的功能)

package multithread4;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;

public class EchoServer {
 private int port = 8000;
 private ServerSocket serverSocket;
 private ExecutorService executorService;
 private final int POOL_SIZE = 4;

 private int portForShutdown = 8001;                   //用於監聽關閉伺服器命令的埠
 private ServerSocket serverSocketForShutdown;        
 private boolean isShutdown = false;                   //伺服器是否關閉標誌

 private Thread shutdownThread = new Thread() {        //負責關閉伺服器的程序
  public void start() {
   this.setDaemon(true);                         //設定為守護程序(也稱為後臺程序)
   super.start();
  }

  public void run() {
   while (!isShutdown) {
    Socket socketForShutdown = null;
    try {
     socketForShutdown = serverSocketForShutdown.accept();
     BufferedReader br = new BufferedReader(
       new InputStreamReader(socketForShutdown
         .getInputStream()));

     String command = br.readLine();
     if (command != null && command.equalsIgnoreCase("shutdown")) {
      long beginTime = System.currentTimeMillis();
      socketForShutdown.getOutputStream().write(
        "伺服器正在關閉/r/n".getBytes());
      isShutdown = true;
      //請求關閉執行緒池
      //執行緒池不再接收新的任務, 但是會繼續執行完工作佇列中現有的任務
      executorService.shutdown();
      
      //等待關閉執行緒池, 每次等待的超時時間為 30 秒
      while (!executorService.isTerminated())
       executorService.awaitTermination(30, TimeUnit.SECONDS);

      serverSocket.close();          //關閉與EchoClient 客戶通訊的 ServerSocket

      long endTime = System.currentTimeMillis();
      socketForShutdown.getOutputStream().write(("伺服器已經關閉,關閉伺服器所用的時間:"
        + (endTime - beginTime) + "毫秒/r/n").getBytes());
        
      socketForShutdown.close();
      serverSocketForShutdown.close();

     }else{
      //接到其他命令的處理
      socketForShutdown.getOutputStream().write("錯誤的命令/r/n".getBytes());
      socketForShutdown.close();
     }
    } catch (Exception e) {
     e.printStackTrace();
    }
   }
  }
 };
 
 public EchoServer() throws IOException{
  serverSocket = new ServerSocket(port);
  serverSocket.setSoTimeout(60000);                  //設定等待客戶連線的超時時間為 60 秒
  
  serverSocketForShutdown = new ServerSocket(portForShutdown);    //啟動關閉伺服器的服務, 監聽 8001 埠
  shutdownThread.start();                                         //啟動負責關閉伺服器的執行緒
  
  //建立執行緒池
  executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * POOL_SIZE);
  
  System.out.println("伺服器啟動");
  
 }
 
 public void service() {
  while (!isShutdown) {

   Socket socket = null;
   try {
    socket = serverSocket.accept();
    //可能會丟擲 SocketTimeoutExcepiton 和 SocketException
    socket.setSoTimeout(60000);              //把等待客戶傳送資料的超時時間設為 60 秒
    
    //如果執行緒池被標示為停止 或者 任務為null, 執行execute()方法會丟擲 RejectedException,
    //有興趣的可以看看ThreadPoolExecutor 的execute() 和 shutdown()方法
    executorService.execute(new Handler(socket));
    
   } catch (SocketTimeoutException e) {
    //不必處理等待客戶連線時出現的超時異常
   } catch (RejectedExecutionException e) {
    //這個是執行緒池被標示為停止後, 執行executorService.execute() 丟擲的異常
    try{
     if(socket != null) socket.close();
    }catch(IOException xe){}
    return;
   }catch (SocketException e) {
    //serverSocket 被 ShutdownThread 執行緒關閉後,
    //在執行 serverSocket.accept() 方法時, 將會丟擲SocketException,
    //如果確實是這個原因導致的異常, 退出 service() 方法
    //作者是寫 socket closed, 其實應該是 Socket is closed
    if(e.getMessage().indexOf("Socket is closed") != -1) return;
   }catch (IOException e) {
    e.printStackTrace();
   }
  }

 }

 public static void main(String[] args) throws IOException {
  new EchoServer().service();

 }
}

/** 負責與單個客戶通訊的任務, 程式碼與 6.1 的例子相同 */
class Handler implements Runnable {....}

      shutdownThread 執行緒負責關閉伺服器. 它一直監聽 8001 埠,  如果接收到了 Adminclient 傳送的"shutdown" 命令, 就把 isShutdown 變數設為 true. shutdownThread 執行緒接著執行 executorService.shutdown() 方法, 該方法請求關閉執行緒池 執行緒池將不再接收新任務, 但是會繼續執行完工作佇列中現有的任務. shutdownThread 執行緒接著等待執行緒池關閉:
       while (!executorService.isTerminated())
       executorService.awaitTermination(30,TimeUnit.SECONDS);     

      當執行緒池的工作佇列中的所有任務執行完畢, executorService.isTerminated() 方法就會返回 true.

      shutdownThread 執行緒接著關閉監聽 8000 埠的 ServerSocket, 最後再關閉監聽 8001 埠的 ServerSocket.

      shutdownThread 執行緒在執行上述程式碼時, 主執行緒正在執行 EchoServer 的 service() 方法. shutdownThread 執行緒一系列操作會對主執行緒造成以下影響:

  • 如果 shutdownThread 執行緒已經把 isShutdown 變數設為 true, 而主執行緒正準備執行 service() 方法的下一輪 while(!isShutdown){...} 迴圈時, 由於 isShutdwon 變數為 true, 就會退出迴圈.
  • 如果 shutdownThread 執行緒已經執行了監聽 8000 埠的 serverSocket 的 close() 方法, 而主執行緒正在執行該 ServerSocket 的 accept() 方法, 那麼該方法會丟擲 SocketException. EchoServer 的 service() 方法捕獲了該異常, 在異常處理程式碼塊中退出了 service() 方法.
  • 如果 shutdownThread 執行緒已經執行了 executorService.shutdown() 方法, 而主執行緒正在執行 executorService.execute() 方法, 那麼該方法會丟擲 RejectedExecutionException. EchoServer 的 service() 方法捕獲了該異常, 在異常處理程式碼塊中退出 service() 方法.
  • 如果 shutdownThread 執行緒已經把 isShutdown 變數設為 true, 但還沒有呼叫監聽 8000 埠的 serverSocket 的 close() 方法, 而主執行緒正在執行 serverSocket 的 accept() 方法, 主執行緒阻塞 60 秒後會丟擲 SocketTimeoutException. 在準備執行 service() 方法的下一輪 while(!isShutdown){...} 迴圈時, 由於 isShutdown 變數為 true, 就會退出迴圈.
  • 由此可見, 當 shutdownThread 執行緒開始執行關閉伺服器的操作時, 主執行緒儘管不會立即終止, 但是遲早會結束執行.

      下面是 AdminClient 的原始碼, 它負責向 EchoServer 傳送 "shutdown" 命令, 從而關閉 EchoServer.

       AdminClient.java

package multithread4;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;

public class AdminClient {

 public static void main(String[] args) {
  Socket socket = null;
  try {
   socket = new Socket("localhost", 8001);
   //傳送關閉命令
   OutputStream socketOut = socket.getOutputStream();
   socketOut.write("shutdown/r/n".getBytes());

   //接收伺服器的反饋
   BufferedReader br = new BufferedReader(new InputStreamReader(socket
     .getInputStream()));
   String msg = null;

   while ((msg = br.readLine()) != null) {
    System.out.println(msg);
   }
  } catch (IOException e) {
   e.printStackTrace();
  } finally {
   try {
    if (socket != null)
     socket.close(); // 斷開連線
   } catch (IOException e) {
   }
  }
 }

}

      下面按照以下方式執行 EchoServer, EchoClient 和 AdminClient, 以觀察 EchoServer 伺服器的關閉過程.

       ⑴ 先執行 EchoServer , 然後執行 AdminClient. EchoServer 與 AdminClient 程序都結束執行, 並且在 AdminClient 的控制檯列印如下結果:

     伺服器正在關閉                                           
     伺服器已經關閉,關閉伺服器所用的時間:0毫秒   

       ⑵ 先執行 EchoServer, 再執行 EchoClient, 然後再執行 AdminClient. EchoServer 程式不會立即結束, 因為它與 EchoClient 的通訊任務還沒有結束. 在 EchoClient 的控制檯中輸入 "bye" , EchoServer, EchoClient 和 AdminClient 程序都會結束執行.

       ⑶ 先執行 EchoServer, 再執行 EchoClient , 然後再執行 AdminClient. EchoServe 程式不會立即結束, 因為它與 EchoClient 的通訊任務還沒有結束. 不要在 EchoClient 的控制檯輸入任何字串, 過 60 秒後, EchoServer 等待 EchoClient 的傳送資料超時, 結束與 EchoClient 的通訊任務, EchoServer 和 AdminClient 程序結束執行. 如果在 EchoClient 的控制檯再輸入字串, 則會丟擲 "連線已斷開" 的 SocketException.(最後一句有問題, EchoClient 是不會丟擲 SocketException 異常的)