天天看點

Java網絡程式設計從入門到精通(25):建立ServerSocket對象

本文為原創,如需轉載,請注明作者和出處,謝謝!

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對象最常用的方式。可以通過如下的構造方法來綁定端口:

如果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已經打開!

端口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。

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

        int queueLength = Integer.parseInt(args[0]);

        ServerSocket serverSocket = new ServerSocket(1234, queueLength);

        System.out.println("端口(1234)已經綁定,請按Enter鍵開始處理用戶端請求!");

        System.in.read();

        int n = 0;

        while (true)

            System.out.println("<準備接收第" + (++n) + "個用戶端請求!");

            Socket socket = serverSocket.accept();

            System.out.println("正在處理第" + n + "個用戶端請求

Java網絡程式設計從入門到精通(25):建立ServerSocket對象

");

            Thread.sleep(3000);

            System.out.println("第" + n + "個用戶端請求已經處理完畢!>");

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

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

java server.SetRequestQueue 2

   運作結果:

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

    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. 按Enter鍵繼續執行SetRequestQueue後,運作結果如下:

<準備接收第1個用戶端請求!

正在處理第1個用戶端請求

Java網絡程式設計從入門到精通(25):建立ServerSocket對象

第1個用戶端請求已經處理完畢!>

<準備接收第2個用戶端請求!

正在處理第2個用戶端請求

Java網絡程式設計從入門到精通(25):建立ServerSocket對象

第2個用戶端請求已經處理完畢!>

<準備接收第3個用戶端請求!

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

三、綁定IP位址

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

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);

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

<a href="http://www.eoeandroid.com/forumdisplay.php?fid=4">國内最棒的Google Android技術社群(eoeandroid),歡迎通路!</a>