【Java TCP/IP Socket】基於線程池的TCP服務器(含代碼)


了解線程池

     http://blog.csdn.net/ns_code/article/details/14105457(讀書筆記一:TCP Socket)這篇博文中,服務器端采用的實現方式是:一個客戶端對應一個線程。但是,每個新線程都會消耗系統資源:創建一個線程會占用CPU周期,而且每個線程都會建立自己的數據結構(如,棧),也要消耗系統內存,另外,當一個線程阻塞時,JVM將保存其狀態,選擇另外一個線程運行,並在上下文轉換(context switch)時恢復阻塞線程的狀態。隨着線程數的增加,線程將消耗越來越多的系統資源,這將最終導致系統花費更多的時間來處理上下文轉換盒線程管理,更少的時間來對連接進行服務。在這種情況下,加入一個額外的線程實際上可能增加客戶端總服務的時間。

     我們可以通過限制線程總數並重復使用線程來避免這個問題。我們讓服務器在啟動時創建一個由固定線程數量組成的線程池,當一個新的客戶端連接請求傳入服務器,它將交給線程池中的一個線程處理,該線程處理完這個客戶端之后,又返回線程池,繼續等待下一次請求。如果連接請求到達服務器時,線程池中所有的線程都已經被占用,它們則在一個隊列中等待,直到有空閑的線程可用。

 

 

    實現步驟

      1、與一客戶一線程服務器一樣,線程池服務器首先創建一個ServerSocket實例。

      2、然后創建N個線程,每個線程反復循環,從(共享的)ServerSocket實例接收客戶端連接。當多個線程同時調用一個ServerSocket實例的accept()方法時,它們都將阻塞等待,直到一個新的連接成功建立,然后系統選擇一個線程,為建立起的連接提供服務,其他線程則繼續阻塞等待。

 

      3、線程在完成對一個客戶端的服務后,繼續等待其他的連接請求,而不終止。如果在一個客戶端連接被創建時,沒有線程在accept()方法上阻塞(即所有的線程都在為其他連接服務),系統則將新的連接排列在一個隊列中,直到下一次調用accept()方法。

 

 

    示例代碼

      我們依然實現http://blog.csdn.net/ns_code/article/details/14105457這篇博客中的功能,客戶端代碼相同,服務器端代碼在其基礎上改為基於線程池的實現,為了方便在匿名線程中調用處理通信細節的方法,我們對多線程類ServerThread做了一些微小的改動,如下:

 

package zyb.org.server;  
  
import java.io.BufferedReader;  
import java.io.InputStreamReader;  
import java.io.PrintStream;  
import java.net.Socket;  
  
/** 
 * 該類為多線程類,用於服務端 
 */  
public class ServerThread implements Runnable {  
  
    private Socket client = null;  
    public ServerThread(Socket client){  
        this.client = client;  
    }  
       
    //處理通信細節的靜態方法,這里主要是方便線程池服務器的調用  
    public static void execute(Socket client){  
        try{  
            //獲取Socket的輸出流,用來向客戶端發送數據    
            PrintStream out = new PrintStream(client.getOutputStream());  
            //獲取Socket的輸入流,用來接收從客戶端發送過來的數據  
            BufferedReader buf = new BufferedReader(new InputStreamReader(client.getInputStream()));  
            boolean flag =true;  
            while(flag){  
                //接收從客戶端發送過來的數據    
                String str =  buf.readLine();  
                if(str == null || "".equals(str)){  
                    flag = false;  
                }else{  
                    if("bye".equals(str)){  
                        flag = false;  
                    }else{  
                        //將接收到的字符串前面加上echo,發送到對應的客戶端    
                        out.println("echo:" + str);  
                    }  
                }  
            }  
            out.close();  
            buf.close();  
            client.close();  
        }catch(Exception e){  
            e.printStackTrace();  
        }  
    }  
    @Override  
    public void run() {  
        execute(client);  
    }  
  
}  

這樣我們就可以很方便地在匿名線程中調用處理通信細節的方法,改進后的服務器端代碼如下:

 

package zyb.org.server;  
  
import java.io.IOException;  
import java.net.ServerSocket;  
import java.net.Socket;  
  
/** 
 * 該類實現基於線程池的服務器 
 */  
public class serverPool {  
      
    private static final int THREADPOOLSIZE = 2;  
  
