本文為原創(chuàng),如需轉(zhuǎn)載,請(qǐng)注明作者和出處,謝謝!
上一篇:Java網(wǎng)絡(luò)編程從入門(mén)到精通(24):實(shí)現(xiàn)HTTP斷點(diǎn)續(xù)傳下載工具(附源代碼)
ServerSocket類(lèi)的構(gòu)造方法有四種重載形式,它們的定義如下:
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
在上面的構(gòu)造方法中涉及到了三個(gè)參數(shù):port、backlog和bindAddr。其中port是ServerSocket對(duì)象要綁定的端口,backlog是請(qǐng)求隊(duì)列的長(zhǎng)度,bindAddr是ServerSocket對(duì)象要綁定的IP地址。
一、通過(guò)構(gòu)造方法綁定端口
通過(guò)構(gòu)造方法綁定端口是創(chuàng)建ServerSocket對(duì)象最常用的方式。可以通過(guò)如下的構(gòu)造方法來(lái)綁定端口:
public ServerSocket(int port) throws IOException
如果port參數(shù)所指定的端口已經(jīng)被綁定,構(gòu)造方法就會(huì)拋出IOException異常。但實(shí)際上拋出的異常是BindException。從圖4.2的異常類(lèi)繼承關(guān)系圖可以看出,所有和網(wǎng)絡(luò)有關(guān)的異常都是IOException類(lèi)的子類(lèi)。因此,為了ServerSocket構(gòu)造方法還可以拋出其他的異常,就使用了IOException。
如果port的值為0,系統(tǒng)就會(huì)隨機(jī)選取一個(gè)端口號(hào)。但隨機(jī)選取的端口意義不大,因?yàn)榭蛻?hù)端在連接服務(wù)器時(shí)需要明確知道服務(wù)端程序的端口號(hào)。可以通過(guò)ServerSocket的toString方法輸出和ServerSocket對(duì)象相關(guān)的信息。下面的代碼輸入了和ServerSocket對(duì)象相關(guān)的信息。
ServerSocket serverSocket = new ServerSocket(1320);
System.out.println(serverSocket);
運(yùn)行結(jié)果:
ServerSocket[addr=0.0.0.0/0.0.0.0,port=0,localport=1320]
上面的輸出結(jié)果中的addr是服務(wù)端綁定的IP地址,如果未綁定IP地址,這個(gè)值是0.0.0.0,在這種情況下,ServerSocket對(duì)象將監(jiān)聽(tīng)服務(wù)端所有網(wǎng)絡(luò)接口的所有IP地址。port永遠(yuǎn)是0。localport是ServerSocket綁定的端口,如果port值為0(不是輸出結(jié)果的port,是ServerSocket構(gòu)造方法的參數(shù)port),localport是一個(gè)隨機(jī)選取的端口號(hào)。
在操作系統(tǒng)中規(guī)定1 ~ 1023為系統(tǒng)使用的端口號(hào)。端口號(hào)的最小值是1,最大值是65535。在Windows中用戶(hù)編寫(xiě)的程序可以綁定端口號(hào)小于1024的端口,但在Linux/Unix下必須使用root登錄才可以綁定小于1024的端口。在前面的文章中曾使用Socket類(lèi)來(lái)判斷本機(jī)打開(kāi)了哪些端口,其實(shí)使用ServerSocket類(lèi)也可以達(dá)到同樣的目的。基本原理是用ServerSocket來(lái)綁定本機(jī)的端口,如果綁定某個(gè)端口時(shí)拋出BindException異常,就說(shuō)明這個(gè)端口已經(jīng)打開(kāi),反之則這個(gè)端口未打開(kāi)。
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 + "已經(jīng)打開(kāi)!");
}
}
}
在上面的代碼中輸出了創(chuàng)建ServerSocket對(duì)象時(shí)拋出的異常類(lèi)的信息。ScanPort通過(guò)命令行參數(shù)將待掃描的端口號(hào)范圍傳入程序,參數(shù)格式為:minPort-maxPort,如果只輸入一個(gè)端口號(hào),ScanPort程序只掃描這個(gè)端口號(hào)。
測(cè)試
java server.ScanPort 1-1023
運(yùn)行結(jié)果
class java.net.BindException
端口80已經(jīng)打開(kāi)!
class java.net.BindException
端口135已經(jīng)打開(kāi)!
二、設(shè)置請(qǐng)求隊(duì)列的長(zhǎng)度
在編寫(xiě)服務(wù)端程序時(shí),一般會(huì)通過(guò)多線程來(lái)同時(shí)處理多個(gè)客戶(hù)端請(qǐng)求。也就是說(shuō),使用一個(gè)線程來(lái)接收客戶(hù)端請(qǐng)求,當(dāng)接到一個(gè)請(qǐng)求后(得到一個(gè)Socket對(duì)象),會(huì)創(chuàng)建一個(gè)新線程,將這個(gè)客戶(hù)端請(qǐng)求交給這個(gè)新線程處理。而那個(gè)接收客戶(hù)端請(qǐng)求的線程則繼續(xù)接收客戶(hù)端請(qǐng)求,這個(gè)過(guò)程的實(shí)現(xiàn)代碼如下:
ServerSocket serverSocket = new ServerSocket(1234); // 綁定端口
// 處理其他任務(wù)的代碼
while(true)
{
Socket socket = serverSocket.accept(); // 等待接收客戶(hù)端請(qǐng)求
// 處理其他任務(wù)的代碼
new ThreadClass(socket).start(); // 創(chuàng)建并運(yùn)行處理客戶(hù)端請(qǐng)求的線程
}
上面代碼中的ThreadClass類(lèi)是Thread類(lèi)的子類(lèi),這個(gè)類(lèi)的構(gòu)造方法有一個(gè)Socket類(lèi)型的參數(shù),可以通過(guò)構(gòu)造方法將Socket對(duì)象傳入ThreadClass對(duì)象,并在ThreadClass對(duì)象的run方法中處理客戶(hù)端請(qǐng)求。這段代碼從表面上看好象是天衣無(wú)縫,無(wú)論有多少客戶(hù)端請(qǐng)求,只要服務(wù)器的配置足夠高,就都可以處理。但仔細(xì)思考上面的代碼,我們可能會(huì)發(fā)現(xiàn)一些問(wèn)題。如果在第2行和第6行有足夠復(fù)雜的代碼,執(zhí)行時(shí)間也比較長(zhǎng),這就意味著服務(wù)端程序無(wú)法及時(shí)響應(yīng)客戶(hù)端的請(qǐng)求。
假設(shè)第2行和第6行的代碼是Thread.sleep(3000),這將使程序延遲3秒。那么在這3秒內(nèi),程序不會(huì)執(zhí)行accept方法,因此,這段程序只是將端口綁定到了1234上,并未開(kāi)始接收客戶(hù)端請(qǐng)求。如果在這時(shí)一個(gè)客戶(hù)端向端口1234發(fā)來(lái)了一個(gè)請(qǐng)求,從理論上講,客戶(hù)端應(yīng)該出現(xiàn)拒絕連接錯(cuò)誤,但客戶(hù)端卻顯示連接成功。究其原因,就是這節(jié)要討論的請(qǐng)求隊(duì)列在起作用。
在使用ServerSocket對(duì)象綁定一個(gè)端口后,操作系統(tǒng)就會(huì)為這個(gè)端口分配一個(gè)先進(jìn)先出的隊(duì)列(這個(gè)隊(duì)列長(zhǎng)度的默認(rèn)值一般是50),這個(gè)隊(duì)列用于保存未處理的客戶(hù)端請(qǐng)求,因此叫請(qǐng)求隊(duì)列。而ServerSocket類(lèi)的accept方法負(fù)責(zé)從這個(gè)隊(duì)列中讀取未處理的客戶(hù)端請(qǐng)求。如果請(qǐng)求隊(duì)列為空,accept則處于阻塞狀態(tài)。每當(dāng)客戶(hù)端向服務(wù)端發(fā)來(lái)一個(gè)請(qǐng)求,服務(wù)端會(huì)首先將這個(gè)客戶(hù)端請(qǐng)求保存在請(qǐng)求隊(duì)列中,然后accept再?gòu)恼?qǐng)求隊(duì)列中讀取。這也可以很好地解釋為什么上面的代碼在還未執(zhí)行到accept方法時(shí),仍然可以接收一定數(shù)量的客戶(hù)端請(qǐng)求。如果請(qǐng)求隊(duì)列中的客戶(hù)端請(qǐng)求數(shù)達(dá)到請(qǐng)求隊(duì)列的最大容量時(shí),服務(wù)端將無(wú)法再接收客戶(hù)端請(qǐng)求。如果這時(shí)客戶(hù)端再向服務(wù)端發(fā)請(qǐng)求,客戶(hù)端將會(huì)拋出一個(gè)SocketException異常。
ServerSocket類(lèi)有兩個(gè)構(gòu)造方法可以使用backlog參數(shù)重新設(shè)置請(qǐng)求隊(duì)列的長(zhǎng)度。在以下幾種情況,仍然會(huì)采用操作系統(tǒng)限定的請(qǐng)求隊(duì)列的最大長(zhǎng)度:
- backlog的值小于等于0。
- backlog的值大于操作系統(tǒng)限定的請(qǐng)求隊(duì)列的最大長(zhǎng)度。
- 在ServerSocket構(gòu)造方法中未設(shè)置backlog參數(shù)。
下面積代碼演示了請(qǐng)求隊(duì)列的一些特性,請(qǐng)求隊(duì)列長(zhǎng)度通過(guò)命令行參數(shù)傳入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("已經(jīng)成功創(chuàng)建第" + String.valueOf(i + 1) + "個(gè)客戶(hù)端連接!");
}
}
}
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)已經(jīng)綁定,請(qǐng)按回車(chē)鍵開(kāi)始處理客戶(hù)端請(qǐng)求!");
System.in.read();
int n = 0;
while (true)
{
System.out.println("<準(zhǔn)備接收第" + (++n) + "個(gè)客戶(hù)端請(qǐng)求!");
Socket socket = serverSocket.accept();
System.out.println("正在處理第" + n + "個(gè)客戶(hù)端請(qǐng)求
");
Thread.sleep(3000);
System.out.println("第" + n + "個(gè)客戶(hù)端請(qǐng)求已經(jīng)處理完畢!>");
}
}
}
測(cè)試(按著以下步驟操作)
1. 執(zhí)行如下命令(在執(zhí)行這條命令后,先不要按回車(chē)鍵):
java server.SetRequestQueue 2
運(yùn)行結(jié)果:
端口(1234)已經(jīng)綁定,請(qǐng)按回車(chē)鍵開(kāi)始處理客戶(hù)端請(qǐng)求!
2. 執(zhí)行如下命令:
java server.TestRequestQueue
運(yùn)行結(jié)果:
已經(jīng)成功創(chuàng)建第1個(gè)客戶(hù)端連接!
已經(jīng)成功創(chuàng)建第2個(gè)客戶(hù)端連接!
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. 按回車(chē)鍵繼續(xù)執(zhí)行SetRequestQueue后,運(yùn)行結(jié)果如下:
端口(1234)已經(jīng)綁定,請(qǐng)按回車(chē)鍵開(kāi)始處理客戶(hù)端請(qǐng)求!
<準(zhǔn)備接收第1個(gè)客戶(hù)端請(qǐng)求!
正在處理第1個(gè)客戶(hù)端請(qǐng)求
第1個(gè)客戶(hù)端請(qǐng)求已經(jīng)處理完畢!>
<準(zhǔn)備接收第2個(gè)客戶(hù)端請(qǐng)求!
正在處理第2個(gè)客戶(hù)端請(qǐng)求
第2個(gè)客戶(hù)端請(qǐng)求已經(jīng)處理完畢!>
<準(zhǔn)備接收第3個(gè)客戶(hù)端請(qǐng)求!
從第二步的運(yùn)行結(jié)果可以看出,當(dāng)TestRequestQueue創(chuàng)建兩個(gè)Socket連接之后,服務(wù)端的請(qǐng)求隊(duì)列已滿,并且服務(wù)端暫時(shí)無(wú)法繼續(xù)執(zhí)行(由于System.in.read()的原因而暫停程序的執(zhí)行,等待用戶(hù)的輸入)。因此,服務(wù)端程序無(wú)法再接收客戶(hù)端請(qǐng)求。這時(shí)TestRequestQueue拋出了一個(gè)SocketException異常。在TestRequestQueue已經(jīng)創(chuàng)建成功的兩個(gè)Socket連接已經(jīng)保存在服務(wù)端的請(qǐng)求隊(duì)列中。在這時(shí)按任意鍵繼續(xù)執(zhí)行SetRequestQueue。accept方法就會(huì)從請(qǐng)求隊(duì)列中將這兩個(gè)客戶(hù)端請(qǐng)求隊(duì)列中依次讀出來(lái)。從第三步的運(yùn)行結(jié)果可以看出,服務(wù)端處理完這兩個(gè)請(qǐng)求后(一個(gè)<…>包含的就是一個(gè)處理過(guò)程),請(qǐng)求隊(duì)列為空,這時(shí)accept處理阻塞狀態(tài),等待接收第三個(gè)客戶(hù)端請(qǐng)求。如果這時(shí)再運(yùn)行TestRequestQueue,服務(wù)端會(huì)接收幾個(gè)客戶(hù)端請(qǐng)求呢?如果將請(qǐng)求隊(duì)列的長(zhǎng)度設(shè)為大于10的數(shù),TestRequestQueue的運(yùn)行結(jié)果會(huì)是什么呢?讀者可以自己做一下這些實(shí)驗(yàn),看看和自己認(rèn)為的結(jié)果是否一致。
三、綁定IP地址
在有多個(gè)網(wǎng)絡(luò)接口或多個(gè)IP地址的計(jì)算機(jī)上可以使用如下的構(gòu)造方法將服務(wù)端綁定在某一個(gè)IP地址上:
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
bindAddr參數(shù)就是要綁定的IP地址。如果將服務(wù)端綁定到某一個(gè)IP地址上,就只有可以訪問(wèn)這個(gè)IP地址的客戶(hù)端才能連接到服務(wù)器上。如一臺(tái)機(jī)器上有兩塊網(wǎng)卡,一塊網(wǎng)卡連接內(nèi)網(wǎng),另一塊連接外網(wǎng)。如果用Java實(shí)現(xiàn)一個(gè)Email服務(wù)器,并且只想讓內(nèi)網(wǎng)的用戶(hù)使用它。就可以使用這個(gè)構(gòu)造方法將ServerSocket對(duì)象綁定到連接內(nèi)網(wǎng)的IP地址上。這樣外網(wǎng)就無(wú)法訪問(wèn)Email服務(wù)器了。可以使用如下代碼來(lái)綁定IP地址:
ServerSocket serverSocket = new
ServerSocket(1234, 0, InetAddress.getByName("192.168.18.10"));
上面的代碼將IP地址綁定到了192.168.18.10上,因此,服務(wù)端程序只能使用綁定了這個(gè)IP地址的網(wǎng)絡(luò)接口進(jìn)行通訊。
四、默認(rèn)構(gòu)造方法的使用
除了使用ServerSocket類(lèi)的構(gòu)造方法綁定端口外,還可以用ServerSocket的bind方法來(lái)完成構(gòu)造方法所做的工作。要想使用bind方法,必須得用ServerSocket類(lèi)的默認(rèn)構(gòu)造方法(沒(méi)有參數(shù)的構(gòu)造方法)來(lái)創(chuàng)建ServerSocket對(duì)象。bind方法有兩個(gè)重載形式,它們的定義如下:
public void bind(SocketAddress endpoint) throws IOException
public void bind(SocketAddress endpoint, int backlog) throws IOException
bind方法不僅可以綁定端口,也可以設(shè)置請(qǐng)求隊(duì)列的長(zhǎng)度以及綁定IP地址。bind方法的作用是為了在建立ServerSocket對(duì)象后設(shè)置ServerSocket類(lèi)的一些選項(xiàng)。而這些選項(xiàng)必須在綁定端口之前設(shè)置,一但綁定了端口后,再設(shè)置這些選項(xiàng)將不再起作用。下面的代碼演示了bind方法的使用及如何設(shè)置ServerSocket類(lèi)的選項(xiàng)。
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);
在上面的代碼中設(shè)置了SO_REUSEADDR
選項(xiàng)(這個(gè)選項(xiàng)將在后面的文章中詳細(xì)討論)。如果使用下面的代碼,這個(gè)選項(xiàng)將不起作用。
ServerSocket serverSocket3 = new ServerSocket(1234);
serverSocket3.setReuseAddress(true);
在第6行綁定了IP地址和端口。使用構(gòu)造方法是無(wú)法得到這個(gè)組合的(想綁定IP地址,必須得設(shè)置backlog參數(shù)),因此,bind方法比構(gòu)造方法更靈活。
下一篇:Java網(wǎng)絡(luò)編程從入門(mén)到精通(26):在服務(wù)端接收和發(fā)送數(shù)據(jù)
新浪微博:http://t.sina.com.cn/androidguy 昵稱(chēng):李寧_Lining