天天看點

java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)

java ServerSocket 用法詳解(一)

分類: java 通信 2015-03-07 21:49  7人閱讀  評論(0)  收藏  舉報

 本篇文章觀點和例子來自 《Java網絡程式設計精解》, 作者為孫衛琴, 出版社為電子工業出版社。

      在客戶/伺服器通信模式中, 伺服器端需要建立監聽端口的 ServerSocket, ServerSocket 負責接收客戶連接配接請求. 本章首先介紹 ServerSocket 類的各個構造方法, 以及成員的用法, 接着介紹伺服器如何用多線程來處理與多個客戶的通信任務.

      本章提供線程池的一種實作方法. 線程池包括一個工作隊列和若幹工作線程. 伺服器程式向工作隊列中加入與客戶通信的任務, 工作線程不斷從工作隊列中取出任務并執行它. 本章還介紹了 java.util.concurrent 包中的線程池類的用法, 在伺服器程式中可以直接使用他們.

 一. 構造 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 指定. 例如, 以下代碼建立了一個與 80 端口綁定的伺服器:

[java]  view plain copy print ?

java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
  1. ServerSocket serverSocket = new ServerSocket(80);      

      如果運作時無法綁定到 80 端口, 以上代碼會抛出 IOException, 更确切地說, 是抛出 BindException, 它是 IOException 的子類. BindException 一般是由以下原因造成的:

  • 端口已經被其他伺服器程序占用;
  • 在某些作業系統中, 如果沒有以超級使用者的身份來運作伺服器程式, 那麼作業系統不允許伺服器綁定到 1-1023 之間的端口.

      如果把參數 port 設為 0, 表示由作業系統來為伺服器配置設定一個任意可用的端口. 有作業系統配置設定的端口也稱為匿名端口. 對于多數伺服器, 會使用明确的端口, 而不會使用匿名端口, 因為客戶程式需要事先知道伺服器的端口, 才能友善地通路伺服器. 在某些場合, 匿名端口有着特殊的用途, 本章 四 會對此作介紹.

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

      當伺服器程序運作時, 可能會同時監聽到多個客戶的連接配接請求. 例如, 每當一個客戶程序執行以下代碼:

[java]  view plain copy print ?

java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
  1. Socket socket = new Socket("www.javathinker.org", 80);      

      就意味着在遠端 www.javathinker.org 主機的 80 端口上, 監聽到了一個客戶的連接配接請求. 管理客戶連接配接請求的任務是由作業系統來完成的. 作業系統把這些連接配接請求存儲在一個先進先出的隊列中. 許多作業系統限定了隊列的最大長度, 一般為 50 . 當隊列中的連接配接請求達到了隊列的最大容量時, 伺服器程序所在的主機會拒絕新的連接配接請求. 隻有當伺服器程序通過 ServerSocket 的 accept() 方法從隊列中取出連接配接請求, 使隊列騰出空位時, 隊列才能繼續加入新的連接配接請求.

      對于客戶程序, 如果它發出的連接配接請求被加入到伺服器的請求連接配接隊列中, 就意味着客戶與伺服器的連接配接建立成功, 客戶程序從 Socket 構造方法中正常傳回. 如果客戶程序發出的連接配接請求被伺服器拒絕, Socket 構造方法就會抛出 ConnectionException.

Tips: 建立綁定端口的伺服器程序後, 當客戶程序的 Socket構造方法傳回成功, 表示客戶程序的連接配接請求被加入到伺服器程序的請求連接配接隊列中. 雖然用戶端成功傳回 Socket對象, 但是還沒跟伺服器程序形成一條通信線路. 必須在伺服器程序通過 ServerSocket 的 accept() 方法從請求連接配接隊列中取出連接配接請求, 并傳回一個Socket 對象後, 伺服器程序這個Socket 對象才與用戶端的 Socket 對象形成一條通信線路.

      ServerSocket 構造方法的 backlog 參數用來顯式設定連接配接請求隊列的長度, 它将覆寫作業系統限定的隊列的最大長度. 值得注意的是, 在以下幾種情況中, 仍然會采用作業系統限定的隊列的最大長度:

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

      以下的 Client.java 和 Server.java 用來示範伺服器的連接配接請求隊列的特性.

      Client.java 略..... , Server.java 略......

      Client 試圖與 Server 進行 100 次連接配接. 在 Server 類中, 把連接配接請求隊列的長度設為 3. 這意味着當隊列中有了 3 個連接配接請求時, 如果Client 再請求連接配接, 就會被 Server 拒絕.  下面按照以下步驟運作 Server 和 Client 程式.

      ⑴ 在Server 中隻建立一個 ServerSocket 對象, 在構造方法中指定監聽的端口為8000 和 連接配接請求隊列的長度為 3 . 構造 Server 對象後, Server 程式睡眠 10 分鐘, 并且在 Server 中不執行 serverSocket.accept() 方法. 這意味着隊列中的連接配接請求永遠不會被取出. 運作Server 程式和 Client 程式後, Client程式的列印結果如下:

 第 1 次連接配接成功

 第 2 次連接配接成功

 第 3 次連接配接成功

 Exception in thread "main" java.net.ConnectException: Connection refused: connect

 ................

      從以上列印的結果可以看出, Client 與 Server 在成功地建立了3 個連接配接後, 就無法再建立其餘的連接配接了, 因為伺服器的隊已經滿了.

      ⑵ 在Server中構造一個跟 ⑴ 相同的 ServerSocket對象, Server程式不睡眠, 在一個 while 循環中不斷執行 serverSocket.accept()方法, 該方法從隊列中取出連接配接請求, 使得隊列能及時騰出空位, 以容納新的連接配接請求. Client 程式的列印結果如下:

  第 1 次連接配接成功

  第 2 次連接配接成功

  第 3 次連接配接成功

  ...........

  第 100 次連接配接成功

      從以上列印結果可以看出, 此時 Client 能順利與 Server 建立 100 次連接配接.(每次while的循環要夠快才行, 如果太慢, 從隊列取連接配接請求的速度比放連接配接請求的速度慢的話, 不一定都能成功連接配接) 

1.3 設定綁定的IP 位址

      如果主機隻有一個IP 位址, 那麼預設情況下, 伺服器程式就與該IP 位址綁定. ServerSocket 的第 4 個構造方法 ServerSocket(int port, int backlog, InetAddress bingAddr) 有一個 bindAddr 參數, 它顯式指定伺服器要綁定的IP 位址, 該構造方法适用于具有多個IP 位址的主機. 假定一個主機有兩個網卡, 一個網卡用于連接配接到 Internet, IP為 222.67.5.94, 還有一個網卡用于連接配接到本地區域網路, IP 位址為 192.168.3.4. 如果伺服器僅僅被本地區域網路中的客戶通路, 那麼可以按如下方式建立 ServerSocket:

[java]  view plain copy print ?

java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
  1. ServerSocket serverSocket = new ServerSocket(8000, 10, InetAddress.getByName("192.168.3.4"));  

1.4 預設構造方法的作用

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

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

      在以下代碼中, 先把 ServerSocket 的 SO_REUSEADDR 選項設為 true, 然後再把它與 8000 端口綁定:

[java]  view plain copy print ?

java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
  1. ServerSocket serverSocket = new ServerSocket();  
  2.  serverSocket.setReuseAddress(true);              //設定 ServerSocket 的選項  
  3.  serverSocket.bind(new InetSocketAddress(8000));  //與8000端口綁定  

      如果把以上程式代碼改為:

[java]  view plain copy print ?

java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
  1. ServerSocket serverSocket = new ServerSocket(8000);  
  2.   serverSocket.setReuseAddress(true);              //設定 ServerSocket 的選項  

      那麼 serverSocket.setReuseAddress(true) 方法就不起任何作用了, 因為 SO_REUSEADDR 選項必須在伺服器綁定端口之前設定才有效.

二. 接收和關閉與客戶的連接配接

      ServerSocket 的 accept() 方法從連接配接請求隊列中取出一個客戶的連接配接請求, 然後建立與客戶連接配接的 Socket 對象, 并将它傳回. 如果隊列中沒有連接配接請求, accept() 方法就會一直等待, 直到接收到了連接配接請求才傳回.

      接下來, 伺服器從 Socket 對象中獲得輸入流和輸出流, 就能與客戶交換資料. 當伺服器正在進行發送資料的操作時, 如果用戶端斷開了連接配接, 那麼伺服器端會抛出一個IOException 的子類 SocketException 異常:

[java]  view plain copy print ?

java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
  1. java.net.SocketException: Connection reset by peer      

      這隻是伺服器與單個客戶通信中出現的異常, 這種異常應該被捕獲, 使得伺服器能繼續與其他客戶通信.

      以下程式顯示了單線程伺服器采用的通信流程:

[java]  view plain copy print ?

java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
  1. public void service()  
  2. {  
  3.  while(true)  
  4.  {  
  5.      Socket socket = null;  
  6.      try  
  7.      {  
  8.          socket = serverSocket.accept();    //從連接配接請求隊列中取出一個連接配接  
  9.          System.out.println("New connection accepted "   
  10.                  + socket.getInetAddress()  + " : " + socket.getPort());  
  11.          //接收和發送資料  
  12.          ............  
  13.      }  
  14.      catch(IOException e)  
  15.      {  
  16.          //這隻是與單個客戶通信時遇到的異常, 可能是由于用戶端過早斷開連接配接引起  
  17.          //這種異常不應該中斷整個while循環  
  18.          e.printStackTrace();  
  19.      }  
  20.      finally  
  21.      {  
  22.          try  
  23.          {  
  24.              if(socket != null) socket.close();  //與一個客戶通信結束後, 要關閉Socket  
  25.          }  
  26.          catch(IOException e)  
  27.          {  
  28.              e.printStackTrace();  
  29.          }  
  30.      }  
  31.  }  
  32. }  

      與單個客戶通信的代碼放在一個try 代碼塊中, 如果遇到異常, 該異常被catch 代碼塊捕獲. try 代碼塊後面還有一個finally 代碼塊, 它保證不管與客戶通信正常結果還是異常結束, 最後都會關閉Socket, 斷開與這個客戶的連接配接.

三. 關閉ServerSocket

     ServerSocket 的 close() 方法使伺服器釋放占用的端口, 并且斷開與所有客戶的連接配接. 當一個伺服器程式運作結束時, 即使沒有執行 ServerSocket 的 close() 方法, 作業系統也會釋放這個伺服器占用的端口. 是以, 伺服器程式不一定要在結束之前執行 ServerSocket 的 close() 方法.

      在某些情況下, 如果希望及時釋放伺服器的端口, 以便讓其他程式能占用該端口, 則可以顯式調用 ServerSocket 的 close() 方法. 例如, 以下代碼用于掃描 1-65535 之間的端口号. 如果 ServerSocket 成功建立, 意味這該端口未被其他伺服器程序綁定, 否則說明該端口已經被其他程序占用:

[java]  view plain copy print ?

java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
  1. for(int port = 1; port <= 65335; port ++)  
  2. {  
  3.  try  
  4.  {  
  5.   ServerSocket serverSocket = new ServerSocket(port);  
  6.   serverSocket.close();   //及時關閉ServerSocket  
  7.  }  
  8.  catch(IOException e)  
  9.  {  
  10.   System.out.println("端口" + port + " 已經被其他伺服器程序占用");  
  11.  }     
  12. }  

     以上程式代碼建立了一個 ServerSocket 對象後, 就馬上關閉它, 以便及時釋放它占用的端口, 進而避免程式臨時占用系統的大多數端口.

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

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

      如果需要确定一個 ServerSocket 已經與特定端口綁定, 并且還沒有被關閉, 則可以采用以下方式:

[java]  view plain copy print ?

java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
  1. </pre><pre name="code" class="java">boolean isOpen = serverSocket.isBound() && !serverSocket.isClosed();    

四. 擷取ServerSocket 的資訊

     ServerSocket 的以下兩個 get 方法可以分别獲得伺服器綁定的 IP 位址, 以及綁定的端口:

  • public InetAddress getInetAddress();
  • public int getLocalPort()

     前面已經講到, 在構造 ServerSocket 時, 如果把端口設為 0 , 那麼将有作業系統為伺服器配置設定一個端口(稱為匿名端口), 程式隻要調用 getLocalPort() 方法就能獲知這個端口号. 如下面的 RandomPort 建立了一個 ServerSocket, 它使用的就是匿名端口.

       RandomPort.java 略...........

      多數伺服器會監聽固定的端口, 這樣才便于客戶程式通路伺服器. 匿名端口一般設用于伺服器與客戶之間的臨時通信, 通信結束, 就斷開連接配接, 并且 ServerSocket 占用的臨時端口也被釋放.

      FTP(檔案傳輸協定) 就使用了匿名端口.  FTP協定用于在本地檔案系統與遠端檔案系統之間傳送檔案.

      FTP 使用兩個并行的TCP 連接配接: 一個是控制連接配接, 一個是資料連接配接. 控制連接配接用于在客戶和伺服器之間發送控制資訊, 如使用者名和密碼、改變遠端目錄的指令或上傳和下載下傳檔案的指令. 資料連接配接用于傳送而檔案. TCP 伺服器在 21 端口上監聽控制連接配接, 如果有客戶要求上傳或下載下傳檔案, 就另外建立一個資料連接配接, 通過它來傳送檔案. 資料連接配接的建立有兩種方式.

      ⑴ TCP 伺服器在 20 端口上監聽資料連接配接, TCP 客戶主動請求建立與該端口的連接配接.

      ⑵ 首先由 TCP 客戶建立一個監聽匿名端口的 ServerSocket, 再把這個 ServerSocket 監聽的端口号發送給 TCP 伺服器, 然後由TCP 伺服器主動請求建立與用戶端的連接配接.

      以上第二種方式就使用了匿名端口, 并且是在用戶端使用的, 用于和伺服器建立臨時的資料連接配接. 在實際應用中, 在伺服器端也可以使用匿名端口.

五. ServerSocket 選項

      ServerSocket 有以下 3 個選項.

  • SO_TIMEOUT: 表示等待客戶連接配接的逾時時間.
  • SO_REUSEADDR: 表示是否允許重用伺服器所綁定的位址.
  • SO_RCVBUF: 表示接收資料的緩沖區的大小.

5.1 SO_TIMEOUT 選項

  • 設定該選項: public void setSoTimeout(int timeout) throws SocketException
  • 讀取該選項: public int getSoTimeout() throws SocketException

      SO_TIMEOUT 表示 ServerSocket 的 accept() 方法等待客戶連接配接的逾時時間, 以毫秒為機關. 如果SO_TIMEOUT 的值為 0 , 表示永遠不會逾時, 這是 SO_TIMEOUT 的預設值.

      當伺服器執行 ServerSocket 的 accept() 方法是, 如果連接配接請求隊列為空, 伺服器就會一直等待, 直到接收到了客戶連接配接才從 accept() 方法傳回. 如果設定了逾時時間, 那麼當伺服器等待的時間查歐哦了逾時時間, 就會抛出 SocketTimeoutException, 它是 InterruptedException 的子類.

[java]  view plain copy print ?

java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
  1. java.net.SocketTimeoutException: Accept timed out     

Tips: 伺服器執行 serverSocket.accept() 方法時, 等待客戶連接配接的過程也稱為阻塞. 本書第 4 章的第一節詳細介紹了阻塞的概念.

5.2 SO_REUSEADDR 選項

  • 設定該選項: public void setResuseAddress(boolean on) throws SocketException
  • 讀取該選項: public boolean getResuseAddress() throws SocketException

      這個選項與Socket 的選項相同, 用于決定如果網絡上仍然有資料向舊的 ServerSocket 傳輸資料, 是否允許新的 ServerSocket 綁定到與舊的 ServerSocket 同樣的端口上. SO_REUSEADDR 選項的預設值與作業系統有關, 在某些作業系統中, 允許重用端口, 而在某些作業系統中不允許重用端口.

      當 ServerSocket 關閉時, 如果網絡上還有發送到這個 ServerSocket 的資料, 這個ServerSocket 不會立即釋放本地端口, 而是會等待一段時間, 確定接收到了網絡上發送過來的延遲資料, 然後再釋放端口.

      許多伺服器程式都使用固定的端口. 當伺服器程式關閉後, 有可能它的端口還會被占用一段時間, 如果此時立刻在同一個主機上重新開機伺服器程式, 由于端口已經被占用, 使得伺服器程式無法綁定到該端口, 伺服器啟動失敗, 并抛出 BindException:

[java]  view plain copy print ?

java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
  1. java.net.BindExcetpion: Address already in use: JVM_Bind   

      為了確定一個程序關閉了 ServerSocket 後, 即使作業系統還沒釋放端口, 同一個主機上的其他程序還可以立即重用該端口, 可以調用 ServerSocket 的 setResuseAddress(true) 方法:

[java]  view plain copy print ?

java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
  1. if(!serverSocket.getReuseAddress()) serverSocket.setReuseAddress(true);     

      值得注意的是, serverSocket.setReuseAddress(true) 方法必須在 ServerSocket 還沒有綁定到一個本地端口之前調用, 否則執行 serverSocket.setReuseAddress(true) 方法無效. 此外, 兩個共用同一個端口的程序必須都調用 serverSocket.setResuseAddress(true) 方法, 才能使得一個程序關閉 ServerSocket 後, 另一個程序的 ServerSocket 還能夠立刻重用相同的端口.

5.3 SO_RCVBUF 選項

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

      SO_RCVBUF 表示伺服器端的用于接收資料的緩沖區的大小, 以位元組為機關. 一般說來, 傳輸大的連續的資料塊(基于HTTP 或 FTP 協定的資料傳輸) 可以使用較大的緩沖區, 這可以減少傳輸資料的次數, 進而提高傳輸資料的效率. 而對于互動頻繁且單次傳送數量比較小的通信(Telnet 和 網絡遊戲), 則應該采用小的緩沖區, 確定能及時把小批量的資料發送給對方.

      SO_RCVBUF 的預設值與作業系統有關. 例如, 在Windows 2000 中運作以下代碼時, 顯示 SO_RCVBUF 的預設值為 8192.

      無論在 ServerSocket綁定到特定端口之前或之後, 調用 setReceiveBufferSize() 方法都有效. 例外情況下是如果要設定大于 64 KB 的緩沖區, 則必須在 ServerSocket 綁定到特定端口之前進行設定才有效.

      執行 serverSocket.setReceiveBufferSize() 方法, 相當于對所有由 serverSocket.accept() 方法傳回的 Socket 設定接收資料的緩沖區的大小.

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

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

     該方法的作用與 Socket 的 setPerformancePreferences 方法的作用相同, 用于設定連接配接時間、延遲和帶寬的相對重要性, 參見 第二章的 5.10.

六. 建立多線程的伺服器

      在第一章的 EchoServer中, 其 service()方法負責接收客戶連接配接, 以及與客戶通信. service() 方法的處理流程, EchoServer 接收到一個客戶連接配接, 就與客戶進行通信, 通信完畢後斷開連接配接, 然後在接收下一個客戶. 假如同時有多個客戶請求連接配接, 這些客戶就必須排隊等候EchoServer 的響應. EchoServer 無法同時與多個客戶通信.

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

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

  • 能同時接收并處理多個客戶連接配接;
  • 對于每個客戶, 都會迅速給予響應.

     伺服器同時處理的客戶連接配接數目越多, 并且對每個客戶作出響應的速度越快, 就表明并發性能越高.

     用多個線程來同時為多個客戶提供服務, 這是提高伺服器的并發性能的最常用的手段. 本結将按照 3 種方式來重新實作 EchoServer, 它們都使用了多線程.

  • 為每個客戶配置設定一個工作線程
  • 建立一個線程池, 由其中的工作線程來為客戶服務.
  • 利用JDK 的 Java 類庫中現成的線程池, 有它的工作線程來為客戶服務.

6.1 為每個客戶配置設定一個線程

      伺服器的主線程負責接收客戶的連接配接, 每次接收到一個客戶連接配接, 就會建立一個工作線程, 由它負責與客戶的通信. 以下是 EchoServer  的 service() 方法的代碼:

[java]  view plain copy print ?

java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
  1. public void service()  
  2. {  
  3.  while(true)  
  4.  {  
  5.      Socket socket = null;  
  6.      try  
  7.      {  
  8.          socket = serverSocket.accept();           //接收客戶連接配接  
  9.          Thread workThread = new Thread(new Handler(socket));         //建立一個工作程序  
  10.          workThread.start();        //啟動工作程序  
  11.      }  
  12.      catch(IOException e)  
  13.      {  
  14.          e.printStackTrace();  
  15.      }  
  16.  }  
  17. }  

      以上工作線程 workThread 執行 Handler 的 run() 方法. Handler 類實作了 Runnable 接口, 它的 run() 方法負責與單個客戶通信, 與客戶通信結束後, 就會斷開連接配接, 執行 Handler 的 run() 方法的工作線程也會自然終止. 下面是 EchoServer 類及 Handler 類的源代碼.

EchoServer.java(為每個任務配置設定一個線程)

[java]  view plain copy print ?

java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
  1. package multithread1;  
  2. import java.io.BufferedReader;  
  3. import java.io.IOException;  
  4. import java.io.InputStream;  
  5. import java.io.InputStreamReader;  
  6. import java.io.OutputStream;  
  7. import java.io.PrintWriter;  
  8. import java.net.ServerSocket;  
  9. import java.net.Socket;  
  10. public class EchoServer  
  11. {  
  12.     private int port = 8000;  
  13.     private ServerSocket serverSocket;  
  14.     public EchoServer() throws IOException  
  15.     {  
  16.         serverSocket = new ServerSocket(8000);  
  17.         System.out.println("伺服器啟動");  
  18.     }  
  19.     public void service()  
  20.     {  
  21.         while (true)  
  22.         {  
  23.             Socket socket = null;  
  24.             try  
  25.             {  
  26.                 socket = serverSocket.accept(); //接收客戶連接配接  
  27.                 Thread workThread = new Thread(new Handler(socket)); //建立一個工作程序  
  28.                 workThread.start(); //啟動工作程序  
  29.             }  
  30.             catch (IOException e)  
  31.             {  
  32.                 e.printStackTrace();  
  33.             }  
  34.         }  
  35.     }  
  36.     public static void main(String[] args) throws IOException  
  37.     {  
  38.         // TODO Auto-generated method stub  
  39.         new EchoServer().service();  
  40.     }  
  41.     class Handler implements Runnable  
  42.     {  
  43.         private Socket socket;  
  44.         public Handler(Socket socket)  
  45.         {  
  46.             this.socket = socket;  
  47.         }  
  48.         private PrintWriter getWriter(Socket socket) throws IOException  
  49.         {  
  50.             OutputStream socketOut = socket.getOutputStream();  
  51.             return new PrintWriter(socketOut, true);  
  52.         }  
  53.         private BufferedReader getReader(Socket socket) throws IOException  
  54.         {  
  55.             InputStream socketIn = socket.getInputStream();  
  56.             return new BufferedReader(new InputStreamReader(socketIn));  
  57.         }  
  58.         public String echo(String msg)  
  59.         {  
  60.             return "echo:" + msg;  
  61.         }  
  62.         public void run()  
  63.         {  
  64.             // TODO Auto-generated method stub  
  65.             try  
  66.             {  
  67.                 System.out.println("New connection accepted " + socket.getInetAddress() + ":" + socket.getPort());  
  68.                 BufferedReader br = getReader(socket);  
  69.                 PrintWriter pw = getWriter(socket);  
  70.                 String msg = null;  
  71.                 while ((msg = br.readLine()) != null)  
  72.                 { //接收和發送資料, 直到通信結束  
  73.                     System.out.println(msg);  
  74.                     pw.println(echo(msg));  
  75.                     if (msg.equals("bye"))  
  76.                     {  
  77.                         break;  
  78.                     }  
  79.                 }  
  80.             }  
  81.             catch (IOException e)  
  82.             {  
  83.                 e.printStackTrace();  
  84.             }  
  85.             finally  
  86.             {  
  87.                 try  
  88.                 {  
  89.                     if (socket != null)  
  90.                         socket.close(); //斷開連接配接  
  91.                 }  
  92.                 catch (IOException e)  
  93.                 {  
  94.                 }  
  95.             }  
  96.         }  
  97.     }  
  98. }  

 6.2 建立線程池

      在 6.1 節介紹的實作方式中, 對每個客戶都配置設定一個新的工作程序. 當工作線程與客戶通信結束, 這個線程就被銷毀. 這種實作方式有以下不足之處.

  • 伺服器建立和銷毀工作線程的開銷(包括所花費的時間和系統資源) 很大. 如果伺服器需要與多個客戶通信, 并且與每個客戶的通信時間都很短, 那麼有可能伺服器為客戶建立新線程的開銷比實際與客戶通信的開銷還要大.
  • 除了建立和銷毀線程的開銷之外, 活動的線程也消耗系統資源.每個線程本書都會占用一定的記憶體(每個線程需要大約 1MB 記憶體), 如果同時有大量客戶連接配接伺服器, 就必須建立大量工作線程, 它們消耗了大量記憶體, 可能會導緻系統的記憶體空間不足.
  • 如果線程數目固定, 并且每個線程都有很長的生命周期, 那麼線程切換也是相對固定的. 不同的作業系統有不同的切換周期, 一般在 20 毫秒左右. 這裡所說的線程切換是指在 Java 虛拟機, 以及底層作業系統的排程下, 線程之間轉讓 CPU 的使用權. 如果頻繁建立和銷毀程序, 那麼将導緻頻繁地切換線程, 因為一個線程被銷毀後, 必然要把 CPU 轉讓給另一個已經就緒的線程, 使該線程獲得運作的機會. 在這種情況下, 線程之間不再遵循系統的固定切換周期, 切換程序的開銷甚至比建立及銷毀線程的開銷還大.

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

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

     下面的 ThreadPool 類提供了線程池的一種實作方案.

        ThreadPool.java

[java]  view plain copy print ?

