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.net.*; 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: 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(); } } } ///:~
每次有新客戶請求建立一個連接時,ServeOneJabber線程都會取得由accept()在main()中生成的Socket對象。然後和往常一樣,它創建一個BufferedReader,並用Socket自動刷新PrintWriter對象。最後,它調用Thread的特殊方法start(),令其進行線程的初始化,然後調用run()。這裡采取的操作與前例是一樣的:從套掃字讀入某些東西,然後把它原樣反饋回去,直到遇到一個特殊的"END"結束標志為止。
同樣地,套接字的清除必須進行謹慎的設計。就目前這種情況來說,套接字是在ServeOneJabber外部創建的,所以清除工作可以“共享”。若ServeOneJabber構建器失敗,那麼只需向調用者“擲”出一個違例即可,然後由調用者負責線程的清除。但假如構建器成功,那麼必須由ServeOneJabber對象負責線程的清除,這是在它的run()裡進行的。
請注意MultiJabberServer有多麼簡單。和以前一樣,我們創建一個ServerSocket,並調用accept()允許一個新連接的建立。但這一次,accept()的返回值(一個套接字)將傳遞給用於ServeOneJabber的構建器,由它創建一個新線程,並對那個連接進行控制。連接中斷後,線程便可簡單地消失。
如果ServerSocket創建失敗,則再一次通過main()擲出違例。如果成功,則位於外層的try-finally代碼塊可以擔保正確的清除。位於內層的try-catch塊只負責防范ServeOneJabber構建器的失敗;若構建器成功,則ServeOneJabber線程會將對應的套接字關掉。
為了證實服務器代碼確實能為多名客戶提供服務,下面這個程序將創建許多客戶(使用線程),並同相同的服務器建立連接。每個線程的“存在時間”都是有限的。一旦到期,就留出空間以便創建一個新線程。允許創建的線程的最大數量是由final int maxthreads決定的。大家會注意到這個值非常關鍵,因為假如把它設得很大,線程便有可能耗盡資源,並產生不可預知的程序錯誤。
//: MultiJabberClient.java // Client that tests the MultiJabberServer // by starting up multiple clients. import java.net.*; import java.io.*; class JabberClientThread extends Thread { private Socket socket; private BufferedReader in; private PrintWriter out; private static int counter = 0; private int id = counter++; private static int threadcount = 0; public static int threadCount() { return threadcount; } public JabberClientThread(InetAddress addr) { System.out.println("Making client " + id); threadcount++; try { socket = new Socket(addr, MultiJabberServer.PORT); } catch(IOException e) { // If the creation of the socket fails, // nothing needs to be cleaned up. } try { in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Enable auto-flush: out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())), true); start(); } catch(IOException e) { // The socket should be closed on any // failures other than the socket // constructor: try { socket.close(); } catch(IOException e2) {} } // Otherwise the socket will be closed by // the run() method of the thread. } public void run() { try { for(int i = 0; i < 25; i++) { out.println("Client " + id + ": " + i); String str = in.readLine(); System.out.println(str); } out.println("END"); } catch(IOException e) { } finally { // Always close it: try { socket.close(); } catch(IOException e) {} threadcount--; // Ending this thread } } } public class MultiJabberClient { static final int MAX_THREADS = 40; public static void main(String[] args) throws IOException, InterruptedException { InetAddress addr = InetAddress.getByName(null); while(true) { if(JabberClientThread.threadCount() < MAX_THREADS) new JabberClientThread(addr); Thread.currentThread().sleep(100); } } } ///:~
JabberClientThread構建器獲取一個InetAddress,並用它打開一個套接字。大家可能已看出了這樣的一個套路:Socket肯定用於創建某種Reader以及/或者Writer(或者InputStream和/或OutputStream)對象,這是運用Socket的唯一方式(當然,我們可考慮編寫一、兩個類,令其自動完成這些操作,避免大量重復的代碼編寫工作)。同樣地,start()執行線程的初始化,並調用run()。在這裡,消息發送給服務器,而來自服務器的信息則在屏幕上回顯出來。然而,線程的“存在時間”是有限的,最終都會結束。注意在套接字創建好以後,但在構建器完成之前,假若構建器失敗,套接字會被清除。否則,為套接字調用close()的責任便落到了run()方法的頭上。
threadcount跟蹤計算目前存在的JabberClientThread對象的數量。它將作為構建器的一部分增值,並在run()退出時減值(run()退出意味著線程中止)。在MultiJabberClient.main()中,大家可以看到線程的數量會得到檢查。若數量太多,則多余的暫時不創建。方法隨後進入“休眠”狀態。這樣一來,一旦部分線程最後被中止,多作的那些線程就可以創建了。大家可試驗一下逐漸增大MAX_THREADS,看看對於你使用的系統來說,建立多少線程(連接)才會使您的系統資源降低到危險程度。