ServerSocket詳解


在客戶/服務器通信模式中,服務器端需要創建監聽特定端口的ServerSocket,ServerSocket負責接收客戶連接請求,並生成與客戶端連接的Socket。

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地址。

1.1 、綁定端口

除不帶參數的構造方法以外,其他構造方法都會使服務器與特定端口綁定,該端口由參數port指定。如果端口被其他服務進程占用,或是,在某些系統中,若沒有以超級用戶身份運行服務器程序,操作系統不允許服務器綁定到1-1023的端口時,會拋出BindException。

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

當服務器進程運行時,可能會同時監聽到多個客戶的連接請求。管理客戶端連接請求的任務是由操作系統來完成的。操作系統將連接請求存儲在一個先進先出隊列中。許多操作系統限定了隊列的最大長度,一般為50。當隊列中的連接請求達到了隊列的最大容量時,服務器進程所在的主機會拒絕新的連接請求。只有當服務器進程通過ServerSocket的accept()方法從隊列中取出連接請求,使隊列騰出空位時,隊列才能繼續加入新的連接請求。

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

ServerSocket構造方法的backlog參數用來顯式設置連接請求隊列的長度,它將覆蓋操作系統限定的隊列的最大長度。

在一下集中情況,仍然采用操作系統限定的隊列最大長度:

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

1.3、設定綁定的IP地址

若主機只有一個地址,則服務器默認綁定該地址;若主機有多個地址,則可以調用ServerSocket(int port, int backlog, InetAddress bindAddr)構造方法設置主機ip地址。

1.4、默認構造方法的作用

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

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

2、接收和關閉與客戶的連接

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

服務器從Socket對象中獲得輸入流和輸出流

,就能與客戶交換數據。當服務器正在進行發送數據的操作時,如果客戶端斷開了連接,那么服務器端會拋出一個IOException的子類SocketException異常:java.net.SocketException: Connection reset by peer。

3、關閉ServerSocket

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

在某些情況下,如果希望及時釋放服務器的端口,以便讓其他程序能占用該端口,則可以顯式調用ServerSocket的close()方法。

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

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

4、獲取ServerSocket的信息

  • public InetAddress getInetAddress():獲取服務器綁定的ip地址;
  • public int getLocalPort():獲取服務器綁定的端口;

在構造ServerSocket時,如果把端口設為0,那么將由操作系統為服務器分配一個端口(稱為匿名端口),程序只要調用getLocalPort()方法就能獲知這個端口號。多數服務器會監聽固定的端口,這樣才便於客戶程序訪問服務器。匿名端口一般適用於服務器與客戶之間的臨時通信,通信結束,就斷開連接,並且ServerSocket占用的臨時端口也被釋放。

5、ServerSocket選項

ServerSocket有以下3個選項。

  • SO_TIMEOUT:表示等待客戶連接的超時時間。
  • SO_REUSEADDR:表示是否允許重用服務器所綁定的地址。
  • SO_RCVBUF:表示接收數據的緩沖區的大小。

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的子類。

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。

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

5.3、SO_RCVBUF選項

  • 設置該選項:public void setReceiveBufferSize(int size) throws SocketException
  • 讀取該選項:public int getReceiveBufferSize() throws SocketException

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

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

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

該方法的作用與Socket的setPerformancePreferences()方法的作用相同,用於設定連接時間、延遲和帶寬的相對重要性。

6、創建多線程服務器

許多實際應用要求服務器具有同時為多個客戶提供服務的能力。HTTP服務器就是最明顯的例子。任何時刻,HTTP服務器都可能接收到大量的客戶請求,每個客戶都希望能快速得到HTTP服務器的響應。如果長時間讓客戶等待,會使網站失去信譽,從而降低訪問量。

可以用並發性能來衡量一個服務器同時響應多個客戶的能力。一個具有好的並發性能的服務器,必須符合兩個條件:

  • 能同時接收並處理多個客戶連接;
  • 對於每個客戶,都會迅速給予響應。

用多個線程來同時為多個客戶提供服務,這是提高服務器的並發性能的最常用的手段。

以下將按照3中方式來實現EchoServer,它們都使用多線程。

  • 為每個客戶分配一個工作線程。
  • 創建一個線程池,由其中的工作線程來為客戶服務。
  • 利用JDK的Java類庫中現成的線程池,由它的工作線程來為客戶服務。

6.1、 為每個客戶分配一個線程

