五、05【Java IO模型】之BIO NIO AIO介绍

IO模型

IO模型就是说用什么样的通道进行数据的发送和接收。

在《Unix网络编程》一书中提到了五种IO模型,分别是:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO

Java支持3种网络编程IO模式:BIO(阻塞IO),NIO(非阻塞IO),AIO(异步非阻塞IO)

BIO(Blocking IO)

同步阻塞IO,就是一个客户端连接对应一个处理线程。

在最开始学Java的时候,IO流那些都是属于BIO模型的。

缺点

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

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

应用场景

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

代码示例

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Socket 流套接字 服务端
 * ...
 */
public class SocketServer {

    public static void main(String[] args) throws IOException {
        // 创建服务器套接字并绑定到指定的端口。
        ServerSocket serverSocket = new ServerSocket(9999);

        // 死循环,保证服务端一直运行在线
        while (true) {
            System.out.println("服务端等待连接:" + System.currentTimeMillis());
            // 监听要建立到此套接字的连接并接受。该方法将一直阻塞,直到建立连接。
            final Socket socket = serverSocket.accept();
            System.out.println("监听到客户端,建立连接");
            new Thread(new Runnable() {
                public void run() {
                    try {
                        handler(socket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

    private static void handler(Socket socket) throws IOException {
        System.out.println("Thread ID = " + Thread.currentThread().getId());
        byte[] bytes = new byte[1024];
        System.out.println("准备read");
        // 接收客户端的数据,也是阻塞方法,没有数据可读时就阻塞
        int read = socket.getInputStream().read(bytes);
        System.out.println("read完毕");
        if (read != -1) {
            System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
            System.out.println("Thread ID = " + Thread.currentThread().getId());
        }
        // 向客户端响应写出数据
        socket.getOutputStream().write("HelloClient".getBytes());
        socket.getOutputStream().flush();
    }
}
import java.io.IOException;
import java.net.Socket;

/**
 * Socket 流套接字 客户端
 * ...
 */
public class SocketClient {

    public static void main(String[] args) throws IOException {
        // 创建一个流套接字并将其连接到指定的主机端口
        Socket socket = new Socket("localhost", 9999);

        // 向服务端发送数据
        socket.getOutputStream().write("HelloServer".getBytes());
        socket.getOutputStream().flush();
        System.out.println("客户端向服务端发送数据结束!");
        byte[] bytes = new byte[1024];

        // 接收服务端响应数据
        socket.getInputStream().read(bytes);
        System.out.println("接收到服务端的数据:" + new String(bytes));

        // 关闭流套接字
        socket.close();
    }
}

BIO模型

 NIO(Non Blocking IO)

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

 应用场景

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

NIO模型

 NIO三大核心组件:Buffer(缓冲区)、 Channel(通道)、Selector(选择器)

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

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

3)selector 可以对应一个或多个线程.

4)NIO 的 Buffer 和 channel 都是既可以读也可以写.

代码示例

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

/**
 * NIO 服务端
 * ...
 */
public class NioServer {

    public static void main(String[] args) throws IOException {
        // 创建一个在本地端口进行监听的服务Socket通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        // 设置非阻塞模式。否则会报错,selector模式本身就是非阻塞模式
        ssc.configureBlocking(false);
        // 绑定端口
        ssc.socket().bind(new InetSocketAddress(9999));
        // 创建一个选择器
        Selector selector = Selector.open();
        // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        // 无限循环保证服务端在线运行
        while (true) {
            System.out.println("服务端等待事件发生");
            // 轮询监听channel里的key,select是阻塞的,accept()也是阻塞的
            int select = selector.select();
            System.out.println("有事件发生了="+select);
            // 有客户端请求,被轮询监听到
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = it.next();
                // 删除当前已处理的key,防止下次select重复处理
                it.remove();
                // 处理监听到key
                handle(key);
            }
        }
    }

    private static void handle(SelectionKey key) throws IOException {
        if (key.isAcceptable()) {
            System.out.println("连接事件");
            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 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 bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes());
            sc.write(bufferToWrite);
            key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
        } else if (key.isWritable()) {
            SocketChannel sc = (SocketChannel) key.channel();
            System.out.println("write事件");
            // NIO事件触发是水平触发
            // 使用Java的NIO编程的时候,在没有数据可以往外写的时候要取消写事件,
            // 在有数据往外写的时候再注册写事件
            key.interestOps(SelectionKey.OP_READ);
            // sc.close();
        }
    }
}

NIO服务端程序步骤

1)创建一个 ServerSocketChannel 和 Selector ,并将 ServerSocketChannel 注册到 Selector

2)selector 通过 select() 方法监听 channel 事件,当客户端连接时,selector 监听到连接事件, 获取到 ServerSocketChannel 注册时绑定的 selectionKey

3)selectionKey 通过 channel() 方法可以获取绑定的 ServerSocketChannel

4)ServerSocketChannel 通过 accept() 方法得到 SocketChannel

5)将 SocketChannel 注册到 Selector 上,关注 read 事件

6)注册后返回一个 SelectionKey,和该 SocketChannel 关联

7)selector 继续通过 select() 方法监听事件,当客户端发送数据给服务端,selector 监听到read事件,获取到 SocketChannel 注册时绑定的 selectionKey

8)selectionKey 通过 channel() 方法可以获取绑定的 socketChannel

9)将 socketChannel 里的数据读取出来

10)用 socketChannel 将服务端数据写回客户端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

/**
 * NIO 客户端
 * ...
 */
public class NioClient {

    // 通道管理器
    private Selector selector;

    /**
     * 启动客户端测试
     *
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        NioClient client = new NioClient();
        client.initClient("127.0.0.1", 9999);
        client.connect();
    }

    /**
     * 获得一个Socket通道,并对该通道做一些初始化的工作
     *
     * @param ip   连接的服务器的ip
     * @param port 连接的服务器的端口号
     * @throws IOException
     */
    public void initClient(String ip, int port) throws IOException {
        // 获得一个Socket通道
        SocketChannel channel = SocketChannel.open();
        // 设置通道为非阻塞
        channel.configureBlocking(false);
        // 获得一个通道管理器
        this.selector = Selector.open();
        // 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调用 channel.finishConnect(); 才能完成连接
        channel.connect(new InetSocketAddress(ip, port));
        // 将通道管理器和该通道绑定,并为该通道注册OP_CONNECT事件
        channel.register(selector, SelectionKey.OP_CONNECT);
    }

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

总结

NIO模型的 selector 就像一个大总管,负责监听各种IO事件,然后转交给后端线程去处理;

NIO相对于BIO非阻塞的体现在,BIO的后端线程需要阻塞等待客户端写数据(比如read方法),如果客户端不写数据线程就要阻塞,NIO把等待客户端操作的事情交给了 selector。selector 负责轮询所有已注册的客户端,监听到有事件发生了才转交给后端线程处理,后端线程不需要做任何阻塞等待,直接处理客户端事件的数据即可,处理完马上结束,或返回线程池供其他客户端事件继续使用。还有 channel 的读写也是非阻塞的。

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

 AIO(NIO 2.0)

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

应用场景

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

代码示例

BIO&NIO&AIO对比

同步&异步

同步(synchronous)同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步。

异步(asynchronous)异步与同步相反,则是这个任务不需要等待当前调用返回,而是通过依靠事件、回调等机制来实现任务间次序关系。

阻塞&非阻塞

阻塞(blocking)在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件满足才能继续,比如数据读取、写入操作完成。

非阻塞(non-blocking)非阻塞则是不管IO操作是否结束,直接返回,相应操作在后台继续处理。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值