3.7 關閉伺服器
前面介紹的EchoServer伺服器都無法關閉自身,隻有依靠作業系統來強行終止伺服器程式。這種強行終止伺服器程式的方式盡管簡單友善,但是會 導緻伺服器中正在執行的任務被突然中斷。如果伺服器處理的任務不是非常重要,允許随時中斷,則可以依靠作業系統來強行終止伺服器程式;如果伺服器處理的任 務非常重要,不允許被突然中斷,則應該由伺服器自身在恰當的時刻關閉自己。
本節介紹的EchoServer伺服器就具有關閉自己的功能。它除了在8000端口監聽普通客戶程式EchoClient的連接配接外,還會在8001 端口監聽管理程式AdminClient的連接配接。當EchoServer伺服器在8001端口接收到了AdminClient發送的“shutdown” 指令時,EchoServer就會開始關閉伺服器,它不會再接收任何新的EchoClient程序的連接配接請求,對于那些已經接收但是還沒有處理的客戶連 接,則會丢棄與該客戶的通信任務,而不會把通信任務加入到線程池的工作隊列中。另外,EchoServer會等到線程池把目前工作隊列中的所有任務執行 完,才結束程式。
如例程3-10所示是EchoServer的源程式,其中關閉伺服器的任務是由shutdown- Thread線程來負責的。
例程3-10 EchoServer.java(具有關閉伺服器的功能)
package multithread4;
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
public class EchoServer {
private int port=8000;
private ServerSocket serverSocket;
private ExecutorService executorService; //線程池
private final int POOL_SIZE=4; //單個CPU時線程池中工作線程的數目
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.equals("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);
//建立線程池
executorService= Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * POOL_SIZE);
shutdownThread.start(); //啟動負責關閉伺服器的線程
System.out.println("伺服器啟動");
}
public void service() {
while (!isShutdown) {
Socket socket=null;
try {
socket = serverSocket.accept();
//可能會抛出SocketTimeoutException和SocketException
socket.setSoTimeout(60000); //把等待客戶發送資料的逾時時間設為60秒
executorService.execute(new Handler(socket));
//可能會抛出RejectedExecutionException
}catch(SocketTimeoutException e){
//不必處理等待客戶連接配接時出現的逾時異常
}catch(RejectedExecutionException e){
try{
if(socket!=null)socket.close();
}catch(IOException x){}
return;
}catch(SocketException e) {
//如果是由于在執行serverSocket.accept()方法時,
//ServerSocket被ShutdownThread線程關閉而導緻的異常,就退出service()方法
if(e.getMessage().indexOf("socket closed")!=-1)return;
}catch(IOException e) {
e.printStackTrace();
}
}
}
public static void main(String args[])throws IOException {
new EchoServer().service();
}
}
/** 負責與單個客戶通信的任務,代碼與3.6.1節的例程3-5的Handler類相同 */
class Handler implements Runnable{…}
|
shutdownThread線程負責關閉伺服器。它一直監聽8001端口,如果接收到了AdminClient發送的“shutdown”指令, 就把isShutdown變量設為true。shutdownThread線程接着執行executorService.shutdown()方法,該方 法請求關閉線程池,線程池将不再接收新的任務,但是會繼續執行完工作隊列中現有的任務。shutdownThread線程接着等待線程池關閉:
while(!executorService.isTerminated())
executorService.awaitTermination(30,TimeUnit.SECONDS); //等待30秒
|
當線程池的工作隊列中的所有任務執行完畢,executorService.isTerminated()方法就會傳回true。
shutdownThread線程接着關閉監聽8000端口的ServerSocket,最後再關閉監聽8001端口的ServerSocket。
shutdownThread線程在執行上述代碼時,主線程正在執行EchoServer的service()方法。
shutdownThread線程一系列操作會對主線程造成以下影響。
◆如果shutdownThread線程已經把isShutdown變量設為true,而主線程正準備執行service()方法的下一輪while(!isShutdown){…}循環時,由于isShutdown變量為true,就會退出循環。
◆如果shutdownThread線程已經執行了監聽8 000端口的ServerSocket的close()方法,而主線程正在執行該 ServerSocket的accept()方法,那麼該方法會抛出SocketException。EchoServer的service()方法捕獲 了該異常,在異常處理代碼塊中退出service()方法。
◆如果shutdownThread線程已經執行了executorService.shutdown()方法,而主線程正在執行 executorService.execute(…)方法,那麼該方法會抛出Rejected- ExecutionException。 EchoServer的service()方法捕獲了該異常,在異常處理代碼塊中退出service()方法。
◆如果shutdownThread線程已經把isShutdown變量設為true,但還沒有調用監聽8 000端口的ServerSocket 的close()方法,而主線程正在執行ServerSocket的accept()方法,主線程阻塞60秒後會抛出 SocketTimeoutException。在準備執行service()方法的下一輪while(!isShutdown){…}循環時,由于 isShutdown變量為true,就會退出循環。
◆由此可見,當shutdownThread線程開始執行關閉伺服器的操作時,主線程盡管不會立即終止,但是遲早會結束運作。
如例程3-11所示是AdminClient的源程式,它負責向EchoServer發送“shutdown”指令,進而關閉EchoServer。
例程3-11 AdminClient.java
package multithread4;
import java.net.*;
import java.io.*;
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){e.printStackTrace();}
}
}
}
|
下面按照以下方式運作EchoServer、EchoClient和AdminClient,以觀察EchoServer伺服器的關閉過程。EchoClient類的源程式參見本書第1章的1.5.2節的例程1-3。
(1)先運作EchoServer,然後運作AdminClient。EchoServer與AdminClient程序都結束運作,并且在AdminClient的控制台列印如下結果:
伺服器正在關閉
伺服器已經關閉,關閉伺服器用了60毫秒
|
(2)先運作EchoServer,再運作EchoClient,然後再運作AdminClient。EchoServer程式不會立即結束,因為 它與EchoClient的通信任務還沒有結束。在EchoClient的控制台中輸入“bye”, EchoServer、EchoClient和 AdminClient程序都會結束運作。
(3)先運作EchoServer,再運作EchoClient,然後再運作AdminClient。EchoServer程式不會立即結束,因為 它與EchoClient的通信任務還沒有結束。不要在EchoClient的控制台中輸入任何字元串,過60秒後,EchoServer等待 EchoClient的發送資料逾時,結束與EchoClient的通信任務,EchoServer和AdminClient程序結束運作。如果在 EchoClient的控制台再輸入字元串,則會抛出“連接配接已斷開”的SocketException。
3.8 小結
在EchoServer的構造方法中可以設定3個參數。
◆參數port:指定伺服器要綁定的端口。
◆參數backlog:指定客戶連接配接請求隊列的長度。
◆參數bindAddr:指定伺服器要綁定的IP位址。
ServerSocket的accept()方法從連接配接請求隊列中取出一個客戶的連接配接請求,然後建立與客戶連接配接的Socket對象,并将它傳回。如 果隊列中沒有連接配接請求,accept()方法就會一直等待,直到接收到了連接配接請求才傳回。SO_TIMEOUT選項表示ServerSocket的 accept()方法等待客戶連接配接請求的逾時時間,以毫秒為機關。如果SO_TIMEOUT的值為0,表示永遠不會逾時,這是SO_TIMEOUT的預設 值。可以通過ServerSocket的setSo- Timeout()方法來設定等待連接配接請求的逾時時間。如果設定了逾時時間,那麼當伺服器等待的時 間超過了逾時時間後,就會抛出SocketTimeoutException,它是Interrupted- Exception的子類。
許多實際應用要求伺服器具有同時為多個客戶提供服務的能力。用多個線程來同時為多個客戶提供服務,這是提高伺服器的并發性能的最常用的手段。本章采用3種方式來重新實作EchoServer,它們都使用了多線程:
(1)為每個客戶配置設定一個工作線程;
(2)建立一個線程池,由其中的工作線程來為客戶服務;
(3)利用java.util.concurrent包中現成的線程池,由它的工作線程來為客戶服務。
第一種方式需要頻繁地建立和銷毀線程,如果線程執行的任務本身很簡短,那麼有可能伺服器在建立和銷毀線程方面的開銷比在實際執行任務上的開銷還要 大。線程池能很好地避免這一問題。線程池先建立了若幹工作線程,每個工作線程執行完一個任務後就會繼續執行下一個任務,線程池減少了建立和銷毀線程的次 數,進而提高了伺服器的運作性能。