程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java網絡編程從入門到精通(25):創建ServerSocket對象

Java網絡編程從入門到精通(25):創建ServerSocket對象

編輯:關於JAVA

ServerSocket類的構造方法有四種重載形式,它們的定義如下:

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

在上面的構造方法中涉及到了三個參數:port、backlog和bindAddr。其中port是ServerSocket對象要綁定的端口,backlog是請求隊列的長度,bindAddr是ServerSocket對象要綁定的IP地址。

一、通過構造方法綁定端口

通過構造方法綁定端口是創建ServerSocket對象最常用的方式。可以通過如下的構造方法來綁定端口:

public ServerSocket(int port) throws IOException

如果port參數所指定的端口已經被綁定,構造方法就會拋出IOException異常。但實際上拋出的異常是BindException。從圖 4.2的異常類繼承關系圖可以看出,所有和網絡有關的異常都是IOException類的子類。因此,為了ServerSocket構造方法還可以拋出其他的異常,就使用了IOException。

如果port的值為0,系統就會隨機選取一個端口號。但隨機選取的端口意義不大,因為客戶端在連接服務器時需要明確知道服務端程序的端口號。可以通過ServerSocket的toString方法輸出和ServerSocket對象相關的信息。下面的代碼輸入了和ServerSocket對象相關的信息。

ServerSocket serverSocket = new ServerSocket(1320);
System.out.println(serverSocket);

運行結果:

ServerSocket[addr=0.0.0.0/0.0.0.0,port=0,localport=1320]

上面的輸出結果中的addr是服務端綁定的IP地址,如果未綁定IP地址,這個值是0.0.0.0,在這種情況下,ServerSocket對象將監聽服務端所有網絡接口的所有IP地址。port永遠是0。localport是ServerSocket綁定的端口,如果port值為0(不是輸出結果的port,是ServerSocket構造方法的參數port),localport是一個隨機選取的端口號。

在操作系統中規定1 ~ 1023為系統使用的端口號。端口號的最小值是1,最大值是65535。在Windows中用戶編寫的程序可以綁定端口號小於1024的端口,但在 Linux/Unix下必須使用root登錄才可以綁定小於1024的端口。在前面的文章中曾使用Socket類來判斷本機打開了哪些端口,其實使用 ServerSocket類也可以達到同樣的目的。基本原理是用ServerSocket來綁定本機的端口,如果綁定某個端口時拋出 BindException異常,就說明這個端口已經打開,反之則這個端口未打開。

package server;

import java.net.*;

public class ScanPort
{
     public static void main(String[] args)
     {
         if (args.length == 0)
             return;
         int minPort = 0, maxPort = 0;
         String ports[] = args[0].split("[-]");
         minPort = Integer.parseInt(ports[0]);
         maxPort = (ports.length > 1) ? Integer.parseInt(ports[1]) : minPort;
         for (int port = minPort; port <= maxPort; port++)
             try
             {
                 ServerSocket serverSocket = new ServerSocket(port);
                 serverSocket.close();
             }
             catch (Exception e)
             {
                 System.err.println(e.getClass());
                 System.err.println("端口" + port + "已經打開!");
             }
     }
}

在上面的代碼中輸出了創建ServerSocket對象時拋出的異常類的信息。ScanPort通過命令行參數將待掃描的端口號范圍傳入程序,參數格式為:minPort-maxPort,如果只輸入一個端口號,ScanPort程序只掃描這個端口號。

測試

java server.ScanPort 1-1023

運行結果

class java.net.BindException
端口80已經打開!
class java.net.BindException
端口135已經打開!

二、設置請求隊列的長度

在編寫服務端程序時,一般會通過多線程來同時處理多個客戶端請求。也就是說,使用一個線程來接收客戶端請求,當接到一個請求後(得到一個 Socket對象),會創建一個新線程,將這個客戶端請求交給這個新線程處理。而那個接收客戶端請求的線程則繼續接收客戶端請求,這個過程的實現代碼如下:

ServerSocket serverSocket = new ServerSocket(1234); // 綁定端口
// 處理其他任務的代碼
while(true)
{
    Socket socket = serverSocket.accept(); // 等待接收客戶端請求
    // 處理其他任務的代碼
    new ThreadClass(socket).start(); // 創建並運行處理客戶端請求的線程
}

