與NIO的第一次親密接觸

前言

隨着互聯網的不斷髮展,用戶羣體越發龐大。
從互聯網初入中國,國內的上網用戶不過數萬,而上網也只能簡單的進行郵件,瀏覽新聞。到現在幾乎每個人都可以通過互聯網進行社交,娛樂,學習。中間不但有社會經濟的發展,java技術也隨之不斷髮展,究竟是科學技術的發展促使了社會的發展,還是社會經濟的發展促使了科學技術?這個問題我想大家應該都知道一句話:科學技術是第一生產力。
隨着用戶全體的龐大,對於網絡通信的要求越發高,BIO越發不適應需求,於是NIO應運而生。
NIO(New IO)又被業內稱之爲:Non Block IO,即非阻塞IO

一、傳統的BIO編程

1、BIO通信模型圖

在這裏插入圖片描述
BIO通信的服務器,對於每一個客戶端的連接,都會使用一個獨立的Acceptor線程,來對客戶端進行連接。
即,他收到客戶端請求後,會爲每一個客戶端創建一個新的線程進行鏈路處理,處理完成之後,通過輸出流返回應答給客戶端,最後線程銷燬。這就是典型的一請求一應答的通信模型。
線程是JVM非常寶貴的資源,當線程數過大後,隨着併發訪問數的繼續增大,系統將會產生堆棧移除,創建線程失敗等事故。最終導致服務器宕機或僵死,無法對外提供服務。

2、BIO的Server源碼分析

package com.xyp.iodemo.nio.server;


import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.logging.Logger;

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-03-28 18:13
 */
public class TimeServer {
    public static Logger XLogUtils = Logger.getLogger("TimeServer");

    public static void main(String [] args) throws IOException {
        Integer port=20022;
        if(args!=null&&args.length>0){
            try {
                port=Integer.valueOf(args[0]);
            }catch (Exception e){

            }
        }
        ServerSocket serverSocket=null;
        try {
            serverSocket=new ServerSocket(port);
            XLogUtils.info("The TimeServer is start in port:"+port);

            Socket socket=null;
            while (true){
                socket=serverSocket.accept();
                new Thread(new TimeServerHandler(socket)).start();
            }

        }finally {
            if(serverSocket!=null){
                XLogUtils.info("The Time is Close");
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                serverSocket=null;
            }
            XLogUtils.info("END>>>>");
        }
    }
}

TimeServer是一個很簡單的類,它負責創建ServerSocket,然後對客戶端的請求進行接受,這裏值得注意的一點就是,這裏的

serverSocket.accept()

是阻塞的,即sever等待客戶端請求接入,如果接入就創建線程處理請求,創建完畢後,繼續等待下一個客戶端的接入。

然後我們再來一起看一下具體針對線程的處理。

package com.xyp.iodemo.nio.server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.logging.Logger;

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-03-28 19:13
 */
public class TimeServerHandler implements Runnable {
    private  Socket socket;
    public static Logger logger = Logger.getLogger("TimeServerHandler");

    public TimeServerHandler(Socket socket){
        this.socket=socket;
        logger.info("TimeServerHandler 構造函數");
    }

    @Override
    public void run() {
        BufferedReader in =null;
        PrintWriter out=null;
        try {
            in=new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out=new PrintWriter(socket.getOutputStream(),true);
            String currentTime=null;
            String body=null;
            logger.info("TimeServerHandler run");
            while (true){
                logger.info("TimeServerHandler wait input");
                body=in.readLine();
                logger.info("TimeServerHandler wait finish:"+String.valueOf(body));
                if(body== null){
                    break;
                }
                logger.info("The time server receive order : "+body);
                currentTime="QUERY TIME ORDER".equalsIgnoreCase(body)?new java.util.Date(
                        System.currentTimeMillis()
                ).toString():"BAD ORDER";
                out.println(currentTime);
            }
        }catch (Exception e){
            if(in!=null){
                try {
                    in.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            if(out!=null){
                out.close();
                out=null;
            }
            if(socket!=null){
                try {
                    socket.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }finally {
                    socket=null;
                }
            }
        }
    }
}

3、Client的源碼分析

package com.xyp.iodemo.bio.client;

import java.io.*;
import java.net.Socket;
import java.util.logging.Logger;

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-04-01 18:21
 */
public class TimeClient {
    public static Logger logger = Logger.getLogger("TimeClient");

    public static void main(String [] args) throws IOException {
        Integer port=20022;
        if(args!=null&&args.length>0){
            try {
                port=Integer.valueOf(args[0]);
            }catch (Exception e){

            }
        }

        Socket socket=null;
        BufferedReader in =null;
        PrintWriter out = null;
        try {
            socket = new Socket("127.0.0.1", port);
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));//用戶獲取用戶輸入

            OutputStream os = socket.getOutputStream();//用於向服務器輸出
            System.out.println("請輸入要發送的文字:");
            String input;
            //等待cons
            while ((input= reader.readLine()) != null) {
                input = input+"\n";//手動加上回車
                os.write(input.getBytes("utf-8"));
                logger.info("向服務的寫入:"+input.toString());
            }

        }catch (Exception e){

        }finally {
            if(out==null){
                out.close();
                out=null;
            }

            if(in==null){
                try {
                    in.close();
                }catch (IOException e){
                    e.printStackTrace();
                }
                in=null;
            }

            if(socket!=null){
                try {
                    socket.close();
                }catch (IOException e2){
                    e2.printStackTrace();
                }
                socket=null;
            }
        }

    }
}

