Netty(二):一文看懂Bio,Nio,Aio

基础概念

基础概念不了解的同学可以去看上一章,非常重要.

BIO(Blocking IO,第一章阻塞IO模型)

同步阻塞模型,一个客户端连接对应一个处理线程 .在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一个ServerSocket,然后在客户端启动Socket来对服务端进行通信.服务端接受到请求后,要指派或新建一个线程去处理客户端的IO请求,直到收到断开连接的指令。

应用场景:

BIO 方式适用于连接数目比较小且固定的架构, 这种方式对服务器资源要求比较高, 但程序简单易理解。

模拟

客户端:用windows上面的telnet localhost 9000可以连接上服务端,再按CTRL+]进入操作界面去模拟客户端连接,send命令发送.

服务端:我们先来看一段Server端的代码,我们监听了服务器的9000端口,然后一个while等待客户端连接,然后读取客户端给我们发的数据,有两个关键点serverSocket.accept(),clientSocket.getInputStream() 这两个是阻塞方法

public class SocketServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9000);
        while (true) {
            System.out.println("等待连接。。");
            //阻塞方法
            Socket clientSocket = serverSocket.accept();
            System.out.println("有客户端连接了。。");
            handler(clientSocket);
        }
    }

    private static void handler(Socket clientSocket) throws IOException {
        byte[] bytes = new byte[1024];
        System.out.println("准备read。。");
        //接收客户端的数据,阻塞方法,没有数据可读时就阻塞
        int read = clientSocket.getInputStream().read(bytes);
        System.out.println("read完毕。。");
        if (read != -1) {
            System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
        }
        clientSocket.getOutputStream().write("HelloClient".getBytes());
        clientSocket.getOutputStream().flush();
    }
}

缺点:

1.单线程,如果我一个客户端连接过来,不做任何读写操作,则线程阻塞,下一个客户端即使连接也做不了任何操作

伪异步 IO/非阻塞:下面我们优化一下这个代码,开启一个线程读取客户端信息,这样我就可以同时处理很多个客户端请求.

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9000);
        while (true) {
            System.out.println("等待连接。。");
            //阻塞方法
            Socket clientSocket = serverSocket.accept();
            System.out.println("有客户端连接了。。");
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        handler(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

缺点:

1、IO代码里read操作是阻塞操作,如果连接不做数据读写操作会导致线程阻塞,浪费资源,你他么连接了我,又不做读写,搞到一直阻塞.

2、如果连接很多就会创建很多线程,会导致服务器线程太多,压力太大,比如C10K问题,即使用了线程池,线程上下文切换也会消耗资源.

NIO(Non Blocking IO)

服务器实现模式为一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就进行处理,JDK1.4开始引入。

应用场景:

NIO方式适用于连接数目多且连接比较短(轻操作) 的架构, 比如聊天服务器, 弹幕系统, 服务器间通讯,编程比较复杂

代码演示

版本1:(第一章非阻塞IO模型)

下述代码服务端是单线程执行,却可以读取所有客户端连接的channel,是不是有点像redis.这是没引入selector的时候,把socket设置为非阻塞,需要我们自己去遍历每一个channel

public class NioServer {

    // 保存客户端连接
    static List<SocketChannel> channelList = new ArrayList<>();

    public static void main(String[] args) throws IOException, InterruptedException {

        /* 创建NIO ServerSocketChannel,与BIO的serverSocket类似
ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 
就像标准IO中的ServerSocket一样。
通过 ServerSocketChannel.accept() 方法监听新进来的连接。
当 accept()方法返回的时候,它返回一个包含新进来的连接的 SocketChannel。
因此, accept()方法会一直阻塞到有新连接到达.
ServerSocketChannel可以设置成非阻塞模式。在非阻塞模式下,
accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是null。 
因此,需要检查返回的SocketChannel是否是null.*/

        
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9000));
        // 设置ServerSocketChannel为非阻塞
        //必须配置为非阻塞才能往selector上注册,否则会报错,selector模式本身就是非阻塞模式
        serverSocket.configureBlocking(false);
        System.out.println("服务启动成功");

        while (true) {
            // 非阻塞模式accept方法不会阻塞,否则会阻塞
            //NIO非阻塞体现:此处accept方法是阻塞的,但是这里因为是发生了连接事件,所以这个方法会马上执行完,不会阻塞
            //处理完连接请求不会继续等待客户端的数据发送
            // NIO的非阻塞是由操作系统内部实现的,底层调用了linux内核的accept函数
            SocketChannel socketChannel = serverSocket.accept();
            if (socketChannel != null) { // 如果有客户端进行连接
                System.out.println("连接成功");
                // 设置SocketChannel为非阻塞,设置为true会报错
                socketChannel.configureBlocking(false);
                // 保存客户端连接在List中
                channelList.add(socketChannel);
            }
            // 遍历连接进行数据读取
            Iterator<SocketChannel> iterator = channelList.iterator();
            while (iterator.hasNext()) {
                SocketChannel sc = iterator.next();
                ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                // 非阻塞模式read方法不会阻塞,否则会阻塞
//NIO非阻塞体现:首先read方法不会阻塞,其次这种事件响应模型,当调用到read方法时肯定是发生了客户端发送数据的事件
                int len = sc.read(byteBuffer);
                // 如果有数据,把数据打印出来
                if (len > 0) {
                    System.out.println("接收到消息:" + new String(byteBuffer.array()));
                } else if (len == -1) { // 如果客户端断开,把socket从集合中去掉
                    iterator.remove();
                    System.out.println("客户端断开连接");
                }
            }
        }
    }
}

