【Java NIO】网络IO模型与Java中的NIO之间的联系

目录

1 前言

2 五种网络IO模型

2.1 阻塞IO模型(BIO)

2.2 非阻塞IO(NIO)

2.3 多路复用IO模型

2.4 异步IO(AIO)

2.5 信号驱动IO模型

3 Java NIO与IO的区别

3.1 面向流与面向缓冲区

3.2 阻塞与非阻塞IO

4 Java NIO与IO模型

4.1 Java NIO实现阻塞IO模型

4.2 Java NIO实现非阻塞IO模型

4.3 Java NIO实现多路复用IO模型


1 前言

学习Java NIO这个知识点花费了几天时间,通过阅读大量的译文和博客文章,终于有点头绪了,虽说不是深入了解,但好歹是明白BIO、NIO、AIO这几种模型之间的区别,附带着了解了Java NIO的相关类源码,也算是有点收获。这里总结一下自己所了解的IO模型,以及它们在Java NIO中的应用。

2 五种网络IO模型

以linux系统为例,分别是阻塞IO模型(BIO)、非阻塞IO模型(NIO)、多路复用IO模型、异步IO模型(AIO)、信号驱动IO模型。对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,它会经历两个阶段:

  1. 等待数据在内核中准备就绪
  2. 将数据从内核拷贝到用户进程中

2.1 阻塞IO模型(BIO)

当进程发起read请求后,产生了一条系统调用:recvfrom,之后进程便陷入了阻塞状态直到内核return OK。内核中经历了 数据未就绪——>数据准备就绪——>将数据从内核拷贝到进程空间中——>return ok 这个过程,所以,阻塞IO模型的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。同理,进程发起的write请求也会阻塞,直到所有的数据都被写入到内核中。

在阻塞IO模型下,socket通信的server同一时刻只能处理一个客户端的连接请求,在多用户连接的场景下会导致其他用户连接等待,所以都会采用“一连接一线程”的方式来处理,server每accept一个connect,就会new一个线程去单独处理这个连接。为了避免过多的创建线程(线程会占用系统资源,同时线程的切换代价也比较大),可以使用线程池去管理,复用已创建的线程资源,减少线程的创建个数。

2.2 非阻塞IO(NIO

当进程发起read请求后,若内核中数据未准备就绪,不会阻塞。内核会返回一个标志,进程通过这个标志可以知道数据未就绪,从而可以做其他事情,在后续操作中,进程可以不断地发起read请求来判断数据是否就绪。若内核中数据就绪,进程会阻塞在本次read请求操作上,直到将数据从内核复制到进程空间中并return OK。write请求也类似,当进程执行write操作时,操作会立刻返回,可以通过内核返回的标志判断数据是否完全写入。

所以,该模型在等待数据就绪阶段是非阻塞的,在数据从内核复制到进程中时是阻塞的。非阻塞的接口相比于阻塞型接口的显著差异在于,在被调用之后立即返回。

在非阻塞IO模型中,可以只用一个线程来管理多个连接,即一个server线程在accept多个连接后,循环对这些连接执行read操作,若当前遍历连接数据已就绪,则读取数据,然后继续遍历。但这种模式决不被推荐,因为,循环调用recv()将大幅度推高CPU 占用率;此外,在这个方案中recv()更多的是起到检测“操作是否完成”的作用,实际操作系统提供了更为高效的检测“操作是否完成“作用的接口,例如select()多路复用模式,可以一次检测多个连接是否活跃。

2.3 多路复用IO模型

它的基本原理就是Linux中select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。从图中可以看到,这次不需要我们主动执行read操作了,而是进程首先调用select函数,调用select之后进程会阻塞,直到内核中数据准备就绪返回标志通知进程,此时进程从阻塞中回复,直接执行read操作读取数据,阻塞直到return OK。

多路复用IO模型在检测连接数据是否就绪和数据复制过程中都会阻塞。

显然,多路复用IO模型比非阻塞IO模型更适合一个线程管理多个连接的场景,因为使用select函数来判断数据是否准备就绪比主动执行recvfrom来轮训具有更好的性能,判断直接发生在内核空间,省去了大量的recvfrom系统调用。

注意

  1. 如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对於单个连接能处理得更快,而是在于能处理更多的连接。
  2. 该模型将数据探测和事件相应夹杂在一起,一旦某个连接的事件执行体庞大,则会大大推迟其它连接的数据探测和事件执行。

2.4 异步IO(AIO)

从图中可以看到,当进程请求一个aio_read系统调用后就直接返回,接下来在内核中自动完成数据的准备的复制工作,之后内核给进程发送一个信号,通知它数据准备完毕,然后进程执行read操作读取即可。

异步模型全程非阻塞,数据的准备工作都在内核中异步完成。

异步IO是真正非阻塞的,它不会对请求进程产生任何的阻塞,因此对高并发的网络服务器实现至关重要。

2.5 信号驱动IO模型

信号驱动IO,调用sigaltion系统调用,当内核中IO数据就绪时以SIGIO信号通知请求进程,请求进程再把数据从内核读入到用户空间,这一步是阻塞的。

3 Java NIO与IO的区别

IO NIO
面向流 面向缓冲区
阻塞IO 非阻塞IO
无选择器 有选择器

 

3.1 面向流与面向缓冲区

 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。

Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

3.2 阻塞与非阻塞IO

参考上面提到的阻塞IO模型与非阻塞IO模型。

Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。

Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

4 Java NIO与IO模型

Java NIO是阻塞IO模型、非阻塞IO模型和多路复用IO模型的组合体,可以单独实现这三种模型中的任何一种,使用最多的是多路复用IO模型。使用的组件包括channel、buffer和selector,这里不介绍了。下面简单使用Java NIO代码体现这三种模型的socket通信。

4.1 Java NIO实现阻塞IO模型

显然,如同Java IO一样,这里socketChannel.read(buf)会阻塞直到内核空间中数据准备和复制完毕,然后将数据读到缓冲区。

// client创建通道并建立连接
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));
// 创建缓冲区并从通道中读数据到缓冲区中
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);

