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 {
        //创建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中什么时候会有数据可读,只能阻塞在这里等待
                        //每一个连接都要消耗一个线程
                        int readLength = inputStream.read(buffer);
                        if (readLength != -1) {
                            String name = new String(buffer, 0, readLength);
                            System.out.println(name);
                            //打印客户端发送过来的数据
                            socket.getOutputStream().write(("hello, " + name).getBytes());
                        }
                    } catch (Throwable e) {
                        try {
                            socket.close();
                        } catch (IOException ioException) {
                        }
                    }
                }
            }
        });
        thread.start();
    }
}

该bio服务端共有3个类,HelloBioServer、BioServerConnector、BioServerHandler,麻雀虽小,五脏俱全,是典型的BIO服务最小架构。

  • HelloBioServer:启动类,创建一个ServerSocket,并交给BioServerConnetor处理
  • BioServerConnector:接受新连接的类,其中创建一个线程,循环阻塞在ServerSocket上等待新连接的到来,每建立一条新连接,就创建一个BioServerHandler,并将该连接交给BioServerHandler处理
  • BioServerHandler:处理连接上数据的类,每个BioServerHandler都创建一个线程循环阻塞在Socket的InputStream上,读取数据,再为该数据拼上“Hello, ”发送回去。

1.2 BIO客户端

/**
 * 欢迎关注公众号“种代码“,获取博主微信深入交流
 *
 * @author wangjianxin
 */
public class HelloBioClient {
    //创建多少个客户端
    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 {
                        //创建socket
                        Socket socket = new Socket();
                        //连接8000端口
                        socket.connect(new InetSocketAddress(8000));
                        while (true) {
                            OutputStream outputStream = socket.getOutputStream();
                            //向服务端发送”zhongdaima" + 客户端编号
                            outputStream.write(("
zhongdaima" + clientIndex).getBytes());
                            InputStream inputStream = socket.getInputStream();
                            byte[] buffer = new byte[1024];
                            int readLength = inputStream.read(buffer);
                            //打印服务端返回数据
                            System.out.println(new String(buffer, 0, readLength));
                            Thread.sleep(1000);
                        }
                    } catch (Throwable e) {

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


BIO客户端共1个类,HelloBioClient,在该客户端中创建了两个线程、两条连接,每个线程处理一条连接,循环向服务端发送“zhongdaima" + 客户端编号,并打印服务端返回的数据。



-     NIO hello word    -


2.1 NIO服务端


/**
 * 欢迎关注公众号“种代码“,获取博主微信深入交流
 * @author wangjianxin
 */

public class HelloNioServer {
    public static void main(String[] args) throws IOException {
        //创建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方法中创建的线程)
        this.selector.wakeup();

    }

    public void start() {
        Thread serverHandler = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {
                        //只需要1个线程就可以监视所有连接
                        //当select方法返回值大于0时,说明注册到selector的Channels有我们感兴趣的事件发生
                        //返回值代表有多少Channel发生了我们感兴趣的事件
                        if (NioServerHandler.this.selector.select() > 0) {
                            //紧接着调用selectedKeys方法获取发生事件的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:启动类,创建一个ServerSocketChannel,将Channel设置为非阻塞的,绑定到8000端口,交给Connector处理。到这里我们应该明白了为什么NIO是none blocking io,这里比BIO多了一步操作,即将Channel设置为非阻塞的。具体哪里体现出了非阻塞,我们继续往下看。

  • NioServerConnector:处理新连接的类,该类接收一个ServerSocketChannel,创建一个Selector,并向Selector注册Channel,感兴趣事件为OP_ACCEPT(即新连接接入),并创一个NioServerHandler的实例。NioServerConnector的start方法中创建一个线程,循环向selector询问是否有新连接接入,一旦发现有新连接接入,就把新连接交给NioServerHandler处理。这里与BIOServerConnector中不同的是,有新连接接入时,不必再创建一个新的Handler,而是所有连接共用一个Handler。

  • NioServerHandler:处理连接数据上类,该类start方法中创建一个线程循环向Selector询问是否有可读事件发生。一旦某些连接上有可读事件发生,就读取这些连接上的数据,并为该数据添加上“Hello, ”再发送回去。然后再处理新的连接注册,将新连接注册到Selector上,感兴趣事件为OP_READ(即可读事件)。与BioServerHandler不同的是BioServerHandler中一个线程只能处理一条连接,而NioServerHandler中一个线程可以处理多条连接。


好了,至此我们已经看到了NIO的非阻塞体现在socketChannel.read()方法是非阻塞的,而BIO的阻塞体现在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() {
                //创建两个客户端
                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 {
                        //向服务端发送“zhongdaima" + 客户端编号
                        for (int j = 0; j < clients.length; j++) {
                            this.clients[j].write(ByteBuffer.wrap(("zhongdaima" + j).getBytes()));
                        }
                        //监视Channel是否有可读事件发生
                        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();
                                    //打印服务端返回数据
                                    System.out.println(new String(buffer.array(), 0, read));
                                }finally {
                                    iterator.remove();
                                }
                            }
                        }
                        Thread.sleep(1000);
                    }catch (Throwable e){

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

    }
}


NIO客户端只有一个类HelloNioClient,其中创建一个线程、两条连接,循环向服务端发送“zhongdaima" + 客户端编号,然后向Selctor循问是否有可读事件发生,一旦有可读事件发生,就读取数据,并打印。与HelloBioClient不同的是HelloNioClient创建了两条连接,却只使用了一个线程,而HelloBioClient中创建了两条连接,使用了两个线程。



-      相比 BIO,NIO 有哪些优势?    -


读到这里,大家仔细品品NIO比BIO的优势在哪里。优势在哪里呢,要首先看看有什么区别,上面的代码具体有什么区别我都加粗表示了,看完之后第一感觉应该就是NIO比BIO节省线程。


3.1 BIO模型

这是BIO的示意图,比较简单,每条连接都需要一个线程来处理,因为无法得得连接中什么时候有数据可以读取,只能傻傻等待。


3.2 NIO模型


这是NIO的示意图,与BIO相比,其中多了一个组件Selector。正是由于Selector的存在让NIO与BIO产生了本质的不同。BIO中线程直接阻塞在1条连接上,直到有数据可读取才返回,而且NIO中线程首先阻塞在Selector上,而Selector上可以注册多条连接。


线程调用select方法向Selector询问是否有感兴趣的事件发生,阻塞在select方法上,直接到1条或者多条连接上有事件发生才返回。此时线程已经知道哪些连接上有事件发生了,于是去处理这些连接上的事件。处理完成之后再次阻塞在Selector的select方法上,如此往复。


至此我们已经发现,BIO和NIO的本质不同在于中间多了一层代理Selector,而Selector具备监视多条连接的能力。


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的非阻塞体现在socketChannel.read()方法是非阻塞的,而BIO的阻塞体现在inputstream.read()方法是阻塞的。


NIO一个线程可以处理多条连接,而BIO一个线程只能处理一条连接,NIO更节省线程资源。


作者:王建新,转转架构部资深Java工程师,主要负责服务治理、RPC框架、分布式调用跟踪、监控系统等。

来源公众号:种代码

浏览 26
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报