BIO-NIO-AIO

一、前言

Java支持的3种网络编程IO模式,分别为:BIO、NIO、AIO

BIO与NIO是同步的,AIO是异步的,AIO是对NIO的封装;

同步与异步主要体现在accept与read方法;

二、BIO

英文全称:Blocking IO;

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

2.1)缺点:

  • IO代码里read操作是阻塞操作,如果链接不做数据读写操作会导致线程阻塞,浪费资源;
  • 如果线程很多,会导致服务器线程太多,压力太大;

2.2)应用场景:

使用于连接数比较小且固定的架构,这种方式对服务器资源要求比较高,但程序简单容易理解; 例如传统的Socket、文件操作等;

2.3)注意:

1)serverSocket.accept()就是一个阻塞方法;一直等待客户端的连接;
2)socket.getInputStream().read(bytes)也是一个阻塞发放,一直等待客户端发送信息;
一旦发送阻塞,后面的逻辑都不会运行,也不会接受新的客户端链接,所以在服务端应该使用多线程的方式进行处理,但是又不可避免多线程过多而对系统造成一定的问题,因此高并发下不可用;

服务端示例代码:

 1 public class SocketServer {
 2     public static void main(String[] values) throws IOException {
 3         ServerSocket serverSocket = new ServerSocket(9000);
 4         while (true) {
 5             System.out.println("等待链接");
 6             /*
 7             * 等待客户端链接
 8             * 这里在客户端没有链接的时候会一直阻塞,直到有客户端建立了链接才会进行下一步
 9             * */
10             Socket socket = serverSocket.accept();
11             System.out.println("客户端连接上了");
12             new Thread(() -> {
13                 try {
14                     handler(socket);
15                 } catch (Exception e) {
16                     e.printStackTrace();
17                 }
18             });
19         }
20     }
21 
22     private static void handler(Socket socket) throws IOException {
23         System.out.println(Thread.currentThread().getId());
24         byte[] bytes = new byte[1024];
25         /*
26         * 这里会一直阻塞,一直要等到客户端发送消息后才会进行下一步
27         * */
28         int read = socket.getInputStream().read(bytes);
29         System.out.println("读取完毕");
30         if (read != -1) {
31             System.out.println("接受到的数据:" + new String(bytes, 0, read));
32             System.out.println("thread id=" + Thread.currentThread().getId());
33 
34         }
35         //回执消息
36         socket.getOutputStream().write("Hi~~~~~~~~~~~~~".getBytes());
37         socket.getOutputStream().flush();
38     }
39 }

客户端示例代码:

public class SocketClient {
    public static void main(String[] values) throws IOException {
        Socket socket = new Socket("127.0.0.1", 9000);
        //发送数据
        socket.getOutputStream().write("123".getBytes());
        socket.getOutputStream().flush();
        System.out.println("发送数据结束");
        byte[] bytes   = new byte[1024];
        //接收信息
        socket.getInputStream().read(bytes);
        System.out.println("接收到的信息:" + new String(bytes));
        socket.close();
    }
}

三、NIO

英文全称:Non Blocking IO

Redis就是一个NIO的经典应用;

同步非阻塞,服务器端实现模式为一个咸亨可以处理多个请求,客户端发送的连接请求都会注册到多路服用器(selector)上,多路服用器轮询到连接有IO请求就进行处理;
IO多路复用一般用的Linux API(select,poll,epoll)来实现,他们区别如下:

  select poll epoll(jdk 1.5及以上)
操作方式 遍历 遍历 回调
底层实现 数组 链表 哈希表
IO效率

每次调用都进行线性遍历,

时间复杂度:O(n)

每次调用都进行线性遍历,

时间复杂度:O(n)

事件通知方式,每当有IO事件就绪,系统注册的回调函数就会备调用,

时间复杂度O(1)

最大连接 有上限 无上限 无上限

3.1)NIO适用场景:

适用于链接数目多且连接比较短的架构,比如:聊天服务器,弹幕系统,服务器之间的通讯,编程比较复杂;是JDK1.4开始支持的;

3.2)NIO三大组件:

Channel:通道,类似于流,每个Channel对应一个buffer缓冲区;
Buffer:缓冲区,其实就是一个数组;
Selector:选择器;

基本概念:
1)channel注册到selector上,由selector根据Channel读写事件的发生将其交由某个空闲的线程处理;
2)selector可以对应一个或多个线程;
3)NIO的Buffer与channel都是即可以读也可以写;