等待控制檯的輸入,按下回車鍵,就會向服務器寫入字節。

4、運行結果

(1)啓動Server的console

四月 08, 2019 4:25:41 下午 com.xyp.iodemo.nio.server.TimeServer main
信息: The TimeServer is start in port:20022

(2)啓動Client的console,在其中輸入了 111後,按下回車

請輸入要發送的文字:
111
四月 08, 2019 4:27:40 下午 com.xyp.iodemo.bio.client.TimeClient main
信息: 向服務的寫入:111

(3)服務器響應輸出。輸出是分步的
當client啓動後,輸出

四月 08, 2019 4:27:30 下午 com.xyp.iodemo.nio.server.TimeServerHandler
信息: TimeServerHandler 構造函數
四月 08, 2019 4:27:30 下午 com.xyp.iodemo.nio.server.TimeServerHandler run
信息: TimeServerHandler run
四月 08, 2019 4:27:30 下午 com.xyp.iodemo.nio.server.TimeServerHandler run
信息: TimeServerHandler wait input

當客戶端按下回車鍵後

四月 08, 2019 4:27:40 下午 com.xyp.iodemo.nio.server.TimeServerHandler run
信息: TimeServerHandler wait finish:111
四月 08, 2019 4:27:40 下午 com.xyp.iodemo.nio.server.TimeServerHandler run
信息: The time server receive order : 111
四月 08, 2019 4:27:40 下午 com.xyp.iodemo.nio.server.TimeServerHandler run
信息: TimeServerHandler wait input

二、NIO基礎概念

1、緩衝區Buffer

Buffer是一個對象,他包含了一些要寫入的或者要讀出的數據。
在NIO中加入Buffer對象,體現了新庫與原始IO的一個重大區別,即,在面流的IO中,可以直接將數據寫入或者直接將數據讀到steam對象中。
Java NIO中的Buffer用於和NIO通道進行交互。如你所知,數據是從通道讀入緩衝區,從緩衝區寫入到通道中的。

緩衝區本質上是一塊可以寫入數據,然後可以從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊內存。

2、通道Channel

Channel是一個通道,可以通過它進行數據的讀取和寫入。如同自來水管道,網絡數據在channel中可以進行雙向的流通,這點不同於steam,steam是單向的,即steam要麼是InputSteam,要麼是OutputSteam。
而通道可以用於讀、寫、或同時進行讀寫。

3、多路複用器 Selector

多路複用器,提供平選擇已就緒任務的能力。
簡單來說,selector會不斷輪詢註冊在其上的Channel,如果某個Channel上有新的Tcp接入,或者有發生讀寫事件,這個Channel就會處於就緒狀態,可以被Selector輪詢出來,然後通過selectedKey獲取就緒的Channel集合,以便進行後續的IO操作。

selector.selectedKeys() 

一個多路複用器可以同時輪詢多個Channel。

4、NIO的Server的時序圖

在這裏插入圖片描述

5、NIO的Server源碼分析

(1)TimeServer

此類的作用主要是聲明端口,以及啓動線程

package com.xyp.iodemo.nio.server;

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-04-02 11:05
 */
public class TimeServer {
    public static void main(String [] args){
        Integer port=8085;
        if(args!=null&&args.length>0){
            try {
                port=Integer.valueOf(args[0]);
            }catch (Exception e){

            }
        }
        MultiplexerTimeServer timeServer=new MultiplexerTimeServer(port);
        new Thread(timeServer,"NIO-MultiplexerTimeServer-001").start();
    }
}
(2)MultiplexerTimeServer
package com.xyp.iodemo.nio.server;


import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Logger;

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-04-02 11:11
 */
public class MultiplexerTimeServer implements Runnable{
    public static Logger logger = Logger.getLogger("MultiplexerTimeServer");
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private volatile Boolean stop;

    public MultiplexerTimeServer(Integer port){
        stop=false;
        try {
            //創建多路複用器
            selector=Selector.open();
            //打開ServerSocketChannel,用於監聽客戶端連接,他是所有的客戶端連接的父管道
            serverSocketChannel=ServerSocketChannel.open();
            //設置爲非阻塞
            serverSocketChannel.configureBlocking(false);
            //綁定監聽端口
            serverSocketChannel.socket().bind(new InetSocketAddress(port),1024);
            //將serverSocketChannel註冊到多路複用器,監聽ACCEPT事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            logger.info("The Time server is start in port: " +port);
        }catch (IOException e){
            e.printStackTrace();
            System.exit(1);
        }
    }

    public void stop(){
        this.stop=true;
    }

    @Override
    public void run() {
        while (!stop){
            try {
                //設置休眠時間爲1s,無論是否有讀寫事件發生,selector每1s都被喚醒一次
                selector.select(1000);
//                logger.info("selector被喚醒>>>>>>>");
                //多路複用器無線循環獲取準備就緒的Key
                Set<SelectionKey> keys=selector.selectedKeys();
//                logger.info("keys len>>>>>>>"+keys.size());

                Iterator<SelectionKey> it=keys.iterator();
                SelectionKey key=null;

                while (it.hasNext()){
                    key=it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    }catch (Exception e){
                        if(key!=null){
                            key.cancel();
                            if(key.channel()!=null){
                                key.channel().close();
                            }
                        }
                    }
                }
            }catch (Throwable e){
                e.printStackTrace();
            }
        }

        if(selector!=null){
            try {
                selector.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }


    private void handleInput(SelectionKey key) throws IOException{
        if(key.isValid()){
            if(key.isAcceptable()){
                //多路複用器監聽到有新的客戶端接入,處理新的接入請求,完成TCP的三次握手
                //建立物理連接
                ServerSocketChannel ssc= (ServerSocketChannel) key.channel();
                logger.info("準備監聽客戶端連接");
                SelectableChannel sc=ssc.accept();
                logger.info("設置監聽完畢");
                //設置客戶端鏈路,爲非阻塞模式
                sc.configureBlocking(false);
                sc.register(selector,SelectionKey.OP_READ);
            }
            if(key.isReadable()){
                logger.info("key.isReadable");

                SocketChannel sc= (SocketChannel) key.channel();
                ByteBuffer byteBuffe=ByteBuffer.allocate(1024);
                //異步讀取客戶端請求消息到緩存區
                logger.info("異步讀取客戶端請求消息到緩存區>>"+byteBuffe.toString());
                int readBytes=sc.read(byteBuffe);
                if(readBytes>0){
                    //將緩衝區的當前limit設置爲postion,position設置爲0,用於後續對緩衝區的讀取操作
                    byteBuffe.flip();
                    byte [] bytes=new byte[byteBuffe.remaining()];
                    byteBuffe.get(bytes);
                    //對ByteBuffer進行編解碼,如果有半包消息指針reset,繼續讀取後續的報文
                    //將解碼成功的消息封裝成Task,投遞到業務線程池中,進行業務邏輯編排
                    String body=new String(bytes,"UTF-8");

                    logger.info("The Time server receive order : "+body);

                    String currentTime="QUERY TIME ORDER".equalsIgnoreCase(body)?
                            new java.util.Date(System.currentTimeMillis()).toString():"BAD ORDER";
                    //調用異步write接口,將消息異步發送到客戶端
                    doWrite(sc,currentTime);
                }else if(readBytes<0){
                    key.channel();
                    sc.close();
                }else {

                }
            }
        }
    }

    private void doWrite(SocketChannel channel,String response) throws IOException {
        if(response!=null&&response.trim().length()>0){
            byte [] bytes=response.getBytes();
            ByteBuffer writeBuffer=ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            writeBuffer.flip();
            channel.write(writeBuffer);
        }
    }
}

6、NIO的Client

1、NIO的Client的時序圖

在這裏插入圖片描述

2、Client源碼分析

(1)TimeClient
主要作用是聲明端口,啓動線程。

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-04-02 16:59
 */
public class TimeClient {
    public static void main(String [] args){
        Integer port=8085;
        if(args!=null&&args.length>0){
            try {
                port=Integer.valueOf(args[0]);
            }catch (Exception e){

            }
        }
        TimeHandle timeServer=new TimeHandle("127.0.0.1",port);
        new Thread(timeServer,"NIO-TimeHandle-001").start();
    }
}

(2)TimeHandle


import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Logger;

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-04-02 17:01
 */
public class TimeHandle implements Runnable{
    private String host;
    private int port;
    private Selector selector;
    private SocketChannel socketChannel;
    private volatile Boolean stop;
    public static Logger logger = Logger.getLogger("TimeHandle");