服務器的主線程負責接收客戶的連接,每次接收到一個客戶連接,就會創建一個工作線程,由它負責與客戶的通信。

代碼示例:

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

以上工作線程執行HandlerServer的run()方法,其負責與單個客戶端通信,通信完畢后斷開連接,線程自然終止。

6.2、創建線程池

對每個客戶都分配一個新的工作線程。當工作線程與客戶通信結束,這個線程就被銷毀。這種實現方式有以下不足之處:

  • 服務器創建和銷毀工作線程的開銷(包括所花費的時間和系統資源)很大。如果服務器需要與許多客戶通信,並且與每個客戶的通信時間都很短,那么有可能服務器為客戶創建新線程的開銷比實際與客戶通信的開銷還要大。
  • 除了創建和銷毀線程的開銷之外,活動的線程也消耗系統資源。每個線程本身都會占用一定的內存(每個線程需要大約1M內存),如果同時有大量客戶連接服務器,就必須創建大量工作線程,它們消耗了大量內存,可能會導致系統的內存空間不足。
  • 如果線程數目固定,並且每個線程都有很長的生命周期,那么線程切換也是相對固定的。不同操作系統有不同的切換周期,一般在20毫秒左右。這里所說的線程切換是指在Java虛擬機,以及底層操作系統的調度下,線程之間轉讓CPU的使用權。如果頻繁創建和銷毀線程,那么將導致頻繁地切換線程,因為一個線程被銷毀后,必然要把CPU轉讓給另一個已經就緒的線程,使該線程獲得運行機會。在這種情況下,線程之間的切換不再遵循系統的固定切換周期,切換線程的開銷甚至比創建及銷毀線程的開銷還大。

線程池為線程生命周期開銷問題和系統資源不足問題提供了解決方案。線程池中預先創建了一些工作線程,它們不斷從工作隊列中取出任務,然后執行該任務。當工作線程執行完一個任務時,就會繼續執行工作隊列中的下一個任務。線程池具有以下優點

  • 減少了創建和銷毀線程的次數,每個工作線程都可以一直被重用,能執行多個任務。
  • 可以根據系統的承載能力,方便地調整線程池中線程的數目,防止因為消耗過量系統資源而導致系統崩潰。

6.3、使用JDK類庫提供的線程池

java.util.concurrent包提供了現成的線程池的實現,其比自己實現的線程池更加健壯,且功能也更加強大。

 
Executor接口繼承圖.png

Executor接口表示線程池,它的execute(Runnable task)方法用來執行Runnable類型的任務。Executor的子接口ExecutorService中聲明了管理線程池的一些方法,比如用於關閉線程池的shutdown()方法等。Executors類中包含一些靜態方法,它們負責生成各種類型的線程池ExecutorService實例。


 
Executors類的靜態方法.png

6.4、使用線程池注意事項

雖然線程池能大大提高服務器的並發性能,但使用它也會存在一定風險。與所有多線程應用程序一樣,用線程池構建的應用程序容易產生各種並發問題,如對共享資源的競爭和死鎖。此外,如果線程池本身的實現不健壯,或者沒有合理地使用線程池,還容易導致與線程池有關的死鎖、系統資源不足和線程泄漏等問題。

6.4.1、死鎖

任何多線程應用程序都有死鎖風險。造成死鎖的最簡單的情形是,線程A持有對象X的鎖,並且在等待對象Y的鎖,而線程B持有對象Y的鎖,並且在等待對象X的鎖。線程A與線程B都不釋放自己持有的鎖,並且等待對方的鎖,這就導致兩個線程永遠等待下去,死鎖就這樣產生了。

雖然任何多線程程序都有死鎖的風險,但線程池還會導致另外一種死鎖。在這種情形下,假定線程池中的所有工作線程都在執行各自任務時被阻塞,它們都在等待某個任務A的執行結果。而任務A依然在工作隊列中,由於沒有空閑線程,使得任務A一直不能被執行。這使得線程池中的所有工作線程都永遠阻塞下去,死鎖就這樣產生了。

6.4.2、系統資源不足

如果線程池中的線程數目非常多,這些線程會消耗包括內存和其他系統資源在內的大量資源,從而嚴重影響系統性能。

6.4.3.並發錯誤

線程池的工作隊列依靠wait()和notify()方法來使工作線程及時取得任務,但這兩個方法都難於使用。

如果編碼不正確,可能會丟失通知,導致工作線程一直保持空閑狀態,無視工作隊列中需要處理的任務。因此使用這些方法時,必須格外小心,即便是專家也可能在這方面出錯。最好使用現有的、比較成熟的線程池。例如,直接使用java.util.concurrent包中的線程池類。