服务端示例代码:

 1 public class NioServer {
 2 
 3 //    /**
 4 //     * 属性描述:这里可以使用线程的方式进行性能的提升
 5 //     * @date : 2019/12/18 0018 下午 6:03
 6 //     */
 7 //    public static ExecutorService pool = Executors.newFixedThreadPool(10);
 8 
 9     public static void main(String[] values) throws IOException {
10         //创建一个通道,通道建立成功表示服务已经建立成功,后面需要对服务进行配置;
11         ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
12         //必须配置为非阻塞,这样才能往Selector上注册,否则会报异常,selector模式本身就是非阻塞模式
13         serverSocketChannel.configureBlocking(false);
14         //绑定一个监听的端口
15         serverSocketChannel.socket().bind(new InetSocketAddress(9000));
16         //创建选择器selector
17         Selector selector = Selector.open();
18         /*
19          * 将ServerSocketChannel注册到Selector上,并且Selector对客户端Accept链接明感;
20          * 这里会得到SelectionKey
21          * */
22         serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
23 
24         while (true) {
25             System.out.println("等待事件被触发");
26             /*
27              * 轮询监听channel里的Key,Select是阻塞的,accept也是阻塞的;
28              * 在客户端没有链接的时候这里是会一直阻塞的;
29              * 一旦发生阻塞,这里就会变成非阻塞,执行后的代码;
30              * */
31             selector.select();
32             /*
33              * 接受到数据后就会进入下一步
34              * */
35             System.out.println("事件被触发");
36             /*
37              * 有客户端请求,被轮询监听到;
38              * selector.selectedKeys()中保存了当前链接的事件信息;
39              * 如果有多个客户端发生链接、写等事件,都会在这里体现;
40              * */
41             Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
42             while (keyIterator.hasNext()) {
43                 SelectionKey selectionKey = keyIterator.next();
44                 //删除本次已经处理的Key,防止下次select被重复处理;
45                 keyIterator.remove();
46                 //这里可以放到线程池里执行,增加并发处理量
47                 handle(selectionKey);
48             }
49         }
50     }
51 
52     private static void handle(SelectionKey selectionKey) throws IOException {
53         //根据事件进行不同的处理
54         if (selectionKey.isAcceptable()) {
55             //链接事件处理
56             System.out.println("有客户端链接成功");
57             ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
58             /*
59              * NIO非阻塞体现:此处accept方法是阻塞的,但是这里因为发送了链接时间,所以这个方法会马上执行万,不会阻塞;
60              * 处理完成链接请求不会继续等待客户端发送的数据;
61              * 注意:这里是通过serverSocketChannel.accept()服务端的API获取到了客户端链接的SocketChannel,
62              * 成服务端的Channel与客户端的Channel是同一个东西,就是通道
63              * */
64             final SocketChannel accept = serverSocketChannel.accept();
65             //配置非阻塞
66             accept.configureBlocking(false);
67             //通过Selector监听Channel时对读事件明感;
68             accept.register(selectionKey.selector(), SelectionKey.OP_READ);
69         } else if (selectionKey.isReadable()) {
70             System.out.println("有客户端的数据读写发送了");
71             SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
72             ByteBuffer buffer = ByteBuffer.allocate(1024);
73             //NIO阻塞体现:首先reda方法不会阻塞,其次这种事件响应模式,当调用到read方法的时候肯定发生了客户端发送数据的事件
74             int len = socketChannel.read(buffer);
75             if (len != -1) {
76                 System.out.println("读取到客户端发送的数据:" + new String(buffer.array(), 0, len));
77             }
78             ByteBuffer bufferToWrite = ByteBuffer.wrap("Hello".getBytes());
79             socketChannel.write(bufferToWrite);
80             selectionKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
81             socketChannel.close();
82         }
83     }
84 
85 }

客户端示例代码:

public class NioClient {

    /**
     * 属性描述:通道管理器
     * @date : 2019/12/17 0017 下午 8:06
     */
    private Selector selector;

    public static void main(String[] values) throws IOException {
        NioClient nioClient = new NioClient();
        nioClient.initClient("127.0.0.1", 9000);
        nioClient.connect();
    }

