編程|伺服器|網路
Java的Socket API提供了一個很方便的對象介面進行網路編程。本文用一個簡單的TCP Echo Server做例子,示範了如何使用Java完成一個網路伺服器。
用作例子的TCP Echo Server是按以下方式工作的:
當一個用戶端通過TCP串連到伺服器後,用戶端可以通過這個串連發送資料到服務端,而服務端接收到資料後會把這些資料用同一個TCP串連發送回用戶端。服務端會一直保持這個串連直到用戶端關閉它為止。
因為伺服器需要能同時處理多個用戶端,我們先選用一個常見的多線程服務模型:
讓一個Thread負責監聽服務連接埠,當有新的串連建立的時候,這個監聽的Thread會為這個串連建立一個新的Thread來處理它。這樣,伺服器可以接受多個串連,並讓多個Thread來分別處理它們。
以下是相應的服務端程式:
|
public class EchoServer implements Runnable { public void run() { try { ServerSocket svr = new ServerSocket(7); while (true) { Socket sock = svr.accept(); new Thread(new EchoSession(sock)).start(); } } catch (IOException ex) { throw new ExceptionAdapter(ex); } } } |
這段代碼先建立了一個ServerSocket的對象並讓其監聽在TCP連接埠7上,然後在一個迴圈中用accept()方法接收新的串連,並建立處理這一串連的Thread。實際處理每個用戶端串連的邏輯包含在EchoSession這個類裡面。
在以上代碼中使用了ExceptionAdapter這個類,它的作用是把一個checked Exception封裝成RuntimeException。詳細的說明可以參考避免在Java中使用Checked Exception 一文。
以下是EchoSession的代碼:
|
public class EchoSession implements Runnable { public EchoSession(Socket s) { _sock = s; } public void run() { |
|
try { try { InputStream input = _sock.getInputStream(); OutputStream output = _sock.getOutputStream(); byte [] buf = new byte [128]; while (true) { int count = input.read(buf); if (count == -1) break; output.write(buf, 0 , count); } } finally { _sock.close(); } } catch (IOException ex) { throw new ExceptionAdapter(ex); } } protected Socket _sock = null; } |
EchoSession接受一個Socket對象作為構造參數,在其run()方法中,它不停的從這個Socket對象的InputStream裡面讀資料並寫回到該Socket的OutputStream中去,直到這個串連被用戶端關閉為止(InputStream的read方法返回-1)。
EchoSession需要一個線程來執行,這容易讓人聯想到用Thread來作為EchoSession的父類。不過,這樣做不夠靈活,開銷也比較大。而選擇讓EchoSession實現Runnable介面就靈活得多。在接下來的使用Thread Pool的Echo Server中可以看到這一點。
以上已經是一個完整的TCP Echo Server,不過隨著客戶不停的串連和斷開,這個伺服器會不停的產生和消除線程,而這兩個都是比較‘昂貴’的操作。為了避免這種消耗,可以考慮採用Thread Pool的機制。
使用在一個簡單的Thread緩衝池的實現一文中Thread Pool的實現,可以對EchoServer作如下修改(EchoSession無需做修改):
|
public class EchoServer implements Runnable { public void run() { try { ServerSocket svr = new ServerSocket(7); |
|
// 初始化Thread Pool SyncQueue queue = new SyncQueue(10); for (int i = 0; i < 10; i ++) { new Thread(new Worker(queue)).start(); } while (true) { Socket sock = svr.accept(); // 把任務放入Thread Pool queue.put(new EchoSession(sock)); } } catch (IOException ex) { throw new ExceptionAdapter(ex); } } } |
這裡可以看出讓EchoSession實現Runnable介面的靈活性,無需修改它就可以在Thread Pool裡使用。
在這個例子裡使用的Thread Pool比較簡單,沒有動態調整Thread數量的功能,所以這個Echo Server最多隻能同時服務10個用戶端。然而通過重載SyncQueue,我們可以很方便地加入這個功能以突破這個限制。
在對網路伺服器的效能以及並發度要求很高的時候,讓每個用戶端由一個專門的Thread來處理有可能不能滿足我們的要求(想象一下同時有數千個用戶端的情況)。這時可以考慮使用Java的NIO API來構建伺服器架構,因為NIO中IO操作都是非阻塞的,我們只需要很少的Thread就可以充分地利用CPU來處理多個用戶端的請求。關於NIO的話題,在這篇文章就不再贅述,希望以後能有機會討論。 :)