zoukankan      html  css  js  c++  java
  • NIO原理解析

    NIO中主要包括几大组件,selector、channel、buffer。selector后面介绍,channel则类似于BIO中的流,但是流的读取是单向的,例如只能读,或只能写,但是channel则是双向的。数据可以从channel读到buffer中,也可以从buffer中写入到channel中。

    针对于客户端请求服务端的场景,NIO实现的结构图如下:

                                                                                         禁止盗图,画了好久。。。

     一、channel

     channel主要包括以下几类

    • FileChannel-------------------------->从文件中读写数据
    • DatagramChannel------------------>通过UDP读写网络中的数据
    • SocketChannel----------------------->通过TCP读写网络中的数据
    • ServerSocketChannel--------------->监听新进来的TCP连接,并生成一个SocketChannel对象

    基本示例:

    //创建能访问任意位置的file文件
    RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
    //获取fileChannel对象
    FileChannel inChannel = aFile.getChannel();
    //分配48字节大小的byteBuffer
    ByteBuffer buf = ByteBuffer.allocate(48);
    //读取channel中的数据到buffer中
    int bytesRead = inChannel.read(buf);
    
    while (bytesRead != -1) {
        System.out.println("Read " + bytesRead);
        //将buffer从写模式切换到读模式
        buf.flip();
        while(buf.hasRemaining()){
            System.out.print((char) buf.get());
        }
      //清空整个缓冲区
        buf.clear();
      //缓冲区已经全部读完,返回-1退出循环
        bytesRead = inChannel.read(buf);
    }
    
    aFile.close();

    另外channel不仅可以读取数据到buffer中,当存在多个channel并且其中有一个channel为fileChannel时,channel之间可以互相传输数据

    transferFrom() :可以将数据从源通道传输到FileChannel中

    RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
    FileChannel fromChannel = fromFile.getChannel();
    RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
    FileChannel toChannel = toFile.getChannel();
    long position = 0;
    long count = fromChannel.size();
    toChannel.transferFrom(position, count, fromChannel);

    transferTo()

    RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
    FileChannel fromChannel = fromFile.getChannel();
    RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
    FileChannel toChannel = toFile.getChannel();
    long position = 0;
    long count = fromChannel.size();
    fromChannel.transferTo(position, count, toChannel);

    二、Buffer

    1、buffer主要包括如下几类:

    • ByteBuffer
    • CharBuffer
    • DoubleBuffer
    • FloatBuffer
    • IntBuffer
    • LongBuffer
    • ShortBuffer

     2、使用buffer的一般步骤:

    • 写入数据到Buffer
    • 调用flip()方法
    • 从Buffer中读取数据
    • 调用clear()方法或者compact()方法------------>clear()方法会清空整个缓冲区,compact()只会清除已经读过的数据

     3、buffer中的概念

    为了理解Buffer的工作原理,需要熟悉它的三个属性:

    • capacity
    • position
    • limit

    position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。

    capacity

    作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。

    position

    当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.

    当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。

    limit

    在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。

    当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)

     4、常用方法

    a、分配大小

    //分配48字节大小的byteBuffer
    ByteBuffer buf = ByteBuffer.allocate(48);
    //分配2014字符大小的charBuffer
    CharBuffer buf = CharBuffer.allocate(1024);

    b、写数据到buffer中

    包括两种方式:一是从channel中读数据到buffer中,另外一种就是调用buffer的put()方法

    //一、channel读取数据到buffer
    int bytesRead = inChannel.read(buf);
    //二、调用channel的put()方法
    buf.put(127);

    c、读取数据

    同样包括两种方式:一是写入数据到channel中,另外一种就是调用buffer的get()方法

    //一、将数据写入到channel中
    int bytesWritten = inChannel.write(buf);
    //二、调用buffer的get()方法读取数据
    byte aByte = buf.get();

    d、读写模式切换

    buffer.flip()

    e、清除buffer

    一旦读完Buffer中的数据,需要让Buffer准备好再次被写入。可以通过clear()或compact()方法来完成。

    如果调用的是clear()方法,position将被设回0,limit被设置成 capacity的值。换句话说,Buffer 被清空了。Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。

    如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。

    如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用compact()方法。

    compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。

    另外NIO支持scatter/gather,说白了,就是一个channel可以读取数据到多个buffer中去,或者多个buffer可以写入到channel中。当第一个buffer写满之后,会紧接着读取到第二个buffer中去,如下图:

    channel-->buffer

    ByteBuffer header = ByteBuffer.allocate(128);
    ByteBuffer body   = ByteBuffer.allocate(1024);
    ByteBuffer[] bufferArray = { header, body };
    channel.read(bufferArray);

    buffer-->channel

    ByteBuffer header = ByteBuffer.allocate(128);
    ByteBuffer body   = ByteBuffer.allocate(1024);
    ByteBuffer[] bufferArray = { header, body };
    channel.write(bufferArray);

    三、selector

    为啥使用selector?在传统的BIO当中,监听每个客户端的请求都需要一个线程去处理,线程数的上升会涉及到大量的上下文切换的操作,这也是非常浪费性能的。NIO中基于事件驱动的理念,使用selector监听各种事件的发生,可以实现只开启一个线程

    就可以管理所有的请求,当然实际情况合理的增加线程数可以大大提高性能。

    注意:与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。

     1、注册channel到selector中

    channel.configureBlocking(false);
    SelectionKey key = channel.register(selector,Selectionkey.OP_READ);

    selectionKey用来描述事件,包括事件类型,以及对应的selector与channel等等。

    第二个入参为事件类型,主要包括四种:

    • Connect
    • Accept
    • Read
    • Write

    分别用常量表示为:

    • SelectionKey.OP_CONNECT
    • SelectionKey.OP_ACCEPT
    • SelectionKey.OP_READ
    • SelectionKey.OP_WRITE

    当然selector监听channel时,可以对多个事件感兴趣,写法如下:

    int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

    可能对selector、channel、事件三者的关系有点乱,用故事来总结一下:selector是父母,channel是孩子,父母监督孩子学习,孩子有很多同学,有些同学学习好,有些同学学习差,父母欢迎学习好的学生来家里玩,排斥成绩差的。那么特定的事件就可以理解

    成那些成绩差的同学来家中,父母监听到了,开始行动,将他们赶走。

    2、selectionKey

    当channel注册到selector中时,会返回一个selectionKey对象,可以理解成事件的描述或是对注册的描述,主要包括这几个部分:

    a、interest集合----------------------->感兴趣的事件集合。

    int interestSet = selectionKey.interestOps();
    //判断事件是否在集合中
    boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;

    b、ready集合-------------------------->通道已经准备就绪的操作的集合

    int readySet = selectionKey.readyOps();
    //事件是否在已准备就绪的集合中,selectionKey提供了如下方法
    selectionKey.isAcceptable();
    selectionKey.isConnectable();
    selectionKey.isReadable();
    selectionKey.isWritable();

    c、Channel

    Channel  channel  = selectionKey.channel();

    d、Selector

    Selector selector = selectionKey.selector();

    e、附加的对象(可选)

    用户也可以将buffer等其他对象加到selectionKey上,方便后续操作

    添加对象:

    //添加对象到selectionKey有两种方式
    selectionKey.attach(theObject);
    SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
    //获取此附加对象
    Object attachedObj = selectionKey.attachment();

    3、select()

    当把channel注册到selector中去之后,可以通过select()方法来监听对应channel的特定事件。主要有三种select方法:

    • int select()-------------------------------------->阻塞到被监听的channel至少有一个事件发生。
    • int select(long timeout)---------------------->在timeout(ms)的时间内阻塞,直到被监听的channel至少有一个事件发生。
    • int selectNow()--------------------------------->非阻塞,不管有没有事件发生,都立马返回。

    4、selectedKeys()

    Set selectedKeys = selector.selectedKeys();

    完整的示例:

    Selector selector = Selector.open();
    channel.configureBlocking(false);
    SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
    while(true) {
        int readyChannels = selector.select();
        if(readyChannels == 0) continue;
        Set selectedKeys = selector.selectedKeys();
        Iterator keyIterator = selectedKeys.iterator();
        while(keyIterator.hasNext()) {
            SelectionKey key = keyIterator.next();
            if(key.isAcceptable()) {
                // a connection was accepted by a ServerSocketChannel.
            } else if (key.isConnectable()) {
                // a connection was established with a remote server.
            } else if (key.isReadable()) {
                // a channel is ready for reading
            } else if (key.isWritable()) {
                // a channel is ready for writing
        }
        keyIterator.remove();
      }
    }

    四、DatagramChannel

    前面的示例都是基于TCP连接,现在讲述一下UDP的示例.

    DatagramChannel是一个能收发UDP包的通道。因为UDP是无连接的网络协议,所以不能像其它通道那样读取和写入。它发送和接收的是数据包。

    1、打开DatagramChannel

    DatagramChannel channel = DatagramChannel.open();
    channel.socket().bind(new InetSocketAddress(9999));

    2、接收数据

    ByteBuffer buf = ByteBuffer.allocate(48);
    buf.clear();
    channel.receive(buf);

    3、发送数据

    String newData = "New String to write to file..." + System.currentTimeMillis();
    ByteBuffer buf = ByteBuffer.allocate(48);
    buf.clear();
    buf.put(newData.getBytes());
    buf.flip();
    int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));
  • 相关阅读:
    喷水装置(一)
    下沙小面的(2)
    +-字符串
    非洲小孩
    寻找最大数(三)
    C
    寻找最大数
    阶乘之和
    背包问题
    python的内存管理机制(zz)
  • 原文地址:https://www.cnblogs.com/alimayun/p/10673993.html
Copyright © 2011-2022 走看看