java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
java ServerSocket 用法詳解(一) java ServerSocket 用法詳解(一)
  1. package multithread2;  
  2. import java.util.LinkedList;  
  3. public class ThreadPool extends ThreadGroup  
  4. {  
  5.     private boolean isClosed = false; //線程池是否關閉  
  6.     private LinkedList<Runnable> workQueue; //工作隊列  
  7.     private static int threadPoolID; //表示線程池ID  
  8.     private int threadID; //表示工作線程ID ,因為 WorkThread是内部類是以才可以這樣  
  9.     public ThreadPool(int poolSize)  
  10.     { //poolSize指定線程池中的工作線程數目  
  11.         super("ThreadPool-" + (threadPoolID++));  
  12.         this.setDaemon(true);  
  13.         workQueue = new LinkedList<Runnable>(); //建立工作隊列  
  14.         for (int i = 0; i < poolSize; i++)  
  15.         {  
  16.             new WorkThread().start(); //建立并啟動工作程序  
  17.         }  
  18.     }  
  19.     public WorkThread getWorkThread()  
  20.     {  
  21.         return new WorkThread();  
  22.     }  
  23.     public synchronized void execute(Runnable task)  
  24.     {  
  25.         if (isClosed)  
  26.         { //線程池被關閉則抛出IllegalStateException 異常  
  27.             throw new IllegalStateException();  
  28.         }  
  29.         if (task != null)  
  30.         {  
  31.             workQueue.add(task); //添加任務到工作隊列中  
  32.             notify(); //呼醒正在getTask()方法中等待任務的某一個工作線程,哪一個是随機的  
  33.         }  
  34.     }  
  35.     protected synchronized Runnable getTask() throws InterruptedException  
  36.     {  
  37.         while (workQueue.size() == 0)  
  38.         {  
  39.             if (isClosed)  
  40.                 return null;  
  41.             wait(); //如果工作隊列中沒有任務, 就等待任務, 對應execute()方法中的notify和join()方法中的notifyAll()  
  42.         }  
  43.         return workQueue.removeFirst();  
  44.     }  
  45.     public synchronized void close()  
  46.     {  
  47.         if (!isClosed)  
  48.         {  
  49.             isClosed = true;  
  50.             workQueue.clear(); //清空工作隊列  
  51.             interrupt(); //中斷所有的的工作線程, 該方法繼承自 ThreadGroup 類  
  52.         }  
  53.     }  
  54.     public void join()  
  55.     {  
  56.         synchronized (this)  
  57.         {  
  58.             isClosed = true;  
  59.             notifyAll(); //呼醒所有在getTask()方法中等待任務的工作線程  
  60.         }  
  61.         Thread[] threads = new Thread[this.activeCount()];  
  62.         //enumerate()方法繼承自 ThreadGroup 類, 獲得線程組中目前所有活着的工作程序 ,并把這些線程放到指定的Thread數組中  
  63.         int count = this.enumerate(threads);  
  64.         for (int i = 0; i < count; i++)  
  65.         { //等待所有工作線程運作結束  
  66.             try  
  67.             {  
  68.                 threads[i].join(); //等待工作程序運作結束  
  69.             }  
  70.             catch (InterruptedException ex)  
  71.             {  
  72.             }  
  73.         }  
  74.     }  
  75.     private class WorkThread extends Thread  
  76.     {  
  77.         public WorkThread()  
  78.         {  
  79.             // 加入到目前 ThreadPool 線程當中  
  80.             super(ThreadPool.this, "WorkThread-" + (threadID++));  
  81.         }  
  82.         public void run()  
  83.         {  
  84.             while (!isInterrupted())  
  85.             { //isInterrupted() 方法繼承自 Thread 類, 判斷線程是否被中斷  
  86.                 Runnable task = null;  
  87.                 try  
  88.                 {  
  89.                     task = getTask(); //取出任務  
  90.                 }  
  91.                 catch (InterruptedException e)  
  92.                 {  
  93.                 }  
  94.                 //如果getTask() 傳回null, 表示線程池已經被關閉了, 結束此線程  
  95.                 if (task == null)  
  96.                     return;  
  97.                 try  
  98.                 {  
  99.                     task.run();  
  100.                 }  
  101.                 catch (Throwable t)  
  102.                 { //捕捉線程run()運作中産生所有的錯誤和異常,為了防止程序被結束  
  103.                     t.printStackTrace();  
  104.                 }  
  105.             }//while  
  106.         }//run  
  107.     }//WorkThread  
  108. }  

繼續閱讀