1. Netty 源碼解析 Part 0——第1篇:BIO vs NIO

        共 41632字,需瀏覽 84分鐘

         ·

        2021-04-22 08:11


        -     前言    -


        本文源碼地址:
        https://gitee.com/wangjianxin199003/netty-source-code-analysis.git

        使用過java的同學想必對BIO和NIO這兩個詞匯并不陌生,即便平時工作中沒有接觸過,也會在招聘需求里見過,或者面試被問到過。那么BIO和NIO到底表示什么意思呢,BIO即blocking io,NIO有的解釋為new io,有的解釋為none blocking io,個人認為none blocking io更為準確,至于為什么,咱們接下來看。


        -     BIO hello word    -


        1.1 BIO服務端
        /**
         * 歡迎關注公眾號“種代碼“,獲取博主微信深入交流
         *
         * @author wangjianxin
         */

        public class HelloBioServer {
            public static void main(String[] args) throws IOException {
                //創(chuàng)建ServerSocket
                ServerSocket serverSocket = new ServerSocket();
                //綁定到8000端口
                serverSocket.bind(new InetSocketAddress(8000));
                new BioServerConnector(serverSocket).start();
            }
        }
        /**
         * 歡迎關注公眾號“種代碼“,獲取博主微信深入交流
         *
         * @author wangjianxin
         */

        public class BioServerConnector {
            private final ServerSocket serverSocket;

            public BioServerConnector(ServerSocket serverSocket) {
                this.serverSocket = serverSocket;
            }

            public void start() {
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        while (true) {
                            Socket newSocket = null;
                            try {
                                //阻塞在這里等待新連接,返回值為一條新的連接
                                newSocket = serverSocket.accept();
                            } catch (IOException e) {
                            }
                            //將新連接交給handler處理
                            new BioServerHandler(newSocket).start();
                        }
                    }
                });
                thread.start();
            }
        }
        /**
         * 歡迎關注公眾號“種代碼“,獲取博主微信深入交流
         *
         * @author wangjianxin
         */

        public class BioServerHandler {
            private final Socket socket;

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

            public void start() {
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        while (true) {
                            try {
                                InputStream inputStream = socket.getInputStream();
                                byte[] buffer = new byte[1024];
                                //阻塞操作,因為不知道inputStream中什么時候會有數(shù)據(jù)可讀,只能阻塞在這里等待
                                //每一個連接都要消耗一個線程
                                int readLength = inputStream.read(buffer);
                                if (readLength != -1) {
                                    String name = new String(buffer, 0, readLength);
                                    System.out.println(name);
                                    //打印客戶端發(fā)送過來的數(shù)據(jù)
                                    socket.getOutputStream().write(("hello, " + name).getBytes());
                                }
                            } catch (Throwable e) {
                                try {
                                    socket.close();
                                } catch (IOException ioException) {
                                }
                            }
                        }
                    }
                });
                thread.start();
            }
        }

        該bio服務端共有3個類,HelloBioServer、BioServerConnector、BioServerHandler,麻雀雖小,五臟俱全,是典型的BIO服務最小架構。

        • HelloBioServer:啟動類,創(chuàng)建一個ServerSocket,并交給BioServerConnetor處理
        • BioServerConnector:接受新連接的類,其中創(chuàng)建一個線程,循環(huán)阻塞在ServerSocket上等待新連接的到來,每建立一條新連接,就創(chuàng)建一個BioServerHandler,并將該連接交給BioServerHandler處理
        • BioServerHandler:處理連接上數(shù)據(jù)的類,每個BioServerHandler都創(chuàng)建一個線程循環(huán)阻塞在Socket的InputStream上,讀取數(shù)據(jù),再為該數(shù)據(jù)拼上“Hello, ”發(fā)送回去。

        1.2 BIO客戶端

        /**
         * 歡迎關注公眾號“種代碼“,獲取博主微信深入交流
         *
         * @author wangjianxin
         */
        public class HelloBioClient {
            //創(chuàng)建多少個客戶端
            private static final int CLIENTS = 2;

            public static void main(String[] args) throws IOException {
                for (int i = 0; i < CLIENTS; i++) {
                    final int clientIndex = i;
                    Thread client = new Thread(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                //創(chuàng)建socket
                                Socket socket = new Socket();
                                //連接8000端口
                                socket.connect(new InetSocketAddress(8000));
                                while (true) {
                                    OutputStream outputStream = socket.getOutputStream();
                                    //向服務端發(fā)送”zhongdaima" + 客戶端編號
                                    outputStream.write(("
        zhongdaima" + clientIndex).getBytes());
                                    InputStream inputStream = socket.getInputStream();
                                    byte[] buffer = new byte[1024];
                                    int readLength = inputStream.read(buffer);
                                    //打印服務端返回數(shù)據(jù)
                                    System.out.println(new String(buffer, 0, readLength));
                                    Thread.sleep(1000);
                                }
                            } catch (Throwable e) {

                            }
                        }
                    });
                    client.start();
                }
            }
        }


        BIO客戶端共1個類,HelloBioClient,在該客戶端中創(chuàng)建了兩個線程、兩條連接,每個線程處理一條連接,循環(huán)向服務端發(fā)送“zhongdaima" + 客戶端編號,并打印服務端返回的數(shù)據(jù)。



        -     NIO hello word    -


        2.1 NIO服務端


        /**
         * 歡迎關注公眾號“種代碼“,獲取博主微信深入交流
         * @author wangjianxin
         */

        public class HelloNioServer {
            public static void main(String[] args) throws IOException {
                //創(chuàng)建ServerSocketChannel
                ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
                //設置Channel為非阻塞
                serverSocketChannel.configureBlocking(false);
                //綁定到8000端口
                serverSocketChannel.bind(new InetSocketAddress(8000));
                //交給Connector
                new NioServerConnector(serverSocketChannel).start();
            }
        }
        /**
         * 歡迎關注公眾號“種代碼“,獲取博主微信深入交流
         * @author wangjianxin
         */

        public class NioServerConnector {
            private final ServerSocketChannel serverSocketChannel;

            private final Selector selector;

            private final NioServerHandler nioServerHandler;

            public NioServerConnector(ServerSocketChannel serverSocketChannel) throws IOException {
                this.selector = Selector.open();
                this.serverSocketChannel = serverSocketChannel;
                //向selector注冊Channel,感興趣事件為OP_ACCEPT(即新連接接入)
                this.serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT, serverSocketChannel);
                this.nioServerHandler = new NioServerHandler();
                this.nioServerHandler.start();
            }

            public void start() {
                Thread serverConnector = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        while (true) {
                            try {
                                if (NioServerConnector.this.selector.select() > 0) {
                                    Set<SelectionKey> selectionKeys = NioServerConnector.this.selector.selectedKeys();
                                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                                    while (iterator.hasNext()) {
                                        SelectionKey key = iterator.next();
                                        try {
                                            if (key.isAcceptable()) {
                                                //新連接接入
                                                SocketChannel socketChannel = ((ServerSocketChannel) key.attachment()).accept();
                                                socketChannel.configureBlocking(false);
                                                //把新連接交給serverHandler
                                                NioServerConnector.this.nioServerHandler.register(socketChannel);
                                            }
                                        } finally {
                                            iterator.remove();
                                        }
                                    }
                                }
                            } catch (IOException e) {

                            }
                        }
                    }
                });
                serverConnector.start();
            }
        }
        /**
         * 歡迎關注公眾號“種代碼“,獲取博主微信深入交流
         * @author wangjianxin
         */

        public class NioServerHandler {
            private final Selector selector;

            private final BlockingQueue<SocketChannel> prepareForRegister = new LinkedBlockingDeque<>();

            public NioServerHandler() throws IOException {
                this.selector = Selector.open();
            }

            public void register(SocketChannel socketChannel) {
                //這里為什么不直接注冊呢,因為當有線程在selector上select時,register操作會阻塞
                //從未注冊過channel時,start方法中的線程會一直阻塞,在這里調用register的線程也會一直阻塞
                //所以我們把待注冊的channel放入隊列中,并且換醒start方法中的線程,讓start方法中的線程去注冊

                //放入待注冊隊列
                try {
                    this.prepareForRegister.put(socketChannel);
                } catch (InterruptedException e) {

                }
                //喚醒阻塞在selector上的線程(即下面start方法中創(chuàng)建的線程)
                this.selector.wakeup();

            }

            public void start() {
                Thread serverHandler = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            while (true) {
                                //只需要1個線程就可以監(jiān)視所有連接
                                //當select方法返回值大于0時,說明注冊到selector的Channels有我們感興趣的事件發(fā)生
                                //返回值代表有多少Channel發(fā)生了我們感興趣的事件
                                if (NioServerHandler.this.selector.select() > 0) {
                                    //緊接著調用selectedKeys方法獲取發(fā)生事件的Key集合
                                    Set<SelectionKey> selectionKeys = NioServerHandler.this.selector.selectedKeys();
                                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                                    //遍歷Key集合,處理Channel io事件
                                    while (iterator.hasNext()) {
                                        SelectionKey key = iterator.next();
                                        try {
                                            if (key.isReadable()) {
                                                SocketChannel socketChannel = (SocketChannel) key.attachment();
                                                ByteBuffer buffer = ByteBuffer.allocate(1024);
                                                int readLength = socketChannel.read(buffer);
                                                buffer.flip();
                                                System.out.println(new String(buffer.array(), 0, readLength));
                                                socketChannel.write(ByteBuffer.wrap(("hello, " + new String(buffer.array(), 0, readLength)).getBytes()));
                                            }
                                        } finally {
                                            iterator.remove();
                                        }
                                    }
                                }
                                SocketChannel socketChannel;
                                while ((socketChannel = NioServerHandler.this.prepareForRegister.poll()) != null) {
                                    //注冊待注冊的channel,感興趣事件為OP_READ(即可讀事件)
                                    socketChannel.register(NioServerHandler.this.selector, SelectionKey.OP_READ, socketChannel);
                                }
                            }
                        } catch (IOException e) {

                        }
                    }
                });
                serverHandler.start();
            }
        }


        和BIO服務端類似,NIO服務端也有3個類,分別是HelloNioServer、NioServerConnector和NioServerHandler。


        • HelloNioServer:啟動類,創(chuàng)建一個ServerSocketChannel,將Channel設置為非阻塞的,綁定到8000端口,交給Connector處理。到這里我們應該明白了為什么NIO是none blocking io,這里比BIO多了一步操作,即將Channel設置為非阻塞的。具體哪里體現(xiàn)出了非阻塞,我們繼續(xù)往下看。

        • NioServerConnector:處理新連接的類,該類接收一個ServerSocketChannel,創(chuàng)建一個Selector,并向Selector注冊Channel,感興趣事件為OP_ACCEPT(即新連接接入),并創(chuàng)一個NioServerHandler的實例。NioServerConnector的start方法中創(chuàng)建一個線程,循環(huán)向selector詢問是否有新連接接入,一旦發(fā)現(xiàn)有新連接接入,就把新連接交給NioServerHandler處理。這里與BIOServerConnector中不同的是,有新連接接入時,不必再創(chuàng)建一個新的Handler,而是所有連接共用一個Handler。

        • NioServerHandler:處理連接數(shù)據(jù)上類,該類start方法中創(chuàng)建一個線程循環(huán)向Selector詢問是否有可讀事件發(fā)生。一旦某些連接上有可讀事件發(fā)生,就讀取這些連接上的數(shù)據(jù),并為該數(shù)據(jù)添加上“Hello, ”再發(fā)送回去。然后再處理新的連接注冊,將新連接注冊到Selector上,感興趣事件為OP_READ(即可讀事件)。與BioServerHandler不同的是BioServerHandler中一個線程只能處理一條連接,而NioServerHandler中一個線程可以處理多條連接。


        好了,至此我們已經看到了NIO的非阻塞體現(xiàn)在socketChannel.read()方法是非阻塞的,而BIO的阻塞體現(xiàn)在inputstream.read()方法是阻塞的。


        2.2 NIO客戶端


        /**
         * 歡迎關注公眾號“種代碼“,獲取博主微信深入交流
         *
         * @author wangjianxin
         */

        public class HelloNioClient {
            private static final int CLIENTS = 2;

            public static void main(String[] args) throws IOException {
                Thread client = new Thread(new Runnable() {
                    final Selector selector = Selector.open();
                    final SocketChannel[] clients = new SocketChannel[CLIENTS];

                    @Override
                    public void run() {
                        //創(chuàng)建兩個客戶端
                        for (int i = 0; i < CLIENTS; i++) {
                            try {
                                //連接8000端口
                                SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(8000));
                                //將channel設置為非阻塞的
                                socketChannel.configureBlocking(false);
                                //注冊到selector
                                socketChannel.register(this.selector, SelectionKey.OP_READ, socketChannel);
                                //保存channel
                                clients[i] = socketChannel;
                            }catch (Throwable e){

                            }
                        }
                        for (int i = 0; i < Integer.MAX_VALUE; i++) {
                            try {
                                //向服務端發(fā)送“zhongdaima" + 客戶端編號
                                for (int j = 0; j < clients.length; j++) {
                                    this.clients[j].write(ByteBuffer.wrap(("zhongdaima" + j).getBytes()));
                                }
                                //監(jiān)視Channel是否有可讀事件發(fā)生
                                if (this.selector.select() > 0) {
                                    Set<SelectionKey> selectionKeys = this.selector.selectedKeys();
                                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                                    while (iterator.hasNext()){
                                        SelectionKey key = iterator.next();
                                        try {
                                            SocketChannel channel = (SocketChannel) key.attachment();
                                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                                            int read = channel.read(buffer);
                                            buffer.flip();
                                            //打印服務端返回數(shù)據(jù)
                                            System.out.println(new String(buffer.array(), 0, read));
                                        }finally {
                                            iterator.remove();
                                        }
                                    }
                                }
                                Thread.sleep(1000);
                            }catch (Throwable e){

                            }
                        }
                    }
                });
                client.start();

            }
        }


        NIO客戶端只有一個類HelloNioClient,其中創(chuàng)建一個線程、兩條連接,循環(huán)向服務端發(fā)送“zhongdaima" + 客戶端編號,然后向Selctor循問是否有可讀事件發(fā)生,一旦有可讀事件發(fā)生,就讀取數(shù)據(jù),并打印。與HelloBioClient不同的是HelloNioClient創(chuàng)建了兩條連接,卻只使用了一個線程,而HelloBioClient中創(chuàng)建了兩條連接,使用了兩個線程。



        -      相比 BIO,NIO 有哪些優(yōu)勢?    -


        讀到這里,大家仔細品品NIO比BIO的優(yōu)勢在哪里。優(yōu)勢在哪里呢,要首先看看有什么區(qū)別,上面的代碼具體有什么區(qū)別我都加粗表示了,看完之后第一感覺應該就是NIO比BIO節(jié)省線程。


        3.1 BIO模型

        這是BIO的示意圖,比較簡單,每條連接都需要一個線程來處理,因為無法得得連接中什么時候有數(shù)據(jù)可以讀取,只能傻傻等待。


        3.2 NIO模型


        這是NIO的示意圖,與BIO相比,其中多了一個組件Selector。正是由于Selector的存在讓NIO與BIO產生了本質的不同。BIO中線程直接阻塞在1條連接上,直到有數(shù)據(jù)可讀取才返回,而且NIO中線程首先阻塞在Selector上,而Selector上可以注冊多條連接。


        線程調用select方法向Selector詢問是否有感興趣的事件發(fā)生,阻塞在select方法上,直接到1條或者多條連接上有事件發(fā)生才返回。此時線程已經知道哪些連接上有事件發(fā)生了,于是去處理這些連接上的事件。處理完成之后再次阻塞在Selector的select方法上,如此往復。


        至此我們已經發(fā)現(xiàn),BIO和NIO的本質不同在于中間多了一層代理Selector,而Selector具備監(jiān)視多條連接的能力。


        3.3 舉個例子


        開一家BIO模型的飯店,飯店里只有1個廚師(相當于Thread),有1位顧客(相當于連接)來吃飯,廚師就一直為這1位顧客做飯,直到這個顧客結賬走了(連接關閉),廚師才開始為下1位顧客做飯。如果需要同時滿足10個顧客吃飯,就要10個廚師。


        開一家NIO模型的飯店,飯店里有1個廚師(相當于Thread),還有1個服務員(相當于Selector),有10位顧客來吃飯,服務員就為這10位顧客點餐(向Selector注冊),并且需要知道顧客你們都點什么菜(向Selector注冊時的興趣事件)。廚師問服務員顧客都點了什么菜(Selector.select()),開始做菜,做完菜之后再問服務員顧客們又點了什么菜,如此往復。只需要1個廚師、1個服務員就可以為多個顧客提供服務。


        很顯然,如果你開飯店,你是開BIO飯店呢,還是NIO飯店呢。



        -      總結    -


        NIO的非阻塞體現(xiàn)在socketChannel.read()方法是非阻塞的,而BIO的阻塞體現(xiàn)在inputstream.read()方法是阻塞的。


        NIO一個線程可以處理多條連接,而BIO一個線程只能處理一條連接,NIO更節(jié)省線程資源。


        作者:王建新,轉轉架構部資深Java工程師,主要負責服務治理、RPC框架、分布式調用跟蹤、監(jiān)控系統(tǒng)等。

        來源公眾號:種代碼

        瀏覽 45
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 极品留学生与老外啪啪 | 成年男女免费视频网站在线观看 | 日本黄色一区二区三区 | 性生交大片免费视频法国 | 影音先锋男人资源在线 |