Netty学习--BIO与NIO比较

190 阅读5分钟

BIO与NIO比较

BIO:

  • 阻塞 :存在两个阻塞点
public static void main(String[] args) throws IOException {
        //创建socket服务,监听10101端口
        ServerSocket serverSocket = new ServerSocket(10101);
        System.out.println("服务器启动成功");
        while(true){
            //第一个阻塞点,accept()
            final Socket socket = serverSocket.accept();
            System.out.println("客户端连接成功!");
            //业务处理
            handler(socket);
        }
    }
            byte[] bytes = new byte[1024];
            InputStream inputStream  = socket.getInputStream();
            while (true){
                //读取数据(第二个阻塞点)
                int read = inputStream.read(bytes);
                if(read!=-1){
                    System.out.println(new java.lang.String(bytes,0,read));
                }else break;

如果服务器处于阻塞状态,又是单线程的话,那么就只能处理一个socket,解决的办法是用线程池,每个线程服务一个socket

 public static void main(String[] args) throws IOException {
        //创建socket服务,监听10101端口
        ServerSocket serverSocket = new ServerSocket(10101);
        System.out.println("服务器启动成功");
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        while(true){
            final Socket socket = serverSocket.accept();
            System.out.println("客户端连接成功!");
            newCachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    //业务处理
                    handler(socket);
                }
            });

服务器启动成功
客户端连接成功!
客户端连接成功!

这个时候就可以同时处理多个线程,但是这种处理方式相当于我们自己的饭店给每一个客人安排一个服务员去服务,显然这样做会极大的提高我们的成本。在计算机中也是如此,我们很难用这种模式去处理高并发的网络请求。


NIO

简单说一下流程:

  • 首先创建severChannelSocket,并设置成非阻塞的,就像是饭店先把门开了
  • serverChannelSocket绑定端口
  • 通过serverChannel.register(selector, SelectionKey.OP_ACCEPT)将serverChannelSocket和selector绑定,就像是让服务员看着点大门,有人来了就去招呼一声。这个时候创建工作就完成的差不多了。
  • selector通过select方法(I/O多路复用的一种方法,如果不太了解就去看看这方面的知识)获取到请求,判断这个请求是连接请求还是传输数据,根据请求去做相应的处理。
  • 如果是连接请求,通过key获取到channel(ServerSocketChannel server = (ServerSocketChannel) key .channel();),对channel进行设置,绑定channel.register(this.selector, SelectionKey.OP_READ),就相当于服务员开始关注这个客人的需求了

其中客户端就是channel

我们来对代码理解一下:

public class NIOServer {
    //通道管理器
 private Selector selector;
 public void initServer(int port) throws IOException {
        // 获得一个ServerSocketChannel,就像是先修个大门,把门开了
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        // 把这个门设置成非阻塞的
        serverChannel.configureBlocking(false);
        // 将这个门绑定到port端口
        serverChannel.socket().bind(new InetSocketAddress(port));
        // 获得一个通道管理器,类似于服务员
        this.selector = Selector.open();
        /**
        serverChannel和selector绑定,绑定事件是监听accept事件,
        就像是让服务员看着点大门,有人进来就招呼招呼。
        **/
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    }
public void listen() throws IOException {
        System.out.println("服务端启动成功!");
        // 轮询访问selector
        while (true) {
            /**
            当注册的事件到达时,方法返回;否则,该方法会一直阻塞,
            select就是IO多路复用的一种方法,
            就像是select监听哪些连接的服务器有了新的请求,
            如果不了解可以去看看IO多路复用
            **/
            selector.select();
            /**
            获得selector中选中的项的迭代器,选中的项为注册的事件,
            **/
            Iterator ite = this.selector.selectedKeys().iterator();
            while (ite.hasNext()) {
                SelectionKey key = (SelectionKey) ite.next();
                // 删除已选的key,以防重复处理
                ite.remove();
                //对拿到的key进行判断,到底是来人了还是有人要点菜
                // 客户端请求连接事件
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key
                            .channel();
                    // 获得和客户端连接的通道
                    SocketChannel channel = server.accept();
                    // 设置成非阻塞
                    channel.configureBlocking(false);

                    //在这里可以给客户端发送信息哦
                    channel.write(ByteBuffer.wrap(new String("向客户端发送了一条信息").getBytes()));
                    //在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
                    channel.register(this.selector, SelectionKey.OP_READ);

                    // 获得了可读的事件
                } else if (key.isReadable()) {
                    read(key);
                }

            }
         public void read(SelectionKey key) throws IOException {
        // 服务器可读取消息:得到事件发生的Socket通道
        SocketChannel channel = (SocketChannel) key.channel();
        // 创建读取的缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(10);
        int read = channel.read(buffer);
        if (read > 0) {
            channel.read(buffer);
            byte[] data = buffer.array();
            String msg = new String(data).trim();
            System.out.println("服务端收到信息:" + msg);
            ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
            channel.write(outBuffer);// 将消息回送给客户端
        }else{
            System.out.println("客户端关闭连接");
            key.cancel();
        }
    }
    
    public static void main(String[] args) throws IOException {
        NIOServer server = new NIOServer();
        server.initServer(8000);
        server.listen();
    }
}

疑问

1.关闭客户端后服务端死循环的问题: 解决方案:

ByteBuffer buffer = ByteBuffer.allocate(10);
        int read = channel.read(buffer);
        if (read > 0) {
            channel.read(buffer);
            byte[] data = buffer.array();
            String msg = new String(data).trim();
            System.out.println("服务端收到信息:" + msg);
            ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
            channel.write(outBuffer);// 将消息回送给客户端
        }else{
            System.out.println("客户端关闭连接");
            key.cancel();
        }

2.既然selector的select方法是阻塞的,为什么我们说NIO是非阻塞的呢?

  • 因为我们判断一个IO是否是阻塞的时候,我们关注的点是在于当我们发送数据或者接收数据的时候能否立即看到结果。
  • 其次,select方法也可以不阻塞,通过selector.select(long timeout)设置一个事件,超过这个事件则不继续阻塞。
  • 或者再selector.select()方法下面加上selector.wakeup();它可以唤醒select,让它立即返回。

3.关于Selector可以详见这篇文章:zhuanlan.zhihu.com/p/36930888

总结

suppose our application needs to read data from multiple sources:

  1. under old IO, we need a separate thread for each read operation
  2. under NIO, one single thread can handle multiple read operations

Note that a thread is a heavy system resource, it's expensive to set up and to do context-switch.

Selector: it can handle multiple channels at the same time, with following steps:

  1. create a channel
  2. configure the channel to be non-blocking
  3. register it with the selector
  4. get the registration key

The events that a channel can fire includes READ/WRITE/CONNECT/ACCEPT

Once it's set up, we call selector.select(), which is a blocking call till some channel registered to the selector has events to be handled.

参考

www.iteye.com/blog/weixia… www.bilibili.com/video/BV1Rb…

总结来源于B站一位朋友的评论