    public static void main(String[] args) throws IOException{  
        //服務端在20006端口監聽客戶端請求的TCP連接   
        final ServerSocket server = new ServerSocket(20006);  
          
        //在線程池中一共只有THREADPOOLSIZE個線程,  
        //最多有THREADPOOLSIZE個線程在accept()方法上阻塞等待連接請求  
        for(int i=0;i<THREADPOOLSIZE;i++){  
            //匿名內部類,當前線程為匿名線程,還沒有為任何客戶端連接提供服務  
            Thread thread = new Thread(){  
                public void run(){  
                    //線程為某連接提供完服務后,循環等待其他的連接請求  
                    while(true){  
                        try {  
                            //等待客戶端的連接  
                            Socket client = server.accept();  
                            System.out.println("與客戶端連接成功!");  
                            //一旦連接成功,則在該線程中與客戶端通信  
                            ServerThread.execute(client);  
                        } catch (IOException e) {  
                            e.printStackTrace();  
                        }  
                    }   
                }  
            };  
            //先將所有的線程開啟  
            thread.start();  
        }  
    }  
}  

  結果分析

 

      為了便於測試,程序中,我們將線程池中的線程總數設置為2,這樣,服務器端最多只能同事連接2個客戶端,如果已有2個客戶端與服務器建立了連接,當我們打開第3個客戶端的時候,便無法再建立連接,服務器端不會打印出第3個“與客戶端連接成功!”的字樣。

      這第3個客戶端如果過了一段時間還沒接收到服務端發回的數據,便會拋出一個SocketTimeoutException異常,從而打印出如下信息(客戶端代碼參見:http://blog.csdn.net/ns_code/article/details/14105457):


      如果在拋出SocketTimeoutException異常之前,有一個客戶端的連接關掉了,則第3個客戶端便會與服務器端建立起連接,從而收到返回的數據


 

 

    改進

          在創建線程池時,線程池的大小是個很重要的考慮因素,如果創建的線程太多(空閑線程太多),則會消耗掉很多系統資源,如果創建的線程太少,客戶端還是有可能等很長時間才能獲得服務。因此,線程池的大小需要根據負載情況進行調整,以使客戶端連接的時間最短,理想的情況是有一個調度的工具,可以在系統負載增加時擴展線程池的大小(低於大上限值),負載減輕時縮減線程池的大小。一種解決的方案便是使用Java中的Executor接口。

      Executor接口代表了一個根據某種策略來執行Runnable實例的對象,其中可能包括了排隊和調度等細節,或如何選擇要執行的任務。Executor接口只定義了一個方法:

interface Executor{

      void execute(Runnable task);

}

      Java提供了大量的內置Executor接口實現,它們都可以簡單方便地使用,ExecutorService接口繼承於Executor接口,它提供了一個更高級的工具來關閉服務器,包括正常的關閉和突然的關閉。我們可以通過調用Executors類的各種靜態工廠方法來獲取ExecutorService實例,而后通過調用execute()方法來為需要處理的任務分配線程,它首先會嘗試使用已有的線程,但如果有必要,它會創建一個新的線程來處理任務,另外,如果一個線程空閑了60秒以上,則將其移出線程池,而且任務是在Executor的內部排隊,而不像之前的服務器那樣是在網絡系統中排隊,因此,這個策略幾乎總是比前面兩種方式實現的TCP服務器效率要高。

改進的代碼如下:

 

package zyb.org.server; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.Executor; import java.util.concurrent.Executors; /** * 該類通過Executor接口實現服務器 */ public class ServerExecutor { public static void main(String[] args) throws IOException{ //服務端在20006端口監聽客戶端請求的TCP連接 ServerSocket server = new ServerSocket(20006); Socket client = null; //通過調用Executors類的靜態方法,創建一個ExecutorService實例 //ExecutorService接口是Executor接口的子接口 Executor service = Executors.newCachedThreadPool(); boolean f = true; while(f){ //等待客戶端的連接 client = server.accept(); System.out.println("與客戶端連接成功!"); //調用execute()方法時,如果必要,會創建一個新的線程來處理任務,但它首先會嘗試使用已有的線程, //如果一個線程空閑60秒以上,則將其移除線程池; //另外,任務是在Executor的內部排隊,而不是在網絡中排隊 service.execute(new ServerThread(client)); } server.close(); } } 

 

 

轉自:http://blog.csdn.net/ns_code/article/details/14451911


免責聲明!

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



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