6.4.4.線程泄漏

使用線程池的一個嚴重風險是線程泄漏。對於工作線程數目固定的線程池,如果工作線程在執行任務時拋出RuntimeException 或Error,並且這些異常或錯誤沒有被捕獲,那么這個工作線程就會異常終止,使得線程池永久失去了一個工作線程。如果所有的工作線程都異常終止,線程池就最終變為空,沒有任何可用的工作線程來處理任務。

導致線程泄漏的另一種情形是,工作線程在執行一個任務時被阻塞,如等待用戶的輸入數據,但是由於用戶一直不輸入數據(可能是因為用戶走開了),導致這個工作線程一直被阻塞。這樣的工作線程名存實亡,它實際上不執行任何任務了。假如線程池中所有的工作線程都處於這樣的阻塞狀態,那么線程池就無法處理新加入的任務了。

6.4.5.任務過載

當工作隊列中有大量排隊等候執行的任務時,這些任務本身可能會消耗太多的系統資源而引起系統資源缺乏。

綜上所述,線程池可能會帶來種種風險,為了盡可能避免它們,使用線程池時需要遵循以下原則。

(1)如果任務A在執行過程中需要同步等待任務B的執行結果,那么任務A不適合加入到線程池的工作隊列中。如果把像任務A一樣的需要等待其他任務執行結果的任務加入到工作隊列中,可能會導致線程池的死鎖。

(2)如果執行某個任務時可能會阻塞,並且是長時間的阻塞,則應該設定超時時間,避免工作線程永久的阻塞下去而導致線程泄漏。在服務器程序中,當線程等待客戶連接,或者等待客戶發送的數據時,都可能會阻塞。可以通過以下方式設定超時時間:

◆調用ServerSocket的setSoTimeout(int timeout)方法,設定等待客戶連接的超時時間;

◆對於每個與客戶連接的Socket,調用該Socket的setSoTimeout(int timeout)方法,設定等待客戶發送數據的超時時間。

(3)了解任務的特點,分析任務是執行經常會阻塞的I/O操作,還是執行一直不會阻塞的運算操作。前者時斷時續地占用CPU,而后者對CPU具有更高的利用率。預計完成任務大概需要多長時間?是短時間任務還是長時間任務?

根據任務的特點,對任務進行分類,然后把不同類型的任務分別加入到不同線程池的工作隊列中,這樣可以根據任務的特點,分別調整每個線程池。

(4)調整線程池的大小。線程池的最佳大小主要取決於系統的可用CPU的數目,以及工作隊列中任務的特點。假如在一個具有 N 個CPU的系統上只有一個工作隊列,並且其中全部是運算性質(不會阻塞)的任務,那么當線程池具有 N 或 N+1 個工作線程時,一般會獲得最大的 CPU 利用率。

如果工作隊列中包含會執行I/O操作並常常阻塞的任務,則要讓線程池的大小超過可用CPU的數目,因為並不是所有工作線程都一直在工作。選擇一個典型的任務,然后估計在執行這個任務的過程中,等待時間(WT)與實際占用CPU進行運算的時間(ST)之間的比例WT/ST。對於一個具有N個CPU的系統,需要設置大約N×(1+WT/ST)個線程來保證CPU得到充分利用。

當然,CPU利用率不是調整線程池大小過程中唯一要考慮的事項。隨着線程池中工作線程數目的增長,還會碰到內存或者其他系統資源的限制,如套接字、打開的文件句柄或數據庫連接數目等。要保證多線程消耗的系統資源在系統的承載范圍之內。

(5)避免任務過載。服務器應根據系統的承載能力,限制客戶並發連接的數目。當客戶並發連接的數目超過了限制值,服務器可以拒絕連接請求,並友好地告知客戶:服務器正忙,請稍后再試。

相關閱讀:
Socket詳解 【https://www.jianshu.com/p/5a294e08efbc

參考博客:

http://expert.51cto.com/art/200702/40196_all.htm

https://blog.csdn.net/lin49940/article/details/4398364

參考書籍:

孫衛琴 《java網絡編程精解》

代碼示例:

https://github.com/zhaozhou11/java-io.git

com.zhaozhou.demo.serversocket包

 
 
21人點贊
 
 


作者:橋頭放牛娃
鏈接:https://www.jianshu.com/p/665994c2e784
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM