• JAVA NIO


    NIO的几个基础概念

    Channel(通道),Buffer(缓冲区),Selector(选择器)

    1.Channel

    1>通道,为某sb提供了了渠道。而下面的例子中InputStream实际上就是读取文件的通道

     1 public class Test {
     2 
     3     public static void main(String[] args) throws IOException  {
     4 
     5         File file = new File("data.txt");
     6 
     7         InputStream inputStream = new FileInputStream(file);
     8 
     9         byte[] bytes = new byte[1024];
    10 
    11       //read(byte[] b) :  从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
    12 
    13      //返回实际读取的整数形式字节数。
    14 
    15         inputStream.read(bytes);
    16 
    17         inputStream.close();
    18 
    19     }  
    20 
    21 }

    如此,我们将NIO中的Channel同传统IO的Stream来类比。不同的是,IO中,Stream是单向的,比如InputStream只能进行读取操作,OutputStream只能进行写操作。而NIO中的Channel是双向的,既可以进行读操作,又可以进行写操作。

    2>以下是常用的几种通道:

    FileChannel  读写本地文件的数据,不支持Selector控制,对应File类
    SocketChannel  通过TCP读写网络中的数据,对应Socket类
    ServerSocketChannel  监听新的TCP连接,并且会创建一个可读写的SocketChannel,对应ServerSocket类
    DatagramChannel  通过UDP读写网络中的数据,对应DatagramSocket类

    3>Channel方法

    int read(ByteBuffer dst)  从Channel到中读取数据到ByteBuffer
    long read(ByteBuffer[] dsts) 将Channel到中的数据“分散”到ByteBuffer[]
    int write(ByteBuffer src) 将ByteBuffer到中的数据写入到Channel
    long write(ByteBuffer[] srcs) 将ByteBuffer[]到中的数据“聚集”到Channel
    ong position() 返回此通道的文件位置
    FileChannel position(long p) 设置此通道的文件位置
    long size()  返回此通道的文件的当前大小
    FileChannel truncate(long s) 将此通道的文件截取为给定大小
    void force(boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中

    4>主要获取方式

    java针对支持通道的类提供了getChannel()方法

    FileInputStream fis = new FileInputStream("D:\1.jpg");

    FileChannel inChannel = fis.getChannel();

    jdk1.7的nio2只对各个通道提供了一个静态方法open()

    FileChannel inChannel = FileChannel.open(Paths.get("D:\1.jpg"),

    StandardOpenOption.READ);

    5>通道之间的数据传输

    read&write

    //将 Buffer 中数据写入 Channel

    outChannel.write(buff)

    //从 Channel 读取数据到 Buffer

    inChannel.read(buff)

    transferFrom

    从源信道读取字节到这个通道的文件中。

     1     //复制图片,利用直接缓存区
     2 
     3     public void test() throws Exception{
     4 
     5         FileChannel inChannel = FileChannel.open(Paths.get("D:\1.jpg"), StandardOpenOption.READ);
     6 
     7         FileChannel outChannel = FileChannel.open(Paths.get("D:\2.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
     8 
     9         outChannel.transferFrom(inChannel,0, inChannel.size()); 
    10 
    11         inChannel.close();
    12 
    13         outChannel.close();
    14 
    15     }

    transferTo

    将字节从这个通道的文件传输到给定的可写字节通道。

     1      //复制图片,利用直接缓存区
     2 
     3     public void test2() throws Exception{
     4 
     5         FileChannel inChannel = FileChannel.open(Paths.get("D:\1.jpg"), StandardOpenOption.READ);
     6 
     7         FileChannel outChannel = FileChannel.open(Paths.get("D:\3.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
     8 
     9         inChannel.transferTo(0, inChannel.size(), outChannel);  
    10 
    11         inChannel.close();
    12 
    13         outChannel.close();
    14 
    15     }
    16 
    17     }

    2.Buffer 

    1>缓冲区,是NIO中非常重要的东西,NIO中所有数据的读和写都离不开Buffer。类似于,上述代码中,读取数据时放在byte数组中,而NIO中,读取的数据只能放在Buffer中,同样的写入数据也是写入到Buffer中。

    2>下图是客户端向服务端发送数据,然后服务端接收数据的过程。

    3>Buffer的属性:

    capacity  容量  Buffer所能够存放的最大容量
    position  位置  下一个被读或写的位置
    limit  上界 可供读写的最大位置,用于限制position position < limit
    mark   标记 标记位置,用于记录某次读写的位置,可以通过reset()方法回到这里

     

    4>Buffer类型

    除了boolean类型意外每个基元类型都会有缓冲区。主要讲解的是ByteBuffer.这里大概说一下,Buffer是一个抽象类,包括一个Buffer的最基本属性,比如,容量,位置,上界,标记.子类通过调用父类构造方法来实例化这几个参数,子类也都有各自的容量实现.比如ByteBuffer类用字节数组当缓冲区.旗下又有两个自己的实现类.

    类型  缓冲区
    byte  ByteBuffer
    char  CharBuffer
    double  DoubleBuffer
    float  FloatBuffer
    int  IntBuffer
    long  LongBuffer
    short  ShortBuffer

    5>Buffer的方法

    方法 说明
    position  移动偏移量指针
    limit  移动限制大小指针
    mark  打标记,寄了当前偏移量的位置。可使用reset恢复到标记位置
    reset  恢复到标记位置
    clear  初始化指针,清理所有数据,转换为写模式(实际只是偏移指针,数据还在)
    flip   转换为读取模式 byteBuffer.flip();
    rewind  重置偏移量指针到初始状态,可以重新写入或重新读取
    remaining  可读或可写容量
    hasRemaining  是否可读或可写
    hasArray 是否有数组缓存,若为堆缓冲区,则会有数据缓存,若为直接缓冲区,则没有
    offset  当前数组偏移量,当把当前数组切片时,无需复制内存,直接指向偏移量。

    为了更清晰的说明缓冲区的功能,接下来以ByteBuffer举例(各类型的功能上基本大同小异)

    方法  说明
    allocate  申请堆缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(8);
    allocateDirect  申请直接缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8);
    wrap  将字节数组包在缓冲区中,可以理解为将字节数组转换为字节堆缓冲区
    slice  缓冲区切片,当前偏移量到当前限制大小的内存生成一个缓冲区,无需复制内存,直接指向偏移量
    duplicate  共享一份缓冲区,缓冲区内容修改会互相影响,读取不影响
    asReadOnlyBuffer  拷贝一份只读的缓冲区
    ix 根据实际的offset偏移,对于外部来说是透明的,比如缓冲区切片之后,生成新的缓冲区实际是同一片内存的缓冲区存在offset偏移量,对切片后的缓冲区读写都会做便宜操作。
    compact  初始化指针,清理已读取数据,转换为写模式(实际只是偏移指针position,数据还在)此时将data初始化,会将未读取的2个字节复制到数组头部,同时转换为写模式。
    getXXX  读取数据  byte[] data1 = new byte[3];  byteBuffer.get(data1);
    putXXX  写入数据 byte[] data = new byte[] {'H','E','L','L','O'}; byteBuffer.put(data);
    asXXXBuffer  转换为指定类型的缓冲区,字节缓冲区可以转换为其他基元类型的缓冲区,其他基元类型缓冲区不能反过来转换

    总结:NIO通过引入缓冲区的概念使得对字节操作比传统字节操作方便一些,但是读写模式需要来回转换会让人有点头晕。

    3.Selector

    1>选择器,多路复用器。NIO的核心之一。Selector的作用就是用来轮询每个Channel的状态是否处于可读、可写。一旦发现Channel有注册的事情发生,便获取事件然后进行处理。如此可以实现单线程管理多个channels,也就是可以管理多个网络连接。

    2>Selector的创建过程

    // 1.创建Selector

    Selector selector = Selector.open();

    // 2.将Channel注册到选择器中

    // ....... new channel的过程 ....

    //Notes:channel要注册到Selector上就必须是非阻塞的,所以FileChannel是不可以使用Selector的,因为FileChannel是阻塞的

    channel.configureBlocking(false);

    // 第二个参数指定了我们对 Channel 的什么类型的事件感兴趣

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

    // 也可以使用或运算|来组合多个事件,例如

    SelectionKey key = channel.register(selector , SelectionKey.OP_READ | SelectionKey.OP_WRITE);

    // 不过值得注意的是,一个 Channel 仅仅可以被注册到一个 Selector 一次, 如果将 Channel 注册到 Selector 多次, 那么其实就是相当于更新 SelectionKey 的 interest set.

    3>一个Channel在Selector注册其代表的是一个SelectionKey事件,SelectionKey类型包括:

    OP_READ 可读事件;值为1<<0
    OP_WRITE 可写事件;值为1<<2
    OP_CONNECT 客户端连接服务端的事件(tcp连接),一般为创建SocketChannel客户端channel;;值为1<<3
    OP_ACCEPT 服务端连接客户端的事件,一般为创建ServerSocketChannel客户端channel;;值为1<<4
      OP_READ OP_WRITE OP_CONNECT ACCEPT
    服务器ServerSocketChannel       Y
    服务器SocketChannel  Y Y    
    客户端SocketChannel Y Y Y  

     4>Selector的10个方法

    open()  创建一个Selector对象
    isOpen() 是否是open状态,如果调用了close()方法则会返回false
    provider() 获取当前Selector的Provider
    keys() 获取当前channel注册在Selector上所有的key
    selectedKeys()  获取当前channel就绪的事件列表
    selectNow() 获取当前是否有事件就绪,该方法立即返回结果,不会阻塞;如果返回值>0,则代表存在一个或多个
    select(long timeout)   selectNow()的阻塞超时方法,超时时间内,有事件就绪时才会返回;否则超过时间也会返回
    select()  selectNow()的阻塞方法,直到有事件就绪时才会返回
    wakeup() 调用该方法会时,阻塞在select()处的线程会立马返回;(ps:下面一句划重点)即使当前不存在线程阻塞在select()处,那么下一个执行select()方法的线程也会立即返回结果,相当于执行了一次selectNow()方法
    close()  用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。channel本身并不会关

    5>SelectionKey

    选择键,谈到Selector就不得不提SelectionKey,两者是紧密关联,配合使用的;每个Channel向Selector注册时,都将会返回一个SelectionKey对象。选择键将Channel与Selector建立了关系,并维护了channel事件。

    可以通过cancel方式取消键,取消的键不会立即从selector中移除,而是添加到cancelledKeys中,在下一次select操作时移除它。所以在调用某个key时,需要使用isValid进行校验。

    6>SelectionKey的方法

    channel()  返回此选择键所关联的通道.即使此key已经被取消,仍然会返回.
    selector() 返回此选择键所关联的选择器,即使此键已经被取消,仍然会返回.
    isValid() 检测此key是否有效.当key被取消,或者通道被关闭,或者selector被关闭,都将导致此key无效.在AbstractSelector.removeKey(key)中,会导致selectionKey被置为无效.
    cancel() 请求将此键取消注册.一旦返回成功,那么该键就是无效的,被添加到selector的cancelledKeys中.cancel操作将key的valid属性置为false,并执行selector.cancel(key)(即将key加入cancelledkey集合)
    interesOps() 获得此键的interes集合.
    interestOps(int ops) 将此键的interst设置为指定值.此操作会对ops和channel.validOps进行校验.如果此ops不会当前channel支持,将抛出异常.
    readyOps()  获取此键上ready操作集合.即在当前通道上已经就绪的事件.
    isReadable() 检测此键是否为"read"事件.等效于:k.,readyOps() & OP_READ != 0
    isWritable() 测试此键的通道是否已准备好进行写入
    isAcceptable() 测试此键的通道是否已准备好接受新的套接字连接
    isConnectable() 测试此键的通道是否已完成其套接字连接操作
    attach(Object ob) 将给定的对象作为附件添加到此key上.在key有效期间,附件可以在多个ops事件中传递.
    attachment() 获取附件.一个channel的附件,可以再当前Channel(或者说是SelectionKey)生命周期中共享,但是attachment数据不会作为socket数据在网络中传输.
  • 相关阅读:
    课堂派题库格式转换程序
    操作系统——进程的状态与转换
    android 通用 Intent
    android上使用蓝牙设备进行语音输入
    讯飞语音听写中数字规整问题
    【Android】隐藏底部虚拟按键
    AudioEffect中文API
    为什么要在onNewIntent的时候要显示的去调用setIntent
    android蓝牙耳机下的语音(输入/识别)及按键监听
    Android如何监听蓝牙耳机的按键事件
  • 原文地址:https://www.cnblogs.com/chensisi/p/13163590.html
Copyright © 2020-2023  润新知