從零開始netty學習筆記之BIO

weixin_33830216發表於2017-12-14

BIO即Block IO,阻塞式IO。 網路程式設計的基本模型是Client/Server模型,也就是兩個程式之間進行相互通訊,其中服務端提供位置資訊(繫結的IP地址和監聽埠),客戶端通過連線操作向服務端監聽的地址發起連線請求,通過三次握手建立連線,如果連線建立成功,雙方就可以通過網路套接字(Socket)進行通訊。 在基於傳統同步阻塞模型開發中,ServerSocket負責繫結IP地址,啟動監聽埠:Socket負責發起連線操作。連線成功之後,雙方通過輸入和輸出流進行同步阻塞式通訊。 ####傳統阻塞式IO BIO服務端通訊模型:採用BIO通訊模型的服務端,通常由一個獨立的Acceptor執行緒負責監聽客戶端的連線,它接收到客戶端連線請求之後為每個客戶端建立一個新的執行緒進行鏈路處理,處理完之後,通過輸出流返回應答給客戶端,執行緒銷燬。 該模型最大的問題就是缺乏彈性伸縮能力,當客戶端併發訪問量則增加後,服務端的執行緒個數和客戶端併發訪問數呈1:1的正比關係,由於執行緒是java虛擬機器非常寶貴的系統資源,當執行緒數膨脹之後,系統的效能 將急劇下降,隨著併發訪問量的繼續增大,系統會發生執行緒堆疊溢位、建立新執行緒失敗等問題,並最終導致程式當機或者僵死,不能對外提供服務。 傳統的BIO 程式碼演示: 服務端

public class TimeServer {

    public static void main(String[] args) {

        int port = 8081;
        if(args!=null&&args.length>0){
            try{
                port = Integer.valueOf(args[0]);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        ServerSocket serverSocket = null;
        try{
            serverSocket = new ServerSocket(port);
            System.out.println("伺服器已經啟動--埠號:"+port);
            Socket socket = null;
            while (true){
                socket = serverSocket.accept();
                new Thread(new TimeServerHandler(socket)).start();
            }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(serverSocket!=null){
                System.out.println("伺服器關閉");
                try{
                    serverSocket.close();
                }catch (Exception e1){
                    e1.printStackTrace();
                }
                serverSocket = null;
            }
        }
    }
}

複製程式碼

服務端處理器

public class TimeServerHandler implements Runnable {

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


    private Socket socket;


    @Override
    public void run() {

        BufferedReader in = null;
        PrintWriter out = null;
        try{
            in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
            out = new PrintWriter(this.socket.getOutputStream(),true);
            String currentTime = null;
            String body = null;
            while(true){
                body = in.readLine();
                if(body == null){
                    break;
                }
                System.out.println("伺服器收到訊息:"+body);
                currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)? new Date(System.currentTimeMillis()).toString():"BAD ORDER";
                out.println(currentTime);
            }

        }catch (Exception e){
            e.printStackTrace();
            if(in!=null){
                try{
                    in.close();
                }catch (Exception e1){
                    e1.printStackTrace();
                }
            }
            if(out!=null){
                out.close();
                out = null;
            }
            if(this.socket!=null){
                try{
                    this.socket.close();
                }catch (Exception e1){
                    e.printStackTrace();
                }
                this.socket = null;
            }
        }

    }
}
複製程式碼

客戶端

public class TimeClient {

    public static void main(String[] args) {

        int port = 8081;
        if(args!=null&&args.length>0){
            try{
                port = Integer.valueOf(args[0]);
            }catch (Exception e){
                e.printStackTrace();
            }

        }
        Socket socket = null;
        BufferedReader in = null;
        PrintWriter out  = null;
        try{
            socket = new Socket("127.0.0.1",port);
            in  = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(),true);
            out.println("QUERY TIME ORDER");
            System.out.println("傳送命令成功");
            String resp = in.readLine();
            System.out.println("收到的訊息:"+resp);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(out!=null){
                out.close();
                out = null;
            }
            if(in!= null){
                try{
                    in.close();
                }catch (Exception e2){
                    e2.printStackTrace();
                }
                in = null;
            }
            if(socket!= null){
                try{
                    socket.close();
                }catch (Exception e2){
                    e2.printStackTrace();
                }
                socket = null;
            }
        }
    }
}
複製程式碼