    /**
    * 功能描述:采用轮询的方式进行监听,监听Selector上是否有需要处理的事件
    * @date : 2019/12/17 0017 下午 8:08
    */
    private void connect() throws IOException {
        //轮询访问selector
        while (true){
            /*
            * 选择一组可以进行I/O操作的事件,放在selector中客户端的方法不会被阻塞
            * 这里与服务端的方法不一样,当至少一个通道被选中时,selector的wakeup方法会被调用,
            * 方法返回,对于客户端来说,通道时一直被选中的
            * */
            selector.select();
            Iterator<SelectionKey> selectionKeys = this.selector.selectedKeys().iterator();
            while (selectionKeys.hasNext()){
                SelectionKey key = selectionKeys.next();
                selectionKeys.remove();
                if(key.isConnectable()){
                    SocketChannel channel = (SocketChannel) key.channel();
                    //如果正在链接,则完成链接;
                    if(channel.isConnectionPending()){
                        channel.finishConnect();
                    }
                    //设置为非阻塞
                    channel.configureBlocking(false);
                    ByteBuffer buffer = ByteBuffer.wrap("Hello.........".getBytes());
                    channel.write(buffer);
                    channel.register(this.selector, SelectionKey.OP_READ);
                }else if(key.isReadable()){
                    read(key);
                }
            }
        }
    }

    /**
    * 功能描述:读取信息的处理
    * @date : 2019/12/17 0017 下午 8:16
    */
    private void read(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(512);
        int len = channel.read(buffer);
        if(len!=-1){
            System.out.println("客户端收到消息:" + new String(buffer.array(), 0, len));
        }
    }

    /**
    * 功能描述:获取一个Socket通道,并对该通道做一些初始化工作
    * @date : 2019/12/17 0017 下午 8:07
    */
    private void initClient(String ip, int port) throws IOException {
        //获取通道
        SocketChannel channel = SocketChannel.open();
        //设置为非阻塞
        channel.configureBlocking(false);
        //获取通道管理器
        this.selector = Selector.open();

        /*
        * 客户端链接服务器,其实方法执行并没有实现链接,需要在listen方法中调用
        * 用channel.finishConnect()才能完成链接
        * */
        channel.connect(new InetSocketAddress(ip, port));
        //将通道管理器和该通道绑定,并未该通道注册SelectionKey.OP_CONNECT事件
        channel.register(selector, SelectionKey.OP_CONNECT);
    }
}

3.3)NIO处理流程:

 

 

服务端:
1)创建服务,指定监听端口及模式,这里注意,链接模式必须为非阻塞模式,否则报错;
2)创建选择器Selector;
3)先注册一个链接事件,这里开始就应该是一个链接事件,后面好进行后续处理;
4)创建循环,不断的通过select()方法监听事件,这个API是阻塞的,当客户端创建链接或者写入数据的时候都会被触发;
如果客户端没有任何事情发生,这里阻塞是正常的,不会对程序造成任何影响,有活干活没事自己一边凉快去;
5)通过选择器的selectedKeys方法获取当前所有的事件描述对象SelectionKey;
6)为了避免重复处理可以将集合中正在处理的SelectionKey移除;
7)通过不同的事件类型进行处理;
8)如果当前事件为”连接“(isAcceptable)类型,可以通过key对象(SelectionKey)可以获取到对应事件的channel,这里的SocketChannel不管是客户端还是服务端,其实都是同一个东西;
这里需要注意,操作服务端通过selectionKey.channel()拿到的就是服务端Channel(ServerSocketChannel),操作客户端通过selectionKey.channel()拿到的是客户端的SocketChannel;通过强转是可以进行转换的;
类型为注册的时候拿到的是服务端Channel(ServerSocketChannel),对客户端信息的读写使用的是客户端Channel(SocketChannel);
ServerSocketChannel.accept方法本身是阻塞的,但是NIO中可以调用此API方法的时候必定是注册事件发生了,所以这里的阻塞可以忽略不计;注意,如果放错地方,比如放到读数据里就会发生阻塞;
SocketChannel.read方法本身是阻塞的,但是在NIO中可以调用此方法的时候必定是写入事件发生了,所以这里的阻塞可以忽略不急;注意,如果放错地方,比如放到注册事件里就会发生阻塞;
9)这里可以进行其他的操作,比如链接成功后再次通过SocketChannel的register方法注册当前的读事件;
10)在写入或读取处理完成后,设置再次处理的感知事件注册,将SocketChannel关闭;

3.4)NIO在开发中存在的问题:

NIO在实际的开发中代码多而容易出错,所以在正常使用的时候,我们一般是采用Netty的方式;
已经链接的处理还没有处理完成的时候,新的链接会被搁置,达到一定数量的时候会拒绝链接;
如果链接处理使用多线程,那么会面对更多的线程并发问题的处理;
ByteBuffer设计很XXXX,使用很迷惑,非常反人类;中间包括了很多读写转换,非常容易出BUG,所以现在一般都是使用Netty来开发;

posted @ 2019-12-18 18:21  xxsd  阅读(166)  评论(0)    收藏  举报