上面代碼中的ThreadClass類是Thread類的子類,這個類的構造方法有一個Socket類型的參數,可以通過構造方法將Socket對象傳入ThreadClass對象,並在ThreadClass對象的run方法中處理客戶端請求。這段代碼從表面上看好象是天衣無縫,無論有多少客戶端請求,只要服務器的配置足夠高,就都可以處理。但仔細思考上面的代碼,我們可能會發現一些問題。如果在第2行和第6行有足夠復雜的代碼,執行時間也比較長,這就意味著服務端程序無法及時響應客戶端的請求。

假設第2行和第6行的代碼是Thread.sleep(3000),這將使程序延遲3秒。那麼在這3秒內,程序不會執行accept方法,因此,這段程序只是將端口綁定到了1234上,並未開始接收客戶端請求。如果在這時一個客戶端向端口1234發來了一個請求,從理論上講,客戶端應該出現拒絕連接錯誤,但客戶端卻顯示連接成功。究其原因,就是這節要討論的請求隊列在起作用。

在使用ServerSocket對象綁定一個端口後,操作系統就會為這個端口分配一個先進先出的隊列(這個隊列長度的默認值一般是50),這個隊列用於保存未處理的客戶端請求,因此叫請求隊列。而ServerSocket類的accept方法負責從這個隊列中讀取未處理的客戶端請求。如果請求隊列為空,accept則處於阻塞狀態。每當客戶端向服務端發來一個請求,服務端會首先將這個客戶端請求保存在請求隊列中,然後accept再從請求隊列中讀取。這也可以很好地解釋為什麼上面的代碼在還未執行到accept方法時,仍然可以接收一定數量的客戶端請求。如果請求隊列中的客戶端請求數達到請求隊列的最大容量時,服務端將無法再接收客戶端請求。如果這時客戶端再向服務端發請求,客戶端將會拋出一個SocketException異常。

ServerSocket類有兩個構造方法可以使用backlog參數重新設置請求隊列的長度。在以下幾種情況,仍然會采用操作系統限定的請求隊列的最大長度:

backlog的值小於等於0。

backlog的值大於操作系統限定的請求隊列的最大長度。

在ServerSocket構造方法中未設置backlog參數。

下面積代碼演示了請求隊列的一些特性,請求隊列長度通過命令行參數傳入SetRequestQueue。

package server;

import java.net.*;

class TestRequestQueue
{
     public static void main(String[] args) throws Exception
     {
         for (int i = 0; i < 10; i++)
         {
             Socket socket = new Socket("localhost", 1234);
             socket.getOutputStream().write(1);
             System.out.println("已經成功創建第" + String.valueOf(i + 1) + "個客戶端連接!");
         }
     }
}
public class SetRequestQueue
{
     public static void main(String[] args) throws Exception
     {
         if (args.length == 0)
             return;
         int queueLength = Integer.parseInt(args[0]);
         ServerSocket serverSocket = new ServerSocket(1234, queueLength);
         System.out.println("端口(1234)已經綁定,請按回車鍵開始處理客戶端請求!");
         System.in.read();
         int n = 0;
         while (true)
         {
             System.out.println("<准備接收第" + (++n) + "個客戶端請求!");
             Socket socket = serverSocket.accept();
             System.out.println("正在處理第" + n + "個客戶端請求");
             Thread.sleep(3000);
             System.out.println("第" + n + "個客戶端請求已經處理完畢!>");
         }
     }
}

測試(按著以下步驟操作)

1. 執行如下命令(在執行這條命令後,先不要按回車鍵):

java server.SetRequestQueue 2

運行結果:

端口(1234)已經綁定,請按回車鍵開始處理客戶端請求!

2. 執行如下命令:  

java server.TestRequestQueue

運行結果:

已經成功創建第1個客戶端連接!
已經成功創建第2個客戶端連接!
Exception in thread "main" java.net.SocketException: Connection reset by peer: socket write error
                        at java.net.SocketOutputStream.socketWrite0(Native Method)
                        at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:92)
                        at java.net.SocketOutputStream.write(SocketOutputStream.java:115)
                        at server.TestRequestQueue.main(SetRequestQueue.java:12)

3. 按回車鍵繼續執行SetRequestQueue後,運行結果如下:

端口(1234)已經綁定,請按回車鍵開始處理客戶端請求!
<准備接收第1個客戶端請求!
正在處理第1個客戶端請求
第1個客戶端請求已經處理完畢!>
<准備接收第2個客戶端請求!
正在處理第2個客戶端請求
第2個客戶端請求已經處理完畢!>
<准備接收第3個客戶端請求!

從第二步的運行結果可以看出,當TestRequestQueue創建兩個Socket連接之後,服務端的請求隊列已滿,並且服務端暫時無法繼續執行(由於System.in.read()的原因而暫停程序的執行,等待用戶的輸入)。因此,服務端程序無法再接收客戶端請求。這時 TestRequestQueue拋出了一個SocketException異常。在TestRequestQueue已經創建成功的兩個Socket連接已經保存在服務端的請求隊列中。在這時按任意鍵繼續執行SetRequestQueue。accept方法就會從請求隊列中將這兩個客戶端請求隊列中依次讀出來。從第三步的運行結果可以看出,服務端處理完這兩個請求後(一個<…>包含的就是一個處理過程),請求隊列為空,這時accept處理阻塞狀態,等待接收第三個客戶端請求。如果這時再運行TestRequestQueue,服務端會接收幾個客戶端請求呢?如果將請求隊列的長度設為大於 10的數,TestRequestQueue的運行結果會是什麼呢?讀者可以自己做一下這些實驗,看看和自己認為的結果是否一致。

三、綁定IP地址

在有多個網絡接口或多個IP地址的計算機上可以使用如下的構造方法將服務端綁定在某一個IP地址上:

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

bindAddr參數就是要綁定的IP地址。如果將服務端綁定到某一個IP地址上,就只有可以訪問這個IP地址的客戶端才能連接到服務器上。如一台機器上有兩塊網卡,一塊網卡連接內網,另一塊連接外網。如果用Java實現一個Email服務器,並且只想讓內網的用戶使用它。就可以使用這個構造方法將 ServerSocket對象綁定到連接內網的IP地址上。這樣外網就無法訪問Email服務器了。可以使用如下代碼來綁定IP地址:

ServerSocket serverSocket = new
ServerSocket(1234, 0, InetAddress.getByName("192.168.18.10"));

上面的代碼將IP地址綁定到了192.168.18.10上,因此,服務端程序只能使用綁定了這個IP地址的網絡接口進行通訊。

四、默認構造方法的使用

除了使用ServerSocket類的構造方法綁定端口外,還可以用ServerSocket的bind方法來完成構造方法所做的工作。要想使用bind 方法,必須得用ServerSocket類的默認構造方法(沒有參數的構造方法)來創建ServerSocket對象。bind方法有兩個重載形式,它們的定義如下:

public void bind(SocketAddress endpoint) throws IOException
public void bind(SocketAddress endpoint, int backlog) throws IOException

bind方法不僅可以綁定端口,也可以設置請求隊列的長度以及綁定IP地址。bind方法的作用是為了在建立ServerSocket對象後設置 ServerSocket類的一些選項。而這些選項必須在綁定端口之前設置,一但綁定了端口後,再設置這些選項將不再起作用。下面的代碼演示了bind方法的使用及如何設置ServerSocket類的選項。

ServerSocket serverSocket1 = new ServerSocket();
serverSocket1.setReuseAddress(true);
serverSocket1.bind(new InetSocketAddress(1234));
ServerSocket serverSocket2 = new ServerSocket();
serverSocket2.setReuseAddress(true);
serverSocket2.bind(new InetSocketAddress("192.168.18.10", 1234));
ServerSocket serverSocket3 = new ServerSocket();
serverSocket3.setReuseAddress(true);
serverSocket3.bind(new InetSocketAddress("192.168.18.10", 1234), 30);

在上面的代碼中設置了SO_REUSEADDR 選項(這個選項將在後面的文章中詳細討論)。如果使用下面的代碼,這個選項將不起作用。

ServerSocket serverSocket3 = new ServerSocket(1234);
serverSocket3.setReuseAddress(true);

在第6行綁定了IP地址和端口。使用構造方法是無法得到這個組合的(想綁定IP地址,必須得設置backlog參數),因此,bind方法比構造方法更靈活。

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