原文地址:http://blog.csdn.net/carolzhang8406/article/details/6772812
套接字(socket)爲兩臺計算機之間的通信提供了一種機制,在 James Gosling 注意到 Java 語言之前,套接字就早已赫赫有名。該語言只是讓您不必瞭解底層操作系統的細節就能有效地使用套接字。多數着重討論 Java 編碼的書或者未涵蓋這個主題,或者給讀者留下很大的想象空間。本教程將告訴您開始在代碼中有效地使用套接字時,您真正需要知道哪些知識。我們將專門討論以下問題:
什麼是套接字 它位於您可能要寫的程序的什麼地方 能工作的最簡單的套接字實現 ― 以幫助您理解基礎知識 詳細剖析另外兩個探討如何在多線程和具有連接池環境中使用套接字的示例 簡要討論一個現實世界中的套接字應用程序
如果您能夠描述如何使用 java.net
包中的類,那麼本教程對您來說也許基礎了點,雖然用它來提高一下還是不錯的。如果您在 PC 和其它平臺上使用套接字已經幾年,那麼最初的部分也許會使您覺得煩。但如果您不熟悉套接字,而且只是想知道什麼是套接字以及如何在 Java 代碼中有效地使用它們,那麼本教程就是一個開始的好地方。
套接字基礎
1. 介紹
多數程序員,不管他們是否使用 Java 語言進行編碼,都不想很多知道關於不同計算機上的應用程序彼此間如何通信的低級細節。程序員們希望處理更容易理解的更高級抽象。Java 程序員希望能用他們熟悉的 Java 構造,通過直觀接口與對象交互。
套接字在兩個領域中都存在 ― 我們寧願避開的低級細節和我們更願處理的抽象層。本教程討論的低級細節將只限於理解抽象應用程序所必須的部分。
2. 計算機組網 101
計算機以一種非常簡單的方式進行相互間的操作和通信。計算機芯片是以 1 和 0 的形式存儲並傳輸數據的開―閉轉換器的集合。當計算機想共享數據時,它們所需做的全部就是以一致的速度、順序、定時等等來回傳輸幾百萬比特和字節的數據流。每次想在兩個應用程序之間進行信息通信時,您怎麼會願意擔心那些細節呢?
爲免除這些擔心,我們需要每次都以相同方式完成該項工作的一組包協議。這將允許我們處理應用程序級的工作,而不必擔心低級網絡細節。這些成包協議稱爲協議棧(stack)。TCP/IP 是當今最常見的協議棧。多數協議棧(包括 TCP/IP)都大致對應於國際標準化組織(International Standards Organization,ISO)的開放系統互連參考模型(Open Systems Interconnect Reference Model,OSIRM)。OSIRM 認爲在一個可靠的計算機組網中有七個邏輯層(見圖)。各個地方的公司都對這個模型某些層的實現做了一些貢獻,從生成電子信號(光脈衝、射頻等等)到提供數據給應用程序。TCP/IP 映射到 OSI 模型中的兩層的情形如圖所示。
我們不想涉及層的太多細節,但您應該知道套接字位於什麼地方。
3. 套接字位於什麼地方
套接字大致駐留在 OSI 模型的會話層(見圖)。會話層夾在其上面嚮應用的層和其下的實時數據通信層之間。會話層爲兩臺計算機之間的數據流提供管理和控制服務。作爲該層的一部分,套接字提供一個隱藏從導線上獲取比特和字節的複雜性的抽象。換句話說,套接字允許我們讓應用程序表明它想發送一些字節即可傳輸數據。套接字隱藏了完成該項工作的具體細節。
當您打電話時,您的聲音傳到傳感器,傳感器把它轉換成可以傳輸的電數據。電話機是人與電信網絡的接口。您無須知道聲音如何傳輸的細節,只要知道想打電話給誰就行了。同樣地,套接字扮演隱藏在未知通道上傳輸 1 和 0 的複雜性的高級接口的角色。
4. 把套接字暴露給應用程序
使用套接字的代碼工作於表示層。表示層提供應用層能夠使用的信息的公共表示。假設您打算把應用程序連接到只能識別 EBCDIC 的舊的銀行系統。應用程序的域對象以 ASCII 格式存儲信息。在這種情況下,您得負責在表示層上編寫把數據從 EBCDIC 轉換成 ASCII 的代碼,然後(比方說)給應用層提供域對象。應用層然後就可以用域對象來做它想做的任何事情。
您編寫的套接字處理代碼只存在於表示層中。您的應用層無須知道套接字如何工作的任何事情。
5. 什麼是套接字?
既然我們已經知道套接字扮演的角色,那麼剩下的問題是:什麼是套接字?Bruce Eckel 在他的《Java 編程思想》一書中這樣描述套接字:
套接字是一種軟件抽象,用於表達兩臺機器之間的連接「終端」。對於一個給定的連接,每臺機器上都有一個套接字,您也可以想象它們之間有一條虛擬的「電纜」,「電纜」的每一端都插入到套接字中。當然,機器之間的物理硬件和電纜連接都是完全未知的。抽象的全部目的是使我們無須知道不必知道的細節。
簡言之,一臺機器上的套接字與另一臺機器上的套接字交談就創建一條通信通道。程序員可以用該通道來在兩臺機器之間發送數據。當您發送數據時,TCP/IP 協議棧的每一層都會添加適當的報頭信息來包裝數據。這些報頭幫助協議棧把您的數據送到目的地。好消息是 Java 語言通過"流"爲您的代碼提供數據,從而隱藏了所有這些細節,這也是爲什麼它們有時候被叫做流套接字(streaming socket)的原因。
把套接字想成兩端電話上的聽筒 ― 我和您通過專用通道在我們的電話聽筒上講話和聆聽。直到我們決定掛斷電話,對話纔會結束(除非我們在使用蜂窩電話)。而且我們各自的電話線路都佔線,直到我們掛斷電話。
如果想在沒有更高級機制如 ORB(以及 CORBA、RMI、IIOP 等等)開銷的情況下進行兩臺計算機之間的通信,那麼套接字就適合您。套接字的低級細節相當棘手。幸運的是,Java 平臺給了您一些雖然簡單但卻強大的更高級抽象,使您可以容易地創建和使用套接字。
6. 套接字的類型
一般而言,Java 語言中的套接字有以下兩種形式:
TCP 套接字(由
Socket
類實現,稍後我們將討論這個類) UDP 套接字(由
DatagramSocket
類實現)
TCP 和 UDP 扮演相同角色,但做法不同。兩者都接收傳輸協議數據包並將其內容向前傳送到表示層。TCP 把消息分解成數據包(數據報,datagrams)並在接收端以正確的順序把它們重新裝配起來。TCP 還處理對遺失數據包的重傳請求。有了 TCP,位於上層的層要擔心的事情就少多了。UDP 不提供裝配和重傳請求這些功能。它只是向前傳送信息包。位於上層的層必須確保消息是完整的並且是以正確的順序裝配的。
一般而言,UDP 強加給您的應用程序的性能開銷更小,但只在應用程序不會突然交換大量數據並且不必裝配大量數據報以完成一條消息的時候。否則,TCP 纔是最簡單或許也是最高效的選擇。
因爲多數讀者都喜歡 TCP 勝過 UDP,所以我們將把討論限制在 Java 語言中面向 TCP 的類。
一個祕密的套接字
1. 介紹
Java 平臺在 java.net
包中提供套接字的實現。在本教程中,我們將與 java.net
中的以下三個類一起工作:
URLConnection
Socket
ServerSocket
java.net
中還有更多的類,但這些是您將最經常碰到的。讓我們從 URLConnection
開始。這個類爲您不必瞭解任何底層套接字細節就能在 Java 代碼中使用套接字提供一種途徑。
2. 甚至不用嘗試就可使用套接字
URLConnection
類是所有在應用程序和 URL 之間創建通信鏈路的類的抽象超類。URLConnection
在獲取 Web 服務器上的文檔方面特別有用,但也可用於連接由 URL 標識的任何資源。該類的實例既可用於從資源中讀,也可用於往資源中寫。例如,您可以連接到一個 servlet 併發送一個格式良好的 XML String
到服務器上進行處理。URLConnection
的具體子類(例如 HttpURLConnection
)提供特定於它們實現的額外功能。對於我們的示例,我們不想做任何特別的事情,所以我們將使用 URLConnection
本身提供的缺省行爲。
連接到 URL 包括幾個步驟:
創建
URLConnection
用各種 setter 方法配置它 連接到 URL 用各種 getter 方法與它交互
接着,我們將看一些演示如何用 URLConnection
來從服務器請求文檔的樣本代碼
3. URLClient 類
我們將從 URLClient
類的結構講起。
- import java.io.*;
- import java.net.*;
-
- public class URLClient {
- protected URLConnection connection;
-
- public static void main(String[] args) {
- }
- public String getDocumentAt(String urlString) {
- }
- }
- import java.io.*;
- import java.net.*;
-
- public class URLClient {
- protected URLConnection connection;
-
- public static void main(String[] args) {
- }
- public String getDocumentAt(String urlString) {
- }
- }
要做的第一件事是導入 java.net
和 java.io
。
我們給我們的類一個實例變量以保存一個 URLConnection
。
我們的類有一個 main()
方法,它處理瀏覽文檔的邏輯流。我們的類還有一個 getDocumentAt()
方法,該方法連接到服務器並向它請求給定文檔。下面我們將分別探究這些方法的細節。
4. 瀏覽文檔
main()
方法處理瀏覽文檔的邏輯流:
- public static void main(String[] args) {
- URLClient client = new URLClient();
- String yahoo = client.getDocumentAt("http://www.yahoo.com");
- System.out.println(yahoo);
- }
- public static void main(String[] args) {
- URLClient client = new URLClient();
- String yahoo = client.getDocumentAt("http://www.yahoo.com");
- System.out.println(yahoo);
- }
我們的 main()
方法只是創建一個新的 URLClient
並用一個有效的 URL String
調用 getDocumentAt()
。當調用返回該文檔時,我們把它存儲在 String
,然後將它打印到控制檯。然而,實際的工作是在getDocumentAt()
方法中完成的。
5. 從服務器請求一個文檔
getDocumentAt()
方法處理獲取 Web 上的文檔的實際工作:
- public String getDocumentAt(String urlString) {
- StringBuffer document = new StringBuffer();
- try {
- URL url = new URL(urlString);
- URLConnection conn = url.openConnection();
- BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
-
- String line = null;
- while ((line = reader.readLine()) != null)
- document.append(line + "\n");
- reader.close();
- } catch (MalformedURLException e) {
- System.out.println("Unable to connect to URL: " + urlString);
- } catch (IOException e) {
- System.out.println("IOException when connecting to URL: " + urlString);
- }
- return document.toString();
- }
- public String getDocumentAt(String urlString) {
- StringBuffer document = new StringBuffer();
- try {
- URL url = new URL(urlString);
- URLConnection conn = url.openConnection();
- BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
-
- String line = null;
- while ((line = reader.readLine()) != null)
- document.append(line + "\n");
- reader.close();
- } catch (MalformedURLException e) {
- System.out.println("Unable to connect to URL: " + urlString);
- } catch (IOException e) {
- System.out.println("IOException when connecting to URL: " + urlString);
- }
- return document.toString();
- }
getDocumentAt()
方法有一個 String
參數,該參數包含我們想獲取的文檔的 URL。我們在開始時創建一個StringBuffer
來保存文檔的行。然後我們用我們傳進去的 urlString
創建一個新 URL
。接着創建一個 URLConnection
並打開它:
- URLConnection conn = url.openConnection();
- URLConnection conn = url.openConnection();
一旦有了一個 URLConnection
,我們就獲取它的 InputStream
幷包裝進 InputStreamReader
,然後我們又把 InputStreamReader
包裝進 BufferedReader
以使我們能夠讀取想從服務器上獲取的文檔的行。在 Java 代碼中處理套接字時,我們將經常使用這種包裝技術,但我們不會總是詳細討論它。在我們繼續往前講之前,您應該熟悉它:
- BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
- BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
有了 BufferedReader
,就使得我們能夠容易地讀取文檔內容。我們在 while
循環中調用 reader
上的 readLine()
:
- String line = null;
- while ((line = reader.readLine()) != null)
- document.append(line + "\n");
- String line = null;
- while ((line = reader.readLine()) != null)
- document.append(line + "\n");
對 readLine()
的調用將直至碰到一個從 InputStream
傳入的行終止符(例如換行符)時才阻塞。如果沒碰到,它將繼續等待。只有當連接被關閉時,它纔會返回 null
。在這個案例中,一旦我們獲取一個行(line),我們就把它連同一個換行符一起附加(append)到名爲 document
的 StringBuffer
上。這保留了服務器端上讀取的文檔的格式。
我們在讀完行之後關閉 BufferedReader
:
如果提供給 URL
構造器的 urlString
是無效的,那麼將拋出 MalformedURLException
。如果發生了別的錯誤,例如當從連接上獲取 InputStream
時,那麼將拋出 IOException
。
6. 總結
實際上,URLConnection
使用套接字從我們指定的 URL 中讀取信息(它只是解析成 IP 地址),但我們無須瞭解它,我們也不關心。但有很多事;我們馬上就去看看。
在繼續往前講之前,讓我們回顧一下創建和使用 URLConnection
的步驟:
用您想連接的資源的有效 URL
String
實例化一個
URL
(如有問題則拋出
MalformedURLException
)。
打開該
URL
上的一個連接。
把該連接的
InputStream
包裝進
BufferedReader
以使您能夠讀取行。
用
BufferedReader
讀文檔。
關閉
BufferedReader
。
附: URLClient
的完整代碼清單:
- import java.io.*;
- import java.net.*;
-
- public class URLClient {
- protected HttpURLConnection connection;
- public String getDocumentAt(String urlString) {
- StringBuffer document = new StringBuffer();
- try {
- URL url = new URL(urlString);
- URLConnection conn = url.openConnection();
- BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
-
- String line = null;
- while ((line = reader.readLine()) != null)
- document.append(line + "\n");
-
- reader.close();
- } catch (MalformedURLException e) {
- System.out.println("Unable to connect to URL: " + urlString);
- } catch (IOException e) {
- System.out.println("IOException when connecting to URL: " + urlString);
- }
-
- return document.toString();
- }
- public static void main(String[] args) {
- URLClient client = new URLClient();
- String yahoo = client.getDocumentAt("http://www.yahoo.com");
-
- System.out.println(yahoo);
- }
- }
一個簡單示例
1. 背景
我們將在本部分討論的示例將闡明在 Java 代碼中如何使用 Socket
和 ServerSocket
。客戶機用 Socket
連接到服務器。服務器用 ServerSocket
在端口 3000 偵聽。客戶機請求服務器 C: 驅動器上的文件內容。
爲清楚起見,我們把示例分解成客戶機端和服務器端。最後我們將把它們組合起來以使您能看到整體模樣。
我們在使用 JDK 1.2 的 IBM VisualAge for Java 3.5 上開發這些代碼。要自己創建這個示例,您應有完好的 JDK 1.1.7 或更高版本。客戶機和服務器將只在一臺機器上運行,所以您不必擔心是否有一個可用的網絡。
2. 創建 RemoteFileClient 類
這裏是 RemoteFileClient
類的結構:
- import java.io.*;
- import java.net.*;
-
- public class RemoteFileClient {
- protected String hostIp;
- protected int hostPort;
- protected BufferedReader socketReader;
- protected PrintWriter socketWriter;
-
- public RemoteFileClient(String aHostIp, int aHostPort) {
- hostIp = aHostIp;
- hostPort = aHostPort;
- }
- public static void main(String[] args) {
- }
- public void setUpConnection() {
- }
- public String getFile(String fileNameToGet) {
- }
- public void tearDownConnection() {
- }
- }
- import java.io.*;
- import java.net.*;
-
- public class RemoteFileClient {
- protected String hostIp;
- protected int hostPort;
- protected BufferedReader socketReader;
- protected PrintWriter socketWriter;
-
- public RemoteFileClient(String aHostIp, int aHostPort) {
- hostIp = aHostIp;
- hostPort = aHostPort;
- }
- public static void main(String[] args) {
- }
- public void setUpConnection() {
- }
- public String getFile(String fileNameToGet) {
- }
- public void tearDownConnection() {
- }
- }
首先我們導入 java.net
和 java.io
。java.net
包爲您提供您需要的套接字工具。java.io
包爲您提供對流進行讀寫的工具,這是您與 TCP 套接字通信的唯一途徑。
我們給我們的類實例變量以支持對套接字流的讀寫和存儲我們將連接到的遠程主機的詳細信息。
我們類的構造器有兩個參數:遠程主機的 IP 地址和端口號各一個,而且構造器將它們賦給實例變量。
我們的類有一個 main()
方法和三個其它方法。稍後我們將探究這些方法的細節。現在您只需知道 setUpConnection()
將連接到遠程服務器,getFile()
將向遠程服務器請求 fileNameToGet
的內容以及 tearDownConnection()
將從遠程服務器上斷開。
3. 實現 main()
這裏我們實現 main()
方法,它將創建 RemoteFileClient
並用它來獲取遠程文件的內容,然後打印結果:
- public static void main(String[] args) {
- RemoteFileClient remoteFileClient = new RemoteFileClient("127.0.0.1", 3000);
- remoteFileClient.setUpConnection();
- String fileContents =
- remoteFileClient.getFile("C:\\WINNT\\Temp\\RemoteFile.txt");
- remoteFileClient.tearDownConnection();
-
- System.out.println(fileContents);
- }
- public static void main(String[] args) {
- RemoteFileClient remoteFileClient = new RemoteFileClient("127.0.0.1", 3000);
- remoteFileClient.setUpConnection();
- String fileContents =
- remoteFileClient.getFile("C:\\WINNT\\Temp\\RemoteFile.txt");
- remoteFileClient.tearDownConnection();
-
- System.out.println(fileContents);
- }
main()
方法用主機的 IP 地址和端口號實例化一個新 RemoteFileClient
(客戶機)。然後,我們告訴客戶機建立一個到主機的連接(稍後有更詳細的討論)。接着,我們告訴客戶機獲取主機上一個指定文件的內容。最後,我們告訴客戶機斷開它到主機的連接。我們把文件內容打印到控制檯,只是爲了證明一切都是按計劃進行的。
4. 建立連接
這裏我們實現 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("Error setting up socket connection: unknown host at " + hostIp + ":" + hostPort);
- } catch (IOException e) {
- System.out.println("Error setting up socket connection: " + e);
- }
- }
- 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("Error setting up socket connection: unknown host at " + hostIp + ":" + hostPort);
- } catch (IOException e) {
- System.out.println("Error setting up socket connection: " + e);
- }
- }
setUpConnection()
方法用主機的 IP 地址和端口號創建一個 Socket
:
- Socket client = new Socket(hostIp, hostPort);
- Socket client = new Socket(hostIp, hostPort);
我們把 Socket
的 InputStream
包裝進 BufferedReader
以使我們能夠讀取流的行。然後,我們把Socket
的 OutputStream
包裝進 PrintWriter
以使我們能夠發送文件請求到服務器:
- socketReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
- socketWriter = new PrintWriter(client.getOutputStream());
- socketReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
- socketWriter = new PrintWriter(client.getOutputStream());
請記住我們的客戶機和服務器只是來回傳送字節。客戶機和服務器都必須知道另一方即將發送的是什麼以使它們能夠作出適當的響應。在這個案例中,服務器知道我們將發送一條有效的文件路徑。
當您實例化一個 Socket
時,將拋出 UnknownHostException
。這裏我們不特別處理它,但我們打印一些信息到控制檯以告訴我們發生了什麼錯誤。同樣地,當我們試圖獲取 Socket
的 InputStream
或 OutputStream
時,如果拋出了一個一般 IOException
,我們也打印一些信息到控制檯。這是本教程的一般做法。在產品代碼中,我們應該做得更完善些。
5. 與主機交談
這裏我們實現 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();
- }
- 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();
- StringBuffer fileLines = new StringBuffer();
在 try{}catch{}
塊中,我們用 PrintWriter
把請求發送到主機,PrintWriter
是我們在創建連接期間建立的。
- socketWriter.println(fileNameToGet);
- socketWriter.flush();
- 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");
- String line = null;
- while ((line = socketReader.readLine()) != null)
- fileLines.append(line + "\n");
6. 斷開連接
這裏我們實現 tearDownConnection()
方法,它將在我們使用完畢連接後負責「清除」:
- public void tearDownConnection() {
- try {
- socketWriter.close();
- socketReader.close();
- } catch (IOException e) {
- System.out.println("Error tearing down socket connection: " + e);
- }
- }
- public void tearDownConnection() {
- try {
- socketWriter.close();
- socketReader.close();
- } catch (IOException e) {
- System.out.println("Error tearing down socket connection: " + e);
- }
- }
tearDownConnection()
方法只是分別關閉我們在 Socket
的 InputStream
和 OutputStream
上創建的 BufferedReader
和 PrintWriter
。這樣做會關閉我們從 Socket
獲取的底層流,所以我們必須捕捉可能的 IOException
。
7. 總結一下客戶機
我們的類研究完了。在我們繼續往前討論服務器端的情況之前,讓我們回顧一下創建和使用 Socket
的步驟:
用您想連接的機器的 IP 地址和端口實例化
Socket
(如有問題則拋出
Exception
)。
獲取
Socket
上的流以進行讀寫。
把流包裝進
BufferedReader
/
PrintWriter
的實例,如果這樣做能使事情更簡單的話。
對
Socket
進行讀寫。
關閉打開的流。
附: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 aHostIp, int aHostPort) {
- hostIp = aHostIp;
- hostPort = aHostPort;
- }
- 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 static void main(String[] args) {
- RemoteFileClient remoteFileClient = new RemoteFileClient("127.0.0.1", 3000);
- remoteFileClient.setUpConnection();
- String fileContents = remoteFileClient.getFile("C:\\WINNT\\Temp\\RemoteFile.txt");
- remoteFileClient.tearDownConnection();
-
- System.out.println(fileContents);
- }
- 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("Error setting up socket connection: unknown host at " + hostIp + ":" + hostPort);
- } catch (IOException e) {
- System.out.println("Error 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);
- }
- }
- }
- import java.io.*;
- import java.net.*;
-
- public class RemoteFileClient {
- protected BufferedReader socketReader;
- protected PrintWriter socketWriter;
- protected String hostIp;
- protected int hostPort;
-
- public RemoteFileClient(String aHostIp, int aHostPort) {
- hostIp = aHostIp;
- hostPort = aHostPort;
- }
- 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 static void main(String[] args) {
- RemoteFileClient remoteFileClient = new RemoteFileClient("127.0.0.1", 3000);
- remoteFileClient.setUpConnection();
- String fileContents = remoteFileClient.getFile("C:\\WINNT\\Temp\\RemoteFile.txt");
- remoteFileClient.tearDownConnection();
-
- System.out.println(fileContents);
- }
- 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("Error setting up socket connection: unknown host at " + hostIp + ":" + hostPort);
- } catch (IOException e) {
- System.out.println("Error 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);
- }
- }
- }
8. 創建 RemoteFileServer 類
這裏是 RemoteFileServer
類的結構:
- import java.io.*;
- import java.net.*;
-
- public class RemoteFileServer {
- protected int listenPort = 3000;
- public static void main(String[] args) {
- }
- public void acceptConnections() {
- }
- public void handleConnection(Socket incomingConnection) {
- }
- }
- import java.io.*;
- import java.net.*;
-
- public class RemoteFileServer {
- protected int listenPort = 3000;
- public static void main(String[] args) {
- }
- public void acceptConnections() {
- }
- public void handleConnection(Socket incomingConnection) {
- }
- }
跟客戶機中一樣,我們首先導入 java.net
的 java.io
。接着,我們給我們的類一個實例變量以保存端口,我們從該端口偵聽進入的連接。缺省情況下,端口是 3000。
我們的類有一個 main()
方法和兩個其它方法。稍後我們將探究這些方法的細節。現在您只需知道 acceptConnections()
將允許客戶機連接到服務器以及 handleConnection()
與客戶機 Socket
交互以將您所請求的文件的內容發送到客戶機。
9. 實現 main()
這裏我們實現 main()
方法,它將創建 RemoteFileServer
並告訴它接受連接:
- public static void main(String[] args) {
- RemoteFileServer server = new RemoteFileServer();
- server.acceptConnections();
- }
- public static void main(String[] args) {
- RemoteFileServer server = new RemoteFileServer();
- server.acceptConnections();
- }
服務器端的 main()
方法甚至比客戶機端的更簡單。我們實例化一個新 RemoteFileServer
,它將在缺省偵聽端口上偵聽進入的連接請求。然後我們調用 acceptConnections()
來告訴該 server 進行偵聽。
10. 接受連接
這裏我們實現 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);
- }
- } <span>
- </span>
- 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);
- }
- } <span>
- </span>
acceptConnections()
用欲偵聽的端口號來創建 ServerSocket
。然後我們通過調用該 ServerSocket
的 accept()
來告訴它開始偵聽。accept()
方法將造成阻塞直到來了一個連接請求。此時,accept()
返回一個新的 Socket
,這個 Socket
綁定到服務器上一個隨機指定的端口,返回的 Socket
被傳遞給handleConnection()
。請注意我們在一個無限循環中處理對連接的接受。這裏不支持任何關機。
無論何時如果您創建了一個無法綁定到指定端口(可能是因爲別的什麼控制了該端口)的 ServerSocket
,Java 代碼都將拋出一個錯誤。所以這裏我們必須捕捉可能的 BindException
。就跟在客戶機端上時一樣,我們必須捕捉 IOException
,當我們試圖在 ServerSocket
上接受連接時,它就會被拋出。請注意,您可以通過用毫秒數調用 setSoTimeout()
來爲 accept()
調用設置超時,以避免實際長時間的等待。調用 setSoTimeout()
將使 accept()
經過指定佔用時間後拋出 IOException
。
11. 處理連接
這裏我們實現 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);
- }
- }
- 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);
- }
- }
跟在客戶機中一樣,我們用 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()));
- BufferedReader bufferedFileReader = new BufferedReader(fileReader);
- String line = null;
- while ((line = bufferedFileReader.readLine()) != null) {
- streamWriter.println(line);
- }
這些代碼值得詳細解釋。讓我們一點一點來看:
- FileReader fileReader = new FileReader(new File(streamReader.readLine()));
- FileReader fileReader = new FileReader(new File(streamReader.readLine()));
首先,我們使用 Socket
的 InputStream
的 BufferedReader
。我們應該獲取一條有效的文件路徑,所以我們用該路徑名構造一個新 File
。我們創建一個新 FileReader
來處理讀文件的操作。
這裏我們把 FileReader
包裝進 BufferedReader
以使我們能夠逐行地讀該文件。
接着,我們調用 BufferedReader
的 readLine()
。這個調用將造成阻塞直到有字節到來。我們獲取一些字節之後就把它們放到本地的 line
變量中,然後再寫出到客戶機上。完成讀寫操作之後,我們就關閉打開的流。
請注意我們在完成從 Socket
的讀操作之後關閉 streamWriter
和 streamReader
。您或許會問我們爲什麼不在讀取文件名之後立刻關閉 streamReader
。原因是當您這樣做時,您的客戶機將不會獲取任何數據。如果您在關閉 streamWriter
之前關閉 streamReader
,則您可以往 Socket
寫任何東西,但卻沒有任何數據能通過通道(通道被關閉了)。
12. 總結一下服務器
在我們接着討論另一個更實際的示例之前,讓我們回顧一下創建和使用 ServerSocket
的步驟:
用一個您想讓它偵聽傳入客戶機連接的端口來實例化一個
ServerSocket
(如有問題則拋出
Exception
)。
調用
ServerSocket
的
accept()
以在等待連接期間造成阻塞。
獲取位於該底層
Socket
的流以進行讀寫操作。
按使事情簡單化的原則包裝流。
對
Socket
進行讀寫。
關閉打開的流(並請記住,
永遠不要
在關閉 Writer 之前關閉 Reader)。
附: RemoteFileServer
的完整的代碼清單
- import java.io.*;
- import java.net.*;
-
- public class RemoteFileServer {
- int listenPort;
- public RemoteFileServer(int aListenPort) {
- listenPort = aListenPort;
- }
- 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);
- }
- }
- public static void main(String[] args) {
- RemoteFileServer server = new RemoteFileServer(3000);
- server.acceptConnections();
- }
- }
- import java.io.*;
- import java.net.*;
-
- public class RemoteFileServer {
- int listenPort;
- public RemoteFileServer(int aListenPort) {
- listenPort = aListenPort;
- }
- 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);
- }
- }
- public static void main(String[] args) {
- RemoteFileServer server = new RemoteFileServer(3000);
- server.acceptConnections();
- }
- }
一個多線程的示例
1. 介紹
前面的示例教給您基礎知識,但並不能令您更深入。如果您到此就停止了,那麼您一次只能處理一臺客戶機。原因是 handleConnection()
是一個阻塞方法。只有當它完成了對當前連接的處理時,服務器才能接受另一個客戶機。在多數時候,您將需要(也有必要)一個多線程服務器。
要開始同時處理多臺客戶機,並不需要對 RemoteFileServer
作太多改變。事實上,要是我們前面討論過待發(backlog),那我們就只需改變一個方法,雖然我們將需要創建一些新東西來處理進入的連接。這裏我們還將向您展示 ServerSocket
如何處理衆多等待(備份)使用服務器的客戶機。本示例對線程的低效使用,所以請耐心點。
2. 接受(太多)連接
這裏我們實現改動過的 acceptConnections()
方法,它將創建一個能夠處理待發請求的 ServerSocket
,並告訴 ServerSocket
接受連接:
- 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 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);
- }
- }
新的 server 仍然需要 acceptConnections()
,所以這些代碼實際上是一樣的。突出顯示的行表示一個重大的不同。對這個多線程版,我們現在可以指定客戶機請求的最大數目,這些請求都能在實例化 ServerSocket
期間處於待發狀態。如果我們沒有指定客戶機請求的最大數目,則我們假設使用缺省值 50。
這裏是它的工作機制。假設我們指定待發數(backlog 值)是 5 並且有五臺客戶機請求連接到我們的服務器。我們的服務器將着手處理第一個連接,但處理該連接需要很長時間。由於我們的待發值是 5,所以我們一次可以放五個請求到隊列中。我們正在處理一個,所以這意味着還有其它五個正在等待。等待的和正在處理的一共有六個。當我們的服務器仍忙於接受一號連接(記住隊列中還有 2―6 號)時,如果有第七個客戶機提出連接申請,那麼,該第七個客戶機將遭到拒絕。我們將在帶有連接池服務器示例中說明如何限定能同時連接的客戶機數目。
3. 處理連接:第 1 部分
這裏我們將討論 handleConnection()
方法的結構,這個方法生成一個新的 Thread
來處理每個連接。我們將分兩部分討論這個問題。這一屏我們將着重該方法本身,然後在下一屏研究該方法所使用的ConnectionHandler
助手類的結構。
- public void handleConnection(Socket connectionToHandle) {
- new Thread(new ConnectionHandler(connectionToHandle)).start();
- }
- public void handleConnection(Socket connectionToHandle) {
- new Thread(new ConnectionHandler(connectionToHandle)).start();
- }
我們對 RemoteFileServer
所做的大改動就體現在這個方法上。我們仍然在服務器接受一個連接之後調用 handleConnection()
,但現在我們把該 Socket
傳遞給 ConnectionHandler
的一個實例,它是 Runnable
的。我們用 ConnectionHandler
創建一個新 Thread
並啓動它。ConnectionHandler
的 run()
方法包含Socket
讀/寫和讀 File
的代碼,這些代碼原來在 RemoteFileServer
的 handleConnection()
中。
4. 處理連接:第 2 部分
這裏是 ConnectionHandler
類的結構:
- import java.io.*;
- import java.net.*;
-
- public class ConnectionHandler implements Runnable{
- Socket socketToHandle;
-
- public ConnectionHandler(Socket aSocketToHandle) {
- socketToHandle = aSocketToHandle;
- }
-
- public void run() {
- }
- }
- import java.io.*;
- import java.net.*;
-
- public class ConnectionHandler implements Runnable{
- Socket socketToHandle;
-
- public ConnectionHandler(Socket aSocketToHandle) {
- socketToHandle = aSocketToHandle;
- }
-
- public void run() {
- }
- }
這個助手類相當簡單。跟我們到目前爲止的其它類一樣,我們導入 java.net
和 java.io
。該類只有一個實例變量 socketToHandle
,它保存由該實例處理的 Socket
。
類的構造器用一個 Socket 實例作參數並將它賦給 socketToHandle
。
請注意該類實現了 Runnable
接口。實現這個接口的類都必須實現 run()
方法,我們的類就是這樣做的。稍後我們將探究 run()
的細節。現在只需知道它將實際處理連接,所用的代碼跟我們先前在 RemoteFileServer
類中看到的是一樣的。
5. 實現 run()
這裏我們實現 run()
方法,它將攫取我們的連接的流,用它來讀寫該連接,並在任務完成之後關閉它:
- 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);
- }
- }
- 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);
- }
- }
ConnectionHandler
的 run()
方法所做的事情就是 RemoteFileServer
上的 handleConnection()
所做的事情。首先,我們把 InputStream
和 OutputStream
分別包裝(用 Socket
的 getOutputStream()
和 getInputStream()
)進 BufferedReader
和 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()));
- BufferedReader bufferedFileReader = new BufferedReader(fileReader);
- String line = null;
- while ((line = bufferedFileReader.readLine()) != null) {
- streamWriter.println(line);
- }
請記住我們應該從客戶機獲取一條有效的文件路徑,這樣用該路徑名構造一個新 File
,把它包裝進 FileReader
以處理讀文件的操作,然後把它包裝進 BufferedReader
以讓我們逐行地讀該文件。我們在 while
循環中調用 BufferedReader
上的 readLine()
直到不再有要讀的行。請記注,對 readLine()
的調用將造成阻塞,直到有字節來到爲止。我們獲取一些字節之後就把它們放到本地的 line
變量中,然後寫出到客戶機上。完成讀寫操作之後,我們關閉打開的流。
6. 總結一下多線程服務器
我們的多線程服務器研究完了。在我們接着討論帶有連接池示例之前,讓我們回顧一下創建和使用「多線程版」的服務器的步驟:
修改
acceptConnections()
以用缺省爲 50(或任何您想要的大於 1 的指定數字)實例化
ServerSocket
。
修改
ServerSocket
的
handleConnection()
以用
ConnectionHandler
的一個實例生成一個新的
Thread
。
借用
RemoteFileServer
的
handleConnection()
方法的代碼實現
ConnectionHandler
類。
附: MultithreadedRemoteFileServer
的完整代碼清單
- import java.io.*;
- import java.net.*;
-
- public class MultithreadedRemoteFileServer {
- protected int listenPort;
- public MultithreadedRemoteFileServer(int aListenPort) {
- listenPort = aListenPort;
- }
- 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(3000);
- server.acceptConnections();
- }
- }
- import java.io.*;
- import java.net.*;
-
- public class MultithreadedRemoteFileServer {
- protected int listenPort;
- public MultithreadedRemoteFileServer(int aListenPort) {
- listenPort = aListenPort;
- }
- 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(3000);
- server.acceptConnections();
- }
- }
ConnectionHandler
的完整代碼清單
- import java.io.*;
- import java.net.*;
-
- public class ConnectionHandler implements Runnable {
- protected Socket socketToHandle;
- public ConnectionHandler(Socket aSocketToHandle) {
- socketToHandle = aSocketToHandle;
- }
- 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);
- }
- }
- }
- import java.io.*;
- import java.net.*;
-
- public class ConnectionHandler implements Runnable {
- protected Socket socketToHandle;
- public ConnectionHandler(Socket aSocketToHandle) {
- socketToHandle = aSocketToHandle;
- }
- 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);
- }
- }
- }
一個帶有連接池的示例
1. 介紹
我們現在已經擁有的 MultithreadedServer
每當有客戶機申請一個連接時都在一個新 Thread
中創建一個新 ConnectionHandler
。這意味着可能有一捆 Thread
「躺」在我們周圍。而且創建 Thread
的系統開銷並不是微不足道的。如果性能成爲了問題(也請不要事到臨頭才意識到它),更高效地處理我們的服務器是件好事。那麼,我們如何更高效地管理服務器端呢?我們可以維護一個進入的連接池,一定數量的ConnectionHandler
將爲它提供服務。這種設計能帶來以下好處:
它限定了允許同時連接的數目。 我們只需啓動
ConnectionHandler
Thread
一次。
幸運的是,跟在我們的多線程示例中一樣,往代碼中添加「池」不需要來一個大改動。事實上,應用程序的客戶機端根本就不受影響。在服務器端,我們在服務器啓動時創建一定數量的 ConnectionHandler
,我們把進入的連接放入「池」中並讓 ConnectionHandler
打理剩下的事情。這種設計中有很多我們不打算討論的可能存在的技巧。例如,我們可以通過限定允許在「池」中建立的連接的數目來拒絕客戶機。
請注意:我們將不會再次討論 acceptConnections()
。這個方法跟前面示例中的完全一樣。它無限循環地調用 ServerSocket
上的 accept()
並把連接傳遞到 handleConnection()
。
2. 創建 PooledRemoteFileServer 類
這裏是 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 static void main(String[] args) {
- }
- public void setUpHandlers() {
- }
- public void acceptConnections() {
- }
- protected void handleConnection(Socket incomingConnection) {
- }
- }
- 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 static void main(String[] args) {
- }
- public void setUpHandlers() {
- }
- public void acceptConnections() {
- }
- protected void handleConnection(Socket incomingConnection) {
- }
- }
請注意一下您現在應該熟悉了的 import
語句。我們給類以下實例變量以保存:
我們的服務器能同時處理的活動客戶機連接的最大數目
進入的連接的偵聽端口(我們沒有指定缺省值,但如果您想這樣做,並不會受到限制)
將接受客戶機連接請求的
ServerSocket
類的構造器用的參數是偵聽端口和連接的最大數目
我們的類有一個 main()
方法和三個其它方法。稍後我們將探究這些方法的細節。現在只須知道 setUpHandlers()
創建數目爲 maxConnections
的大量 PooledConnectionHandler
,而其它兩個方法則與我們前面已經看到的相似:acceptConnections()
在 ServerSocket
上偵聽傳入的客戶機連接,而 handleConnection
則在客戶機連接一旦被建立後就實際處理它。
3. 實現 main()
這裏我們實現需作改動的 main()
方法,該方法將創建能夠處理給定數目的客戶機連接的 PooledRemoteFileServer
,並告訴它接受連接:
- public static void main(String[] args) {
- PooledRemoteFileServer server = new PooledRemoteFileServer(3000, 3);
- server.setUpHandlers();
- server.acceptConnections();
- }
- public static void main(String[] args) {
- PooledRemoteFileServer server = new PooledRemoteFileServer(3000, 3);
- server.setUpHandlers();
- server.acceptConnections();
- }
我們的 main()
方法很簡單。我們實例化一個新的 PooledRemoteFileServer
,它將通過調用setUpHandlers()
來建立三個 PooledConnectionHandler
。一旦服務器就緒,我們就告訴它acceptConnections()
。
4. 建立連接處理程序
- public void setUpHandlers() {
- for (int i = 0; i < maxConnections; i++) {
- PooledConnectionHandler currentHandler = new PooledConnectionHandler();
- new Thread(currentHandler, "Handler " + i).start();
- }
- }
- 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
,而且一旦服務器運行,這就不能被改變。
5. 處理連接
這裏我們實現需作改動的 handleConnections()
方法,它將委派 PooledConnectionHandler
處理連接:
- protected void handleConnection(Socket connectionToHandle) {
- PooledConnectionHandler.processRequest(connectionToHandle);
- }
- protected void handleConnection(Socket connectionToHandle) {
- PooledConnectionHandler.processRequest(connectionToHandle);
- }
我們現在叫 PooledConnectionHandler
處理所有進入的連接(processRequest()
是一個靜態方法)。
這裏是 PooledConnectionHandler
類的結構:
- 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() {
- }
- public static void processRequest(Socket requestToHandle) {
- }
- public void run() {
- }
- }
- 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() {
- }
- public static void processRequest(Socket requestToHandle) {
- }
- public void run() {
- }
- }
這個助手類與 ConnectionHandler
非常相似,但它帶有處理連接池的手段。該類有兩個實例變量:
connection
是當前正在處理的
Socket
名爲
pool
的靜態
LinkedList
保存需被處理的連接
6. 填充連接池
這裏我們實現 PooledConnectionHandler
上的 processRequest()
方法,它將把傳入請求添加到池中,並告訴其它正在等待的對象該池已經有一些內容:
- public static void processRequest(Socket requestToHandle) {
- synchronized (pool) {
- pool.add(pool.size(), requestToHandle);
- pool.notifyAll();
- }
- }
- public static void processRequest(Socket requestToHandle) {
- synchronized (pool) {
- pool.add(pool.size(), requestToHandle);
- pool.notifyAll();
- }
- }
理解這個方法要求有一點關於 Java 的關鍵字 synchronized
如何工作的背景知識。我們將簡要講述一下線程。
先來看一些定義:
原子方法。
在執行過程中不能被中斷的方法(或代碼塊)
互斥鎖。
客戶機欲執行原子方法時必須獲得的單個「鎖」
因此,當對象 A 想使用對象 B 的 synchronized
方法 doSomething()
時,對象 A 必須首先嚐試獲取對象 B 的互斥鎖。是的,這意味着當對象 A 擁有該互斥鎖時,沒有其它對象可以調用對象 B 上任何其它synchronized
方法。
synchronized
塊是個稍微有些不同的東西。您可以同步任何對象上的一個塊,而不只是在本身的某個方法中含有該塊的對象。在我們的示例中,processRequest()
方法包含有一個 pool
(請記住它是一個 LinkedList
,保存等待處理的連接池)的 synchronized
塊。我們這樣做的原因是確保沒有別人能跟我們同時修改連接池。
既然我們已經保證了我們是唯一「涉水」池中的人,我們就可以把傳入的 Socket
添加到 LinkedList
的尾端。一旦我們添加了新的連接,我們就用以下代碼通知其它正在等待該池的 Thread
,池現在已經可用:
Object
的所有子類都繼承這個 notifyAll()
方法。這個方法,連同我們下一屏將要討論的 wait()
方法一起,就使一個 Thread
能夠讓另一個 Thread
知道一些條件已經具備。這意味着該第二個 Thread
一定正在等待那些條件的滿足。
7. 從池中獲取連接
這裏我們實現 PooledConnectionHandler
上需作改動的 run()
方法,它將在連接池上等待,並且池中一有連接就處理它:
- public void run() {
- while (true) {
- synchronized (pool) {
- while (pool.isEmpty()) {
- try {
- pool.wait();
- } catch (InterruptedException e) {
- return;
- }
- }
- connection = (Socket) pool.remove(0);
- }
- handleConnection();
- }
- }
- public void run() {
- while (true) {
- synchronized (pool) {
- while (pool.isEmpty()) {
- try {
- pool.wait();
- } catch (InterruptedException e) {
- return;
- }
- }
- 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);
- connection = (Socket) pool.remove(0);
處理程序一旦有一個連接可以使用,就調用 handleConnection()
處理它。
在我們的示例中,池中可能永遠不會有多個連接,只是因爲事情很快就被處理掉了。如果池中有一個以上連接,那麼其它處理程序將不必等待新的連接被添加到池。當它們檢查 pool.isEmpty()
條件時,將發現其值爲假,然後就從池中攫取一個連接並處理它。
還有另一件事需注意。當 run()
擁有池的互斥鎖時,processRequest()
如何能夠把連接放到池中呢?答案是對池上的 wait()
的調用釋放鎖,而 wait()
接着就在自己返回之前再次攫取該鎖。這就使得池對象的其它同步代碼可以獲取該鎖。
8. 處理連接:再一次
這裏我們實現需做改動的 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("Could not find requested file on the server.");
- } catch (IOException e) {
- System.out.println("Error handling a client: " + e);
- }
- }
- 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("Could not find requested file on the server.");
- } catch (IOException e) {
- System.out.println("Error handling a client: " + e);
- }
- }
跟在多線程服務器中不同,我們的 PooledConnectionHandler
有一個 handleConnection()
方法。這個方法的代碼跟非池式的 ConnectionHandler
上的 run()
方法的代碼完全一樣。首先,我們把 OutputStream
和 InputStream
分別包裝進(用 Socket
上的 getOutputStream()
和 getInputStream()
)BufferedReader
和 PrintWriter
。然後我們逐行讀目標文件,就象我們在多線程示例中做的那樣。再一次,我們獲取一些字節之後就把它們放到本地的 line
變量中,然後寫出到客戶機。完成讀寫操作之後,我們關閉 FileReader
和打開的流。
9. 總結一下帶有連接池的服務器
我們的帶有連接池的服務器研究完了。讓我們回顧一下創建和使用「池版」服務器的步驟:
創建一個新種類的連接處理程序(我們稱之爲
PooledConnectionHandler
)來處理池中的連接。
修改服務器以創建和使用一組
PooledConnectionHandler
。
附:
PooledRemoteFileServer
的完整代碼清單
- import java.io.*;
- import java.net.*;