轉:用Java Socket開發小型服務器,支持上千個并發(上)
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")); //remoteFileClient.tearDownConnection(); 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); } } //與客戶機Socket交互以將客戶機所請求的文件的內容發送到客戶機 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)。 |
posted on 2009-01-04 12:55 liujg 閱讀(293) 評論(0) 編輯 收藏 所屬分類: Java基礎