Java Socket
套接字(socket)為兩台計算機之間的通信提供了一種機制,在JamesGosling注意到Java語言之前,套接字就早已赫赫有名。該語言只是讓您不必了解底層操作系統的細節就能有效地使用套接字。
1客戶機/服務器模型
在飯店裡,菜單上各種具有異國情調的食品映入你的眼簾,於是你要了一份pizza。幾分鐘後,你用力咀嚼澆著融化的乳酪和其他你喜歡的配料的熱pizza。你不知道,也不想知道:侍者從那裡弄來了pizza,在制作過程中加進了什麼,以及配料是如何獲得的。
上例中包含的實體有:美味的pizza、接受你定餐的侍者、制作pizza的廚房,當然還有你。你是定pizza的顧客或客戶。制作pizza的過程對於你而言是被封裝的。你的請求在廚房中被處理,pizza制作完成後,由侍者端給你。
你所看到的就是一個客戶機/服務器模型。客戶機向服務器發送一個請求或命令。服務器處理客戶機的請求。客戶機和服務器之間的通訊是客戶機/服務器模型中的一個重要組成部分,通常通過網絡進行。
客戶機/服務器模型是一個應用程序開發框架,該框架是為了將數據的表示與其內部的處理和存儲分離開來而設計的。客戶機請求服務,服務器為這些請求服務。請求通過網絡從客戶機傳遞到服務器。服務器所進行的處理對客戶機而言是隱藏的。一個服務器可以為多台客戶機服務。
多台客戶機訪問服務器
服務器和客戶機不一定是硬件組件。它們可以是工作啊同一機器或不同機器上的程序。、
考慮一個航空定票系統中的數據輸入程序:數據----乘客名、航班號、飛行日期、目的地等可以被輸入到前端----客戶機的應用程序中。一旦數據輸入之後,客戶機將數據發送到後端----服務器端。服務器處理數據並在數據庫中保存數據。客戶機/服務器模型的重要性在於所有的數據都存放在同一地點。客戶機從不同的地方訪問同一數據源,服務器對所有的輸入數據應用同樣的檢驗規則。
萬維網為‘為什麼要將數據的表示與其存儲、處理分離開來’提供了一個很好的例子。在Web上,你無需控制最終用戶用來訪問你數據的平台和軟件。你可以考慮編寫出適用與每一種潛在的目標平台的應用程序。
‘客戶機/服務器應用程序的服務器部分’管理通過多個客戶機訪問服務器的、多個用戶共享的資源。表明‘客戶機/服務器程序的服務器部分’強大功能的最好例子應該是Web服務器,它通過Internet將HTML頁傳遞給不同的Web用戶。
Java編程語言中最基本的特點是在Java中創建的程序的代碼的可移植性。因為具有其他語言所不具備的代碼可移植性,Java允許用戶只要編寫一次應用程序,就可以在任何客戶機系統上發布它,並可以讓客戶機系統解釋該程序。這意味著:你只要寫一次代碼,就能使其在任何平台上運行。
2協議
當你同朋友交談時,你們遵循一些暗含的規則(或協議)。例如:你們倆不能同時開始說話,或連續不間斷地說話。如果你們這樣作的話,誰也不能理解對方所說的東西。當你說話時,你的朋友傾聽,反之亦然。你們以雙方都能理解的語言和速度進行對話。
當計算機之間進行通訊的時候,也需要遵循一定的規則。數據以包的形式從一台機器發送到另一台。這些規則管理數據打包、數據傳輸速度和重新數據將其恢復成原始形式。這些規則被稱為網絡協議。網絡協議是通過網絡進行通訊的系統所遵循的一系列規則和慣例。連網軟件通常實現有高低層次之分的多層協議。網絡協議的例子有:TCP/IP、UDP、AppleTalk和NetBEUI。
Java提供了一個豐富的、支持網絡的類庫,這些類使得應用程序能方便地訪問網絡資源。Java提供了兩種通訊工具。它們是:使用用戶報文協議(UDP)的報文和使用傳輸控制協議/因特網協議(TCP/IP)的Sockets(套接字)。
數據報包是一個字節數組從一個程序(發送程序)傳送到另一個(接受程序)。由於數據報遵守UDP,不保證發出的數據包必須到達目的地。數據報並不是可信賴的。因此,僅當傳送少量數據時才使用,而且發送者和接受者之間的距離間隔不大,假如是網絡交通高峰,或接受程序正處理來自其他程序的多個請求,就有機會出現數據報包的丟失。
Sockets套接字用TCP來進行通訊。套接字模型同其他模型相比,優越性在於其不受客戶請求來自何處的影響。只要客戶機遵循TCP/IP協議,服務器就會對它的請求提供服務。這意味著客戶機可以是任何類型的計算機。客戶機不再局限為UNIX、Windows、DOS或Macintosh平台,因此,網上所有遵循TCP/IP協議的計算機可以通過套接字互相通訊。
3 Sockets套接字
3.1Sockets概況
在客戶機/服務器應用程序中,服務器提供象處理數據庫查詢或修改數據庫中的數據之類的服務。發生在客戶機和服務器之間的通訊必須是可靠的,同時數據在客戶機上的次序應該和服務器發送出來的次序相同。
什麼是套接字?
既然我們已經知道套接字扮演的角色,那麼剩下的問題是:什麼是套接字?BruceEckel在他的《Java編程思想》一書中這樣描述套接字:套接字是一種軟件抽象,用於表達兩台機器之間的連接“終端”。對於一個給定的連接,每台機器上都有一個套接字,您也可以想象它們之間有一條虛擬的“電纜”,“電纜”的每一端都插入到套接字中。當然,機器之間的物理硬件和電纜連接都是完全未知的。抽象的全部目的是使我們無須知道不必知道的細節。
簡言之,一台機器上的套接字與另一台機器上的套接字交談就創建一條通信通道。程序員可以用該通道來在兩台機器之間發送數據。當您發送數據時,TCP/IP協議棧的每一層都會添加適當的報頭信息來包裝數據。這些報頭幫助協議棧把您的數據送到目的地。好消息是Java語言通過"流"為您的代碼提供數據,從而隱藏了所有這些細節,這也是為什麼它們有時候被叫做流套接字(streamingsocket)的原因。
把套接字想成兩端電話上的聽筒,我和您通過專用通道在我們的電話聽筒上講話和聆聽。直到我們決定掛斷電話,對話才會結束(除非我們在使用蜂窩電話)。而且我們各自的電話線路都占線,直到我們掛斷電話。
如果想在沒有更高級機制如ORB(以及CORBA、RMI、IIOP等等)開銷的情況下進行兩台計算機之間的通信,那麼套接字就適合您。套接字的低級細節相當棘手。幸運的是,Java平台給了您一些雖然簡單但卻強大的更高級抽象,使您可以容易地創建和使用套接字。
傳輸控制協議(TCP)提供了一條可靠的、點對點的通訊通道,客戶機/服務器應用程序可以用該通道互相通訊。要通過TCP進行通訊,客戶機和服務器程序建立連接並綁定套接字。套接字用於處理通過網絡連接的應用程序之間的通訊。客戶機和服務器之間更深入的通訊通過套接字完成。
Java被設計成一種連網語言。它通過將連接功能封裝到套接字類裡而使得網絡編程更加容易。套接字類即Socket類(它創建一個客戶套接字)和ServerSocket類(它創建一個服務器套接字)。套接字類大致介紹如下:
l Socket是基類,它支持TCP協議。TCP是一個可靠的流網絡連接協議。Socket類提供了流輸入/輸出的方法,使得從套接字中讀出數據和往套接字中寫數據都很容易。該類對於編寫因特網上的通訊程序而言是必不可少的。
l ServerSocket是一個因特網服務程序用來監聽客戶請求的類。ServerSocket實際上並不執行服務;而是創建了一個Socket對象來代表客戶機。通訊由創建的對象來完成。
3.2IP地址和端口
因特網服務器可以被認為是一組套接字類,它們提供了一般稱為服務的附加功能。服務的例子有:電子郵件、遠程登錄的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可以將分配給jinnuoLtd.的一系列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類
1.import java.io.*;
2.import java.net.*;
3.public class RemoteFileClient {
4. protected BufferedReader socketReader;
5. protected PrintWriter socketWriter;
6. protected String hostIp;
7. protected int hostPort;
8. //構造方法
9. public RemoteFileClient(String hostIp, int hostPort) {
10. this.hostIp = hostIp;
11. this.hostPort=hostPort;
12. }
13. //向服務器請求文件的內容
14. public String getFile(String fileNameToGet) {
15. StringBuffer fileLines = new StringBuffer();
16. try {
17. socketWriter.println(fileNameToGet);
18. socketWriter.flush();
19. String line = null;
20. while((line=socketReader.readLine())!=null)
21. fileLines.append(line+"\n");
22. }
23. catch(IOException e) {
24. System.out.println("Error reading from file: "+fileNameToGet);
25. }
26. return fileLines.toString();
27. }
28. //連接到遠程服務器
29. public void setUpConnection() {
30. try {
31. Socket client = new Socket(hostIp,hostPort);
32. socketReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
33. socketWriter = new PrintWriter(client.getOutputStream());
34. }
35. catch(UnknownHostException e) {
36. System.out.println("Error1 setting up socket connection: unknown host at "+hostIp+":"+hostPort);
37. }
38. catch(IOException e) {
39. System.out.println("Error2 setting up socket connection: "+e);
40. }
41. }
42. //斷開遠程服務器
43. public void tearDownConnection() {
44. try {
45. socketWriter.close();
46. socketReader.close();
47. }catch(IOException e) {
48. System.out.println("Error tearing down socket connection: "+e);
49. }
50. }
51. public static void main(String args[]) {
52. RemoteFileClient remoteFileClient = new RemoteFileClient("127.0.0.1",1001);
53. remoteFileClient.setUpConnection();
54. StringBuffer fileContents = new StringBuffer();
55. fileContents.append(remoteFileClient.getFile("RemoteFileServer.java"));
56. //remoteFileClient.tearDownConnection();
57. System.out.println(fileContents);
58. }
59.}
首先我們導入java.net和java.io。java.net包為您提供您需要的套接字工具。java.io包為您提供對流進行讀寫的工具,這是您與TCP套接字通信的唯一途徑。
我們給我們的類實例變量以支持對套接字流的讀寫和存儲我們將連接到的遠程主機的詳細信息。
我們類的構造器有兩個參數:遠程主機的IP地址和端口號各一個,而且構造器將它們賦給實例變量。
我們的類有一個main()方法和三個其它方法。稍後我們將探究這些方法的細節。現在您只需知道setUpConnection()將連接到遠程服務器,getFile()將向遠程服務器請求fileNameToGet的內容以及tearDownConnection()將從遠程服務器上斷開。
實現main()
這裡我們實現main()方法,它將創建RemoteFileClient並用它來獲取遠程文件的內容,然後打印結果。main()方法用主機的IP地址和端口號實例化一個新RemoteFileClient(客戶機)。然後,我們告訴客戶機建立一個到主機的連接。接著,我們告訴客戶機獲取主機上一個指定文件的內容。最後,我們告訴客戶機斷開它到主機的連接。我們把文件內容打印到控制台,只是為了證明一切都是按計劃進行的。
建立連接
這裡我們實現setUpConnection()方法,它將創建我們的Socket並讓我們訪問該套接字的流:
1.public void setUpConnection() {
2. try {
3. Socket client = new Socket(hostIp,hostPort);
4. socketReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
5. socketWriter = new PrintWriter(client.getOutputStream());
6. }
7. catch(UnknownHostException e) {
8. System.out.println("Error1 setting up socket connection: unknown host at "+hostIp+":"+hostPort);
9. }
10. catch(IOException e) {
11. System.out.println("Error2 setting up socket connection: "+e);
12. }
13. }
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()方法,它將告訴服務器我們想要什麼文件並在服務器傳回其內容時接收該內容。
1.public String getFile(String fileNameToGet) {
2. StringBuffer fileLines = new StringBuffer();
3. try {
4. socketWriter.println(fileNameToGet);
5. socketWriter.flush();
6. String line = null;
7. while((line=socketReader.readLine())!=null)
8. fileLines.append(line+"\n");
9. }
10. catch(IOException e) {
11. System.out.println("Error reading from file: "+fileNameToGet);
12. }
13. return fileLines.toString();
14. }
對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()來達到這個目的。我們把每一個返回行附加到fileLinesStringBuffer(帶有一個換行符以保護行):
String line = null;
while((line=socketReader.readLine())!=null)
fileLines.append(line+"\n");
斷開連接
這裡我們實現tearDownConnection()方法,它將在我們使用完畢連接後負責“清除”。tearDownConnection()方法只是分別關閉我們在Socket的InputStream和OutputStream上創建的BufferedReader和PrintWriter。這樣做會關閉我們從Socket獲取的底層流,所以我們必須捕捉可能的IOException。