NIO原理解析

 

Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式。

相关概念介绍:

1)阻塞(Block)和非阻塞(Non-Block):

关注的是线程或者进程在运行过程中是否等待调用结果的状态,多用于server端数据处理方式。

阻塞:程序运行过程中,当前线程或者进程在数据返回前会被操作系统挂起,只有等到返回数据后才继续执行(BIO模型)

非阻塞:当前线程或者进程运行过程中,不管数据有没有准备好都不会被操作系统挂起而是直接继续执行其他工作。

2)同步(Synchronization)和异步(Asynchronous)的方式:

关注的消息通信机制,相对于IO操作而言,大部分指client端调用时,对于结果返回是否等待:
同步:调用者主动等待调用结果,在同一时间只能完成一个操作(JDK NIO模型)
异步:调用者无需等待结果直接返回,被调用者通知调用者结果,在同一时间能够同时完成多个操作(JDK AIO模型)

所以对于对于标准的IO属于同步阻塞方式,NIO属于同步非阻塞模式,AIO属于异步非阻塞模式。

 BIO通信模型:

在介绍NIO前先了解简单一下BIO,因为BIO是同步阻塞的,基本通信过程是每次client请求过来时server会启动一个线程进行通信,这种通信模型最大的缺点就是:一连接一线程的模型导致服务端无法承受大量客户端的并发连接。

以下时BIO的通信模型:

 

NIO通信模型:

对于NIO因为是同步非阻塞的,所以在事件处理模型上跟BIO有所不同,为了达到非阻塞目的采用一个线程来管理多个客户端的连接请求,这样能够确保在连接过程中不会被阻塞,在将建立好的连接分配给对应的处理线程。

 

在java NIO由几个核心部门组成:缓存Buffers;通道Channels;选择器Selectors。

缓冲区(buffer):本质上是一个数组,它包含一些要读写的数据,任何时候访问 NIO 数据,都是通过 Buffer 进行;
通道(channel):是一个通道,通过它读写 Buffer 中的数据;
多路复用器(selector):多路复用器,Selector 不断轮询注册在其上的 Channel,如果某个 Channel 有新的 TCP 链接接入、读和写事件,这个 Channel 就处于就绪状态,会被 Selector 轮组出来,然后通过SelectionKey() 可以获取就绪 Channel 的集合,进行后续的 IO

一、缓存Buffers

   缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在NIO 库中,所有数据都是用缓冲区处理的。

  使用Buffer读写数据一般遵循以下四个步骤:
   1)写入数据到Buffer: 直接将数据写入buffer
   2)调用flip()方法:通过flip()将buffer从写模式切换到读模式
   3)从Buffer中读取数据:
   4)调用clear()方法或者compact()方法:
    读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

  在NIO 中,所有的缓冲区类型都继承于抽象类Buffer,最常用的就是ByteBuffer,对于Java中的基本类型,基本都有一个具体Buffer 类型与之相对应,它们之间的继承关系如下图所示:

  在缓冲区中,最重要的属性有下面三个,它们一起合作完成对缓冲区内部状态的变化跟踪:
    position:指定了下一个将要被写入或者读取的元素索引,它的值由get()/put()方法自动更新,在新创建一个Buffer 对象时,position 被初始化为0。
    limit:指定还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。
    capacity:指定了可以存储在缓冲区中的最大数据容量,实际上,它指定了底层数组的大小,或者至少是指定了准许我们使用的底层数组的容量。

 

二、通道Channels

通道是一个对象,通过它可以读取和写入数据,但所有数据的读写都先通过Buffer对象来处理再到通道中。

Java NIO中几个最重要的通道的实现
FileChannel:从文件中读写数据
DatagramChannel:能通过UDP读写网络中的数据
SocketChannel:能通过TCP读写网络中的数据
ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel

他们之间的继承关系如下:

 

1)通过通道读取过程:

- 从FileInputStream 获取Channel
- 创建Buffer
- 将数据从Channel 读取到Buffer中

