Java網絡編程精解之ServerSocket用法詳解一 | ||||||||||||||||||||||||||||||||
在客戶/服務器通信模式中,服務器端需要創建監聽特定端口的ServerSocket,ServerSocket負責接收客戶連接請求。本章首先介紹ServerSocket類的各個構造方法,以及成員方法的用法,接着介紹服務器如何用多線程來處理與多個客戶的通信任務。 本章提供線程池的一種實現方式。線程池包括一個工作隊列和若干工作線程。服務器程序向工作隊列中加入與客戶通信的任務,工作線程不斷從工作隊列中取出任務並執行它。本章還介紹了java.util.concurrent包中的線程池類的用法,在服務器程序中可以直接使用它們。 3.1 構造ServerSocket ServerSocket的構造方法有以下幾種重載形式: ◆ServerSocket()throws IOException 在以上構造方法中,參數port指定服務器要綁定的端口(服務器要監聽的端口),參數backlog指定客戶連接請求隊列的長度,參數bindAddr指定服務器要綁定的IP地址。 3.1.1 綁定端口 除了第一個不帶參數的構造方法以外,其他構造方法都會使服務器與特定端口綁定,該端口由參數port指定。例如,以下代碼創建了一個與80端口綁定的服務器:
如果運行時無法綁定到80端口,以上代碼會拋出IOException,更確切地說,是拋出BindException,它是IOException的子類。BindException一般是由以下原因造成的: ◆端口已經被其他服務器進程佔用; 如果把參數port設爲0,表示由操作系統來爲服務器分配一個任意可用的端口。由操作系統分配的端口也稱爲匿名端口。對於多數服務器,會使用明確的端口,而不會使用匿名端口,因爲客戶程序需要事先知道服務器的端口,才能方便地訪問服務器。在某些場合,匿名端口有着特殊的用途,本章3.4節會對此作介紹。 3.1.2 設定客戶連接請求隊列的長度 當服務器進程運行時,可能會同時監聽到多個客戶的連接請求。例如,每當一個客戶進程執行以下代碼:
就意味着在遠程www.javathinker.org主機的80端口上,監聽到了一個客戶的連接請求。管理客戶連接請求的任務是由操作系統來完成的。操作系統把這些連接請求存儲在一個先進先出的隊列中。許多操作系統限定了隊列的最大長度,一般爲50。當隊列中的連接請求達到了隊列的最大容量時,服務器進程所在的主機會拒絕新的連接請求。只有當服務器進程通過ServerSocket的accept()方法從隊列中取出連接請求,使隊列騰出空位時,隊列才能繼續加入新的連接請求。 對於客戶進程,如果它發出的連接請求被加入到服務器的隊列中,就意味着客戶與服務器的連接建立成功,客戶進程從Socket構造方法中正常返回。如果客戶進程發出的連接請求被服務器拒絕,Socket構造方法就會拋出ConnectionException。 ServerSocket構造方法的backlog參數用來顯式設置連接請求隊列的長度,它將覆蓋操作系統限定的隊列的最大長度。值得注意的是,在以下幾種情況中,仍然會採用操作系統限定的隊列的最大長度: ◆backlog參數的值大於操作系統限定的隊列的最大長度; 以下例程3-1的Client.java和例程3-2的Server.java用來演示服務器的連接請求隊列的特性。 例程3-1 Client.java
#p# 例程3-2 Server.java
Client試圖與Server進行100次連接。在Server類中,把連接請求隊列的長度設爲3。這意味着當隊列中有了3個連接請求時,如果Client再請求連接,就會被Server拒絕。下面按照以下步驟運行Server和Client程序。 (1)把Server類的main()方法中的“server.service();”這行程序代碼註釋掉。這使得服務器與8000端口綁定後,永遠不會執行serverSocket.accept()方法。這意味着隊列中的連接請求永遠不會被取出。先運行Server程序,然後再運行Client程序,Client程序的打印結果如下:
從以上打印結果可以看出,Client與Server在成功地建立了3個連接後,就無法再創建其餘的連接了,因爲服務器的隊列已經滿了。 (2)把Server類的main()方法按如下方式修改:
作了以上修改,服務器與8 000端口綁定後,就會在一個while循環中不斷執行serverSocket.accept()方法,該方法從隊列中取出連接請求,使得隊列能及時騰出空位,以容納新的連接請求。先運行Server程序,然後再運行Client程序,Client程序的打印結果如下:
從以上打印結果可以看出,此時Client能順利與Server建立100次連接。 3.1.3 設定綁定的IP地址 如果主機只有一個IP地址,那麼默認情況下,服務器程序就與該IP地址綁定。ServerSocket的第4個構造方法ServerSocket(int port, int backlog, InetAddress bindAddr)有一個bindAddr參數,它顯式指定服務器要綁定的IP地址,該構造方法適用於具有多個IP地址的主機。假定一個主機有兩個網卡,一個網卡用於連接到Internet, IP地址爲222.67.5.94,還有一個網卡用於連接到本地局域網,IP地址爲192.168.3.4。如果服務器僅僅被本地局域網中的客戶訪問,那麼可以按如下方式創建ServerSocket:
3.1.4 默認構造方法的作用 ServerSocket有一個不帶參數的默認構造方法。通過該方法創建的ServerSocket不與任何端口綁定,接下來還需要通過bind()方法與特定端口綁定。 這個默認構造方法的用途是,允許服務器在綁定到特定端口之前,先設置ServerSocket的一些選項。因爲一旦服務器與特定端口綁定,有些選項就不能再改變了。 在以下代碼中,先把ServerSocket的SO_REUSEADDR選項設爲true,然後再把它與8000端口綁定:
如果把以上程序代碼改爲:
那麼serverSocket.setReuseAddress(true)方法就不起任何作用了,因爲SO_ REUSEADDR選項必須在服務器綁定端口之前設置纔有效。 #p# 3.2 接收和關閉與客戶的連接 ServerSocket的accept()方法從連接請求隊列中取出一個客戶的連接請求,然後創建與客戶連接的Socket對象,並將它返回。如果隊列中沒有連接請求,accept()方法就會一直等待,直到接收到了連接請求才返回。 接下來,服務器從Socket對象中獲得輸入流和輸出流,就能與客戶交換數據。當服務器正在進行發送數據的操作時,如果客戶端斷開了連接,那麼服務器端會拋出一個IOException的子類SocketException異常:
這只是服務器與單個客戶通信中出現的異常,這種異常應該被捕獲,使得服務器能繼續與其他客戶通信。 以下程序顯示了單線程服務器採用的通信流程:
與單個客戶通信的代碼放在一個try代碼塊中,如果遇到異常,該異常被catch代碼塊捕獲。try代碼塊後面還有一個finally代碼塊,它保證不管與客戶通信正常結束還是異常結束,最後都會關閉Socket,斷開與這個客戶的連接。 3.3 關閉ServerSocket ServerSocket的close()方法使服務器釋放佔用的端口,並且斷開與所有客戶的連接。當一個服務器程序運行結束時,即使沒有執行ServerSocket的close()方法,操作系統也會釋放這個服務器佔用的端口。因此,服務器程序並不一定要在結束之前執行ServerSocket的close()方法。 在某些情況下,如果希望及時釋放服務器的端口,以便讓其他程序能佔用該端口,則可以顯式調用ServerSocket的close()方法。例如,以下代碼用於掃描1~65535之間的端口號。如果ServerSocket成功創建,意味着該端口未被其他服務器進程綁定,否者說明該端口已經被其他進程佔用:
以上程序代碼創建了一個ServerSocket對象後,就馬上關閉它,以便及時釋放它佔用的端口,從而避免程序臨時佔用系統的大多數端口。 ServerSocket的isClosed()方法判斷ServerSocket是否關閉,只有執行了ServerSocket的close()方法,isClosed()方法才返回true;否則,即使ServerSocket還沒有和特定端口綁定,isClosed()方法也會返回false。 ServerSocket的isBound()方法判斷ServerSocket是否已經與一個端口綁定,只要ServerSocket已經與一個端口綁定,即使它已經被關閉,isBound()方法也會返回true。 如果需要確定一個ServerSocket已經與特定端口綁定,並且還沒有被關閉,則可以採用以下方式:
3.4 獲取ServerSocket的信息 ServerSocket的以下兩個get方法可分別獲得服務器綁定的IP地址,以及綁定的端口: ◆public InetAddress getInetAddress() 前面已經講到,在構造ServerSocket時,如果把端口設爲0,那麼將由操作系統爲服務器分配一個端口(稱爲匿名端口),程序只要調用getLocalPort()方法就能獲知這個端口號。如例程3-3所示的RandomPort創建了一個ServerSocket,它使用的就是匿名端口。 #p# 例程3-3 RandomPort.java
多次運行RandomPort程序,可能會得到如下運行結果:
多數服務器會監聽固定的端口,這樣才便於客戶程序訪問服務器。匿名端口一般適用於服務器與客戶之間的臨時通信,通信結束,就斷開連接,並且ServerSocket佔用的臨時端口也被釋放。 FTP(文件傳輸)協議就使用了匿名端口。如圖3-1所示,FTP協議用於在本地文件系統與遠程文件系統之間傳送文件。 圖3-1 FTP協議用於在本地文件系統與遠程文件系統之間傳送文件 FTP使用兩個並行的TCP連接:一個是控制連接,一個是數據連接。控制連接用於在客戶和服務器之間發送控制信息,如用戶名和口令、改變遠程目錄的命令或上傳和下載文件的命令。數據連接用於傳送文件。TCP服務器在21端口上監聽控制連接,如果有客戶要求上傳或下載文件,就另外建立一個數據連接,通過它來傳送文件。數據連接的建立有兩種方式。 (1)如圖3-2所示,TCP服務器在20端口上監聽數據連接,TCP客戶主動請求建立與該端口的連接。 圖3-2 TCP服務器在20端口上監聽數據連接 (2)如圖3-3所示,首先由TCP客戶創建一個監聽匿名端口的ServerSocket,再把這個ServerSocket監聽的端口號(調用ServerSocket的getLocalPort()方法就能得到端口號)發送給TCP服務器,然後由TCP服務器主動請求建立與客戶端的連接。 圖3-3 TCP客戶在匿名端口上監聽數據連接 以上第二種方式就使用了匿名端口,並且是在客戶端使用的,用於和服務器建立臨時的數據連接。在實際應用中,在服務器端也可以使用匿名端口。 3.5 ServerSocket選項 ServerSocket有以下3個選項。 ◆SO_TIMEOUT:表示等待客戶連接的超時時間。 3.5.1 SO_TIMEOUT選項 ◆設置該選項:public void setSoTimeout(int timeout) throws SocketException SO_TIMEOUT表示ServerSocket的accept()方法等待客戶連接的超時時間,以毫秒爲單位。如果SO_TIMEOUT的值爲0,表示永遠不會超時,這是SO_TIMEOUT的默認值。 當服務器執行ServerSocket的accept()方法時,如果連接請求隊列爲空,服務器就會一直等待,直到接收到了客戶連接才從accept()方法返回。如果設定了超時時間,那麼當服務器等待的時間超過了超時時間,就會拋出SocketTimeoutException,它是InterruptedException的子類。 如例程3-4所示的TimeoutTester把超時時間設爲6秒鐘。 #p#例程3-4 TimeoutTester.java
運行以上程序,過6秒鐘後,程序會從serverSocket.accept()方法中拋出Socket- TimeoutException:
如果把程序中的“serverSocket.setSoTimeout(6000)”註釋掉,那麼serverSocket. accept()方法永遠不會超時,它會一直等待下去,直到接收到了客戶的連接,纔會從accept()方法返回。 Tips:服務器執行serverSocket.accept()方法時,等待客戶連接的過程也稱爲阻塞。本書第4章的4.1節(線程阻塞的概念)詳細介紹了阻塞的概念。 3.5.2 SO_REUSEADDR選項 ◆設置該選項:public void setResuseAddress(boolean on) throws SocketException 這個選項與Socket的SO_REUSEADDR選項相同,用於決定如果網絡上仍然有數據向舊的ServerSocket傳輸數據,是否允許新的ServerSocket綁定到與舊的ServerSocket同樣的端口上。SO_REUSEADDR選項的默認值與操作系統有關,在某些操作系統中,允許重用端口,而在某些操作系統中不允許重用端口。 當ServerSocket關閉時,如果網絡上還有發送到這個ServerSocket的數據,這個ServerSocket不會立刻釋放本地端口,而是會等待一段時間,確保接收到了網絡上發送過來的延遲數據,然後再釋放端口。 許多服務器程序都使用固定的端口。當服務器程序關閉後,有可能它的端口還會被佔用一段時間,如果此時立刻在同一個主機上重啓服務器程序,由於端口已經被佔用,使得服務器程序無法綁定到該端口,服務器啓動失敗,並拋出BindException:
爲了確保一個進程關閉了ServerSocket後,即使操作系統還沒釋放端口,同一個主機上的其他進程還可以立刻重用該端口,可以調用ServerSocket的setResuse- Address(true)方法:
值得注意的是,serverSocket.setResuseAddress(true)方法必須在ServerSocket還沒有綁定到一個本地端口之前調用,否則執行serverSocket.setResuseAddress(true)方法無效。此外,兩個共用同一個端口的進程必須都調用serverSocket.setResuseAddress(true)方法,才能使得一個進程關閉ServerSocket後,另一個進程的ServerSocket還能夠立刻重用相同端口。 3.5.3 SO_RCVBUF選項 ◆設置該選項:public void setReceiveBufferSize(int size) throws SocketException SO_RCVBUF表示服務器端的用於接收數據的緩衝區的大小,以字節爲單位。一般說來,傳輸大的連續的數據塊(基於HTTP或FTP協議的數據傳輸)可以使用較大的緩衝區,這可以減少傳輸數據的次數,從而提高傳輸數據的效率。而對於交互式的通信(Telnet和網絡遊戲),則應該採用小的緩衝區,確保能及時把小批量的數據發送給對方。 SO_RCVBUF的默認值與操作系統有關。例如,在Windows 2000中運行以下代碼時,顯示SO_RCVBUF的默認值爲8192:
無論在ServerSocket綁定到特定端口之前或之後,調用setReceiveBufferSize()方法都有效。例外情況下是如果要設置大於64K的緩衝區,則必須在ServerSocket綁定到特定端口之前進行設置纔有效。例如,以下代碼把緩衝區設爲128K:
執行serverSocket.setReceiveBufferSize()方法,相當於對所有由serverSocket.accept()方法返回的Socket設置接收數據的緩衝區的大小。 3.5.4 設定連接時間、延遲和帶寬的相對重要性 ◆public void setPerformancePreferences(int connectionTime,int latency,int bandwidth) 該方法的作用與Socket的setPerformancePreferences()方法的作用相同,用於設定連接時間、延遲和帶寬的相對重要性,參見本書第2章的2.5.10節(設定連接時間、延遲和帶寬的相對重要性)。 相關文章鏈接: (責任編輯 火鳳凰 [email protected] QQ:34067741 TEL:(010)68476636-8007) |
||||||||||||||||||||||||||||||||
Java網絡編程精解之ServerSocket用法詳解一
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.