    public TimeHandle(String host,int port){
        this.host=host==null?"127.0.0.1":host;
        this.port=port;
        this.stop=false;
        try {
            selector=Selector.open();
            socketChannel=SocketChannel.open();
            socketChannel.configureBlocking(false);
        }catch (Exception e){
            e.printStackTrace();
            System.exit(1);
        }
        logger.info("TimeHandle構造完畢");
    }

    @Override
    public void run() {
        try {
            logger.info("run >> 開始連接");
            doConnect();
            logger.info("run >> 連接完畢");
        }catch (Exception e){
            e.printStackTrace();
            System.exit(1);
        }
        while (!stop){
            try {
                selector.select(1000);
//                logger.info("selector被喚醒>>>>>>>");
                Set<SelectionKey> selectionKeys=selector.selectedKeys();
//                logger.info("keys len>>>>>>>"+selectionKeys.size());
                Iterator<SelectionKey> it=selectionKeys.iterator();
                SelectionKey key=null;
                while (it.hasNext()){
                    key=it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    }catch (Exception e){
                        if(key!=null){
                            key.cancel();
                            if(key.channel()!=null){
                                key.channel().close();
                            }
                        }
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
                System.exit(1);
            }
        }

        if(selector!=null){
            try {
                selector.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }

    }


    private void handleInput(SelectionKey key) throws IOException {
//        logger.info("handleInput>>");
        if(key.isValid()){
//            logger.info("key.isValid>>");

            SocketChannel sc= (SocketChannel) key.channel();
            //測試此鍵的通道是否已完成其套接字連接操作。
            if(key.isConnectable()){
                logger.info("sc.isConnected>>");
                if(sc.finishConnect()){
                    logger.info("sc.finishConnect>>");

                    sc.register(selector,SelectionKey.OP_READ);
                    doWrite(sc);
                }else {
                    logger.info("Link fail exit...");
                    System.exit(1);
                }

                if(key.isReadable()){
                    ByteBuffer readBuffer=ByteBuffer.allocate(1024);
                    int readBytes=sc.read(readBuffer);
                    if(readBytes>0){
                        readBuffer.flip();
                        byte [] bytes=new byte[readBuffer.remaining()];
                        readBuffer.get(bytes);
                        String body=new String(bytes,"UTF-8");
                        System.out.println("Now is : "+body);
                        this.stop=true;
                    }else if(readBytes<0){
                        key.cancel();
                        sc.close();
                    }else {
                        //ignore
                    }
                }
            }
        }



    }


    private void doConnect() throws IOException {
        logger.info("連接>>"+host+">>"+port);
        if(socketChannel.connect(new InetSocketAddress(host,port))){
            socketChannel.register(selector,SelectionKey.OP_READ);
            doWrite(socketChannel);
        }else {
            socketChannel.register(selector,SelectionKey.OP_CONNECT);
        }
    }


    private void doWrite(SocketChannel channel) throws IOException {
        logger.info("doWrite>>>>");
        byte [] bytes="Hi Service O_O".getBytes();
        ByteBuffer writeBuffer=ByteBuffer.allocate(bytes.length);
        writeBuffer.put(bytes);
        writeBuffer.flip();
        channel.write(writeBuffer);
        if(!writeBuffer.hasRemaining()){
            logger.info("Send order 2 server succeed.");
        }
    }
}
3、運行結果

(1)啓動服務器

四月 08, 2019 5:09:25 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer
信息: The Time server is start in port: 8085

(2)啓動客戶端

四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle
信息: TimeHandle構造完畢
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle run
信息: run >> 開始連接
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle doConnect
信息: 連接>>127.0.0.1>>8085
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle run
信息: run >> 連接完畢
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle handleInput
信息: sc.isConnected>>
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle handleInput
信息: sc.finishConnect>>
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle doWrite
信息: doWrite>>>>
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle doWrite
信息: Send order 2 server succeed.

(3)服務器響應

四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer handleInput
信息: 準備監聽客戶端連接
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer handleInput
信息: 設置監聽完畢
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer handleInput
信息: key.isReadable
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer handleInput
信息: 異步讀取客戶端請求消息到緩存區>>java.nio.HeapByteBuffer[pos=0 lim=1024 cap=1024]
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer handleInput
信息: The Time server receive order : Hi Service O_O

以上就是NIO的運行流程。

參考文檔:
《Netty權威指南》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章