public class FileInputProgram {
    static public void main( String args[] ) throws Exception {
    FileInputStream fin = new FileInputStream("c:\\test.txt");
    // 获取通道
    FileChannel fc = fin.getChannel();
    // 创建缓冲区
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    // 读取数据到缓冲区
    fc.read(buffer);
    buffer.flip();
    while (buffer.remaining()>0) {
        byte b = buffer.get();
        System.out.print(((char)b));
    }
    fin.close();
}

2)使用NIO 写入数据

可以分为下面三个步骤:
- 从FileInputStream 获取Channel
-  创建缓存Buffer
- 将数据从Channel写入到Buffer中

public class FileOutputProgram {
    static private final byte message [] = { 83, 111, 109, 101, 32,
    98, 121, 116, 101, 115, 46 };
    static public void main(String args[]) throws Exception {
    FileOutputStream fout = new FileOutputStream("e:\\test.txt");
    //新建渠道
    FileChannel fc = fout.getChannel();
    //新建buffer
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    for (int i = 0; i < message.length; ++ i) {
            buffer.put(message[i]);
        }
        buffer.flip();
        //写入渠道
        fc.write(buffer);
        fout.close();
    }
}

 

三、selector选择器

  Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。

  使用NIO 中非阻塞I/O 编写服务器处理程序,大体上可以分为下面三个步骤:
  1)向Selector 对象注册感兴趣的事件
  2)从Selector 中获取感兴趣的事件
  3)根据不同的事件进行相应的处理

注册事件:

如果想要同时注册几个事件时,可以通过“位或”操作符将常量连接起来,比如:SelectionKey.OP_READ | SelectionKey.OP_WRITE  

/*
* 向selector注册事件
* 注册的事件包含四种:
* SelectionKey.OP_CONNECT
* SelectionKey.OP_ACCEPT
* SelectionKey.OP_READ
* SelectionKey.OP_WRITE
* */
private Selector getSelector() throws IOException {
    // 创建Selector 对象
    Selector sel = Selector.open();
    // 创建可选择通道,并配置为非阻塞模式
    ServerSocketChannel server = ServerSocketChannel.open();
    server.configureBlocking(false);
    // 绑定通道到指定端口
    ServerSocket socket = server.socket();
    InetSocketAddress address = new InetSocketAddress(port);
    socket.bind(address);
    // 向Selector 中注册感兴趣的事件
    server.register(sel, SelectionKey.OP_ACCEPT);
    return sel;
}

 获取感兴趣的事件:

/*
* 开始监听
* */
public void listen() {
    System.out.println("listen on " + port);
    try {
        while(true) {
            // 该调用会阻塞,直到至少有一个事件发生
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iter = keys.iterator();
            while (iter.hasNext()) {
                SelectionKey key = (SelectionKey) iter.next();
                iter.remove();
                //最终处理事件
                process(key);
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

对事件进行处理,可以针对不同的事件进行不同的处理流程:

/*
* 根据不同的事件做处理
* */
private void process(SelectionKey key) throws IOException{
    // 接收请求
    if (key.isAcceptable()) {
        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        SocketChannel channel = server.accept();
        channel.configureBlocking(false);
        channel.register(selector, SelectionKey.OP_READ);
    }
    // 读信息
    else if (key.isReadable()) {
        SocketChannel channel = (SocketChannel) key.channel();
        int len = channel.read(buffer);
        if (len > 0) {
            buffer.flip();
            name = new String(buffer.array(),0,len);
            SelectionKey sKey = channel.register(selector, SelectionKey.OP_WRITE);
            sKey.attach(name);
        } else {
            channel.close();
        }
        buffer.clear();
    }
    // 写事件
    else if (key.isWritable()) {
        SocketChannel channel = (SocketChannel) key.channel();
        String name = (String) key.attachment();
        ByteBuffer block = ByteBuffer.wrap(("Hello " + name).getBytes());
        if(block != null){
            channel.write(block);
        }
        else{
            channel.close();
        }
    }
}

 

posted on 2018-09-11 08:51  kma  阅读(2387)  评论(0编辑  收藏  举报

导航