程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java 套接字(Socket)詳解

Java 套接字(Socket)詳解

編輯:關於JAVA

套接字(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協議的計算機可以通過套接字互相通訊。

3Sockets套接字

3.1Sockets概況

在客戶機/服務器應用程序中,服務器提供象處理數據庫查詢或修改數據庫中的數據之類的服務。發生在客戶機和服務器之間的通訊必須是可靠的,同時數據在客戶機上的次序應該和服務器發送出來的次序相同。

什麼是套接字?

既然我們已經知道套接字扮演的角色,那麼剩下的問題是:什麼是套接字?BruceEckel在他的《Java編程思想》一書中這樣描述套接字:套接字是一種軟件抽象,用於表達兩台機器之間的連接“終端”。對於一個給定的連接,每台機器上都有一個套接字,您也可以想象它們之間有一條虛擬的“電纜”,“電纜”的每一端都插入到套接字中。當然,機器之間的物理硬件和電纜連接都是完全未知的。抽象的全部目的是使我們無須知道不必知道的細節。

簡言之,一台機器上的套接字與另一台機器上的套接字交談就創建一條通信通道。程序員可以用該通道來在兩台機器之間發送數據。當您發送數據時,TCP/IP協議棧的每一層都會添加適當的報頭信息來包裝數據。這些報頭幫助協議棧把您的數據送到目的地。好消息是Java語言通過"流"為您的代碼提供數據,從而隱藏了所有這些細節,這也是為什麼它們有時候被叫做流套接字(streamingsocket)的原因。

把套接字想成兩端電話上的聽筒,我和您通過專用通道在我們的電話聽筒上講話和聆聽。直到我們決定掛斷電話,對話才會結束(除非我們在使用蜂窩電話)。而且我們各自的電話線路都占線,直到我們掛斷電話。

如果想在沒有更高級機制如ORB(以及CORBA、RMI、IIOP等等)開銷的情況下進行兩台計算機之間的通信,那麼套接字就適合您。套接字的低級細節相當棘手。幸運的是,Java平台給了您一些雖然簡單但卻強大的更高級抽象,使您可以容易地創建和使用套接字。

傳輸控制協議(TCP)提供了一條可靠的、點對點的通訊通道,客戶機/服務器應用程序可以用該通道互相通訊。要通過TCP進行通訊,客戶機和服務器程序建立連接並綁定套接字。套接字用於處理通過網絡連接的應用程序之間的通訊。客戶機和服務器之間更深入的通訊通過套接字完成。

Java被設計成一種連網語言。它通過將連接功能封裝到套接字類裡而使得網絡編程更加容易。套接字類即Socket類(它創建一個客戶套接字)和ServerSocket類(它創建一個服務器套接字)。套接字類大致介紹如下:

lSocket是基類,它支持TCP協議。TCP是一個可靠的流網絡連接協議。Socket類提供了流輸入/輸出的方法,使得從套接字中讀出數據和往套接字中寫數據都很容易。該類對於編寫因特網上的通訊程序而言是必不可少的。

lServerSocket是一個因特網服務程序用來監聽客戶請求的類。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。

如果沒有指明端口號,則使用服務文件中服務器的端口。每種協議有一個缺省的端口號,在端口號未指明時使用該缺省端口號。

端口號應用

21FTP.傳輸文件

23Telnet.提供遠程登錄

25SMTP.傳遞郵件信息

67BOOTP.在啟動時提供配置情況

80HTTP.傳輸Web頁

109POP.使用戶能訪問遠程系統中的郵箱

讓我們再來看一下URL:http://www.jinnuo.com

URL的第一部分(http)意味著你正在使用超文本傳輸協議(HTTP),該協議處理Web文檔。如果沒有指明文件,大多數的Web服務器會取一個叫index.Html文件。因此,IP地址和端口既可以通過明確指出URL各部分來決定,也可以由缺省值決定。

4創建Socket客戶

我們將在本部分討論的示例將闡明在Java代碼中如何使用Socket和ServerSocket。客戶機用Socket連接到服務器。服務器用ServerSocket在端口1001偵聽。客戶機請求服務器C:驅動器上的文件內容。

創建RemoteFileClIEnt類
importJava.io.*;
importJava.Net.*;
publicclassRemoteFileClIEnt{protectedBufferedReadersocketReader;
protectedPrintWritersocketWriter;
protectedStrinGhostIp;
protectedinthostPort;
//構造方法publicRemoteFileClIEnt(StrinGhostIp,inthostPort){this.hostIp=hostIp;this.hostPort=hostPort;}
//向服務器請求文件的內容
publicStringgetFile(StringfileNameToGet){StringBufferfileLines=newStringBuffer();
try{socketWriter.println(fileNameToGet);
socketWriter.flush();
Stringline=null;
while((line=socketReader.readLine())!=null)fileLines.append(line+"\n");}
catch(IOExceptione){System.out.println("Errorreadingfromfile:"+fileNameToGet);}returnfileLines.toString();}
//連接到遠程服務器
publicvoidsetUpConnection(){try{SocketclIEnt=newSocket(hostIp,hostPort);
socketReader=newBufferedReader(newInputStreamReader(clIEnt.getInputStream()));
socketWriter=newPrintWriter(clIEnt.getOutputStream());}
catch(UnknownHostExceptione){System.out.println("Error1settingupsocketconnection:unknownhostat"+hostIp+":"+hostPort);}
catch(IOExceptione){System.out.println("Error2settingupsocketconnection:"+e);}}
//斷開遠程服務器
publicvoidtearDownConnection(){try{socketWriter.close();
socketReader.close();}catch(IOExceptione){System.out.println("Errortearingdownsocketconnection:"+e);}}publicstaticvoidmain(Stringargs[]){RemoteFileClientremoteFileClient=newRemoteFileClIEnt("127.0.0.1",1001);
remoteFileClIEnt.setUpConnection();
StringBufferfileContents=newStringBuffer();
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並讓我們訪問該套接字的流:
publicvoidsetUpConnection(){try{SocketclIEnt=newSocket(hostIp,hostPort);
socketReader=newBufferedReader(newInputStreamReader(clIEnt.getInputStream()));
socketWriter=newPrintWriter(clIEnt.getOutputStream());}
catch(UnknownHostExceptione){System.out.println("Error1settingupsocketconnection:unknownhostat"+hostIp+":"+hostPort);}
catch(IOExceptione){System.out.println("Error2settingupsocketconnection:"+e);}}

setUpConnection()方法用主機的IP地址和端口號創建一個Socket:

SocketclIEnt=newSocket(hostIp,hostPort);

我們把Socket的InputStream包裝進BufferedReader以使我們能夠讀取流的行。然後,我們把Socket的OutputStream包裝進PrintWriter以使我們能夠發送文件請求到服務器:

socketReader=newBufferedReader(newInputStreamReader(clIEnt.getInputStream()));
socketWriter=newPrintWriter(clIEnt.getOutputStream());

請記住我們的客戶機和服務器只是來回傳送字節。客戶機和服務器都必須知道另一方即將發送的是什麼以使它們能夠作出適當的響應。在這個案例中,服務器知道我們將發送一條有效的文件路徑。

當您實例化一個Socket時,將拋出UnknownHostException。這裡我們不特別處理它,但我們打印一些信息到控制台以告訴我們發生了什麼錯誤。同樣地,當我們試圖獲取Socket的InputStream或OutputStream時,如果拋出了一個一般IOException,我們也打印一些信息到控制台。

與主機交談

這裡我們實現getFile()方法,它將告訴服務器我們想要什麼文件並在服務器傳回其內容時接收該內容。
publicStringgetFile(StringfileNameToGet){StringBufferfileLines=newStringBuffer();
try{socketWriter.println(fileNameToGet);
socketWriter.flush();
Stringline=null;
while((line=socketReader.readLine())!=null)fileLines.append(line+"\n");}
catch(IOExceptione){System.out.println("Errorreadingfromfile:"+fileNameToGet);}returnfileLines.toString();}

對getFile()方法的調用要求一個有效的文件路徑String。它首先創建名為fileLines的StringBuffer,fileLines用於存儲我們讀自服務器上的文件的每一行。

StringBufferfileLines=newStringBuffer();

在try{}catch{}塊中,我們用PrintWriter把請求發送到主機,PrintWriter是我們在創建連接期間建立的。

socketWriter.println(fileNameToGet);socketWriter.flush();

請注意這裡我們是flush()該PrintWriter,而不是關閉它。這迫使數據被發送到服務器而不關閉Socket。

一旦我們已經寫到Socket,我們就希望有一些響應。我們不得不在Socket的InputStream上等待它,我們通過在while循環中調用BufferedReader上的readLine()來達到這個目的。我們把每一個返回行附加到fileLinesStringBuffer(帶有一個換行符以保護行):

Stringline=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類
importJava.io.*;
importJava.Net.*;
publicclassRemoteFileServer{intlistenPort;publicRemoteFileServer(intlistenPort){this.listenPort=listenPort;}
//允許客戶機連接到服務器,等待客戶機請求
publicvoidacceptConnections(){try{ServerSocketserver=newServerSocket(listenPort);
SocketincomingConnection=null;
while(true){incomingConnection=server.accept();
handleConnection(incomingConnection);}}
catch(BindExceptione){System.out.println("Unabletobindtoport"+listenPort);}
catch(IOExceptione){System.out.println("UnabletoinstantiateaServerSocketonport:"+listenPort);}}
//與客戶機Socket交互以將客戶機所請求的文件的內容發送到客戶機
publicvoidhandleConnection(SocketincomingConnection){try{OutputStreamoutputToSocket=incomingConnection.getOutputStream();
InputStreaminputFromSocket=incomingConnection.getInputStream();
BufferedReaderstreamReader=newBufferedReader(newInputStreamReader(inputFromSocket));
FileReaderfileReader=newFileReader(newFile(streamReader.readLine()));
BufferedReaderbufferedFileReader=newBufferedReader(fileReader);
PrintWriterstreamWriter=newPrintWriter(incomingConnection.getOutputStream());
Stringline=null;while((line=bufferedFileReader.readLine())!=null){streamWriter.println(line);}fileReader.close();
streamWriter.close();
streamReader.close();}
catch(Exceptione){System.out.println("ErrorhandlingaclIEnt:"+e);
e.printStackTrace();}}
publicstaticvoidmain(Stringargs[]){RemoteFileServerserver=newRemoteFileServer(1001);
server.acceptConnections();}}

跟客戶機中一樣,我們首先導入Java.Net的Java.io。接著,我們給我們的類一個實例變量以保存端口,我們從該端口偵聽進入的連接。缺省情況下,端口是1001。

我們的類有一個main()方法和兩個其它方法。稍後我們將探究這些方法的細節。現在您只需知道acceptConnections()將允許客戶機連接到服務器以及handleConnection()與客戶機Socket交互以將您所請求的文件的內容發送到客戶機。

實現main()

這裡我們實現main()方法,它將創建RemoteFileServer並告訴它接受連接:服務器端的main()方法中,我們實例化一個新RemoteFileServer,它將在偵聽端口(1001)上偵聽進入的連接請求。然後我們調用acceptConnections()來告訴該server進行偵聽。

接受連接

這裡我們實現acceptConnections()方法,它將創建一個ServerSocket並等待連接請求:

publicvoidacceptConnections(){try{ServerSocketserver=newServerSocket(listenPort);
SocketincomingConnection=null;
while(true){incomingConnection=server.accept();
handleConnection(incomingConnection);}}
catch(BindExceptione){System.out.println("Unabletobindtoport"+listenPort);}
catch(IOExceptione){System.out.println("UnabletoinstantiateaServerSocketonport:"+listenPort);}}

acceptConnections()用欲偵聽的端口號來創建ServerSocket。然後我們通過調用該ServerSocket的accept()來告訴它開始偵聽。accept()方法將造成阻塞直到來了一個連接請求。此時,accept()返回一個新的Socket,這個Socket綁定到服務器上一個隨機指定的端口,返回的Socket被傳遞給handleConnection()。請注意我們在一個無限循環中處理對連接的接受。這裡不支持任何關機。

無論何時如果您創建了一個無法綁定到指定端口(可能是因為別的什麼控制了該端口)的ServerSocket,Java代碼都將拋出一個錯誤。所以這裡我們必須捕捉可能的BindException。就跟在客戶機端上時一樣,我們必須捕捉IOException,當我們試圖在ServerSocket上接受連接時,它就會被拋出。請注意,您可以通過用毫秒數調用setSoTimeout()來為accept()調用設置超時,以避免實際長時間的等待。調用setSoTimeout()將使accept()經過指定占用時間後拋出IOException。

處理連接

這裡我們實現handleConnection()方法,它將用連接的流來接收輸入和寫輸出:

publicvoidhandleConnection(SocketincomingConnection){try{OutputStreamoutputToSocket=incomingConnection.getOutputStream();
InputStreaminputFromSocket=incomingConnection.getInputStream();
BufferedReaderstreamReader=newBufferedReader(newInputStreamReader(inputFromSocket));
FileReaderfileReader=newFileReader(newFile(streamReader.readLine()));
BufferedReaderbufferedFileReader=newBufferedReader(fileReader);
PrintWriterstreamWriter=newPrintWriter(incomingConnection.getOutputStream());
Stringline=null;
while((line=bufferedFileReader.readLine())!=null){streamWriter.println(line);}fileReader.close();
streamWriter.close();streamReader.close();}
catch(Exceptione){System.out.println("ErrorhandlingaclIEnt:"+e);
e.printStackTrace();}}

跟在客戶機中一樣,我們用getOutputStream()和getInputStream()來獲取與我們剛創建的Socket相關聯的流。跟在客戶機端一樣,我們把InputStream包裝進BufferedReader,把OutputStream包裝進PrintWriter。在服務器端上,我們需要添加一些代碼,用來讀取目標文件和把內容逐行發送到客戶機。這裡是重要的代碼:
FileReaderfileReader=newFileReader(newFile(streamReader.readLine()));
BufferedReaderbufferedFileReader=newBufferedReader(fileReader);
Stringline=null;
while((line=bufferedFileReader.readLine())!=null){streamWriter.println(line);}

這些代碼值得詳細解釋。讓我們一點一點來看:

FileReaderfileReader=newFileReader(newFile(streamReader.readLine()));

首先,我們使用Socket的InputStream的BufferedReader。我們應該獲取一條有效的文件路徑,所以我們用該路徑名構造一個新File。我們創建一個新FileReader來處理讀文件的操作。

BufferedReaderbufferedFileReader=newBufferedReader(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類

importJava.io.*;
importJava.Net.*;
publicclassMultithreadedRemoteFileServer{intlistenPort;
publicMultithreadedRemoteFileServer(intlistenPort){this.listenPort=listenPort;}
//允許客戶機連接到服務器,等待客戶機請求
publicvoidacceptConnections(){try{ServerSocketserver=newServerSocket(listenPort,5);
SocketincomingConnection=null;
while(true){incomingConnection=server.accept();
handleConnection(incomingConnection);}}
catch(BindExceptione){System.out.println("Unabletobindtoport"+listenPort);}
catch(IOExceptione){System.out.println("UnabletoinstantiateaServerSocketonport:"+listenPort);}}
//與客戶機Socket交互以將客戶機所請求的文件的內容發送到客戶機
publicvoidhandleConnection(SocketconnectionToHandle){newThread(newConnectionHandler(connectionToHandle)).start();}
publicstaticvoidmain(Stringargs[]){MultithreadedRemoteFileServerserver=newMultithreadedRemoteFileServer(1001);
server.acceptConnections();}}

這裡我們實現改動過acceptConnections()方法,它將創建一個能夠處理待發請求的ServerSocket,並告訴ServerSocket接受連接。

新的server仍然需要acceptConnections(),所以這些代碼實際上是一樣的。突出顯示的行表示一個重大的不同。對這個多線程版,我們現在可以指定客戶機請求的最大數目,這些請求都能在實例化ServerSocket期間處於待發狀態。如果我們沒有指定客戶機請求的最大數目,則我們假設使用缺省值50。

這裡是它的工作機制。假設我們指定待發數(backlog值)是5並且有五台客戶機請求連接到我們的服務器。我們的服務器將著手處理第一個連接,但處理該連接需要很長時間。由於我們的待發值是5,所以我們一次可以放五個請求到隊列中。我們正在處理一個,所以這意味著還有其它五個正在等待。等待的和正在處理的一共有六個。當我們的服務器仍忙於接受一號連接(記住隊列中還有2?6號)時,如果有第七個客戶機提出連接申請,那麼,該第七個客戶機將遭到拒絕。我們將在帶有連接池服務器示例中說明如何限定能同時連接的客戶機數目。

處理連接:

publicvoidhandleConnection(SocketconnectionToHandle){newThread(newConnectionHandler(connectionToHandle)).start();}

我們對RemoteFileServer所做的大改動就體現在這個方法上。我們仍然在服務器接受一個連接之後調用handleConnection(),但現在我們把該Socket傳遞給ConnectionHandler的一個實例,它是Runnable的。我們用ConnectionHandler創建一個新Thread並啟動它。ConnectionHandler的run()方法包Socket讀/寫和讀File的代碼,這些代碼原來在RemoteFileServer的handleConnection()中。

創建ConnectionHandler類

importJava.io.*;
importJava.Net.*;
publicclassConnectionHandlerimplementsRunnable{protectedSocketsocketToHandle;
publicConnectionHandler(SocketsocketToHandle){this.socketToHandle=socketToHandle;}
publicvoidrun(){try{PrintWriterstreamWriter=newPrintWriter(socketToHandle.getOutputStream());
BufferedReaderstreamReader=newBufferedReader(newInputStreamReader(socketToHandle.getInputStream()));
StringfileToRead=streamReader.readLine();
BufferedReaderfileReader=newBufferedReader(newFileReader(fileToRead));
Stringline=null;
while((line=fileReader.readLine())!=null){streamWriter.println(line);}fileReader.close();
streamWriter.close();streamReader.close();}
catch(Exceptione){System.out.println("ErrorhandlingaclIEnt:"+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。然後我們用這些代碼逐行地讀目標文件:

PrintWriterstreamWriter=newPrintWriter(socketToHandle.getOutputStream());
BufferedReaderstreamReader=newBufferedReader(newInputStreamReader(socketToHandle.getInputStream()));
StringfileToRead=streamReader.readLine();BufferedReaderfileReader=newBufferedReader(newFileReader(fileToRead));
Stringline=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將為它提供服務。這種設計能帶來以下好處:

•它限定了允許同時連接的數目。

•我們只需啟動ConnectionHandlerThread一次。

幸運的是,跟在我們的多線程示例中一樣,往代碼中添加“池”不需要來一個大改動。事實上,應用程序的客戶機端根本就不受影響。在服務器端,我們在服務器啟動時創建一定數量的ConnectionHandler,我們把進入的連接放入“池”中並讓ConnectionHandler打理剩下的事情。這種設計中有很多我們不打算討論的可能存在的技巧。例如,我們可以通過限定允許在“池”中建立的連接的數目來拒絕客戶機。

請注意:我們將不會再次討論acceptConnections()。這個方法跟前面示例中的完全一樣。它無限循環地調用ServerSocket上的accept()並把連接傳遞到handleConnection()。

創建PooledRemoteFileServer類
importJava.io.*;
importJava.Net.*;
importJava.util.*;
publicclassPooledRemoteFileServer{protectedintmaxConnections;
protectedintlistenPort;protectedServerSocketserverSocket;
publicPooledRemoteFileServer(intaListenPort,intmaxConnections){listenPort=aListenPort;
this.maxConnections=maxConnections;}
publicvoidacceptConnections(){try{ServerSocketserver=newServerSocket(listenPort,5);
SocketincomingConnection=null;
while(true){incomingConnection=server.accept();
handleConnection(incomingConnection);}}catch(BindExceptione){System.out.println("");}
catch(IOExceptione){System.out.println(""+listenPort);}}protectedvoidhandleConnection(SocketconnectionToHandle){PooledConnectionHandler.processRequest(connectionToHandle);}
publicvoidsetUpHandlers(){for(inti=0;inewThread(currentHandler,"Handler"+i).start();}}publicstaticvoidmain(Stringargs[]){PooledRemoteFileServerserver=newPooledRemoteFileServer(1001,3);
server.setUpHandlers();
server.acceptConnections();}}

請注意一下您現在應該熟悉了的import語句。我們給類以下實例變量以保存:

•我們的服務器能同時處理的活動客戶機連接的最大數目

•進入的連接的偵聽端口(我們沒有指定缺省值,但如果您想這樣做,並不會受到限制)

•將接受客戶機連接請求的ServerSocket

類的構造器用的參數是偵聽端口和連接的最大數目

我們的類有一個main()方法和三個其它方法。稍後我們將探究這些方法的細節。現在只須知道setUpHandlers()創建數目為maxConnections的大量PooledConnectionHandler,而其它兩個方法則與我們前面已經看到的相似:acceptConnections()在ServerSocket上偵聽傳入的客戶機連接,而handleConnection則在客戶機連接一旦被建立後就實際處理它。

實現main()

這裡我們實現需作改動的main()方法,該方法將創建能夠處理給定數目的客戶機連接的PooledRemoteFileServer,並告訴它接受連接:

publicstaticvoidmain(Stringargs[]){PooledRemoteFileServerserver=newPooledRemoteFileServer(1001,3);
server.setUpHandlers();server.acceptConnections();}

我們的main()方法很簡單。我們實例化一個新的PooledRemoteFileServer,它將通過調用setUpHandlers()來建立三個PooledConnectionHandler。一旦服務器就緒,我們就告訴它acceptConnections()。

建立連接處理程序

publicvoidsetUpHandlers(){for(inti=0;i<maxConnections;i++){PooledConnectionHandlercurrentHandler=newPooledConnectionHandler();
newThread(currentHandler,"Handler"+i).start();}}

setUpHandlers()方法創建maxConnections(例如3)個PooledConnectionHandler並在新Thread中激活它們。用實現了Runnable的對象來創建Thread使我們可以在Thread調用start()並且可以期望在Runnable上調用了run()。換句話說,我們的PooledConnectionHandler將等著處理進入的連接,每個都在它自己的Thread中進行。我們在示例中只創建三個Thread,而且一旦服務器運行,這就不能被改變。

處理連接

這裡我們實現需作改動的handleConnections()方法,它將委派PooledConnectionHandler處理連接:

protectedvoidhandleConnection(SocketconnectionToHandle){PooledConnectionHandler.processRequest(connectionToHandle);}

我們現在叫PooledConnectionHandler處理所有進入的連接(processRequest()是一個靜態方法)。

創建PooledRemoteFileServer類

importJava.io.*;
importJava.Net.*;
importJava.util.*;
publicclassPooledConnectionHandlerimplementsRunnable{protectedSocketconnection;
protectedstaticListpool=newLinkedList();
publicPooledConnectionHandler(){}publicvoidhandleConnection(){try{PrintWriterstreamWriter=newPrintWriter(connection.getOutputStream());
BufferedReaderstreamReader=newBufferedReader(newInputStreamReader(connection.getInputStream()));
StringfileToRead=streamReader.readLine();
BufferedReaderfileReader=newBufferedReader(newFileReader(fileToRead));
Stringline=null;
while((line=fileReader.readLine())!=null)streamWriter.println(line);
fileReader.close();
streamWriter.close();
streamReader.close();}
catch(FileNotFoundExceptione){System.out.println("");}catch(IOExceptione){System.out.println(""+e);}}
publicstaticvoidprocessRequest(SocketrequestToHandle){synchronized(pool){pool.add(pool.size(),requestToHandle);pool.notifyAll();}}
publicvoidrun(){while(true){synchronized(pool){while(pool.isEmpty()){try{pool.wait();}
catch(InterruptedExceptione){e.printStackTrace();}}connection=(Socket)pool.remove(0);}
handleConnection();}}}

這個助手類與ConnectionHandler非常相似,但它帶有處理連接池的手段。該類有兩個實例變量:

•connection是當前正在處理的Socket

•名為pool的靜態LinkedList保存需被處理的連接

填充連接池

這裡我們實現PooledConnectionHandler上的processRequest()方法,它將把傳入請求添加到池中,並告訴其它正在等待的對象該池已經有一些內容:

publicstaticvoidprocessRequest(SocketrequestToHandle){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()方法,它將在連接池上等待,並且池中一有連接就處理它:
publicvoidrun(){while(true){synchronized(pool){while(pool.isEmpty()){try{pool.wait();}
catch(InterruptedExceptione){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()方法,該方法將攫取連接的流,使用它們,並在任務完成之後清除它們:

publicvoidhandleConnection()
{try{PrintWriterstreamWriter=newPrintWriter(connection.getOutputStream());
BufferedReaderstreamReader=newBufferedReader(newInputStreamReader(connection.getInputStream()));
StringfileToRead=streamReader.readLine();
BufferedReaderfileReader=newBufferedReader(newFileReader(fileToRead));
Stringline=null;while((line=fileReader.readLine())!=null)streamWriter.println(line);
fileReader.close();
streamWriter.close();
streamReader.close();}
catch(FileNotFoundExceptione){System.out.println("");}
catch(IOExceptione){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內),您仍然必須管理稍顯雜亂的字節細節,但您只須做一次就可以了。更好的是,您可以在將來的項目中重用這些低級別的助手類。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved