1. 程式人生 > >ServerSocket 用法詳解

ServerSocket 用法詳解

本篇文章觀點和例子來自 《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 埠繫結的伺服器:

   ServerSocket serverSocket = new ServerSocket(80);                                  

  如果執行時無法繫結到 80 埠, 以上程式碼會丟擲 IOException, 更確切地說, 是丟擲 BindException, 它是 IOException 的子類. BindException 一般是由以下原因造成的:

埠已經被其他伺服器程序佔用;
在某些作業系統中, 如果沒有以超級使用者的身份來執行伺服器程式, 那麼作業系統不允許伺服器繫結到 1-1023 之間的埠.

  如果把引數 port 設為 0, 表示由作業系統來為伺服器分配一個任意可用的埠. 有作業系統分配的埠也稱為匿名埠. 對於多數伺服器, 會使用明確的埠, 而不會使用匿名埠, 因為客戶程式需要事先知道伺服器的埠, 才能方便地訪問伺服器. 在某些場合, 匿名埠有著特殊的用途, 本章 四 會對此作介紹.

1.2 設定客戶連線請求佇列的長度

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

   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

import java.net.Socket;

public class Client {

 public static void main(String[] args) throws Exception{
  final int length = 100;
  String host = "localhost";
  int port = 1122;
  Socket[] socket = new Socket[length];
  for(int i = 0;i<length;i++){
   socket[i] = new Socket(host,port);
   System.out.println("第"+(i+1)+"次連線成功!");
  }
  Thread.sleep(3000);
  for(int i=0;i<length;i++){
   socket[i].close();
  }
 }
}



Server.java 

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
 private int port = 1122;
 private ServerSocket serverSocket;

 public Server() throws Exception{
  serverSocket = new ServerSocket(port,3);
  System.out.println("伺服器啟動!");
 }

 public void service(){
  while(true){
   Socket socket = null;
   try {
    socket = serverSocket.accept();
    System.out.println("New connection accepted "+
      socket.getInetAddress()+":"+socket.getPort());
   } catch (IOException e) {
    e.printStackTrace();
   }finally{
    if(socket!=null){
     try {
      socket.close();
     } catch (IOException e) {
      e.printStackTrace();
     }
    }
   }
  }
 }

 public static void main(String[] args) throws Exception{
  Server server = new Server();
  Thread.sleep(60000*10);
  server.service();
 }
} 
  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:

   ServerSocket serverSocket = new ServerSocket(8000, 10, InetAddress.getByName("192.168.3.4"));

1.4 預設構造方法的作用

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

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



  在以下程式碼中, 先把 ServerSocket 的 SO_REUSEADDR 選項設為 true, 然後再把它與 8000 埠繫結:

ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true); //設定 ServerSocket 的選項
serverSocket.bind(new InetSocketAddress(8000)); //與8000埠繫結

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

ServerSocket serverSocket = new ServerSocket(8000);
serverSocket.setReuseAddress(true); //設定 ServerSocket 的選項

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

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

  ServerSocket 的 accept() 方法從連線請求佇列中取出一個客戶的連線請求, 然後建立與客戶連線的 Socket 物件, 並將它返回. 如果佇列中沒有連線請求, accept() 方法就會一直等待, 直到接收到了連線請求才返回.



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

    java.net.SocketException: Connection reset by peer                                           

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



  以下程式顯示了單執行緒伺服器採用的通訊流程:

public void service(){
while(true){
Socket socket = null;
try{
socket = serverSocket.accept(); //從連線請求佇列中取出一個連線
System.out.println(“New connection accepted ”
+ socket.getInetAddress() + ” : ” + socket.getPort());
//接收和傳送資料
…………
}catch(IOException e)
//這只是與單個客戶通訊時遇到的異常, 可能是由於客戶端過早斷開連線引起
//這種異常不應該中斷整個while迴圈
e.printStackTrace();
}finally{
try{
if(socket != null) socket.close(); //與一個客戶通訊結束後, 要關閉Socket
}catch(IOException e){
e.printStackTrace();}
}
}
}

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

三. 關閉ServerSocket

 ServerSocket 的 close() 方法使伺服器釋放佔用的埠, 並且斷開與所有客戶的連線. 當一個伺服器程式執行結束時, 即使沒有執行 ServerSocket 的 close() 方法, 作業系統也會釋放這個伺服器佔用的埠. 因此, 伺服器程式不一定要在結束之前執行 ServerSocket 的 close() 方法.



  在某些情況下, 如果希望及時釋放伺服器的埠, 以便讓其他程式能佔用該埠, 則可以顯式呼叫 ServerSocket 的 close() 方法. 例如, 以下程式碼用於掃描 1-65535 之間的埠號. 如果 ServerSocket 成功建立, 意味這該埠未被其他伺服器程序繫結, 否則說明該埠已經被其他程序佔用:

for(int port = 1; port <= 65335; port ++){
try{
ServerSocket serverSocket = new ServerSocket(port);
serverSocket.close(); //及時關閉ServerSocket
}catch(IOException e){
System.out.println(“埠” + port + ” 已經被其他伺服器程序佔用”);
}
}

 以上程式程式碼建立了一個 ServerSocket 物件後, 就馬上關閉它, 以便及時釋放它佔用的埠, 從而避免程式臨時佔用系統的大多數埠.



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



  ServerSocket 的 isBound() 方法判斷 ServerSocket 是否已經與一個埠繫結, 只要 ServerSocket 已經與一個埠繫結, 即使它已經被關閉, isBound() 方法也會返回 true.



  如果需要確定一個 ServerSocket 已經與特定埠繫結, 並且還沒有被關閉, 則可以採用以下方式:

    boolean isOpen = serverSocket.isBound() && !serverSocket.isClosed();                             

四. 獲取ServerSocket 的資訊

 ServerSocket 的以下兩個 get 方法可以分別獲得伺服器繫結的 IP 地址, 以及繫結的埠:

public InetAddress getInetAddress();
public int getLocalPort()

 前面已經講到, 在構造 ServerSocket 時, 如果把埠設為 0 , 那麼將有作業系統為伺服器分配一個埠(稱為匿名埠), 程式只要呼叫 getLocalPort() 方法就能獲知這個埠號. 如下面的 RandomPort 建立了一個 ServerSocket, 它使用的就是匿名埠.



   RandomPort.java 

import java.net.ServerSocket;

public class RandomPort {

public static void main(String[] args) throws Exception{
ServerSocket serverSocket = new ServerSocket(0);
System.out.println(“監聽的埠為:”+serverSocket.getLocalPort());
}
}

  多數伺服器會監聽固定的埠, 這樣才便於客戶程式訪問伺服器. 匿名埠一般設用於伺服器與客戶之間的臨時通訊, 通訊結束, 就斷開連線, 並且 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.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.net.BindExcetpion: Address already in use: JVM_Bind                                               



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

    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() 方法的程式碼:

public void service(){
while(true){
Socket socket = null;
try{
socket = serverSocket.accept(); //接收客戶連線
Thread workThread = new Thread(new Handler(socket)); //建立一個工作程序
workThread.start(); //啟動工作程序
}catch(IOException e){
e.printStackTrace();
}
}
}

  以上工作執行緒 workThread 執行 Handler 的 run() 方法. Handler 類實現了 Runnable 介面, 它的 run() 方法負責與單個客戶通訊, 與客戶通訊結束後, 就會斷開連線, 執行 Handler 的 run() 方法的工作執行緒也會自然終止. 下面是 EchoServer 類及 Handler 類的原始碼.

EchoServer.java(為每個任務分配一個執行緒)

package multithread1;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class EchoServer {

private int port = 8000;
private ServerSocket serverSocket;

public EchoServer() throws IOException{
serverSocket = new ServerSocket(8000);
System.out.println(“伺服器啟動”);
}

public void service(){
while(true){
Socket socket = null;
try{
socket = serverSocket.accept(); //接收客戶連線
Thread workThread = new Thread(new Handler(socket)); //建立一個工作程序
workThread.start(); //啟動工作程序
}catch(IOException e){
e.printStackTrace();
}
}
}

public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
new EchoServer().service();
}

class Handler implements Runnable{

private Socket socket;

public Handler(Socket socket) {
this.socket = socket;
}

private PrintWriter getWriter(Socket socket) throws IOException{
OutputStream socketOut = socket.getOutputStream();
return new PrintWriter(socketOut,true);
}

private BufferedReader getReader(Socket socket) throws IOException{
InputStream socketIn = socket.getInputStream();
return new BufferedReader(new InputStreamReader(socketIn));
}

public String echo(String msg){
return “echo:” + msg;
}

public void run() {
// TODO Auto-generated method stub
try{
System.out.println(“New connection accepted ”
+ socket.getInetAddress() + “:” + socket.getPort());
BufferedReader br = getReader(socket);
PrintWriter pw = getWriter(socket);

String msg = null;
while((msg = br.readLine()) != null){   //接收和傳送資料, 直到通訊結束
 System.out.println(msg);
 pw.println(echo(msg));
 if(msg.equals("bye")){
  break;
 }

}

}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(socket != null) socket.close(); //斷開連線
}catch(IOException e){}
}
}

}

}

6.2 建立執行緒池

  在 6.1 節介紹的實現方式中, 對每個客戶都分配一個新的工作程序. 當工作執行緒與客戶通訊結束, 這個執行緒就被銷燬. 這種實現方式有以下不足之處.

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

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

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

 下面的 ThreadPool 類提供了執行緒池的一種實現方案.

ThreadPool.java

package multithread2;

import java.util.LinkedList;

public class ThreadPool extends ThreadGroup {
 private boolean isClosed = false;   //執行緒池是否關閉
 private LinkedList<Runnable> workQueue;  //工作佇列
    private static int threadPoolID;   //表示執行緒池ID
    private int threadID;      //表示工作執行緒ID ,因為 WorkThread是內部類所以才可以這樣


 public ThreadPool(int poolSize) {       //poolSize指定執行緒池中的工作執行緒數目
  super("ThreadPool-" + (threadPoolID ++));
  this.setDaemon(true);
  workQueue = new LinkedList<Runnable>();      //建立工作佇列
  for(int i=0; i<poolSize; i++){
   new WorkThread().start();                //建立並啟動工作程序
  }
 }
 public WorkThread getWorkThread(){
  return new WorkThread();
 }

 /**
  * 向工作佇列中加入一個新任務, 由工作程序去執行該任務
  */
 public synchronized void execute(Runnable task){
  if(isClosed){              //執行緒池被關閉則丟擲IllegalStateException 異常
   throw new IllegalStateException();
  }
  if(task != null){
   workQueue.add(task);      //新增任務到工作佇列中
   notify();                 //呼醒正在getTask()方法中等待任務的某一個工作執行緒,哪一個是隨機的
  }
 }

 /**
  * @throws InterruptedException wait()中被interrupt()將產生這個異常
  * 從工作佇列中取出一個任務, 工作執行緒會呼叫此方法
  */
 protected synchronized Runnable getTask() throws InterruptedException{
  while(workQueue.size() == 0){
   if(isClosed) return null;
   wait();   //如果工作佇列中沒有任務, 就等待任務, 對應execute()方法中的notify和join()方法中的notifyAll()
  }
  return workQueue.removeFirst();
 }

 /** 關閉執行緒池 */
 public synchronized void close(){
  if(!isClosed){
   isClosed = true;
   workQueue.clear();    //清空工作佇列
   interrupt();                 //中斷所有的的工作執行緒, 該方法繼承自 ThreadGroup 類
  }
 }

 /** 等待工作執行緒把所有任務執行完 */
 public void join(){
  synchronized(this){
   isClosed = true;
   notifyAll();            //呼醒所有在getTask()方法中等待任務的工作執行緒
  }
  Thread[] threads = new Thread[this.activeCount()];
  //enumerate()方法繼承自 ThreadGroup 類, 獲得執行緒組中當前所有活著的工作程序 ,並把這些執行緒放到指定的Thread陣列中
  int count = this.enumerate(threads);
  for(int i=0; i<count; i++){         //等待所有工作執行緒執行結束
   try{
    threads[i].join();          //等待工作程序執行結束
   }catch(InterruptedException ex){}
  }
 } 

    /** 內部類: 工作執行緒(注意, 在本類中, 內部類(不是static的)和外部類可以互相訪問對方的所有成員變數和方法, 即使是private的方法和成員變數,
     *                  本類內部訪問限定符對內部類和外部類都沒有影響, 就好像是在所有成員變數和方法視為一個同等的存在一樣處理.
     *                  但是如果你是在其他類中就訪問本類中的外部類和內部類的成員變數和方法, 那就會受到訪問限定符的限制的.
     *                  最後還要注意, 不要在本類中的main() 中測試, 理由不用說了吧) */
 private class WorkThread extends Thread {
  public WorkThread() {
   // 加入到當前 ThreadPool 執行緒當中
   super(ThreadPool.this, "WorkThread-" + (threadID++));
  }

  public void run() {
   while(!isInterrupted()){       //isInterrupted() 方法繼承自 Thread 類, 判斷執行緒是否被中斷
    Runnable task = null;
    try{
     task = getTask();   //取出任務
    }catch(InterruptedException e){}

    //如果getTask() 返回null, 表示執行緒池已經被關閉了, 結束此執行緒
    if(task == null) return;

    try{
     task.run();
    }catch(Throwable t){ //捕捉執行緒run()執行中產生所有的錯誤和異常,為了防止程序被結束
     t.printStackTrace();
    }

   }//while
  }//run
 }//WorkThread

}

還有一些常見的網路問題需要調整os的引數來控制,例如大量TIME_WAIT,TIME_WAIT是主動關閉連線的一端所處的狀態,TIME_WAIT後需要等到2MSL(Max Segment Lifetime,linux上可通過sysctl net.ipv4.tcp_fin_timeout來檢視具體的值,單位為秒)才會被徹底關閉,而處於TIME_WAIT的連線也是要佔用開啟的檔案數的,因此如果太多的話會導致開啟的檔案數到達瓶頸,要避免TIME_WAIT太多,通常可以調整以下幾個os引數:
net.ipv4.tcp_tw_reuse = 1 #表示可重用time_wait的socket
net.ipv4.tcp_tw_recycle = 1 #表示開啟time_wait sockets的快速回收
net.ipv4.tcp_fin_timeout = 30 #表示msl的時間

如果碰到的是大量的CLOSE_WAIT則通常都是程式碼裡的問題,就是伺服器端關閉連線了,但客戶端一直沒關。

相關推薦

三. ServerSocket 用法(二) .

   本篇文章觀點和例子來自 《Java網路程式設計精解》, 作者為孫衛琴, 出版社為電子工業出版社。       在ThreadPool 類中定義了一個LinkedList 型別的 workQueue 成員變數, 它表示工作佇列, 用來存放執行緒池要執行的任務, 每個

ServerSocket 用法

本篇文章觀點和例子來自 《Java網路程式設計精解》, 作者為孫衛琴, 出版社為電子工業出版社。 在客戶/伺服器通訊模式中, 伺服器端需要建立監聽埠的 ServerSocket, ServerSocket 負責接收客戶連線請求. 本章首先介紹 Server

JavaScript中return的用法

style 返回 www log tle blog 意思 charset fun 1、定義:return 從字面上的看就是返回,官方定義return語句將終止當前函數並返回當前函數的值,可以看下下面的示例代碼: <!DOCTYPE html><html l

SVN trunk(主線) branch(分支) tag(標記) 用法和詳細操作步驟

trac load mar span 必須 最可 objc copy 右鍵 原文地址:http://blog.csdn.net/vbirdbest/article/details/51122637 使用場景: 假如你的項目(這裏指的是手機客戶端項目)的某個版本(例如1.0

js 定時器用法——setTimeout()、setInterval()、clearTimeout()、clearInterval()

ntb 幫助 .get tint num 用法 -c 函數 tel 在js應用中,定時器的作用就是可以設定當到達一個時間來執行一個函數,或者每隔幾秒重復執行某段函數。這裏面涉及到了三個函數方法:setInterval()、setTimeout()、clearI

