在客戶/服務(wù)器通信模式中,服務(wù)器端需要創(chuàng)建監(jiān)聽特定端口的ServerSocket擅笔,ServerSocket負責接收客戶連接請求险胰,并生成與客戶端連接的Socket汹押。
1、構(gòu)造ServerSocket
ServerSocket的構(gòu)造方法有以下幾種重載形式:
- ServerSocket()throws IOException
- ServerSocket(int port) throws IOException
- ServerSocket(int port, int backlog) throws IOException
- ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
在以上構(gòu)造方法中起便,參數(shù)port指定服務(wù)器要綁定的端口(服務(wù)器要監(jiān)聽的端口)鲸阻,參數(shù)backlog指定客戶連接請求隊列的長度,參數(shù)bindAddr指定服務(wù)器要綁定的IP地址缨睡。
1.1 鸟悴、綁定端口
除不帶參數(shù)的構(gòu)造方法以外,其他構(gòu)造方法都會使服務(wù)器與特定端口綁定奖年,該端口由參數(shù)port指定细诸。如果端口被其他服務(wù)進程占用,或是陋守,在某些系統(tǒng)中震贵,若沒有以超級用戶身份運行服務(wù)器程序,操作系統(tǒng)不允許服務(wù)器綁定到1-1023的端口時水评,會拋出BindException猩系。
1.2、設(shè)定客戶連接請求隊列的長度
當服務(wù)器進程運行時中燥,可能會同時監(jiān)聽到多個客戶的連接請求寇甸。管理客戶端連接請求的任務(wù)是由操作系統(tǒng)來完成的。操作系統(tǒng)將連接請求存儲在一個先進先出隊列中。許多操作系統(tǒng)限定了隊列的最大長度拿霉,一般為50吟秩。當隊列中的連接請求達到了隊列的最大容量時,服務(wù)器進程所在的主機會拒絕新的連接請求绽淘。只有當服務(wù)器進程通過ServerSocket的accept()方法從隊列中取出連接請求涵防,使隊列騰出空位時,隊列才能繼續(xù)加入新的連接請求沪铭。
對于客戶進程壮池,如果它發(fā)出的連接請求被加入到服務(wù)器的隊列中,就意味著客戶與服務(wù)器的連接建立成功杀怠,客戶進程從Socket構(gòu)造方法中正常返回火窒。如果客戶進程發(fā)出的連接請求被服務(wù)器拒絕,Socket構(gòu)造方法就會拋出ConnectionException驮肉。
ServerSocket構(gòu)造方法的backlog參數(shù)用來顯式設(shè)置連接請求隊列的長度,它將覆蓋操作系統(tǒng)限定的隊列的最大長度已骇。
在一下集中情況离钝,仍然采用操作系統(tǒng)限定的隊列最大長度:
- backlog參數(shù)的值大于操作系統(tǒng)限定的隊列的最大長度;
- backlog參數(shù)的值小于或等于0褪储;
- 在ServerSocket構(gòu)造方法中沒有設(shè)置backlog參數(shù)卵渴。
1.3、設(shè)定綁定的IP地址
若主機只有一個地址鲤竹,則服務(wù)器默認綁定該地址浪读;若主機有多個地址,則可以調(diào)用ServerSocket(int port, int backlog, InetAddress bindAddr)構(gòu)造方法設(shè)置主機ip地址辛藻。
1.4碘橘、默認構(gòu)造方法的作用
ServerSocket有一個不帶參數(shù)的默認構(gòu)造方法。通過該方法創(chuàng)建的ServerSocket不與任何端口綁定吱肌,接下來還需要通過bind()方法與特定端口綁定痘拆。
這個默認構(gòu)造方法的用途是,允許服務(wù)器在綁定到特定端口之前氮墨,先設(shè)置ServerSocket的一些選項纺蛆。因為一旦服務(wù)器與特定端口綁定,有些選項就不能再改變了规揪。
2桥氏、接收和關(guān)閉與客戶的連接
ServerSocket的accept()方法從連接請求隊列中取出一個客戶的連接請求,然后創(chuàng)建與客戶連接的Socket對象猛铅,并將它返回字支。如果隊列中沒有連接請求,accept()方法就會一直等待,直到接收到了連接請求才返回祥款。
服務(wù)器從Socket對象中獲得輸入流和輸出流
清笨,就能與客戶交換數(shù)據(jù)。當服務(wù)器正在進行發(fā)送數(shù)據(jù)的操作時刃跛,如果客戶端斷開了連接抠艾,那么服務(wù)器端會拋出一個IOException的子類SocketException異常:java.net.SocketException: Connection reset by peer。
3桨昙、關(guān)閉ServerSocket
ServerSocket的close()方法使服務(wù)器釋放占用的端口检号,并且斷開與所有客戶的連接。當一個服務(wù)器程序運行結(jié)束時蛙酪,即使沒有執(zhí)行ServerSocket的close()方法齐苛,操作系統(tǒng)也會釋放這個服務(wù)器占用的端口。因此桂塞,服務(wù)器程序并不一定要在結(jié)束之前執(zhí)行ServerSocket的close()方法凹蜂。
在某些情況下,如果希望及時釋放服務(wù)器的端口阁危,以便讓其他程序能占用該端口玛痊,則可以顯式調(diào)用ServerSocket的close()方法。
ServerSocket的isClosed()方法判斷ServerSocket是否關(guān)閉狂打,只有執(zhí)行了ServerSocket的close()方法擂煞,isClosed()方法才返回true;否則趴乡,即使ServerSocket還沒有和特定端口綁定对省,isClosed()方法也會返回false。
ServerSocket的isBound()方法判斷ServerSocket是否已經(jīng)與一個端口綁定晾捏,只要ServerSocket已經(jīng)與一個端口綁定蒿涎,即使它已經(jīng)被關(guān)閉,isBound()方法也會返回true惦辛。
4同仆、獲取ServerSocket的信息
- public InetAddress getInetAddress():獲取服務(wù)器綁定的ip地址;
- public int getLocalPort():獲取服務(wù)器綁定的端口裙品;
在構(gòu)造ServerSocket時俗批,如果把端口設(shè)為0,那么將由操作系統(tǒng)為服務(wù)器分配一個端口(稱為匿名端口)市怎,程序只要調(diào)用getLocalPort()方法就能獲知這個端口號岁忘。多數(shù)服務(wù)器會監(jiān)聽固定的端口,這樣才便于客戶程序訪問服務(wù)器区匠。匿名端口一般適用于服務(wù)器與客戶之間的臨時通信干像,通信結(jié)束帅腌,就斷開連接,并且ServerSocket占用的臨時端口也被釋放麻汰。
5速客、ServerSocket選項
ServerSocket有以下3個選項。
- SO_TIMEOUT:表示等待客戶連接的超時時間五鲫。
- SO_REUSEADDR:表示是否允許重用服務(wù)器所綁定的地址溺职。
- SO_RCVBUF:表示接收數(shù)據(jù)的緩沖區(qū)的大小。
5.1位喂、SO_TIMEOUT選項
- 設(shè)置該選項:public void setSoTimeout(int timeout) throws SocketException
- 讀取該選項:public int getSoTimeout () throws IOException
SO_TIMEOUT表示ServerSocket的accept()方法等待客戶連接的超時時間浪耘,以毫秒為單位。 如果SO_TIMEOUT的值為0塑崖,表示永遠不會超時七冲,這是SO_TIMEOUT的默認值。
當服務(wù)器執(zhí)行ServerSocket的accept()方法時规婆,如果連接請求隊列為空澜躺,服務(wù)器就會一直等待,直到接收到了客戶連接才從accept()方法返回抒蚜。如果設(shè)定了超時時間掘鄙,那么當服務(wù)器等待的時間超過了超時時間,就會拋出SocketTimeoutException削锰,它是InterruptedException的子類。
5.2毕莱、SO_REUSEADDR選項
- 設(shè)置該選項:public void setResuseAddress(boolean on) throws SocketException
- 讀取該選項:public boolean getResuseAddress() throws SocketException
這個選項與Socket的SO_REUSEADDR選項相同器贩,用于決定如果網(wǎng)絡(luò)上仍然有數(shù)據(jù)向舊的ServerSocket傳輸數(shù)據(jù),是否允許新的ServerSocket綁定到與舊的ServerSocket同樣的端口上朋截。SO_REUSEADDR選項的默認值與操作系統(tǒng)有關(guān)蛹稍,在某些操作系統(tǒng)中,允許重用端口部服,而在某些操作系統(tǒng)中不允許重用端口唆姐。
當ServerSocket關(guān)閉時,如果網(wǎng)絡(luò)上還有發(fā)送到這個ServerSocket的數(shù)據(jù)廓八,這個ServerSocket不會立刻釋放本地端口奉芦,而是會等待一段時間,確保接收到了網(wǎng)絡(luò)上發(fā)送過來的延遲數(shù)據(jù)剧蹂,然后再釋放端口
許多服務(wù)器程序都使用固定的端口声功。當服務(wù)器程序關(guān)閉后,有可能它的端口還會被占用一段時間宠叼,如果此時立刻在同一個主機上重啟服務(wù)器程序先巴,由于端口已經(jīng)被占用,使得服務(wù)器程序無法綁定到該端口,服務(wù)器啟動失敗伸蚯,并拋出BindException摩渺。
為了確保一個進程關(guān)閉了ServerSocket后,即使操作系統(tǒng)還沒釋放端口剂邮,同一個主機上的其他進程還可以立刻重用該端口摇幻,可以調(diào)用ServerSocket.setResuseAddress(true)方法
5.3、SO_RCVBUF選項
- 設(shè)置該選項:public void setReceiveBufferSize(int size) throws SocketException
- 讀取該選項:public int getReceiveBufferSize() throws SocketException
SO_RCVBUF表示服務(wù)器端的用于接收數(shù)據(jù)的緩沖區(qū)的大小抗斤,以字節(jié)為單位囚企。一般說來,傳輸大的連續(xù)的數(shù)據(jù)塊(基于HTTP或FTP協(xié)議的數(shù)據(jù)傳輸)可以使用較大的緩沖區(qū)瑞眼,這可以減少傳輸數(shù)據(jù)的次數(shù)龙宏,從而提高傳輸數(shù)據(jù)的效率。而對于交互式的通信(Telnet和網(wǎng)絡(luò)游戲)伤疙,則應(yīng)該采用小的緩沖區(qū)银酗,確保能及時把小批量的數(shù)據(jù)發(fā)送給對方。
5.4徒像、設(shè)定連接時間黍特、延遲和帶寬的相對重要性
public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)
該方法的作用與Socket的setPerformancePreferences()方法的作用相同,用于設(shè)定連接時間锯蛀、延遲和帶寬的相對重要性灭衷。
6、創(chuàng)建多線程服務(wù)器
許多實際應(yīng)用要求服務(wù)器具有同時為多個客戶提供服務(wù)的能力旁涤。HTTP服務(wù)器就是最明顯的例子翔曲。任何時刻,HTTP服務(wù)器都可能接收到大量的客戶請求劈愚,每個客戶都希望能快速得到HTTP服務(wù)器的響應(yīng)瞳遍。如果長時間讓客戶等待,會使網(wǎng)站失去信譽菌羽,從而降低訪問量掠械。
可以用并發(fā)性能來衡量一個服務(wù)器同時響應(yīng)多個客戶的能力。一個具有好的并發(fā)性能的服務(wù)器注祖,必須符合兩個條件:
- 能同時接收并處理多個客戶連接猾蒂;
- 對于每個客戶,都會迅速給予響應(yīng)是晨。
用多個線程來同時為多個客戶提供服務(wù)婚夫,這是提高服務(wù)器的并發(fā)性能的最常用的手段。
以下將按照3中方式來實現(xiàn)EchoServer署鸡,它們都使用多線程案糙。
- 為每個客戶分配一個工作線程限嫌。
- 創(chuàng)建一個線程池,由其中的工作線程來為客戶服務(wù)时捌。
- 利用JDK的Java類庫中現(xiàn)成的線程池怒医,由它的工作線程來為客戶服務(wù)。
6.1奢讨、 為每個客戶分配一個線程
服務(wù)器的主線程負責接收客戶的連接稚叹,每次接收到一個客戶連接,就會創(chuàng)建一個工作線程拿诸,由它負責與客戶的通信扒袖。
代碼示例:
public static void start(){
try{
ServerSocket serverSocket = new ServerSocket(PORT);
System.out.println("server listen on port:" + PORT);
while (true){
try {
Socket client = serverSocket.accept();
System.out.println("receive client connect, localPort=" + client.getPort());
new Thread(new EchoServer.HandlerServer(client)).start();
}catch (Exception e){
System.out.println("client exception,e=" + e.getMessage());
}
}
}catch(Exception e){
System.out.println("server exception,e=" + e.getMessage());
}
}
以上工作線程執(zhí)行HandlerServer的run()方法,其負責與單個客戶端通信亩码,通信完畢后斷開連接季率,線程自然終止。
6.2描沟、創(chuàng)建線程池
對每個客戶都分配一個新的工作線程飒泻。當工作線程與客戶通信結(jié)束,這個線程就被銷毀吏廉。這種實現(xiàn)方式有以下不足之處:
- 服務(wù)器創(chuàng)建和銷毀工作線程的開銷(包括所花費的時間和系統(tǒng)資源)很大泞遗。如果服務(wù)器需要與許多客戶通信,并且與每個客戶的通信時間都很短席覆,那么有可能服務(wù)器為客戶創(chuàng)建新線程的開銷比實際與客戶通信的開銷還要大史辙。
- 除了創(chuàng)建和銷毀線程的開銷之外,活動的線程也消耗系統(tǒng)資源佩伤。每個線程本身都會占用一定的內(nèi)存(每個線程需要大約1M內(nèi)存)聊倔,如果同時有大量客戶連接服務(wù)器,就必須創(chuàng)建大量工作線程畦戒,它們消耗了大量內(nèi)存方库,可能會導(dǎo)致系統(tǒng)的內(nèi)存空間不足结序。
- 如果線程數(shù)目固定障斋,并且每個線程都有很長的生命周期,那么線程切換也是相對固定的徐鹤。不同操作系統(tǒng)有不同的切換周期垃环,一般在20毫秒左右。這里所說的線程切換是指在Java虛擬機返敬,以及底層操作系統(tǒng)的調(diào)度下遂庄,線程之間轉(zhuǎn)讓CPU的使用權(quán)。如果頻繁創(chuàng)建和銷毀線程劲赠,那么將導(dǎo)致頻繁地切換線程涛目,因為一個線程被銷毀后秸谢,必然要把CPU轉(zhuǎn)讓給另一個已經(jīng)就緒的線程,使該線程獲得運行機會霹肝。在這種情況下估蹄,線程之間的切換不再遵循系統(tǒng)的固定切換周期,切換線程的開銷甚至比創(chuàng)建及銷毀線程的開銷還大沫换。
線程池為線程生命周期開銷問題和系統(tǒng)資源不足問題提供了解決方案臭蚁。線程池中預(yù)先創(chuàng)建了一些工作線程,它們不斷從工作隊列中取出任務(wù)讯赏,然后執(zhí)行該任務(wù)垮兑。當工作線程執(zhí)行完一個任務(wù)時,就會繼續(xù)執(zhí)行工作隊列中的下一個任務(wù)漱挎。線程池具有以下優(yōu)點
- 減少了創(chuàng)建和銷毀線程的次數(shù)系枪,每個工作線程都可以一直被重用,能執(zhí)行多個任務(wù)识樱。
- 可以根據(jù)系統(tǒng)的承載能力嗤无,方便地調(diào)整線程池中線程的數(shù)目,防止因為消耗過量系統(tǒng)資源而導(dǎo)致系統(tǒng)崩潰怜庸。
6.3当犯、使用JDK類庫提供的線程池
java.util.concurrent包提供了現(xiàn)成的線程池的實現(xiàn),其比自己實現(xiàn)的線程池更加健壯割疾,且功能也更加強大嚎卫。
Executor接口表示線程池,它的execute(Runnable task)方法用來執(zhí)行Runnable類型的任務(wù)宏榕。Executor的子接口ExecutorService中聲明了管理線程池的一些方法拓诸,比如用于關(guān)閉線程池的shutdown()方法等。Executors類中包含一些靜態(tài)方法麻昼,它們負責生成各種類型的線程池ExecutorService實例奠支。
6.4、使用線程池注意事項
雖然線程池能大大提高服務(wù)器的并發(fā)性能抚芦,但使用它也會存在一定風險倍谜。與所有多線程應(yīng)用程序一樣,用線程池構(gòu)建的應(yīng)用程序容易產(chǎn)生各種并發(fā)問題叉抡,如對共享資源的競爭和死鎖尔崔。此外,如果線程池本身的實現(xiàn)不健壯褥民,或者沒有合理地使用線程池季春,還容易導(dǎo)致與線程池有關(guān)的死鎖、系統(tǒng)資源不足和線程泄漏等問題消返。
6.4.1载弄、死鎖
任何多線程應(yīng)用程序都有死鎖風險耘拇。造成死鎖的最簡單的情形是,線程A持有對象X的鎖宇攻,并且在等待對象Y的鎖驼鞭,而線程B持有對象Y的鎖,并且在等待對象X的鎖尺碰。線程A與線程B都不釋放自己持有的鎖挣棕,并且等待對方的鎖,這就導(dǎo)致兩個線程永遠等待下去亲桥,死鎖就這樣產(chǎn)生了洛心。
雖然任何多線程程序都有死鎖的風險,但線程池還會導(dǎo)致另外一種死鎖题篷。在這種情形下词身,假定線程池中的所有工作線程都在執(zhí)行各自任務(wù)時被阻塞,它們都在等待某個任務(wù)A的執(zhí)行結(jié)果番枚。而任務(wù)A依然在工作隊列中法严,由于沒有空閑線程,使得任務(wù)A一直不能被執(zhí)行葫笼。這使得線程池中的所有工作線程都永遠阻塞下去深啤,死鎖就這樣產(chǎn)生了。
6.4.2路星、系統(tǒng)資源不足
如果線程池中的線程數(shù)目非常多溯街,這些線程會消耗包括內(nèi)存和其他系統(tǒng)資源在內(nèi)的大量資源,從而嚴重影響系統(tǒng)性能洋丐。
6.4.3.并發(fā)錯誤
線程池的工作隊列依靠wait()和notify()方法來使工作線程及時取得任務(wù)呈昔,但這兩個方法都難于使用。
如果編碼不正確友绝,可能會丟失通知堤尾,導(dǎo)致工作線程一直保持空閑狀態(tài),無視工作隊列中需要處理的任務(wù)迁客。因此使用這些方法時郭宝,必須格外小心,即便是專家也可能在這方面出錯哲泊。最好使用現(xiàn)有的剩蟀、比較成熟的線程池催蝗。例如切威,直接使用java.util.concurrent包中的線程池類。
6.4.4.線程泄漏
使用線程池的一個嚴重風險是線程泄漏丙号。對于工作線程數(shù)目固定的線程池先朦,如果工作線程在執(zhí)行任務(wù)時拋出RuntimeException 或Error缰冤,并且這些異常或錯誤沒有被捕獲喳魏,那么這個工作線程就會異常終止棉浸,使得線程池永久失去了一個工作線程。如果所有的工作線程都異常終止刺彩,線程池就最終變?yōu)榭彰灾#瑳]有任何可用的工作線程來處理任務(wù)。
導(dǎo)致線程泄漏的另一種情形是创倔,工作線程在執(zhí)行一個任務(wù)時被阻塞嗡害,如等待用戶的輸入數(shù)據(jù),但是由于用戶一直不輸入數(shù)據(jù)(可能是因為用戶走開了)畦攘,導(dǎo)致這個工作線程一直被阻塞霸妹。這樣的工作線程名存實亡,它實際上不執(zhí)行任何任務(wù)了知押。假如線程池中所有的工作線程都處于這樣的阻塞狀態(tài)叹螟,那么線程池就無法處理新加入的任務(wù)了。
6.4.5.任務(wù)過載
當工作隊列中有大量排隊等候執(zhí)行的任務(wù)時台盯,這些任務(wù)本身可能會消耗太多的系統(tǒng)資源而引起系統(tǒng)資源缺乏罢绽。
綜上所述,線程池可能會帶來種種風險静盅,為了盡可能避免它們有缆,使用線程池時需要遵循以下原則。
(1)如果任務(wù)A在執(zhí)行過程中需要同步等待任務(wù)B的執(zhí)行結(jié)果温亲,那么任務(wù)A不適合加入到線程池的工作隊列中棚壁。如果把像任務(wù)A一樣的需要等待其他任務(wù)執(zhí)行結(jié)果的任務(wù)加入到工作隊列中,可能會導(dǎo)致線程池的死鎖栈虚。
(2)如果執(zhí)行某個任務(wù)時可能會阻塞袖外,并且是長時間的阻塞,則應(yīng)該設(shè)定超時時間魂务,避免工作線程永久的阻塞下去而導(dǎo)致線程泄漏曼验。在服務(wù)器程序中,當線程等待客戶連接粘姜,或者等待客戶發(fā)送的數(shù)據(jù)時鬓照,都可能會阻塞」陆簦可以通過以下方式設(shè)定超時時間:
◆調(diào)用ServerSocket的setSoTimeout(int timeout)方法豺裆,設(shè)定等待客戶連接的超時時間;
◆對于每個與客戶連接的Socket,調(diào)用該Socket的setSoTimeout(int timeout)方法臭猜,設(shè)定等待客戶發(fā)送數(shù)據(jù)的超時時間躺酒。
(3)了解任務(wù)的特點,分析任務(wù)是執(zhí)行經(jīng)常會阻塞的I/O操作蔑歌,還是執(zhí)行一直不會阻塞的運算操作羹应。前者時斷時續(xù)地占用CPU,而后者對CPU具有更高的利用率次屠。預(yù)計完成任務(wù)大概需要多長時間园匹?是短時間任務(wù)還是長時間任務(wù)?
根據(jù)任務(wù)的特點劫灶,對任務(wù)進行分類偎肃,然后把不同類型的任務(wù)分別加入到不同線程池的工作隊列中,這樣可以根據(jù)任務(wù)的特點浑此,分別調(diào)整每個線程池累颂。
(4)調(diào)整線程池的大小。線程池的最佳大小主要取決于系統(tǒng)的可用CPU的數(shù)目凛俱,以及工作隊列中任務(wù)的特點紊馏。假如在一個具有 N 個CPU的系統(tǒng)上只有一個工作隊列,并且其中全部是運算性質(zhì)(不會阻塞)的任務(wù)蒲犬,那么當線程池具有 N 或 N+1 個工作線程時朱监,一般會獲得最大的 CPU 利用率。
如果工作隊列中包含會執(zhí)行I/O操作并常常阻塞的任務(wù)原叮,則要讓線程池的大小超過可用CPU的數(shù)目赫编,因為并不是所有工作線程都一直在工作。選擇一個典型的任務(wù)奋隶,然后估計在執(zhí)行這個任務(wù)的過程中擂送,等待時間(WT)與實際占用CPU進行運算的時間(ST)之間的比例WT/ST。對于一個具有N個CPU的系統(tǒng)唯欣,需要設(shè)置大約N×(1+WT/ST)個線程來保證CPU得到充分利用嘹吨。
當然,CPU利用率不是調(diào)整線程池大小過程中唯一要考慮的事項境氢。隨著線程池中工作線程數(shù)目的增長蟀拷,還會碰到內(nèi)存或者其他系統(tǒng)資源的限制,如套接字萍聊、打開的文件句柄或數(shù)據(jù)庫連接數(shù)目等问芬。要保證多線程消耗的系統(tǒng)資源在系統(tǒng)的承載范圍之內(nèi)。
(5)避免任務(wù)過載寿桨。服務(wù)器應(yīng)根據(jù)系統(tǒng)的承載能力此衅,限制客戶并發(fā)連接的數(shù)目印蔬。當客戶并發(fā)連接的數(shù)目超過了限制值挖诸,服務(wù)器可以拒絕連接請求驮捍,并友好地告知客戶:服務(wù)器正忙叁温,請稍后再試漠畜。
相關(guān)閱讀:
Socket詳解 【http://www.reibang.com/p/5a294e08efbc】
參考博客:
http://expert.51cto.com/art/200702/40196_all.htm
https://blog.csdn.net/lin49940/article/details/4398364
參考書籍:
孫衛(wèi)琴 《java網(wǎng)絡(luò)編程精解》
代碼示例:
https://github.com/zhaozhou11/java-io.git
com.zhaozhou.demo.serversocket包