三. 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 所示是執行緒池的類框圖.
圖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 異常的)