Socket Server-基於線程池的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();
    }
}
複製代碼
發佈了2 篇原創文章 · 獲贊 2 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章