傳統的BIO每當一個新的客戶端請求接入時,服務端必須建立一個新的執行緒處理接入的客戶端鏈路,一個執行緒只能處理一個客戶端連線。在高效能伺服器應用領域,往往需要面向成千上萬個客戶端的併發連線,所以這種模型肯定無法滿足高效能高併發的 場景。

####偽非同步IO程式設計

偽非同步的原理就是後端通過一個執行緒池來處理過個客戶端的請求接入,形成客戶端個數M:執行緒池最大執行緒數N的比例關係,其中M可以遠遠大於N。通過執行緒池可以靈活的調配執行緒資源,設定執行緒的最大值,防止由於海量併發接入導致執行緒耗盡。

具體實現:當有新的客戶端接入時,將客戶端的Socket封裝成一個Task(該任務實現Runnable介面)投遞到後端的執行緒池中進行處理,JDK的執行緒池維護一個訊息佇列和N個活躍執行緒,對訊息佇列中的任務進行處理。由於執行緒池可以設定訊息佇列的大小和最大執行緒數,因此,它的資源佔用是可控的,無論多少個客戶端併發訪問,都不會導致資源的耗盡和當機。 程式碼演示: 執行緒池

public class TimeServerHandlerExecutePool {

    private ExecutorService executorService;

    public TimeServerHandlerExecutePool(int maxPoolSize,int queueSize){

        this.executorService = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
                maxPoolSize,120L, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(queueSize));
    }
    public void execute(Runnable task){
        executorService.execute(task);
    }
}

複製程式碼

server程式碼和原來差不多,只是將原來的建立執行緒改為使用執行緒池來執行這個任務。


 serverSocket = new ServerSocket(port);
            System.out.println("伺服器已經啟動--埠號:"+port);
            Socket socket = null;
            TimeServerHandlerExecutePool  singleExecutor = new TimeServerHandlerExecutePool(50,10000);//建立IO任務執行緒池
            while (true){
                socket = serverSocket.accept();
                singleExecutor.execute(new TimeServerHandler(socket));

            }
複製程式碼

偽非同步IO弊端分析: read 當對Socket的輸入流進行讀取操作的時候,它會一直阻塞下去,知道發生如下三種事件。

  • 有資料可讀
  • 可用資料已經讀取完畢
  • 發生空指標異常或者IO異常 這意味著當對方傳送請求或者應答訊息比較緩慢,或者網路傳輸較慢時,讀取輸入流一方的通訊執行緒將被長時間阻塞,如果對方要60s才能夠將資料傳送完畢,讀取一方的IO執行緒也將被同步阻塞60s,在此期間,其他接入訊息只能在訊息佇列中排隊。 write 當呼叫OutputStream的write方法寫輸出流的時候,它將會被阻塞,知道所有要傳送的位元組全部寫入完畢,或者發生異常。學習過TCP/IP相關知識的人都知道,當訊息的接收方處理緩慢的時候,將不能及時的從TCP緩衝區讀取資料,這將會導致傳送方的TCP window size不斷減少,知道為0,雙方處於Keep-Alive狀態,訊息傳送方將不能再向TCP緩衝區寫入訊息,這時如果採用的是同步阻塞IO,write操作將會被無線阻塞,知道TCP window size大於0或者發生IO異常。

通過對輸入和輸出流的API進行分析,讀和寫操作都是同步阻塞的,阻塞的時間取決於對方對方IO執行緒的處理速度和網路IO的傳輸速度。本質上來講,我們無法保證生產環境的網路狀況和對端的應用程式能足夠快,如果我們的應用程式依賴對方的處理速度,它的可靠性就非常差。

相關文章