zoukankan      html  css  js  c++  java
  • NIO 入门

    NIO 入门

    输入/输出:概念性描述

    传统IO:

    • 使用的方式完成IO。
    • 所有I/O被视为单个的字节来移动。
    • 通过Stream的对象一次移动一个字节

    流与块的比较

    • 传统IO与NIO的区别在于数据的打包和传输的方式
    • 传统IO ==> 以流的方式处理数据。
    • NIO ==> 以块的方式处理数据。
    • 流式I/O系统:
      • 一次一个字节地处理数据。
      • 一个输入(输出)流产生(消费)一个字节的数据。
    • 块式I/O系统:
      • 每个操作都是在一步中产生或消费一个数据块。
      • 比流式字节处理速度快。

    通道和缓冲区

    • 通道:到任何目的地(从任何目的地来)的所有数据都必须通过Channel对象。
    • 缓冲区:一个Buffer是一个容器对象。发送给一个通道的所有对象都放在缓冲区中(从通道读取数据也一样)。

    缓冲区

    • Buffer是一个对象,包含要读出或写入的数据。
    • NIO中的所有数据都用缓冲区进行处理。读入的数据放在缓冲区,写出的数据写到缓冲区中。
    • 缓冲区本质是一个字节数组(或其他类型数组)。

    缓冲区类型

    • 常用的缓冲区类型为ByteBuffer
    • 每种基本Java类型都有一个缓冲区类型:
      • ByteBuffer
      • CharBuffer
      • ShortBuffer
      • IntBuffer
      • LongBuffer
      • FloatBuffer
      • DoubleBuffer

    通道

    • Channel是一个对象,用来读取和写入数据。
    • 将数据写入到缓冲区而非通道中。
    • 将数据从通道写入缓冲区,再由缓冲区获取数据。

    通道类型

    • :单向的,只在一个方向流动。(InputStreamOutputStream)
    • 通道:双向的,可用于读,写或同时读写。

    实践:NIO的读与写

    • 读取:
      • 创建缓冲区。
      • 通过通道将数据读到缓冲区中。
    • 写入:
      • 创建缓冲区。
      • 将数据写入缓冲区。
      • 让通道使用该数据执行写入操作。

    从文件中读取

    步骤:

    1. FileInputStream获取Channel
    2. 创建Buffer
    3. 使用Channel将数据读到Buffer中。

    示例:

    // 获取通道
    FileInputStream fin = new FileInputStream("test.md");
    FileChannel fc = fin.getChannel();
    
    // 创建缓冲区
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    
    // 利用通道将数据读到缓冲区
    fc.read(buffer);
    

    写入文件

    步骤:

    1. FileOutputStream获取Channel
    2. 创建Buffer
    3. 将数据放入读入Buffer中。
    4. 设置指针为缓冲区开始位置。
    5. 通过Channel将数据从缓冲区写出。

    示例:

    // 创建输出流对象
    FileOutputStream fout = new FileOutputStream();
    
    // 由输出流对象获得通道
    FileChannel fc = fout.getChannel();
    
    // 创建缓冲区
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    
    // 将数据放到缓冲区中
    for(int i = 0; i < message.length;++i){
        buffer.put(message[i]);
    }
    
    // 设置指针指向缓冲区开始
    buffer.flip();
    
    // 利用通道将缓冲区数据写出
    fc.write(buffer);
    

    读写结合

    将一个文件的所有内容拷贝到另外一个文件中。

    步骤:

    1. 创建缓冲区。
    2. 获取输入,输出通道。
    3. 将数据通过输入通道读到缓冲区。
    4. 利用输出通道将缓冲区数据写出到目标文件。

    示例:

    FileInputStream fin = new FileInputStream(infile); // 输入流对象
    FileOutputStream fou = new FileOutputStream(outfile); // 输出流对象
    
    FileChannel fcin = fin.getChannel(); // 获取输入通道
    FileChannel fcout = fou.getChannel(); // 获取输出通道
    
    ByteBuffer buffer = ByteBuffer.allocate(1024); // 创建缓冲区
    
    // 拷贝文件
    while (true){
        buffer.clear(); // 清空缓冲区
    
        int r = fcin.read(buffer); // 通过输入通道将数据读入
    
        if (r == -1) // 若到文件末尾,则退出
            break;
    
        buffer.flip(); // 将指针至到缓冲区开始,从开始位置输出数据
    
        fcout.write(buffer); // 通过输出通道将数据从缓冲区写出到文件
    }
    

    缓冲区内部细节

    缓冲区有两个重要的组件:

    • 状态变量
    • 访问方法(accessor)

    状态变量

    缓冲区有三个状态变量指示当前的状态:

    • position
    • limit
    • capacity

    Position

    • 缓冲区实际上是一个数组.
    • position用来指示下一次操作所指向的数组索引位置.
    • 若为读取数据,则position指示下一个读入的数据应该放在数组的位置.
    • 若为写出数据,则position指示下一个写出的数据在数组的位置.

    Limit

    • 若为读取数据,limit指示position读入数据的位置不能超过限制.
    • 若为写出数据,limit指示position写出的数据不能超过的位置.
    • position不能超过limit指示的位置.

    Capacity

    • 存储在缓冲区的最大数据容量,即底层数组的大小.
    • limit不能超过capacity.

    示例

    1. 创建一个大小为n的缓冲区.则此时capacityn,limitn,position为0.
    2. 第一个读入a个字节后,position指向位置a,其他保持不变.
    3. 第二次读入b个字节后,position指向位置a+b,其他保持不变.
    4. 要将缓冲区的数据输出,先调用flip().其将limit设置为position指向的位置a+b,position设置为0.
    5. 第一次写出a个字节,则此时position指向位置a.
    6. 第二次写出b个字节,则此时position指向位置a+b.
    7. 调用clear()方法,将清空缓冲区,并将position设置为0,limit设置为capacity.

    访问方法

    get()方法

    分类:

    1. byte get() ==> 获取单个字符,操作影响positon
    2. ByteBuffer get(byte dst[]); ==> 将一组字符读入数组中,操作影响position
    3. ByteBuffer get(byte dst[], int offset, int length); ==> 将一组字符读入数组中,操作影响position
    4. byte get(int index); ==> 从特定位置获取字符,与position无关

    put()方法

    分类:

    1. ByteBuffer put(byte b); ==> 写入单个字节,影响position
    2. ByteBuffer put(byte src[]); ==> 写入一组字节,影响position
    3. ByteBuffer put(byte src[], int offset, int length); ==> 写入一组字节,影响position
    4. ByteBuffer put(ByteBuffer src); ==> 从ByteBuffer写入到当前ByteBuffer,影响position
    5. ByteBuffer put(int index, byte b); ==> 将字节写入到指定的位置,与position无关

    类型化的get()put()方法

    对于不同的类型有不同的方法.


    关于缓冲区的额外内容

    缓冲区分配和包装

    • 在创建缓冲区时,可使用静态方法allocate(缓冲区大小)分配缓冲区.
    • 也可以使用wrap()方法将已有的数组转换成缓冲区.
    • 若使用wrap()方法获得缓冲区,则通过原数组也可访问底层数据.

    缓冲区分片

    • slice()由已有的缓冲区创建一个子缓冲区.
    • 子缓冲区与原缓冲区的部分共享数据.
    • 通过对子缓冲区操作,将影响原缓冲区中的数据.

    只读缓冲区

    • 只读缓冲区:可以读取,不能向其写入.
    • 使用asReadOnlyBuffer()方法将普通缓冲区转换为只读缓冲区.
    • 方法返回的缓冲区与原缓冲区完全相同,但是只读.
    • 返回的缓冲区与原缓冲区共享数据,原缓冲区的修改导致只读缓冲区受到影响.
    • 不能将只读缓冲区转换为可写缓冲区.

    直接和间接缓冲区

    直接缓冲区:
    加快I/O速度,以特殊的方式分配其内存的缓冲区.

    给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。

    内存映射文件I/O

    通过使文件中的数据以内存数组的内容来完成.
    一般只有实际读取或写入的部分才会送入内存中.
    其提供底层操作系统的机制调用.

    示例

    MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE,0,1024);
    // 将FileChannel的前1024个字节映射到内存中
    // 返回值为MappedByteBuffer,为ByteBuffer子类
    

    分散和聚集

    • 分散/聚集I/O使用多个缓冲区保存数据.
    • 分散读取:将数据读到一个缓冲区数组.
    • 聚集写入:向缓冲区数组写入数据.

    分散/聚集I/O

    两个接口:

    • ScatteringByteChannel
    • GatheringByteChannel

    ScatteringByteChannel的两个读方法

    • long read(ByteBuffer[] dsts);
    • long read(ByteBuffer[] dsts, int offset, int length);

    特点:

    • 分散读取:依次填充每个缓冲区.填满一个缓冲区后,填充下一个缓冲区.

    聚集写入的两个写方法

    • long write(ByteBuffer[] srcs);
    • long write(ByteBuffer[] srcs, int offset, int length);

    应用

    有一个网络应用,每个消息被划分成固定长度的头部固定长度的正文.

    创建一个容纳头部的缓冲区和一个容纳正文的缓冲区.


    文件锁定

    文件锁定:不阻止任何形式的数据访问,而是通过锁的共享和获得运行不同的部分相互协调.

    共享锁:其他人可以获得共享锁,但不能获得排它锁.
    排它锁:其他人不能获得同一文件的锁.

    锁定文件

    • 使用写方式打开文件.
    • 获得对应文件的锁.

    示例:

    RandomAccessFile raf = new RandomAccessFile("test.md","rw");
    FileChannel fc = raf.getChannel();
    FileLock lock = fc.lock(start,end,false);
    

    可移植性

    原则:

    • 只使用排它锁.
    • 将所有的锁视为劝告式的(advisory).

    连网和异步I/O

    异步I/O

    • 没有阻塞地读写数据.
    • 通过注册特定I/O事件(可读数据的到达,新套接字连接).当发生注册事件,系统发出通知.
    • 异步I/O可以监听任意数量的通道上的事件而不用额外的线程.

    示例

    private int ports[];
    private ByteBuffer echoBuffer = ByteBuffer.allocate( 1024 );
    
    private void go() throws IOException {
        // 创建一个selector
        // 是注册对I/O事件感兴趣的地方,当事件发送时,selector发出通知
        Selector selector = Selector.open();
        
        // 对每个端口打开一个监听器,并注册到selector中
        for (int i=0; i<ports.length; ++i) {
          // 为监听每个端口,每个端口需要一个ServerSocketChannel
          ServerSocketChannel ssc = ServerSocketChannel.open();
          // 设置为非阻塞式
          ssc.configureBlocking( false );
          // 新建一个socket
          ServerSocket ss = ssc.socket();
          // 新建socket地址
          InetSocketAddress address = new InetSocketAddress( ports[i] );
          // socket绑定端口地址
          ss.bind( address );
          // 将ServerSocketChannels注册到selector上
          // 第一个参数是selector,第二个参数是指定监听的事件
          // 返回值表示通道在此selector上的注册,当通知发生事件时,是提供该事件的selectionKey进行的
          SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );
        
          System.out.println( "Going to listen on "+ports[i] );
        }
        
        while (true) {
          // 阻塞,直到一个或多个事件发生
          // 返回发生的事件数
          int num = selector.select();
          // 返回所有事件对应的selectedKey的集合
          Set selectedKeys = selector.selectedKeys();
          Iterator it = selectedKeys.iterator();
          //对于每个事件的处理
          while (it.hasNext()) {
            SelectionKey key = (SelectionKey)it.next();
            // 获取selectKey对应事件的类型,若有新连接,则接收
            if ((key.readyOps() & SelectionKey.OP_ACCEPT)
              == SelectionKey.OP_ACCEPT) {
              // 创建ServerSocketChannel
              ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
              SocketChannel sc = ssc.accept(); // 接受连接
              sc.configureBlocking( false ); // 设置为非阻塞式
        
              // 接收完后,更新新的selectionKey,用来接收新连接
              SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ ); // 注册为读取
              it.remove(); // 将处理完的selectionKey从集合中删除,否则会被再次处理
        
              System.out.println( "Got connection from "+sc );
            }
            // 若套接字的数据到达,则接收数据
            else if ((key.readyOps() & SelectionKey.OP_READ)
              == SelectionKey.OP_READ) {
              // 获取处理的通道
              SocketChannel sc = (SocketChannel)key.channel();
        
              int bytesEchoed = 0;
              while (true) {
                echoBuffer.clear();
                // 获取读取结果
                int r = sc.read( echoBuffer );
                // 小于等于0则传输结束
                if (r<=0) {
                  break;
                }
        
                echoBuffer.flip();
                // 写出缓冲区数据
                sc.write( echoBuffer );
                bytesEchoed += r;
              }
        
              System.out.println( "Echoed "+bytesEchoed+" from "+sc );
              // 移除selectionKey,避免重复处理
              it.remove();
            }
    

    字符集

    Charset:十六位Unicode字符序列与字节序列之间的一个命名映射.

    编码

    读文本:CharsetDecoder.(逐位将字符转换为char值)
    写文本:CharsetEncoder.(将字符转换为位)

    Java支持的字符编码:

    • US-ASCII
    • ISO-8859-1
    • UTF-8
    • UTF-16BE
    • UTF-16LE
    • UTF-16

    示例:

    // 创建字符集实例
    Charset latin1 = Charset.forName( "ISO-8859-1" );
    CharsetDecoder decoder = latin1.newDecoder(); // 字符集对应的解码器
    CharsetEncoder encoder = latin1.newEncoder(); // 字符集对应的编码器
    
    CharBuffer cb = decoder.decode( inputData ); // 将字符数据解码,生成缓冲区
    
    ByteBuffer outputData = encoder.encode( cb ); // 将缓冲区数据编码
    
    outc.write( outputData ); // 输出编码后的数据
    
    inf.close();
    outf.close();
    

    NIO浅析

    NIO(Non-blocking I/O):同步非阻塞的I/O模型,I/O多路复用的基础.

    传统BIO模型(Blocking I/O)

    传统服务器端同步阻塞I/O处理:

    ExecutorService executor = Excutors.newFixedThreadPollExecutor(100); // 线程池
    ServerSocket serverSocket = new ServerSocket(); // 创建新socket
    serverSocket.bind(8088); // socket绑定端口
    while(!Thread.currentThread.isInturrupted){ // 线程循环等待新连接
      Socket socket = serverSocket.accept(); // 接收新连接
      executor.submit(new ConnectIOnHandler(socket)); // 为新连接创建一个新线程
    }
    
    class ConnectIOnHandler extends Thread{
      private Socket socket;
      public ConnectIOnHandler(Socket socket){
        this.socket = socket;
      }
      public void run(){
        // 循环处理读写事件
        while(!Thread.currentThread.isTnturrupted()&&!socket.isClosed()){
          String someThing = socket.read(); // 读取数据
          if(someThing != null){
            // 处理数据
            socket.write(); // 写数据
          }
        }
      }
    }
    

    特点:

    • 每个连接对应一个线程.
    • 多个线程是因为socket的accpet(),read()write()是**同步阻塞的,即:每个连接处理I/O时是阻塞的.
    • 模型简单,适用于连接数较少的情况.
    • 线程的创建和销毁成本高.
    • 线程占用内存大.
    • 线程间的切换成本高(保留上下文,系统调用,切换时间可能大于执行时间==>load高,sy使用高,系统不可用).
    • 造成锯齿状系统负载(大量阻塞线程使系统负载压力大).

    NIO工作原理

    常见I/O模型

    I/O的两个阶段:

    • 等待就绪
    • 操作

    常见I/O模型

    • BIO:read()方法若没有收到数据,则一直阻塞,直到收到数据后返回数据.
    • NIO:若有数据,则将数据读到内存,并返回;否则直接返回0,不阻塞.
    • AIO:等待就绪是非阻塞的,读取数据也是异步的.

    参考:

  • 相关阅读:
    树莓派交叉编译环境搭建
    手机购买怎样识别假货——一点心得体会分享!
    Ubuntu 网站服务器环境搭建
    转载:Raspberry Pi 树莓派入门
    Python中的条件选择和循环语句
    关于VMare中安装Ubuntu的一些说明
    如何去掉系统快捷方式的箭头和更改登录界面背景图片
    重装系统后,硬盘分区丢失的解决办法
    Python中的字符串
    Python的基础语法
  • 原文地址:https://www.cnblogs.com/truestoriesavici01/p/13235951.html
Copyright © 2011-2022 走看看