socketChannel.close();

4.2 Java NIO实现非阻塞IO模型

通过socketChannel.configureBlocking(false);可以将通道设置为非阻塞模式,该模式下read方法执行候会直接返回,通过返回的int值可以判断是否有数据可读(0)、读到了多少字节数据(>0)、是否读完了数据(-1)。

// client创建通道并建立连接
SocketChannel socketChannel = SocketChannel.open();
// #############这里配置非阻塞模式###################
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));
// 创建缓冲区并从通道中读数据到缓冲区中
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);        // bytesRead为读到的字节个数
byte[] bytes = new byte[Integer.MAX_VALUE];

// bytesRead = -1表示读到了尾部
while (bytesRead != -1){
    if (bytesRead == 0){
        // 没有数据可读
        // do something else
    }

    // 从缓冲区中取出读到的数据
    buf.flip();
    buf.get(bytes);
    buf.clear();
    bytesRead = socketChannel.read(buf);
}
// 打印结果
System.out.println(new String(bytes));

socketChannel.close();

4.3 Java NIO实现多路复用IO模型

通过selector选择器,可以让一个server线程管理大量建立的连接,阅读下面代码你就会发现设计的精妙之处。

public class Server implements Runnable{
    //1 多路复用器(管理所有的通道)
    private Selector seletor;
    //2 建立缓冲区
    private ByteBuffer readBuf = ByteBuffer.allocate(1024);
    private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
    public Server(int port){
        try {
            //1 打开路复用器
            this.seletor = Selector.open();
            //2 打开服务器通道
            ServerSocketChannel ssc = ServerSocketChannel.open();
            //3 设置服务器通道为非阻塞模式
            ssc.configureBlocking(false);
            //4 绑定地址
            ssc.bind(new InetSocketAddress(port));
            //5 把服务器通道注册到多路复用器上,并且监听连接事件
            ssc.register(this.seletor, SelectionKey.OP_ACCEPT);

            System.out.println("Server start, port :" + port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while(true){
            try {
                //1 必须要让多路复用器开始监听
                this.seletor.select();
                //2 返回多路复用器已经选择的结果集
                Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();
                //3 进行遍历
                while(keys.hasNext()){
                    //4 获取一个选择的元素
                    SelectionKey key = keys.next();
                    //5 直接从容器中移除就可以了
                    keys.remove();
                    //6 如果是有效的
                    if(key.isValid()){
                        //7 如果为阻塞状态
                        if(key.isAcceptable()){
                            this.accept(key);
                        }
                        //8 如果为可读状态
                        if(key.isReadable()){
                            this.read(key);
                        }
                        //9 写数据
                        if(key.isWritable()){
                            this.write(key); 
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void write(SelectionKey key){}

    private void read(SelectionKey key) {
        try {
            //1 清空缓冲区旧的数据
            this.readBuf.clear();
            //2 获取之前注册的socket通道对象
            SocketChannel sc = (SocketChannel) key.channel();
            //3 读取数据
            int count = sc.read(this.readBuf);
            //4 如果没有数据
            if(count == -1){
                key.channel().close();
                key.cancel();
                return;
            }
            //5 有数据则进行读取 读取之前需要进行复位方法(把position 和limit进行复位)
            this.readBuf.flip();
            //6 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据
            byte[] bytes = new byte[this.readBuf.remaining()];
            //7 接收缓冲区数据
            this.readBuf.get(bytes);
            //8 打印结果
            String body = new String(bytes).trim();
            System.out.println("Server : " + body);

            // 9..可以写回给客户端数据 
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void accept(SelectionKey key) {
        try {
            //1 获取服务通道
            ServerSocketChannel ssc =  (ServerSocketChannel) key.channel();
            //2 执行阻塞方法
            SocketChannel sc = ssc.accept();
            //3 设置阻塞模式
            sc.configureBlocking(false);
            //4 注册到多路复用器上,并设置读取标识
            sc.register(this.seletor, SelectionKey.OP_READ);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Thread(new Server(7788)).start();;
    }
}

参考:

http://ifeve.com/socket-channel/

http://ifeve.com/java-nio-vs-io/

https://www.cnblogs.com/barrywxx/p/8430790.html

https://blog.csdn.net/qq_34039868/article/details/105577401

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章