缺点:如果连接数太多的话,会有大量的无效遍历channel,假如有10000个连接,其中只有1000个连接有写数据,但是由于其他9000个连接并没有断开,我们还是要每次轮询遍历一万次,其中有十分之九的遍历都是无效的,这显然不是一个让人很满意的状态。那么我们能不能只获取有数据的channel来遍历呢?

版本2:(第一章IO多路复用模型)

NIO引入多路复用器代码示例,我们可以由一个线程去处理我们几十万个客户端连接,并且只对有事件发生的channel进行操作.

这里的多路复用器Selector,其实就是我们基础篇里面的IO多路复用模型,他可以把所有的连接都阻塞到select(poll,epoll,这三个函数功能一致,具体看下一章)函数,当有事件的时候我们才去做操作,除去很多无用功,由系统内核帮我们去完成事件的发现,我们就可以用一个或者少量线程去处理成千上万个连接了.

public class NioSelectorServer {

    public static void main(String[] args) throws IOException, InterruptedException {

        // 创建NIO ServerSocketChannel
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9000));
        // 设置ServerSocketChannel为非阻塞
        //必须配置为非阻塞才能往selector上注册,否则会报错,selector模式本身就是非阻塞模式
        serverSocket.configureBlocking(false);
        // 打开Selector处理Channel,即创建epoll
        Selector selector = Selector.open();
        // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务启动成功");

        while (true) {
            // 阻塞等待需要处理的事件发生
            selector.select();

            // 获取selector中注册的全部事件的 SelectionKey 实例
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            // 遍历SelectionKey对事件进行处理
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                // 如果是OP_ACCEPT事件,则进行连接获取和事件注册
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
            //NIO非阻塞体现:此处accept方法是阻塞的,但是这里因为是发生了连接事件,所以这个方法会马上执行完,不会阻塞
                    SocketChannel socketChannel = server.accept();
                 //设置为true会报错
                    socketChannel.configureBlocking(false);
                    // 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接成功");
                } else if (key.isReadable()) {  // 如果是OP_READ事件,则进行读取和打印
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(128);
          //NIO非阻塞体现:首先read方法不会阻塞,其次这种事件响应模型,当调用到read方法时肯定是发生了客户端发送数据的事件
                    int len = socketChannel.read(byteBuffer);
                    // 如果有数据,把数据打印出来
                    if (len > 0) {
                        System.out.println("接收到消息:" + new String(byteBuffer.array()));
                    } else if (len == -1) { // 如果客户端断开连接,关闭Socket
                        System.out.println("客户端断开连接");
                        socketChannel.close();
                    }
                } else if (key.isWritable()) {
                SocketChannel sc = (SocketChannel) key.channel();
                System.out.println("write事件");
                // NIO事件触发是水平触发
                // 使用Java的NIO编程的时候,在没有数据可以往外写的时候要取消写事件,
                // 在有数据往外写的时候再注册写事件
                key.interestOps(SelectionKey.OP_READ);
                //sc.close();
        }
                //从事件集合里删除本次处理的key,防止下次select重复处理
                iterator.remove();
            }
        }
    }
}

