Java網絡編程精解之ServerSocket用法詳解一

 
Java網絡編程精解之ServerSocket用法詳解一
 
 
第3章 ServerSocket用法詳解 第10章 Java語言的反射機制 第13章 基於MVC和RMI的分佈
ServerSocket用法詳解一 Java語言的反射機制一 基於MVC和RMI的分佈式應用一
ServerSocket用法詳解二 Java語言的反射機制二 基於MVC和RMI的分佈式應用二
ServerSocket用法詳解三

在客戶/服務器通信模式中,服務器端需要創建監聽特定端口的ServerSocket,ServerSocket負責接收客戶連接請求。本章首先介紹ServerSocket類的各個構造方法,以及成員方法的用法,接着介紹服務器如何用多線程來處理與多個客戶的通信任務。

本章提供線程池的一種實現方式。線程池包括一個工作隊列和若干工作線程。服務器程序向工作隊列中加入與客戶通信的任務,工作線程不斷從工作隊列中取出任務並執行它。本章還介紹了java.util.concurrent包中的線程池類的用法,在服務器程序中可以直接使用它們。

3.1  構造ServerSocket

ServerSocket的構造方法有以下幾種重載形式:

◆ServerSocket()throws IOException
◆ServerSocket(int port) throws IOException
◆ServerSocket(int port, int backlog) throws IOException
◆ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException   

在以上構造方法中,參數port指定服務器要綁定的端口(服務器要監聽的端口),參數backlog指定客戶連接請求隊列的長度,參數bindAddr指定服務器要綁定的IP地址。

3.1.1  綁定端口

除了第一個不帶參數的構造方法以外,其他構造方法都會使服務器與特定端口綁定,該端口由參數port指定。例如,以下代碼創建了一個與80端口綁定的服務器:

ServerSocket serverSocket=new ServerSocket(80);

如果運行時無法綁定到80端口,以上代碼會拋出IOException,更確切地說,是拋出BindException,它是IOException的子類。BindException一般是由以下原因造成的:

◆端口已經被其他服務器進程佔用;
◆在某些操作系統中,如果沒有以超級用戶的身份來運行服務器程序,那麼操作系統不允許服務器綁定到1~1023之間的端口。

如果把參數port設爲0,表示由操作系統來爲服務器分配一個任意可用的端口。由操作系統分配的端口也稱爲匿名端口。對於多數服務器,會使用明確的端口,而不會使用匿名端口,因爲客戶程序需要事先知道服務器的端口,才能方便地訪問服務器。在某些場合,匿名端口有着特殊的用途,本章3.4節會對此作介紹。

3.1.2  設定客戶連接請求隊列的長度

當服務器進程運行時,可能會同時監聽到多個客戶的連接請求。例如,每當一個客戶進程執行以下代碼:

Socket socket=new Socket(www.javathinker.org,80);

就意味着在遠程www.javathinker.org主機的80端口上,監聽到了一個客戶的連接請求。管理客戶連接請求的任務是由操作系統來完成的。操作系統把這些連接請求存儲在一個先進先出的隊列中。許多操作系統限定了隊列的最大長度,一般爲50。當隊列中的連接請求達到了隊列的最大容量時,服務器進程所在的主機會拒絕新的連接請求。只有當服務器進程通過ServerSocket的accept()方法從隊列中取出連接請求,使隊列騰出空位時,隊列才能繼續加入新的連接請求。

對於客戶進程,如果它發出的連接請求被加入到服務器的隊列中,就意味着客戶與服務器的連接建立成功,客戶進程從Socket構造方法中正常返回。如果客戶進程發出的連接請求被服務器拒絕,Socket構造方法就會拋出ConnectionException。

ServerSocket構造方法的backlog參數用來顯式設置連接請求隊列的長度,它將覆蓋操作系統限定的隊列的最大長度。值得注意的是,在以下幾種情況中,仍然會採用操作系統限定的隊列的最大長度:

◆backlog參數的值大於操作系統限定的隊列的最大長度;
◆backlog參數的值小於或等於0;
◆在ServerSocket構造方法中沒有設置backlog參數。

以下例程3-1的Client.java和例程3-2的Server.java用來演示服務器的連接請求隊列的特性。

例程3-1  Client.java

import java.net.*; public class Client {   public static void main(String args[])throws Exception{     final int length=100;     String host="localhost";     int port=8000;

    Socket[] sockets=new Socket[length];     for(int i=0;i      sockets[i]=new Socket(host, port);       System.out.println("第"+(i+1)+"次連接成功");     }     Thread.sleep(3000);     for(int i=0;i      sockets[i].close();      //斷開連接     }   } }

#p#

例程3-2  Server.java

import java.io.*; import java.net.*; public class Server {   private int port=8000;   private ServerSocket serverSocket;

  public Server() throws IOException {     serverSocket = new ServerSocket(port,3);    //連接請求隊列的長度爲3     System.out.println("服務器啓動");   }

  public void service() {     while (true) {       Socket socket=null;       try {         socket = serverSocket.accept();     //從連接請求隊列中取出一個

連接         System.out.println("New connection accepted " +         socket.getInetAddress() + ":" +socket.getPort());       }catch (IOException e) {          e.printStackTrace();       }finally {          try{            if(socket!=null)socket.close();          }catch (IOException e) {e.printStackTrace();}       }     }   }

  public static void main(String args[])throws Exception {     Server server=new Server();     Thread.sleep(60000*10);      //睡眠10分鐘     //server.service();   } }

Client試圖與Server進行100次連接。在Server類中,把連接請求隊列的長度設爲3。這意味着當隊列中有了3個連接請求時,如果Client再請求連接,就會被Server拒絕。下面按照以下步驟運行Server和Client程序。

(1)把Server類的main()方法中的“server.service();”這行程序代碼註釋掉。這使得服務器與8000端口綁定後,永遠不會執行serverSocket.accept()方法。這意味着隊列中的連接請求永遠不會被取出。先運行Server程序,然後再運行Client程序,Client程序的打印結果如下:

第1次連接成功 第2次連接成功 第3次連接成功 Exception in thread "main" java.net.ConnectException: Connection refused: connect         at java.net.PlainSocketImpl.socketConnect(Native Method)         at java.net.PlainSocketImpl.doConnect(Unknown Source)         at java.net.PlainSocketImpl.connectToAddress(Unknown Source)         at java.net.PlainSocketImpl.connect(Unknown Source)         at java.net.SocksSocketImpl.connect(Unknown Source)         at java.net.Socket.connect(Unknown Source)         at java.net.Socket.connect(Unknown Source)         at java.net.Socket.(Unknown Source)         at java.net.Socket.(Unknown Source)         at Client.main(Client.java:10)

從以上打印結果可以看出,Client與Server在成功地建立了3個連接後,就無法再創建其餘的連接了,因爲服務器的隊列已經滿了。

(2)把Server類的main()方法按如下方式修改:

public static void main(String args[])throws Exception {     Server server=new Server();     //Thread.sleep(60000*10);  //睡眠10分鐘     server.service();   }

作了以上修改,服務器與8 000端口綁定後,就會在一個while循環中不斷執行serverSocket.accept()方法,該方法從隊列中取出連接請求,使得隊列能及時騰出空位,以容納新的連接請求。先運行Server程序,然後再運行Client程序,Client程序的打印結果如下:

第1次連接成功 第2次連接成功 第3次連接成功 … 第100次連接成功

從以上打印結果可以看出,此時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:

ServerSocket serverSocket=new ServerSocket(8000,10,InetAddress.getByName ("192.168.3.4"));

3.1.4  默認構造方法的作用

ServerSocket有一個不帶參數的默認構造方法。通過該方法創建的ServerSocket不與任何端口綁定,接下來還需要通過bind()方法與特定端口綁定。

這個默認構造方法的用途是,允許服務器在綁定到特定端口之前,先設置ServerSocket的一些選項。因爲一旦服務器與特定端口綁定,有些選項就不能再改變了。

在以下代碼中,先把ServerSocket的SO_REUSEADDR選項設爲true,然後再把它與8000端口綁定:

ServerSocket serverSocket=new ServerSocket(); serverSocket.setReuseAddress(true);      //設置ServerSocket的選項 serverSocket.bind(new InetSocketAddress(8000));   //與8000端口綁定

如果把以上程序代碼改爲:

ServerSocket serverSocket=new ServerSocket(8000); serverSocket.setReuseAddress(true);      //設置ServerSocket的選項

那麼serverSocket.setReuseAddress(true)方法就不起任何作用了,因爲SO_ REUSEADDR選項必須在服務器綁定端口之前設置纔有效。

#p#

3.2  接收和關閉與客戶的連接

ServerSocket的accept()方法從連接請求隊列中取出一個客戶的連接請求,然後創建與客戶連接的Socket對象,並將它返回。如果隊列中沒有連接請求,accept()方法就會一直等待,直到接收到了連接請求才返回。

接下來,服務器從Socket對象中獲得輸入流和輸出流,就能與客戶交換數據。當服務器正在進行發送數據的操作時,如果客戶端斷開了連接,那麼服務器端會拋出一個IOException的子類SocketException異常:

java.net.SocketException: Connection reset by peer

這只是服務器與單個客戶通信中出現的異常,這種異常應該被捕獲,使得服務器能繼續與其他客戶通信。

以下程序顯示了單線程服務器採用的通信流程:

public void service() {   while (true) {     Socket socket=null;     try {       socket = serverSocket.accept();    //從連接請求隊列中取出一個連接       System.out.println("New connection accepted " +       socket.getInetAddress() + ":" +socket.getPort());       //接收和發送數據       …     }catch (IOException e) {       //這只是與單個客戶通信時遇到的異常,可能是由於客戶端過早斷開連接引起的       //這種異常不應該中斷整個while循環        e.printStackTrace();     }finally {        try{          if(socket!=null)socket.close();    //與一個客戶通信結束後,要關閉

Socket        }catch (IOException e) {e.printStackTrace();}     }   } }

與單個客戶通信的代碼放在一個try代碼塊中,如果遇到異常,該異常被catch代碼塊捕獲。try代碼塊後面還有一個finally代碼塊,它保證不管與客戶通信正常結束還是異常結束,最後都會關閉Socket,斷開與這個客戶的連接。

3.3  關閉ServerSocket

ServerSocket的close()方法使服務器釋放佔用的端口,並且斷開與所有客戶的連接。當一個服務器程序運行結束時,即使沒有執行ServerSocket的close()方法,操作系統也會釋放這個服務器佔用的端口。因此,服務器程序並不一定要在結束之前執行ServerSocket的close()方法。

在某些情況下,如果希望及時釋放服務器的端口,以便讓其他程序能佔用該端口,則可以顯式調用ServerSocket的close()方法。例如,以下代碼用於掃描1~65535之間的端口號。如果ServerSocket成功創建,意味着該端口未被其他服務器進程綁定,否者說明該端口已經被其他進程佔用:

for(int port=1;port<=65535;port++){   try{ ServerSocket serverSocket=new ServerSocket(port); serverSocket.close();   //及時關閉ServerSocket   }catch(IOException e){     System.out.println("端口"+port+" 已經被其他服務器進程佔用"); } }

以上程序代碼創建了一個ServerSocket對象後,就馬上關閉它,以便及時釋放它佔用的端口,從而避免程序臨時佔用系統的大多數端口。

ServerSocket的isClosed()方法判斷ServerSocket是否關閉,只有執行了ServerSocket的close()方法,isClosed()方法才返回true;否則,即使ServerSocket還沒有和特定端口綁定,isClosed()方法也會返回false。

ServerSocket的isBound()方法判斷ServerSocket是否已經與一個端口綁定,只要ServerSocket已經與一個端口綁定,即使它已經被關閉,isBound()方法也會返回true。

如果需要確定一個ServerSocket已經與特定端口綁定,並且還沒有被關閉,則可以採用以下方式:

boolean isOpen=serverSocket.isBound() && !serverSocket.isClosed();

3.4  獲取ServerSocket的信息

ServerSocket的以下兩個get方法可分別獲得服務器綁定的IP地址,以及綁定的端口:

◆public InetAddress getInetAddress()
◆public int getLocalPort()

前面已經講到,在構造ServerSocket時,如果把端口設爲0,那麼將由操作系統爲服務器分配一個端口(稱爲匿名端口),程序只要調用getLocalPort()方法就能獲知這個端口號。如例程3-3所示的RandomPort創建了一個ServerSocket,它使用的就是匿名端口。

#p#

例程3-3  RandomPort.java

import java.io.*; import java.net.*;

public class RandomPort{   public static void main(String args[])throws IOException{     ServerSocket serverSocket=new ServerSocket(0);     System.out.println("監聽的端口爲:"+serverSocket.getLocalPort());   } }

多次運行RandomPort程序,可能會得到如下運行結果:

C:\chapter03\classes>java RandomPort 監聽的端口爲:3000 C:\chapter03\classes>java RandomPort 監聽的端口爲:3004 C:\chapter03\classes>java RandomPort 監聽的端口爲:3005

多數服務器會監聽固定的端口,這樣才便於客戶程序訪問服務器。匿名端口一般適用於服務器與客戶之間的臨時通信,通信結束,就斷開連接,並且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:表示等待客戶連接的超時時間。
◆SO_REUSEADDR:表示是否允許重用服務器所綁定的地址。
◆SO_RCVBUF:表示接收數據的緩衝區的大小。

3.5.1  SO_TIMEOUT選項

◆設置該選項:public void setSoTimeout(int timeout) throws SocketException
◆讀取該選項:public int getSoTimeout () throws IOException

SO_TIMEOUT表示ServerSocket的accept()方法等待客戶連接的超時時間,以毫秒爲單位。如果SO_TIMEOUT的值爲0,表示永遠不會超時,這是SO_TIMEOUT的默認值。

當服務器執行ServerSocket的accept()方法時,如果連接請求隊列爲空,服務器就會一直等待,直到接收到了客戶連接才從accept()方法返回。如果設定了超時時間,那麼當服務器等待的時間超過了超時時間,就會拋出SocketTimeoutException,它是InterruptedException的子類。

如例程3-4所示的TimeoutTester把超時時間設爲6秒鐘。

#p#

例程3-4  TimeoutTester.java

import java.io.*; import java.net.*;

public class TimeoutTester{   public static void main(String args[])throws IOException{     ServerSocket serverSocket=new ServerSocket(8000);     serverSocket.setSoTimeout(6000); //等待客戶連接的時間不超過6秒     Socket socket=serverSocket.accept();     socket.close();     System.out.println("服務器關閉");   } }

運行以上程序,過6秒鐘後,程序會從serverSocket.accept()方法中拋出Socket- TimeoutException:

C:\chapter03\classes>java TimeoutTester Exception in thread "main" java.net.SocketTimeoutException: Accept timed out         at java.net.PlainSocketImpl.socketAccept(Native Method)         at java.net.PlainSocketImpl.accept(Unknown Source)         at java.net.ServerSocket.implAccept(Unknown Source)         at java.net.ServerSocket.accept(Unknown Source)         at TimeoutTester.main(TimeoutTester.java:8)

如果把程序中的“serverSocket.setSoTimeout(6000)”註釋掉,那麼serverSocket. accept()方法永遠不會超時,它會一直等待下去,直到接收到了客戶的連接,纔會從accept()方法返回。

Tips:服務器執行serverSocket.accept()方法時,等待客戶連接的過程也稱爲阻塞。本書第4章的4.1節(線程阻塞的概念)詳細介紹了阻塞的概念。

3.5.2  SO_REUSEADDR選項

◆設置該選項:public void setResuseAddress(boolean on) throws SocketException
◆讀取該選項:public boolean getResuseAddress() throws SocketException

這個選項與Socket的SO_REUSEADDR選項相同,用於決定如果網絡上仍然有數據向舊的ServerSocket傳輸數據,是否允許新的ServerSocket綁定到與舊的ServerSocket同樣的端口上。SO_REUSEADDR選項的默認值與操作系統有關,在某些操作系統中,允許重用端口,而在某些操作系統中不允許重用端口。

當ServerSocket關閉時,如果網絡上還有發送到這個ServerSocket的數據,這個ServerSocket不會立刻釋放本地端口,而是會等待一段時間,確保接收到了網絡上發送過來的延遲數據,然後再釋放端口。

許多服務器程序都使用固定的端口。當服務器程序關閉後,有可能它的端口還會被佔用一段時間,如果此時立刻在同一個主機上重啓服務器程序,由於端口已經被佔用,使得服務器程序無法綁定到該端口,服務器啓動失敗,並拋出BindException:

Exception in thread "main" java.net.BindException: Address already in use: JVM_Bind

爲了確保一個進程關閉了ServerSocket後,即使操作系統還沒釋放端口,同一個主機上的其他進程還可以立刻重用該端口,可以調用ServerSocket的setResuse- Address(true)方法:

if(!serverSocket.getResuseAddress())serverSocket.setResuseAddress(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
◆讀取該選項:public int getReceiveBufferSize() throws SocketException

SO_RCVBUF表示服務器端的用於接收數據的緩衝區的大小,以字節爲單位。一般說來,傳輸大的連續的數據塊(基於HTTP或FTP協議的數據傳輸)可以使用較大的緩衝區,這可以減少傳輸數據的次數,從而提高傳輸數據的效率。而對於交互式的通信(Telnet和網絡遊戲),則應該採用小的緩衝區,確保能及時把小批量的數據發送給對方。

SO_RCVBUF的默認值與操作系統有關。例如,在Windows 2000中運行以下代碼時,顯示SO_RCVBUF的默認值爲8192:

ServerSocket serverSocket=new ServerSocket(8000); System.out.println(serverSocket.getReceiveBufferSize());    //打印8192

無論在ServerSocket綁定到特定端口之前或之後,調用setReceiveBufferSize()方法都有效。例外情況下是如果要設置大於64K的緩衝區,則必須在ServerSocket綁定到特定端口之前進行設置纔有效。例如,以下代碼把緩衝區設爲128K:

ServerSocket serverSocket=new ServerSocket(); int size=serverSocket.getReceiveBufferSize(); if(size<131072) serverSocket.setReceiveBufferSize(131072);  //把緩衝區的大小設爲128K serverSocket.bind(new InetSocketAddress(8000));     //與8 000端口綁定

執行serverSocket.setReceiveBufferSize()方法,相當於對所有由serverSocket.accept()方法返回的Socket設置接收數據的緩衝區的大小。

3.5.4  設定連接時間、延遲和帶寬的相對重要性

◆public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)

該方法的作用與Socket的setPerformancePreferences()方法的作用相同,用於設定連接時間、延遲和帶寬的相對重要性,參見本書第2章的2.5.10節(設定連接時間、延遲和帶寬的相對重要性)。

相關文章鏈接:

Java網絡編程精解之ServerSocket用法詳解二

Java網絡編程精解之ServerSocket用法詳解三

(責任編輯 火鳳凰 [email protected]  QQ:34067741  TEL:(010)68476636-8007)

 
發佈了41 篇原創文章 · 獲贊 5 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章