selenium用法

key url enc element api code 需要 int question selenium用法詳解 selenium主要是用來做自動化測試,支持多種瀏覽器,爬蟲中主要用來解決JavaScript渲染問題。 模擬瀏覽器進行網頁加載,當requests,url

C# ListView用法

ont 結束 server 發生 匹配 鼠標 之前 小圖標 order 一、ListView類 1、常用的基本屬性: (1)FullRowSelect:設置是否行選擇模式。(默認為false) 提示:只有在Details視圖該屬性才有意義

linux cp命令參數及用法---linux 復制文件命令cp

linux file linux cp命令參數及用法詳解---linux 復制文件命令cp [root@Linux ~]# cp [-adfilprsu] 來源檔(source) 目的檔(destination)[root@linux

Python數據類型方法簡介一————字符串的用法

python 字符串連接 字符串用法 符串是Python中的重要的數據類型之一,並且字符串是不可修改的。 字符串就是引號(單、雙和三引號)之間的字符集合。(字符串必須在引號之內,引號必須成對)註:單、雙和三引號在使用上並無太大的區別; 引號之間可以采取交叉使用的方式避免過多轉義;

C# ListView用法(轉)

分組 創建 cti 排列 checkbox 定義 com 程序 erl 一、ListView類 1、常用的基本屬性: (1)FullRowSelect:設置是否行選擇模式。(默認為false) 提示:只有在Details視圖該屬性才有

java中的instanceof用法

定義 xtend print 繼承 interface 參數 保留 如果 ack   instanceof是Java的一個二元操作符(運算符),也是Java的保留關鍵字。它的作用是判斷其左邊對象是否為其右邊類的實例,返回的是boolean類型的數據。用它來判斷某個對象是否是

@RequestMapping 用法

同時 get() turn example track find 說明 tex -h 簡介: @RequestMapping RequestMapping是一個用來處理請求地址映射的註解,可用於類或方法上。用於類上,表示類中的所有響應請求的方法都是以該地址作為父路徑。

Css中路徑data:image/png;base64的用法 (轉載)

javascrip base64編碼 asc cda 文件的 color 情況 ont 背景圖片 大家可能註意到了,網頁上有些圖片的src或css背景圖片的url後面跟了一大串字符,比如: background-image:url(data:image/png;bas

global用法

global 在函數內傳遞參數1、global一般用在函數內,將外部變量參數傳遞至函數內部,用法為:<?php $name = "why"; function changeName(){ global $name; $name = "what";

java中靜態代碼塊的用法—— static用法

super關鍵字 了解 裝載 static關鍵字 super 屬於 註意 lock 自動 (一)java 靜態代碼塊 靜態方法區別一般情況下,如果有些代碼必須在項目啟動的時候就執行的時候,需要使用靜態代碼塊,這種代碼是主動執行的;需要在項目啟動的時候就初始化,在不創建對象的

<!CDATA[]]用法

引號 ica lap 用法 bsp mar ret message eight 所有 XML 文檔中的文本均會被解析器解析。 只有 CDATA 區段(CDATA section)中的文本會被解析器忽略。 PCDATA PCDATA 指的是被解析的字符數據(Parsed

Es6 Promise 用法

set 問題 得到 math clas promise 回調 console spa Promise是什麽?? 打印出來看看 console.dir(Promise) 這麽一看就明白了,Promise是一個構造函數,自己身上有all、reject、r

[轉] angular2-highcharts用法

ppc tip option select sel nbsp 用法詳解 points ttr 1、 使用npm安裝angular2-highcharts npm install angular2-highcharts --save 2、主模塊中引入 app.module.t

常見<meta>的基本用法

代碼 簡介 clas 元素 word spa wid min mpat <meta charset="utf-8"> 定義與name 屬性相關的信息,使用 utf-8編碼方式編譯字符 <meta http-equiv="X-UA-Compatible" c

oracle中的exists 和not exists 用法

sdn ref 用法詳解 html nbsp e30 .net tail sin oracle中的exists 和not exists 用法詳解 http://blog.csdn.net/zhiweianran/article/details/7868894oracle