根据上述代码和下图我们来理解一下NIO工作流程

1.服务端开启ServerSocketChannel监听9000端口

2.新建了一个多路复用器Selector对象,把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣

3.selector等待客户端的事件发生

4.突然client1连接上了服务端,客户端socketChannel和服务端建立起了连接(accept事件),这个事件通过linux底层的硬中断感知到了,然后放进rdlist就绪事件列表里面.

5.selector.accpet 感知到了有事件发生(其实就是我们的rdlist有数据了,只遍历rdlist里面的数据),拿一下这个selectKey集合,这个key集合里面有所有和selector关联的channel的一个key,我们可以通过key拿到对应的channel.

6.拿到keyList我们肯定要遍历一下,看一下这些都是什么事件

7.因为客户端连接进来了,发生了accept事件我们根据key拿到了客户端的socketchannel

8.拿到客户端的scoketclient我们就把他关联到我们的selector,并且对他的写事件感兴趣.

9.然后循环回到selector.accpet 阻塞

10.client1发了一条数据给服务端,我们的selector感知到了有写事件发生,然后继续走上述流程

0

总结:版本一和版本二

为什么我们要交给系统的selelct去帮我们轮询有什么事件发生,而不在我们java程序轮询?

版本一是我们应用程序for循环去轮询,版本二是我们selector调用内核函数select(poll,epoll),select去轮询.

因为java程序轮询最终也是调用系统的函数,造成不必要的额外开销,而直接用系统函数,会把用户态的数据放进去内核态,帮你轮询. 

AIO(NIO 2.0)

异步非阻塞, 由操作系统完成后回调通知服务端程序启动线程去处理, 一般适用于连接数较多且连接时间较长的应用

应用场景:

AIO方式适用于连接数目多且连接比较长(重操作)的架构,JDK7 开始支持

AIO代码示例:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

public class AIOServer {

    public static void main(String[] args) throws Exception {
        final AsynchronousServerSocketChannel serverChannel =
                AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000));

        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            @Override
            public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
                try {
                    System.out.println("2--"+Thread.currentThread().getName());
                    // 再此接收客户端连接,如果不写这行代码后面的客户端连接连不上服务端
                    serverChannel.accept(attachment, this);
                    System.out.println(socketChannel.getRemoteAddress());
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer result, ByteBuffer buffer) {
                            System.out.println("3--"+Thread.currentThread().getName());
                            buffer.flip();
                            System.out.println(new String(buffer.array(), 0, result));
                            socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));
                        }

                        @Override
                        public void failed(Throwable exc, ByteBuffer buffer) {
                            exc.printStackTrace();
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                exc.printStackTrace();
            }
        });

        System.out.println("1--"+Thread.currentThread().getName());
        Thread.sleep(Integer.MAX_VALUE);
    }
}
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;

public class AIOClient {

    public static void main(String... args) throws Exception {
        AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000)).get();
        socketChannel.write(ByteBuffer.wrap("HelloServer".getBytes()));
        ByteBuffer buffer = ByteBuffer.allocate(512);
        Integer len = socketChannel.read(buffer).get();
        if (len != -1) {
            System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len));
        }
    }
}

由结果我们可以看到,服务端接收客户端连接,还有读取客户端消息都是不同的线程,不阻塞,异步.

BIO、 NIO、 AIO 对比

0

为什么Netty使用NIO而不是AIO?

在Linux系统上,AIO的底层实现仍使用Epoll,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化,Linux上AIO还不够成熟。Netty是异步非阻塞框架,Netty在NIO上做了很多异步的封装。

参考:操作系统中的中断(详细介绍+图片理解)_Ding_0110M的博客-CSDN博客
        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值