原文地址:http://blog.csdn.net/lidh04/archive/2008/12/11/3500708.aspx
Java Socket
套接字(socket)為兩臺計算機之間的通信提供了一種機制,在James Gosling注意到Java 語言之前,套接字就早已赫赫有名。該語言只是讓您不必了解底層操作系統的細節就能有效地使用套接字。
1 客戶機/服務器模型
在飯店里,菜單上各種具有異國情調的食品映入你的眼簾,于是你要了一份pizza。幾分鐘后,你用力咀嚼澆著融化的乳酪和其他你喜歡的配料的熱pizza。你不知道,也不想知道:侍者從那里弄來了pizza,在制作過程中加進了什么,以及配料是如何獲得的。
上例中包含的實體有:美味的pizza、接受你定餐的侍者、制作pizza的廚房,當然還有你。你是定pizza的顧客或客戶。制作pizza的過程對于你而言是被封裝的。你的請求在廚房中被處理,pizza制作完成后,由侍者端給你。
你所看到的就是一個客戶機/服務器模型。客戶機向服務器發送一個請求或命令。服務器處理客戶機的請求。客戶機和服務器之間的通訊是客戶機/服務器模型中的一個重要組成部分,通常通過網絡進行。
客戶機/服務器模型是一個應用程序開發框架,該框架是為了將數據的表示與其內部的處理和存儲分離開來而設計的。客戶機請求服務,服務器為這些請求服務。請求通過網絡從客戶機傳遞到服務器。服務器所進行的處理對客戶機而言是隱藏的。一個服務器可以為多臺客戶機服務。
多臺客戶機訪問服務器
服務器和客戶機不一定是硬件組件。它們可以是工作啊同一機器或不同機器上的程序。、
考慮一個航空定票系統中的數據輸入程序:數據----乘客名、航班號、飛行日期、目的地等可以被輸入到前端----客戶機的應用程序中。一旦數據輸入之后,客戶機將數據發送到后端----服務器端。服務器處理數據并在數據庫中保存數據。客戶機/服務器模型的重要性在于所有的數據都存放在同一地點。客戶機從不同的地方訪問同一數據源,服務器對所有的輸入數據應用同樣的檢驗規則。
萬維網為‘為什么要將數據的表示與其存儲、處理分離開來’提供了一個很好的例子。在Web上,你無需控制最終用戶用來訪問你數據的平臺和軟件。你可以考慮編寫出適用與每一種潛在的目標平臺的應用程序。
‘客戶機/服務器應用程序的服務器部分’管理通過多個客戶機訪問服務器的、多個用戶共享的資源。表明‘客戶機/服務器程序的服務器部分’強大功能的最好例子應該是Web服務器,它通過Internet將HTML頁傳遞給不同的Web用戶。
Java 編程語言中最基本的特點是在Java中創建的程序的代碼的可移植性。因為具有其他語言所不具備的代碼可移植性,Java允許用戶只要編寫一次應用程序,就可以在任何客戶機系統上發布它,并可以讓客戶機系統解釋該程序。這意味著:你只要寫一次代碼,就能使其在任何平臺上運行。
2 協議
當你同朋友交談時,你們遵循一些暗含的規則(或協議)。例如:你們倆不能同時開始說話,或連續不間斷地說話。如果你們這樣作的話,誰也不能理解對方所說的東西。當你說話時,你的朋友傾聽,反之亦然。你們以雙方都能理解的語言和速度進行對話。
當計算機之間進行通訊的時候,也需要遵循一定的規則。數據以包的形式從一臺機器發送到另一臺。這些規則管理數據打包、數據傳輸速度和重新 數據將其恢復成原始形式。這些規則被稱為網絡協議。網絡協議是通過網絡進行通訊的系統所遵循的一系列規則和慣例。連網軟件通常實現有高低層次之分的多層協議。網絡協議的例子有:TCP/IP、UDP、Apple Talk和NetBEUI。
Java提供了一個豐富的、支持網絡的類庫,這些類使得應用程序能方便地訪問網絡資源。Java提供了兩種通訊工具。它們是:使用用戶報文協議(UDP)的報文和使用傳輸控制協議/因特網協議(TCP/IP)的Sockets(套接字)。
數據報包是一個字節數組從一個程序(發送程序)傳送到另一個(接受程序)。由于數據報遵守UDP,不保證發出的數據包必須到達目的地。數據報并不是可信賴的。因此,僅當傳送少量數據時才使用,而且發送者和接受者之間的距離間隔不大,假如是網絡交通高峰,或接受程序正處理來自其他程序的多個請求,就有機會出現數據報包的丟失。
Sockets套接字用TCP來進行通訊。套接字模型同其他模型相比,優越性在于其不受客戶請求來自何處的影響。只要客戶機遵循TCP/IP協議,服務器就會對它的請求提供服務。這意味著客戶機可以是任何類型的計算機。客戶機不再局限為UNIX、Windows、DOS或 Macintosh平臺,因此,網上所有遵循TCP/IP協議的計算機可以通過套接字互相通訊。
3 Sockets套接字
3.1 Sockets概況
在客戶機/服務器應用程序中,服務器提供象處理數據庫查詢或修改數據庫中的數據之類的服務。發生在客戶機和服務器之間的通訊必須是可靠的,同時數據在客戶機上的次序應該和服務器發送出來的次序相同。
什么是套接字?
既然我們已經知道套接字扮演的角色,那么剩下的問題是:什么是套接字?Bruce Eckel 在他的《Java 編程思想》一書中這樣描述套接字:套接字是一種軟件抽象,用于表達兩臺機器之間的連接“終端”。對于一個給定的連接,每臺機器上都有一個套接字,您也可以想象它們之間有一條虛擬的“電纜”,“電纜”的每一端都插入到套接字中。當然,機器之間的物理硬件和電纜連接都是完全未知的。抽象的全部目的是使我們無須知道不必知道的細節。
簡言之,一臺機器上的套接字與另一臺機器上的套接字交談就創建一條通信通道。程序員可以用該通道來在兩臺機器之間發送數據。當您發送數據時,TCP/IP 協議棧的每一層都會添加適當的報頭信息來包裝數據。這些報頭幫助協議棧把您的數據送到目的地。好消息是 Java 語言通過"流"為您的代碼提供數據,從而隱藏了所有這些細節,這也是為什么它們有時候被叫做流套接字(streaming socket)的原因。
把套接字想成兩端電話上的聽筒,我和您通過專用通道在我們的電話聽筒上講話和聆聽。直到我們決定掛斷電話,對話才會結束(除非我們在使用蜂窩電話)。而且我們各自的電話線路都占線,直到我們掛斷電話。
如果想在沒有更高級機制如 ORB(以及 CORBA、RMI、IIOP 等等)開銷的情況下進行兩臺計算機之間的通信,那么套接字就適合您。套接字的低級細節相當棘手。幸運的是,Java 平臺給了您一些雖然簡單但卻強大的更高級抽象,使您可以容易地創建和使用套接字。
傳輸控制協議(TCP)提供了一條可靠的、點對點的通訊通道,客戶機/服務器應用程序可以用該通道互相通訊。要通過TCP進行通訊,客戶機和服務器程序建立連接并綁定套接字。套接字用于處理通過網絡連接的應用程序之間的通訊。客戶機和服務器之間更深入的通訊通過套接字完成。
Java被設計成一種連網語言。它通過將連接功能封裝到套接字類里而使得網絡編程更加容易。套接字類即Socket類(它創建一個客戶套接字)和ServerSocket類(它創建一個服務器套接字)。套接字類大致介紹如下:
l Socket是基類,它支持TCP協議。TCP是一個可靠的流網絡連接協議。Socket類提供了流輸入/輸出的方法,使得從套接字中讀出數據和往套接字中寫數據都很容易。該類對于編寫因特網上的通訊程序而言是必不可少的。
l ServerSocket是一個因特網服務程序用來監聽客戶請求的類。ServerSocket實際上并不執行服務;而是創建了一個Socket對象來代表客戶機。通訊由創建的對象來完成。
3.2 IP地址和端口
因特網服務器可以被認為是一組套接字類,它們提供了一般稱為服務的附加功能。服務的例子有:電子郵件、遠程登錄的Telnet、和通過網絡傳輸文件的文件傳輸協議(FTP)。每種服務都與一個端口相聯系。端口是一個數值地址,通過它來處理服務請求(就象請求Web頁一樣)。
TCP協議需要兩個數據項:IP地址和端口號。因此,當鍵入
http://www.jinnuo.com時,你是如何進入金諾的主頁呢?
因特網協議(IP)提供每一項網絡設備。這些設備都帶有一個稱為IP地址的邏輯地址。由因特網協議提供的IP地址具有特定的形式。每個IP地址都是32位的數值,表示4個范圍在0到255之間的8位數值金諾已經注冊了它的名字,分配給
http://www.jinnuo.com的IP地址為192.168.0.110。
注意:域名服務或DNS服務是將
http://www.jinnuo.com翻譯成192.168.0.110的服務。這使你可以鍵入
http://www.jinnuo.com而不必記住IP地址。想象一下,怎么可能記住所有需要訪問的站點的IP地址!有趣的是一個網絡名可以映射到許多IP地址。對于經常訪問的站點可能需要這一功能,因為這些站點容納大量的信息,并需要多個IP地址來提供業務服務。例如:192.168.0.110的實際的內部名稱為
http://www.jinnuo.com。DNS可以將分配給jinnuo Ltd.的一系列IP地址翻譯成
http://www.jinnuo.com。
如果沒有指明端口號,則使用服務文件中服務器的端口。每種協議有一個缺省的端口號,在端口號未指明時使用該缺省端口號。
端口號 應用
21 FTP.傳輸文件
23 Telnet.提供遠程登錄
25 SMTP.傳遞郵件信息
67 BOOTP.在啟動時提供配置情況
80 HTTP.傳輸Web頁
109 POP.使用戶能訪問遠程系統中的郵箱
讓我們再來看一下URL:
http://www.jinnuo.com
URL的第一部分(http)意味著你正在使用超文本傳輸協議(HTTP),該協議處理Web文檔。如果沒有指明文件,大多數的Web服務器會取一個叫index.html文件。因此,IP地址和端口既可以通過明確指出URL各部分來決定,也可以由缺省值決定。
4 創建Socket客戶
我們將在本部分討論的示例將闡明在 Java 代碼中如何使用 Socket 和 ServerSocket。客戶機用 Socket 連接到服務器。服務器用 ServerSocket 在端口 1001 偵聽。客戶機請求服務器 C: 驅動器上的文件內容。
創建 RemoteFileClient 類
- import java.io.*;
- import java.net.*;
- public class RemoteFileClient {
- protected BufferedReader socketReader;
- protected PrintWriter socketWriter;
- protected String hostIp;
- protected int hostPort;
-
- public RemoteFileClient(String hostIp, int hostPort) {
- this.hostIp = hostIp;
- this.hostPort=hostPort;
- }
-
- public String getFile(String fileNameToGet) {
- StringBuffer fileLines = new StringBuffer();
- try {
- socketWriter.println(fileNameToGet);
- socketWriter.flush();
- String line = null;
- while((line=socketReader.readLine())!=null)
- fileLines.append(line+"\n");
- }
- catch(IOException e) {
- System.out.println("Error reading from file: "+fileNameToGet);
- }
- return fileLines.toString();
- }
-
- public void setUpConnection() {
- try {
- Socket client = new Socket(hostIp,hostPort);
- socketReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
- socketWriter = new PrintWriter(client.getOutputStream());
- }
- catch(UnknownHostException e) {
- System.out.println("Error1 setting up socket connection: unknown host at "+hostIp+":"+hostPort);
- }
- catch(IOException e) {
- System.out.println("Error2 setting up socket connection: "+e);
- }
- }
-
- public void tearDownConnection() {
- try {
- socketWriter.close();
- socketReader.close();
- }catch(IOException e) {
- System.out.println("Error tearing down socket connection: "+e);
- }
- }
- public static void main(String args[]) {
- RemoteFileClient remoteFileClient = new RemoteFileClient("127.0.0.1",1001);
- remoteFileClient.setUpConnection();
- StringBuffer fileContents = new StringBuffer();
- fileContents.append(remoteFileClient.getFile("RemoteFileServer.java"));
-
- System.out.println(fileContents);
- }
- }
首先我們導入 java.net 和 java.io。java.net 包為您提供您需要的套接字工具。java.io 包為您提供對流進行讀寫的工具,這是您與 TCP 套接字通信的唯一途徑。
我們給我們的類實例變量以支持對套接字流的讀寫和存儲我們將連接到的遠程主機的詳細信息。
我們類的構造器有兩個參數:遠程主機的IP地址和端口號各一個,而且構造器將它們賦給實例變量。
我們的類有一個 main() 方法和三個其它方法。稍后我們將探究這些方法的細節。現在您只需知道 setUpConnection() 將連接到遠程服務器,getFile() 將向遠程服務器請求 fileNameToGet 的內容以及 tearDownConnection() 將從遠程服務器上斷開。
實現 main()
這里我們實現 main() 方法,它將創建 RemoteFileClient 并用它來獲取遠程文件的內容,然后打印結果。main() 方法用主機的 IP 地址和端口號實例化一個新 RemoteFileClient(客戶機)。然后,我們告訴客戶機建立一個到主機的連接。接著,我們告訴客戶機獲取主機上一個指定文件的內容。最后,我們告訴客戶機斷開它到主機的連接。我們把文件內容打印到控制臺,只是為了證明一切都是按計劃進行的。
建立連接
這里我們實現 setUpConnection() 方法,它將創建我們的 Socket 并讓我們訪問該套接字的流:
- public void setUpConnection() {
- try {
- Socket client = new Socket(hostIp,hostPort);
- socketReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
- socketWriter = new PrintWriter(client.getOutputStream());
- }
- catch(UnknownHostException e) {
- System.out.println("Error1 setting up socket connection: unknown host at "+hostIp+":"+hostPort);
- }
- catch(IOException e) {
- System.out.println("Error2 setting up socket connection: "+e);
- }
- }
setUpConnection() 方法用主機的 IP 地址和端口號創建一個 Socket:
Socket client = new Socket(hostIp, hostPort);
我們把 Socket 的 InputStream 包裝進 BufferedReader 以使我們能夠讀取流的行。然后,我們把 Socket 的 OutputStream 包裝進 PrintWriter 以使我們能夠發送文件請求到服務器:
socketReader = new BufferedReader(new InputStreamReader(client.getInputStream()));socketWriter = new PrintWriter(client.getOutputStream());
請記住我們的客戶機和服務器只是來回傳送字節。客戶機和服務器都必須知道另一方即將發送的是什么以使它們能夠作出適當的響應。在這個案例中,服務器知道我們將發送一條有效的文件路徑。
當您實例化一個 Socket 時,將拋出 UnknownHostException。這里我們不特別處理它,但我們打印一些信息到控制臺以告訴我們發生了什么錯誤。同樣地,當我們試圖獲取 Socket 的 InputStream 或 OutputStream 時,如果拋出了一個一般 IOException,我們也打印一些信息到控制臺。
與主機交談
這里我們實現 getFile() 方法,它將告訴服務器我們想要什么文件并在服務器傳回其內容時接收該內容。
- public String getFile(String fileNameToGet) {
- StringBuffer fileLines = new StringBuffer();
- try {
- socketWriter.println(fileNameToGet);
- socketWriter.flush();
- String line = null;
- while((line=socketReader.readLine())!=null)
- fileLines.append(line+"\n");
- }
- catch(IOException e) {
- System.out.println("Error reading from file: "+fileNameToGet);
- }
- return fileLines.toString();
- }
對getFile()方法的調用要求一個有效的文件路徑String。它首先創建名為fileLines的 StringBuffer,fileLines 用于存儲我們讀自服務器上的文件的每一行。
StringBuffer fileLines = new StringBuffer();
在 try{}catch{} 塊中,我們用 PrintWriter 把請求發送到主機,PrintWriter 是我們在創建連接期間建立的。
socketWriter.println(fileNameToGet); socketWriter.flush();
請注意這里我們是 flush() 該 PrintWriter,而不是關閉它。這迫使數據被發送到服務器而不關閉 Socket。
一旦我們已經寫到 Socket,我們就希望有一些響應。我們不得不在 Socket 的 InputStream 上等待它,我們通過在 while 循環中調用 BufferedReader 上的 readLine() 來達到這個目的。我們把每一個返回行附加到 fileLines StringBuffer(帶有一個換行符以保護行):
String line = null; while((line=socketReader.readLine())!=null) fileLines.append(line+"\n");
斷開連接
這里我們實現 tearDownConnection() 方法,它將在我們使用完畢連接后負責“清除”。tearDownConnection()方法只是分別關閉我們在Socket的InputStream和OutputStream上創建的 BufferedReader和PrintWriter。這樣做會關閉我們從Socket獲取的底層流,所以我們必須捕捉可能的 IOException。
總結一下客戶機
我們的類研究完了。在我們繼續往前討論服務器端的情況之前,讓我們回顧一下創建和使用 Socket 的步驟:
1. 用您想連接的機器的 IP 地址和端口實例化 Socket(如有問題則拋出 Exception)。
2. 獲取 Socket 上的流以進行讀寫。
3. 把流包裝進 BufferedReader/PrintWriter 的實例,如果這樣做能使事情更簡單的話。
4. 對 Socket 進行讀寫。
5. 關閉打開的流。
5 創建服務器Socket
創建 RemoteFileServer 類
- import java.io.*;
- import java.net.*;
- public class RemoteFileServer {
- int listenPort;
- public RemoteFileServer(int listenPort) {
- this.listenPort=listenPort;
- }
-
- public void acceptConnections() {
- try {
- ServerSocket server = new ServerSocket(listenPort);
- Socket incomingConnection = null;
- while(true) {
- incomingConnection = server.accept();
- handleConnection(incomingConnection);
- }
- }
- catch(BindException e) {
- System.out.println("Unable to bind to port "+listenPort);
- }
- catch(IOException e) {
- System.out.println("Unable to instantiate a ServerSocket on port: "+listenPort);
-
- }
- }
-
- public void handleConnection(Socket incomingConnection) {
- try {
- OutputStream outputToSocket = incomingConnection.getOutputStream();
- InputStream inputFromSocket = incomingConnection.getInputStream();
- BufferedReader streamReader = new BufferedReader(new InputStreamReader(inputFromSocket));
- FileReader fileReader = new FileReader(new File(streamReader.readLine()));
- BufferedReader bufferedFileReader = new BufferedReader(fileReader);
- PrintWriter streamWriter = new PrintWriter(incomingConnection.getOutputStream());
- String line = null;
- while((line=bufferedFileReader.readLine())!=null){
- streamWriter.println(line);
- }
- fileReader.close();
- streamWriter.close();
- streamReader.close();
- }
- catch(Exception e) {
- System.out.println("Error handling a client: "+e);
- e.printStackTrace();
- }
- }
- public static void main(String args[]) {
- RemoteFileServer server = new RemoteFileServer(1001);
- server.acceptConnections();
- }
- }
跟客戶機中一樣,我們首先導入java.net的java.io。接著,我們給我們的類一個實例變量以保存端口,我們從該端口偵聽進入的連接。缺省情況下,端口是1001。
我們的類有一個main()方法和兩個其它方法。稍后我們將探究這些方法的細節。現在您只需知道acceptConnections()將允許客戶機連接到服務器以及handleConnection()與客戶機Socket交互以將您所請求的文件的內容發送到客戶機。
實現 main()
這里我們實現main()方法,它將創建RemoteFileServer并告訴它接受連接:服務器端的main()方法中,我們實例化一個新 RemoteFileServer,它將在偵聽端口(1001)上偵聽進入的連接請求。然后我們調用acceptConnections()來告訴該 server進行偵聽。
接受連接
這里我們實現 acceptConnections() 方法,它將創建一個 ServerSocket 并等待連接請求:
- public void acceptConnections() {
- try {
- ServerSocket server = new ServerSocket(listenPort);
- Socket incomingConnection = null;
- while(true) {
- incomingConnection = server.accept();
- handleConnection(incomingConnection);
- }
- }
- catch(BindException e) {
- System.out.println("Unable to bind to port "+listenPort);
- }
- catch(IOException e) {
- System.out.println("Unable to instantiate a ServerSocket on port: "+listenPort);
-
- }
- }
acceptConnections()用欲偵聽的端口號來創建ServerSocket。然后我們通過調用該ServerSocket的 accept()來告訴它開始偵聽。accept()方法將造成阻塞直到來了一個連接請求。此時,accept()返回一個新的Socket,這個 Socket綁定到服務器上一個隨機指定的端口,返回的Socket被傳遞給handleConnection()。請注意我們在一個無限循環中處理對連接的接受。這里不支持任何關機。
無論何時如果您創建了一個無法綁定到指定端口(可能是因為別的什么控制了該端口)的 ServerSocket,Java代碼都將拋出一個錯誤。所以這里我們必須捕捉可能的BindException。就跟在客戶機端上時一樣,我們必須捕捉IOException,當我們試圖在ServerSocket上接受連接時,它就會被拋出。請注意,您可以通過用毫秒數調用 setSoTimeout()來為accept()調用設置超時,以避免實際長時間的等待。調用setSoTimeout()將使accept()經過指定占用時間后拋出IOException。
處理連接
這里我們實現handleConnection()方法,它將用連接的流來接收輸入和寫輸出:
- public void handleConnection(Socket incomingConnection) {
- try {
- OutputStream outputToSocket = incomingConnection.getOutputStream();
- InputStream inputFromSocket = incomingConnection.getInputStream();
- BufferedReader streamReader = new BufferedReader(new InputStreamReader(inputFromSocket));
- FileReader fileReader = new FileReader(new File(streamReader.readLine()));
- BufferedReader bufferedFileReader = new BufferedReader(fileReader);
- PrintWriter streamWriter = new PrintWriter(incomingConnection.getOutputStream());
- String line = null;
- while((line=bufferedFileReader.readLine())!=null){
- streamWriter.println(line);
- }
- fileReader.close();
- streamWriter.close();
- streamReader.close();
- }
- catch(Exception e) {
- System.out.println("Error handling a client: "+e);
- e.printStackTrace();
- }
- }
跟在客戶機中一樣,我們用getOutputStream()和getInputStream()來獲取與我們剛創建的Socket相關聯的流。跟在客戶機端一樣,我們把InputStream包裝進BufferedReader,把OutputStream包裝進PrintWriter。在服務器端上,我們需要添加一些代碼,用來讀取目標文件和把內容逐行發送到客戶機。這里是重要的代碼:
FileReader fileReader = new FileReader(new File(streamReader.readLine())); BufferedReader bufferedFileReader = new BufferedReader(fileReader); String line = null; while((line=bufferedFileReader.readLine())!=null) { streamWriter.println(line); }
這些代碼值得詳細解釋。讓我們一點一點來看:
FileReader fileReader = new FileReader(new File(streamReader.readLine()));
首先,我們使用Socket 的InputStream的BufferedReader。我們應該獲取一條有效的文件路徑,所以我們用該路徑名構造一個新File。我們創建一個新FileReader來處理讀文件的操作。
BufferedReader bufferedFileReader = new BufferedReader(fileReader);
這里我們把FileReader包裝進BufferedReader以使我們能夠逐行地讀該文件。
接著,我們調用BufferedReader的readLine()。這個調用將造成阻塞直到有字節到來。我們獲取一些字節之后就把它們放到本地的line變量中,然后再寫出到客戶機上。完成讀寫操作之后,我們就關閉打開的流。
請注意我們在完成從Socket的讀操作之后關閉streamWriter和streamReader。您或許會問我們為什么不在讀取文件名之后立刻關閉 streamReader。原因是當您這樣做時,您的客戶機將不會獲取任何數據。如果您在關閉streamWriter之前關閉 streamReader,則您可以往Socket寫任何東西,但卻沒有任何數據能通過通道(通道被關閉了)。
總結一下服務器
在我們接著討論另一個更實際的示例之前,讓我們回顧一下創建和使用ServerSocket的步驟:
1. 用一個您想讓它偵聽傳入客戶機連接的端口來實例化一個ServerSocket(如有問題則拋出 Exception)。
2. 調用ServerSocket的accept()以在等待連接期間造成阻塞。
3. 獲取位于該底層Socket的流以進行讀寫操作。
4. 按使事情簡單化的原則包裝流。
5. 對Socket進行讀寫。
6. 關閉打開的流(并請記住,永遠不要在關閉Writer之前關閉Reader)。
6 創建多線程Socket服務器
前面的示例教給您基礎知識,但并不能令您更深入。如果您到此就停止了,那么您一次只能處理一臺客戶機。原因是handleConnection()是一個阻塞方法。只有當它完成了對當前連接的處理時,服務器才能接受另一個客戶機。在多數時候,您將需要(也有必要)一個多線程服務器。
創建 MultithreadedRemoteFileServer 類
- import java.io.*;
- import java.net.*;
- public class MultithreadedRemoteFileServer {
- int listenPort;
- public MultithreadedRemoteFileServer(int listenPort) {
- this.listenPort=listenPort;
- }
-
- public void acceptConnections() {
- try {
- ServerSocket server = new ServerSocket(listenPort, 5);
- Socket incomingConnection = null;
- while(true) {
- incomingConnection = server.accept();
- handleConnection(incomingConnection);
- }
- }
- catch(BindException e) {
- System.out.println("Unable to bind to port "+listenPort);
- }
- catch(IOException e) {
- System.out.println("Unable to instantiate a ServerSocket on port: "+listenPort);
- }
- }
-
- public void handleConnection(Socket connectionToHandle) {
- new Thread(new ConnectionHandler(connectionToHandle)).start();
- }
- public static void main(String args[]) {
- MultithreadedRemoteFileServer server = new MultithreadedRemoteFileServer(1001);
- server.acceptConnections();
- }
- }
這里我們實現改動過acceptConnections()方法,它將創建一個能夠處理待發請求的ServerSocket,并告訴ServerSocket接受連接。
新的 server 仍然需要acceptConnections(),所以這些代碼實際上是一樣的。突出顯示的行表示一個重大的不同。對這個多線程版,我們現在可以指定客戶機請求的最大數目,這些請求都能在實例化ServerSocket期間處于待發狀態。如果我們沒有指定客戶機請求的最大數目,則我們假設使用缺省值50。
這里是它的工作機制。假設我們指定待發數(backlog 值)是5并且有五臺客戶機請求連接到我們的服務器。我們的服務器將著手處理第一個連接,但處理該連接需要很長時間。由于我們的待發值是5,所以我們一次可以放五個請求到隊列中。我們正在處理一個,所以這意味著還有其它五個正在等待。等待的和正在處理的一共有六個。當我們的服務器仍忙于接受一號連接(記住隊列中還有 2?6 號)時,如果有第七個客戶機提出連接申請,那么,該第七個客戶機將遭到拒絕。我們將在帶有連接池服務器示例中說明如何限定能同時連接的客戶機數目。
處理連接:
public void handleConnection(Socket connectionToHandle) {
new Thread(new ConnectionHandler(connectionToHandle)).start();
}
我們對RemoteFileServer所做的大改動就體現在這個方法上。我們仍然在服務器接受一個連接之后調用handleConnection(),但現在我們把該Socket傳遞給ConnectionHandler的一個實例,它是 Runnable的。我們用ConnectionHandler創建一個新 Thread 并啟動它。ConnectionHandler的run()方法包Socket讀/寫和讀File的代碼,這些代碼原來在 RemoteFileServer的handleConnection()中。
創建 ConnectionHandler 類
- import java.io.*;
- import java.net.*;
- public class ConnectionHandler implements Runnable {
- protected Socket socketToHandle;
- public ConnectionHandler(Socket socketToHandle) {
- this.socketToHandle=socketToHandle;
- }
- public void run() {
- try {
- PrintWriter streamWriter = new PrintWriter(socketToHandle.getOutputStream());
- BufferedReader streamReader = new BufferedReader(new InputStreamReader(socketToHandle.getInputStream()));
- String fileToRead = streamReader.readLine();
- BufferedReader fileReader = new BufferedReader(new FileReader(fileToRead));
- String line =null;
- while((line=fileReader.readLine())!=null) {
- streamWriter.println(line);
- }
- fileReader.close();
- streamWriter.close();
- streamReader.close();
- }
- catch(Exception e) {
- System.out.println("Error handling a client: "+e);
- e.printStackTrace();
- }
- }
- }
這個助手類相當簡單。跟我們到目前為止的其它類一樣,我們導入java.net和java.io。該類只有一個實例變量socketToHandle,它保存由該實例處理的Socket。
類的構造器用一個Socket實例作參數并將它賦給socketToHandle。
請注意該類實現了Runnable接口。實現這個接口的類都必須實現run()方法。這里我們實現run()方法,它將攫取我們的連接的流,用它來讀寫該連接,并在任務完成之后關閉它。ConnectionHandler的run()方法所做的事情就是RemoteFileServer上的 handleConnection()所做的事情。首先,我們把InputStream和OutputStream分別包裝(用Socket的 getOutputStream()和 getInputStream())進BufferedReader和PrintWriter。然后我們用這些代碼逐行地讀目標文件:
- PrintWriter streamWriter = new PrintWriter(socketToHandle.getOutputStream());
- BufferedReader streamReader = new BufferedReader(new InputStreamReader(socketToHandle.getInputStream()));
- String fileToRead = streamReader.readLine();
- BufferedReader fileReader = new BufferedReader(new FileReader(fileToRead));
- String line =null;
- while((line=fileReader.readLine())!=null) {
- streamWriter.println(line);
- }
請記住我們應該從客戶機獲取一條有效的文件路徑,這樣用該路徑名構造一個新File,把它包裝進FileReader以處理讀文件的操作,然后把它包裝進 BufferedReader以讓我們逐行地讀該文件。我們while循環中調用BufferedReader上的readLine()直到不再有要讀的行。請記注,對readLine()的調用將造成阻塞,直到有字節來到為止。我們獲取一些字節之后就把它們放到本地的line變量中,然后寫出到客戶機上。完成讀寫操作之后,我們關閉打開的流。
總結一下多線程服務器
讓我們回顧一下創建和使用“多線程版”的服務器的步驟:
1. 修改 acceptConnections() 以用缺省為 50(或任何您想要的大于 1 的指定數字)實例化 ServerSocket。
2. 修改 ServerSocket 的 handleConnection() 以用 ConnectionHandler 的一個實例生成一個新的 Thread。
3. 借用 RemoteFileServer 的 handleConnection() 方法的代碼實現 ConnectionHandler 類。
7 創建帶有連接池的Socket服務器
我們現在已經擁有的 MultithreadedServer 每當有客戶機申請一個連接時都在一個新Thread中創建一個新 ConnectionHandler。這意味著可能有一捆Thread“躺”在我們周圍。而且創建Thread的系統開銷并不是微不足道的。如果性能成為了問題(也請不要事到臨頭才意識到它),更高效地處理我們的服務器是件好事。那么,我們如何更高效地管理服務器端呢?我們可以維護一個進入的連接池,一定數量的ConnectionHandler將為它提供服務。這種設計能帶來以下好處:
• 它限定了允許同時連接的數目。
• 我們只需啟動ConnectionHandler Thread一次。
幸運的是,跟在我們的多線程示例中一樣,往代碼中添加“池”不需要來一個大改動。事實上,應用程序的客戶機端根本就不受影響。在服務器端,我們在服務器啟動時創建一定數量的 ConnectionHandler,我們把進入的連接放入“池”中并讓ConnectionHandler打理剩下的事情。這種設計中有很多我們不打算討論的可能存在的技巧。例如,我們可以通過限定允許在“池”中建立的連接的數目來拒絕客戶機。
請注意:我們將不會再次討論acceptConnections()。這個方法跟前面示例中的完全一樣。它無限循環地調用ServerSocket上的 accept() 并把連接傳遞到handleConnection()。
創建 PooledRemoteFileServer 類
- import java.io.*;
- import java.net.*;
- import java.util.*;
- public class PooledRemoteFileServer {
- protected int maxConnections;
- protected int listenPort;
- protected ServerSocket serverSocket;
- public PooledRemoteFileServer(int aListenPort, int maxConnections) {
- listenPort= aListenPort;
- this.maxConnections = maxConnections;
- }
- public void acceptConnections() {
- try {
- ServerSocket server = new ServerSocket(listenPort, 5);
- Socket incomingConnection = null;
- while(true) {
- incomingConnection = server.accept();
- handleConnection(incomingConnection);
- }
- }
- catch(BindException e) {
- System.out.println("");
- }
- catch(IOException e) {
- System.out.println(""+listenPort);
- }
- }
- protected void handleConnection(Socket connectionToHandle) {
- PooledConnectionHandler.processRequest(connectionToHandle);
- }
- public void setUpHandlers() {
- for(int i=0; i<maxConnections; i++) {
- PooledConnectionHandler currentHandler = new PooledConnectionHandler();
- new Thread(currentHandler, "Handler " + i).start();
- }
- }
- public static void main(String args[]) {
- PooledRemoteFileServer server = new PooledRemoteFileServer(1001, 3);
- server.setUpHandlers();
- server.acceptConnections();
- }
- }
請注意一下您現在應該熟悉了的 import 語句。我們給類以下實例變量以保存:
• 我們的服務器能同時處理的活動客戶機連接的最大數目
• 進入的連接的偵聽端口(我們沒有指定缺省值,但如果您想這樣做,并不會受到限制)
• 將接受客戶機連接請求的 ServerSocket
類的構造器用的參數是偵聽端口和連接的最大數目
我們的類有一個 main() 方法和三個其它方法。稍后我們將探究這些方法的細節。現在只須知道setUpHandlers()創建數目為 maxConnections的大量PooledConnectionHandler,而其它兩個方法則與我們前面已經看到的相似:acceptConnections()在ServerSocket上偵聽傳入的客戶機連接,而handleConnection則在客戶機連接一旦被建立后就實際處理它。
實現 main()
這里我們實現需作改動的main()方法,該方法將創建能夠處理給定數目的客戶機連接的PooledRemoteFileServer,并告訴它接受連接:
public static void main(String args[]) {
PooledRemoteFileServer server = new PooledRemoteFileServer(1001, 3);
server.setUpHandlers();
server.acceptConnections();
}
我們的main()方法很簡單。我們實例化一個新的PooledRemoteFileServer,它將通過調用setUpHandlers()來建立三個 PooledConnectionHandler。一旦服務器就緒,我們就告訴它acceptConnections()。
建立連接處理程序
public void setUpHandlers() {
for(int i=0; i<maxConnections; i++) {
PooledConnectionHandler currentHandler = new PooledConnectionHandler();
new Thread(currentHandler, "Handler " + i).start();
}
}
setUpHandlers() 方法創建maxConnections(例如 3)個PooledConnectionHandler并在新Thread中激活它們。用實現了 Runnable的對象來創建Thread使我們可以在Thread調用start()并且可以期望在Runnable上調用了run()。換句話說,我們的PooledConnectionHandler將等著處理進入的連接,每個都在它自己的Thread中進行。我們在示例中只創建三個Thread,而且一旦服務器運行,這就不能被改變。
處理連接
這里我們實現需作改動的handleConnections()方法,它將委派PooledConnectionHandler處理連接:
protected void handleConnection(Socket connectionToHandle) {
PooledConnectionHandler.processRequest(connectionToHandle);
}
我們現在叫 PooledConnectionHandler 處理所有進入的連接(processRequest() 是一個靜態方法)。
創建 PooledRemoteFileServer 類
- import java.io.*;
- import java.net.*;
- import java.util.*;
- public class PooledConnectionHandler implements Runnable {
- protected Socket connection;
- protected static List pool = new LinkedList();
- public PooledConnectionHandler() {}
- public void handleConnection() {
- try {
- PrintWriter streamWriter = new PrintWriter(connection.getOutputStream());
- BufferedReader streamReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
- String fileToRead = streamReader.readLine();
- BufferedReader fileReader = new BufferedReader(new FileReader(fileToRead));
- String line = null;
- while((line=fileReader.readLine())!=null)
- streamWriter.println(line);
- fileReader.close();
- streamWriter.close();
- streamReader.close();
- }
- catch(FileNotFoundException e) {
- System.out.println("");
- }
- catch(IOException e) {
- System.out.println(""+e);
- }
- }
- public static void processRequest(Socket requestToHandle) {
- synchronized(pool) {
- pool.add(pool.size(), requestToHandle);
- pool.notifyAll();
- }
- }
- public void run() {
- while(true) {
- synchronized(pool) {
- while(pool.isEmpty()) {
- try {
- pool.wait();
- }
- catch(InterruptedException e) {
- e.printStackTrace();
- }
- }
- connection= (Socket)pool.remove(0);
- }
- handleConnection();
- }
- }
- }
這個助手類與 ConnectionHandler 非常相似,但它帶有處理連接池的手段。該類有兩個實例變量:
• connection 是當前正在處理的 Socket
• 名為 pool 的靜態 LinkedList 保存需被處理的連接
填充連接池
這里我們實現PooledConnectionHandler上的processRequest()方法,它將把傳入請求添加到池中,并告訴其它正在等待的對象該池已經有一些內容:
public static void processRequest(Socket requestToHandle) {
synchronized(pool) {
pool.add(pool.size(), requestToHandle);
pool.notifyAll();
}
}
synchronized 塊是個稍微有些不同的東西。您可以同步任何對象上的一個塊,而不只是在本身的某個方法中含有該塊的對象。在我們的示例中,processRequest() 方法包含有一個 pool(請記住它是一個 LinkedList,保存等待處理的連接池)的 synchronized塊。我們這樣做的原因是確保沒有別人能跟我們同時修改連接池。
既然我們已經保證了我們是唯一“涉水”池中的人,我們就可以把傳入的Socket添加到LinkedList的尾端。一旦我們添加了新的連接,我們就用以下代碼通知其它正在等待該池的Thread,池現在已經可用:
pool.notifyAll();
Object的所有子類都繼承這個notifyAll()方法。這個方法,連同我們下一屏將要討論的wait()方法一起,就使一個Thread能夠讓另一個Thread知道一些條件已經具備。這意味著該第二個Thread一定正在等待那些條件的滿足。
從池中獲取連接
這里我們實現PooledConnectionHandler上需作改動的run()方法,它將在連接池上等待,并且池中一有連接就處理它:
public void run() {
while(true) {
synchronized(pool) {
while(pool.isEmpty()) {
try {
pool.wait();
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
connection= (Socket)pool.remove(0);
}
handleConnection();
}
}
回想一下在前面講過的:一個Thread正在等待有人通知它連接池方面的條件已經滿足了。在我們的示例中,請記住我們有三個 PooledConnectionHandler在等待使用池中的連接。每個PooledConnectionHandler都在它自已的Thread中運行,并通過調用pool.wait()產生阻塞。當我們的processRequest()在連接池上調用notifyAll()時,所有正在等待的 PooledConnectionHandler都將得到“池已經可用”的通知。然后各自繼續前行調用pool.wait(),并重新檢查 while(pool.isEmpty())循環條件。除了一個處理程序,其它池對所有處理程序都將是空的,因此,在調用pool.wait()時,除了一個處理程序,其它所有處理程序都將再次產生阻塞。恰巧碰上非空池的處理程序將跳出while(pool.isEmpty())循環并攫取池中的第一個連接:
connection= (Socket)pool.remove(0);
處理程序一旦有一個連接可以使用,就調用 handleConnection() 處理它。
在我們的示例中,池中可能永遠不會有多個連接,只是因為事情很快就被處理掉了。如果池中有一個以上連接,那么其它處理程序將不必等待新的連接被添加到池。當它們檢查pool.isEmpty()條件時,將發現其值為假,然后就從池中攫取一個連接并處理它。
還有另一件事需注意。當run()擁有池的互斥鎖時,processRequest()如何能夠把連接放到池中呢?答案是對池上的wait()的調用釋放鎖,而wait()接著就在自己返回之前再次攫取該鎖。這就使得池對象的其它同步代碼可以獲取該鎖。
處理連接:再一次
這里我們實現需做改動的handleConnection()方法,該方法將攫取連接的流,使用它們,并在任務完成之后清除它們:
- public void handleConnection() {
- try {
- PrintWriter streamWriter = new PrintWriter(connection.getOutputStream());
- BufferedReader streamReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
- String fileToRead = streamReader.readLine();
- BufferedReader fileReader = new BufferedReader(new FileReader(fileToRead));
- String line = null;
- while((line=fileReader.readLine())!=null)
- streamWriter.println(line);
- fileReader.close();
- streamWriter.close();
- streamReader.close();
- }
- catch(FileNotFoundException e) {
- System.out.println("");
- }
- catch(IOException e) {
- System.out.println(""+e);
- }
- }
跟在多線程服務器中不同,我們的PooledConnectionHandler有一個handleConnection()方法。這個方法的代碼跟非池式的ConnectionHandler上的run()方法的代碼完全一樣。首先,我們把OutputStream和InputStream分別包裝進(用Socket上的getOutputStream()和getInputStream())BufferedReader和PrintWriter。然后我們逐行讀目標文件,就象我們在多線程示例中做的那樣。再一次,我們獲取一些字節之后就把它們放到本地的line變量中,然后寫出到客戶機。完成讀寫操作之后,我們關閉FileReader和打開的流。
總結一下帶有連接池的服務器
讓我們回顧一下創建和使用“池版”服務器的步驟:
1. 創建一個新種類的連接處理程序(我們稱之為 PooledConnectionHandler)來處理池中的連接。
2. 修改服務器以創建和使用一組 PooledConnectionHandler。
Java 語言簡化了套接字在應用程序中的使用。它的基礎實際上是 java.net 包中的 Socket 和 ServerSocket 類。一旦您理解了表象背后發生的情況,就能容易地使用這些類。在現實生活中使用套接字只是這樣一件事,即通過貫徹優秀的 OO 設計原則來保護應用程序中各層間的封裝。我們為您展示了一些有幫助的類。這些類的結構對我們的應用程序隱藏了 Socket 交互作用的低級細節 ? 使應用程序能只使用可插入的 ClientSocketFacade 和 ServerSocketFacade。在有些地方(在 Facade 內),您仍然必須管理稍顯雜亂的字節細節,但您只須做一次就可以了。更好的是,您可以在將來的項目中重用這些低級別的助手類
posted on 2008-12-11 23:13
一葉笑天 閱讀(206)
評論(0) 編輯 收藏 所屬分類:
JAVA技術