BIO,NIO,AIO总结

BIO

阻塞IO, 最常见的就是Socket连接了。

上代码:

服务端:

public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(9998, 100, InetAddress.getByName("127.0.0.1"));
            System.out.println("等待连接到达..........");
          while (true){
            // 是阻塞的,直到客户端连接到达
            Socket socket = serverSocket.accept();
            System.out.println("接收到一个连接");
            // 多线程处理,可以让socket接收更多的客户端,而不至于阻塞在等待客户端回写数据的地方
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        handler(socket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
//            如果使用同步的,下面等待客户端响应数据的地方一直阻塞住,如果这时候有另外一个客户端连接过来,服务端是没法连接上的
//            新的客户端那边会发送数据成功,但是这边接收不到,就丢了数据
//            handler(socket);
          }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handler(Socket socket) throws IOException {
          byte []leng=new byte[1024];
          // 这个方法也是阻塞的,如果客户端不发送数据,一直等待
        System.out.println("等待客户端输入数据..........");
         socket.getInputStream().read(leng);
         System.out.println(new String(leng));
        System.out.println("客户端输入数据结束。");
        }

 

客户端:

public static void main(String[] args) {
        try {
            Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 9998);
              OutputStream outputStream = socket.getOutputStream();
                outputStream.write("你好服务端".getBytes());
                outputStream.flush();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

 BIO(Blocking IO)

同步阻塞模型,一个客户端连接对应一个处理线程。

IO模型:

 

 

缺点:

1:IO代码里read操作是阻塞操作,如果连接不做数据读写操作会导致线程阻塞,浪费资源。

2:如果线程很多,会导致服务器线程太多,压力太大。

应用场景:

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

NIO(Non Blocking IO)

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

I/O多路复用底层一般用Linux API (select ,poll,epoll)来实现,他们的区别如下表:最新的也是效率最高的是epoll,从主动轮询变成被动接收消息通知。

  select poll epoll(jdk1.5以上)
操作方式 遍历 遍历 回调
底层实现 数组 链表 哈希表
IO效率 每次调用都进行线程遍历,时间复杂度O(n) 每次调用都进行线程遍历,时间复杂度O(n) 事件通知方式,每当有IO事件就绪,系统注册的回调函数就会被调用,时间复杂度O(1)
最大连接 有上限 无上限 无上限

  应用场景:

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

 

 NIO的三大组件:Channel(通道) ,Buffer(缓冲区),Selector(选择器)

IO模型:

 

 1:channel类似于流,每个channel对应一个buffer缓冲区,buffer底层是个数组。

2:channel会注册到selector上,由selector根据channel读写事件的发生将其交由某个空闲的线程处理。

3:selector可以对应一个或多个线程。

4:NIO的buffer和channel都是既可以读也可以写。

 

服务端:单线程

  public static void main(String[] args) throws IOException {
//        创建一个在本地端口进行监听的服务Scoket通道,并设置为非阻塞的
        ServerSocketChannel ssc = ServerSocketChannel.open();
//        必须配置为非阻塞才能往selector上注册,否则会报错,selector模式本身就是非阻塞模式
        ssc.configureBlocking(false);
//      绑定本地的一个端口号
        ssc.socket().bind(new InetSocketAddress(1009));
//        创建一个选择器selector   对于linux操作系统来说  会调用epoll_create的内核函数。创建一个epoll的实例。
        Selector selector = Selector.open();
//        把ServerSocketChannel 注册到selector上,并且selector对客户端的accept事件感兴趣,就是监听这个事件
//        每个注册channel都对应一个key  注册表也是根据这个key找到对应的channel   
        SelectionKey selectionKey = ssc.register(selector, SelectionKey.OP_ACCEPT);

        while (true){
            System.out.println("等待事件发生");
//            轮询监听key select 是阻塞的, BIO 中 accept()是阻塞的 ,多路复用器,这里不再阻塞之后,就可以得到那些有IO事件发生的连接,比如连接事件,读取事件。那些没有IO事件发生的连接这里不会获取出来。
// 底层调用linux操作系统 内核epoll_ctl 函数,开始真正的监听事件,然后调用epoll_wait函数等待事件的发生
selector.select(); System.out.println("有事件发生了"); // 有客户端请求,被轮询监听到了 Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); // 如果有多个事件同时到达,会按顺序处理 如果处理的时间过长,有新的事件到达还会处理吗???????? while (iterator.hasNext()){ SelectionKey key = iterator.next(); // 删除本次已经处理的key,防止下次select重复处理 iterator.remove(); handle(key); } } } private static void handle(SelectionKey key) throws IOException { // 根据传入的key 判断是哪一种事件 if (key.isAcceptable()){ System.out.println("有客户端连接事件发生了"); // 知道它是连接事件,所以可以知道它是什么类型的 这个事件是服务端这边的事件 所以这个channel是服务端这边的 ServerSocketChannel ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); // NIO 非阻塞体现: 此处accept方法是阻塞的,但是这里因为是发生了了连接事件,所以这个方法会马上执行完,不会阻塞 // 处理完连接请求 不会继续等待客户端的数据 SocketChannel sc = ssc.accept(); sc.configureBlocking(false); // 通过Selector监听Channel时对读事件感兴趣 sc.register(key.selector(),SelectionKey.OP_READ); }else if (key.isReadable()){ System.out.println("有客户端有可读事件发生"); // 因为这个事件是客户端发送请求的,所以这个通道是客户端那边的通道 SocketChannel // 每个channel都有自己绑定的key SocketChannel sc= (SocketChannel) key.channel(); // ByteBuffer buffer = ByteBuffer.allocate(1024); // NIO非阻塞的体现: 首先read方法不会阻塞,其次是这种事件响应模型,当调用到read方法时肯定是客户端发生了发送数据的事件 int len = sc.read(buffer); if (len!=-1){ System.out.println("客户端读取到的数据:"+new String(buffer.array(),0,len)); } ByteBuffer bufferWrite = ByteBuffer.wrap("helloClient".getBytes()); // 通道是双向的,可以从客户端读和向客户端写 sc.write(bufferWrite); // 设置一下channel的监听事件 是通过key来绑定的 key.interestOps(SelectionKey.OP_READ| SelectionKey.OP_WRITE); sc.close(); } }

 

客户端:

public static void main(String[] args) {
        NIOClient nioClient = new NIOClient();
        try {
            nioClient.initClient("127.0.0.1",1009);
            nioClient.connect();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private void connect() throws IOException {
//        轮询访问selector
        while(true){
//            选择一组可以进行IO操作的事件,放在slector中,客户端的该方法不会阻塞,
//            这里和服务端的方法不一样,查看API注释可以知道,当至少有一个通道被选中时,
//            selector的wakeup方法被调用 方法返回  而对于客户端来说,通道一直是被选中的
            this.selector.select();
//            获取slector中选中的项的迭代器
            Iterator<SelectionKey> iterator = this.selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                 SelectionKey key=   iterator.next();
//                 删除已选的key ,以免重复处理。
                iterator.remove();
//                连接事件发生
                if (key.isConnectable()){
                    SocketChannel channel= (SocketChannel) key.channel();
//                    如果正在连接 则完成连接
                     if (channel.isConnectionPending()){
                         channel.finishConnect();
                     }
//                     设置成非阻塞的
                    channel.configureBlocking(false);
//                     在这里可以给服务器发送信息
                    ByteBuffer buffer = ByteBuffer.wrap("你好服务端".getBytes());
                    channel.write(buffer);
//                    在和服务端连接成功之后  为了可以接收到服务端的信息  需要给通道设置读的事件
                    channel.register(this.selector,SelectionKey.OP_READ);

                }else if (key.isReadable()){
//                    和服务端的read方法一样
//                    服务器可读取消息  得到事件发生的Socket通道
                    SocketChannel channel= (SocketChannel) key.channel();
//                    创建读取的缓冲区
                    ByteBuffer allocate = ByteBuffer.allocate(512);
                    int read = channel.read(allocate);
                    if (read!=-1){
                        System.out.println("客户端收取到信息:"+new String(allocate.array(),0,read
                        ));
                    }
                }

            }
        }
    }
    public void initClient(String ip,int port) throws IOException {
//          获取一个Socket通道
        SocketChannel channel = SocketChannel.open();
//        设置通道为非阻塞的
        channel.configureBlocking(false);
//        设置一个通道管理器
        this.selector=Selector.open();
//        客户端连接服务器 其实方法执行并没有实现连接 需要在listen()方法中 调用
//        channel.finishConnect() 才能完成连接
        boolean connect = channel.connect(new InetSocketAddress(ip, port));
//   将通道管理器和该通道绑定  并为通道注册 SelectionKey.OP_CONNECT
//        注意这里注册和服务端事件不一样  服务端是ACCEPT
        SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_CONNECT);
    } 

 

 

 

 

 

 

NIO流程:

 

 

NIO流程说明:

 

 Resdis就是典型的NIO线程模型,selector收集所有连接的事件并且转交给后端线程,线程连续执行所有事件命令并将结果回写客户端。

 

 AIO (NIO 2.0)

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

应用场景:

AIO方式适用于连接数目多且连接比较长(重操作)的架构,从JDK1.7开始支持。AIO其实是对NIO进行的封装。虽然AIO是对NIO的封装,但是它的效率不一定比NIO的高,在工作用AIO的也不多。

异步和同步以及阻塞和非阻塞的区别:

网上举例最多的就是水壶烧水的例子:

 

 

 

 同步和异步在我们的IO通信中就是,得到连接事件或读写事件是主动发现的还是被动告知的,如果是主动发现的,比如BIO,NIO中都有while循环,调用accept,select才能知道有什么事件到达。异步中如果有事件到达是主动通知调用方的。阻塞和非阻塞在IO通信中就是对于处理事件的线程的来说,接收事件的消息内容是不是阻塞的,如果是阻塞的就是一直等待事件消息的到来,如果是非阻塞的就是不用一直等待消息内容的到来,可以先去处理其他事情,等事件有消息内容到来的时候再去处理。

 我们从上面的NIO的线程模型知道,在linux系统中内核版本2.6+,主要是靠epoll相关函数实现的,主要是epoll_create,epoll_ctl,epoll_wait。在redis中单线程也能处理高并发也是靠NIO的这个线程模型实现的。

Reactor 响应式编程,基于事件驱动的响应式编程。

其实上面的NIO模型就是典型的基于事件驱动的响应式编程模型,它这个事件是由操作系统中断程序触发的,进一步解释就是当有新的IO事件到达操作系统时候,操作系统的中断程序会判断这是那一个socket的连接,然后放到就绪事件列表里面,epoll_wait不再阻塞,select() 不再阻塞。

基础的响应式编程模型如下:

单线程模型:

 

 

这是最基本的单Reactor单线程模型。其中Reactor线程,负责多路分离套接字,有新连接到来触发connect 事件之后,交由Acceptor进行处理,有IO读写事件之后交给hanlder 处理。

Acceptor主要任务就是构建handler ,在获取到和client相关的SocketChannel之后 ,绑定到相应的hanlder上,对应的SocketChannel有读写事件之后,基于racotor 分发,hanlder就可以处理了(所有的IO事件都绑定到selector上,由Reactor分发)。

该模型 适用于处理器链中业务处理组件能快速完成的场景。不过,这种单线程模型不能充分利用多核资源,所以实际使用的不多。

 

单Reactor多线程模型

 

 相对于第一种单线程的模式来说,在处理业务逻辑,也就是获取到IO的读写事件之后,交由线程池来处理,这样可以减小主reactor的性能开销,从而更专注的做事件分发工作了,从而提升整个应用的吞吐。

3:多Reactor多线程模型 ,主从模型

 

 第三种模型比起第二种模型,是将Reactor分成两部分:

  1. mainReactor负责监听server socket,用来处理新连接的建立,将建立的socketChannel指定注册给subReactor。

  2. subReactor维护自己的selector, 基于mainReactor 注册的socketChannel多路分离IO读写事件,读写网 络数据,对业务处理的功能,另其扔给worker线程池来完成。

第三种模型中,我们可以看到,mainReactor 主要是用来处理网络IO 连接建立操作,通常一个线程就可以处理,而subReactor主要做和建立起来的socket做数据交互和事件业务处理操作,它的个数上一般是和CPU个数等同,每个subReactor一个线程来处理。

此种模型中,每个模块的工作更加专一,耦合度更低,性能和稳定性也大量的提升,支持的可并发客户端数量可达到上百万级别。

 

 

 

 

 



 

posted @ 2021-07-25 18:26  蒙恬括  阅读(103)  评论(0编辑  收藏  举报