NIO---buffer,channel,selector

1、IO操作流程:以read磁盘文件操作为例

    ①、将数据从磁盘读取到磁盘驱动

    ②、从磁盘驱动读取到操作系统内核buffer

    ③、从操作系统内核buffer读取到用户线程

2、IO与NIO区别

2.1、IO是面向流的单向读/写操作

      

2.2、NIO是面向缓冲区buffer的,读写操作发生在缓冲区内,是缓冲区内的读写模式进行操作,读模式用于读取缓冲区中的数据,写模式用于向缓冲区写入数据。

         

3、阻塞式IO与非阻塞式IO

     描述的是用户线程调用操作系统内核IO操作的方式

3.1、阻塞:是指调用IO操作需要等待结果的完成才能继续执行,会影响后续的动作执行,对CPU资源利用率不足

                    

          案例:小明去火车站排队买票,排队两天才买到票

3.2、非阻塞:是指IO操作被调用后,立即返回一个状态给用户线程,无需等待IO操作彻底完成,但是为了得到结果,仍需要定时重复请求结果数据,造成CPU资源大量消耗

                       

               案例:小明到火车站买票,告知没票,每隔3小时去火车站询问一次,两天后买到票

3.3、IO多路复用:异步阻塞 IO 模型,为改善同步非阻塞中线程轮询等待的问题,基于操作系统内核提供的多路分离函数select(),可以实现IO多路复用

               使用select以后最大的优势是用户可以在一个线程内同时处理多个socket 的 IO 请求。用户可以注册多个 socket,然后不断地调用 select 读取被激活的 socket,即可达到在同一个线程内同时处理多个 IO 请求的目的。

               而在同步阻塞 模型中,必须通过多线程的方式才能达到这个目的。 

                             

            这里的 select 函数是阻塞的,因此多路 IO 复用模型也被称为异步阻塞 IO 模型。

            注意,这里的所说的阻塞是指 select 函数执行时线程被阻塞,而不是指 socket。     

4、同步与异步:

      描述的是用户线程与操作系统内核的交互方式

4.1、同步:是指用户线程调用内核IO操作需要等待或轮询等待结果,才能继续进行。其中轮询等待的问题可以使用IO多路复用模型实现

           

4.2、异步:用户调用内核IO操作后,继续执行后续操作,当内核IO操作完成后会通知用户线程或这调用用户线程注册的回调函数

                

5、NIO缺点:

    ①、在请求量比较大的情况下,会出现部分请求响应时间比较长的现象

    ②、不适用于长任务场景,不然会导致其他任务无法执行

6、BIO与NIO的区别:

       

BIO

NIO

同步阻塞

同步非阻塞

单向传输数据

可以双向传输数据

一对一的连接方式

一对多的连接方式

面向流操作

面向缓冲区操作

适合于请求少、长连接场景

适合于大量请求、短连接的场景

7、buffer 缓冲区

      缓冲区本质上是一块可以读写操作的内存区域,这块内存被包装成NIO Buffer对象,并提供了一组方法用于便捷的操作这块内存。

7.1、Java NIO 关键Buffer的实现:

        ByteBuffer     CharBuffer      ShortBuffer     IntBuffer     LongBuffer     DoubleBuffer     FloatBuffer      

7.2   多级缓存:

        物理磁盘IO操作要比内存IO操作慢很多倍,所以一般为了提高性能,通常会对数据进行缓存,JAVA应用程序与物理磁盘之间通常会有多级缓存:

         

             1) Disk Drive Buffer(磁盘缓存):位于磁盘驱动器中的 RAM,将磁盘数据移 动到磁盘缓冲区是一件相当耗时的操作。

             2) OS Buffer(系统缓存):操作系统自己缓存,可以在应用程序间共享数据

             3) Application Buffer(应用缓存):应用程序的私有缓存。 

7.3、Buffer的基本应用:

        使用Buffer读写数据一般遵循四个步骤:

        ①、写入数据到Buffer

        ②、调用flip()方法,切换读写模式

        ③、从Buffer中读取数据

        ④、调用clear()方法清除所有缓存的数据,或者compact()方法清除已经读过的数据

        

      案例:

public class NIODemo {
    static void doPrint(Integer positon, Integer limit, Integer capacity){
        System.out.println("positon=" + positon);//指针位
        System.out.println("limit=" + limit);//限制位
        System.out.println("capacity=" + capacity);//容量
    }

    public static void main(String[] args) {
        //创建ByteBuffer
        //参数:用于存储数据的数组的长度,字节为单位
        ByteBuffer buffer = ByteBuffer.allocate(10);//从JVM中分配一块内存区域
        ByteBuffer buffer2 = ByteBuffer.allocateDirect(10);//从OS中分配一块内存区域

        System.out.println("写入数据前");
        doPrint(buffer.position(), buffer.limit(), buffer.capacity());
        //向缓冲区写入数据
        buffer.put("hello".getBytes());
        System.out.println("写入数据后");
        doPrint(buffer.position(), buffer.limit(), buffer.capacity());

//        //获取第一个字符数据,需要把position位挪到下标为0的位置
//        buffer.position(0);
//        byte b = buffer.get();

//        //遍历存入的数据,需要先把limit位移动到position位,再把position位归零
//        buffer.limit(buffer.position());
//        buffer.position(0);

        //上面挪动limit和position位称为反转缓冲区,可用flip()代替
        buffer.flip();//切换为读模式
        System.out.println("读数据前");
        doPrint(buffer.position(), buffer.limit(), buffer.capacity());
        byte c1=buffer.get();
        System.out.println((char)c1);
        System.out.println("===读数据之后===");
        doPrint(buffer.position(),buffer.limit(),buffer.capacity());

//        // while(buffer.position() < buffer.limit()){
//        //     buffer.hasRemaining() 就是把buffer.position() < buffer.limit()进行了封装而已
//        while(buffer.hasRemaining()){
//            byte a = buffer.get();
//            System.out.println(a);
//        }
    }
}

7.4、相关API 

方法

作用

allocate(int capacity)

从JVM中分配内存,创建缓冲区的时候指定缓冲区容量的大小,实际上是指定缓冲区底层的字节数组的大小

allocateDirect(int capacity)从操作系统中分配内存

wrap(byte[] array)

利用传入的字节数组来构建缓冲区

array()

获取缓冲区底层的字节数组

get()

获取缓冲区中position位置上的字节

get(byte[] dst)

将缓冲区中的数据写到传入的字节数组中

get(int index)

获取指定下标上的字节

put(byte b)

向position位置上放入指定的字节

put(byte[] src)

向position位置上放入指定的字节数组

put(byte[] src, int offset, int length)

向position位置上放入指定的字节数组的部分元素

put(ByteBuffer src)

将字节缓冲区放入

put(int index, byte b)

向指定位置插入指定的字节

capacity()

获取容量位

clear()

清空缓冲区:position = 0; limit = capacity; mark = -1;

compact()q清除已经读取过的缓冲区

flip()

反转缓冲区:limit = position; position = 0; mark = -1;

切换读写模式

hasRemaing()

判断position和limit之间是否还有空余

limit()

获取限制位

limit(int newLimit)

设置限制位

mark()

设置标记位

position()

获取操作位

position(int newPosition)

设置操作位

remaining()

获取position和limit之间剩余的元素个数

reset()

重置缓冲区:position = mark

rewind()

重绕缓冲区:position = 0; mark = -1

8、Channel 通道

      NIO是基于通道Channel和缓冲区Buffer进行操作,数据从Channel读取到Buffer中,从Buffer写入到Channel中

      

    NIO中Channel的具体实现类:

     ①、FileChannel:从文件中读写数据

     ②、Data'gramChannel:通过UDP协议读写网络中的数据

     ③、SocketChannel:通过TCP协议读写网络中的数据

     ④、ServerSocketChannel:可以监听新进来的TCP连接,向Web服务器一样

8.1、FileChannel基本使用

/**
 * 一次性全部读取文件内容
 * @author zxj
 * @date 2020/5/15 13:56
 */
public class FileChannelDemo01 {
    public static void main(String[] args) throws IOException {
        //创建buffer,在jvm中分配1024b的内存
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //创建FileChannel,指定位READ读模式,把数据读取到channel
        FileChannel fc = FileChannel.open(Paths.get("data.txt"), StandardOpenOption.READ);
        //将通道中的数据读取到buffer
        fc.read(buffer);
        //转换位读模式
        buffer.flip();
        //一次全部读取,当buffer的内存足够大时
        byte[] arr = buffer.array();
        System.out.println(buffer.position());
        System.out.println(new String(arr));
        //此处不是清除数据,而是标记为无用数据(脏数据)
        buffer.clear();
        //关闭通道
        fc.close();
    }
}
/**
 * 多次顺序读取文件内容
 * @author zxj
 * @date 2020/5/15 13:56
 */
public class FileChannelDemo02 {
    public static void main(String[] args) throws IOException {
        //创建buffer,在jvm中分配2b的内存
        ByteBuffer buffer = ByteBuffer.allocate(2);
        //创建FileChannel,指定位READ读模式,把数据读取到channel
        FileChannel fc = FileChannel.open(Paths.get("data.txt"), StandardOpenOption.READ);
        int len = -1;
        do {
            //将通道中的数据读取到buffer
            len = fc.read(buffer);
            //转换为读模式
            buffer.flip();
            while (buffer.hasRemaining()){
                //单个字符读取
                System.out.println((char) buffer.get());
            }
            //转换为写模式
            buffer.flip();
            //每次读数据应将原数据设置为无效
            buffer.clear();

        }while (len != -1);
        //关闭通道
        fc.close();
    }
}
  1. 如果是通过FileInputStream获取FileChannel,那么只能进行读取操作
  2. 如果是通过FileOutputStream获取FileChannel,那么只能进行写入操作
  3. 如果是通过RandomAccessFile获取FileChannel,那么可以进行读写操作
@Test
public void readFile() throws Exception {
 
    // 创建RandomAccessFile对象。指定模式为读写模式
    RandomAccessFile raf = new RandomAccessFile("F:\\a.txt", "rw");
    // 获取FileChannel对象
    FileChannel fc = raf.getChannel();
    // 创建缓冲区用于存储数据
    ByteBuffer buffer = ByteBuffer.allocate(10);
    // 记录读取的字节个数
    int len;
    // 读取数据
    while ((len = fc.read(buffer)) != -1) {
        System.out.println(new String(buffer.array(), 0, len));
        buffer.flip();
    }
    // 关流
    raf.close();
 
}

写入过程
@Test
public void writeFile() throws Exception {
 
    // 创建RandomAccessFile对象。指定模式为读写模式
    RandomAccessFile raf = new RandomAccessFile("F:\\test.txt", "rw");
    // 获取FileChannel对象
    FileChannel fc = raf.getChannel();
    // 创建缓冲区,并且将数据放入缓冲区
    ByteBuffer src = ByteBuffer.wrap("hello".getBytes());
    // 利用通道写出数据
    fc.write(src);
    // 关流
    raf.close();
 
}

复制文件
@Test
public void copyFile() throws Exception {
 
    // 创建流对象指向对应的文件
    FileInputStream in = new FileInputStream("F:\\a.txt");
    FileOutputStream out = new FileOutputStream("E:\\a.txt");
 
    // 获取FileChannel对象
    FileChannel src = in.getChannel();
    FileChannel dest = out.getChannel();
 
    // 准备缓冲区
    ByteBuffer buffer = ByteBuffer.allocate(10);
    // 读取数据,将读取到的数据写出
    while (src.read(buffer) != -1) {
        buffer.flip();
        dest.write(buffer);
        buffer.clear();
    }
    // 关流
    in.close();
    out.close();
 
}

8.2、SocketChannel 基本使用,客户端与服务端通信

   /**
     * 服务端
     * @throws IOException
     */
    @Test
    public void server() throws IOException {
        //创建ServerSocketChannel
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //指定监听端口,ip为本机
        ssc.socket().bind(new InetSocketAddress(9999));
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        //等待客户端的连接
        while(true){
            SocketChannel sc =ssc.accept();
            int byteReader = sc.read(buffer);
            buffer.flip();
            System.out.println("server收到的信息:" + new String(buffer.array()));
            sc.close();
        }
    }

    /**
     * 客户端
     */
    @Test
    public void client() throws IOException {
        SocketChannel sc = SocketChannel.open();
        sc.connect(new InetSocketAddress("127.0.0.1", 9999));
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.put("hello server!".getBytes());
        buffer.flip();
        sc.write(buffer);
        sc.close();
    }

 8.3 Selector

       Selector 是 Java NIO 中实现多路复用技术的关键,多路复用技术又是提高通 讯性能的一个重要因素。项目中可以基于 selector 对象实现了一个线程管理多 个 channel 对象,多个网络链接的目的。例如:在一个单线程中使用一个 Selector 处理 3 个 Channel,如图所示 

       

         为什么使用 Selector? 
仅用单个线程来处理多个 Channel 的好处是:只用一个线程处理所有的通道, 可以有效避免线程之间上下文切换带来的开销,而且每个线程都要占用系统的一 些资源(如内存)。因此,使用的线程越少越好。   

        通道触发了一个事件意思是该事件已经就绪。所以,某个 channel 成功连接到 另一个服务器称为“连接就绪”。一个 server socket channel 准备好接收新 进入的连接称为“接收就绪”。一个有数据可读的通道可以说是“读就绪”。等 待写数据的通道可以说是“写就绪”。 
 
        这四种事件用 SelectionKey 的四个常量来表示: 
 
       1) SelectionKey.OP_CONNECT  

       2) SelectionKey.OP_ACCEPT

.      3) SelectionKey.OP_READ

       4) SelectionKey.OP_WRITE 

 

/**
 * Selector 多路复用选择器
 *   用于实现Channel的双向传输
 *   selector 上注册的通道必须是非阻塞的
 *
 * @author zxj
 * @date 2020/3/17 8:46
 */
public class Server {
    public static void main(String[] args) throws IOException {
        // 开启服务端通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        // 绑定监听端口
        ssc.bind(new InetSocketAddress(8070));
        // 设置为非阻塞
        ssc.configureBlocking(false);
        // 开启选择器
        Selector selec = Selector.open();
        // 将通道注册到选择器上
        ssc.register(selec, SelectionKey.OP_ACCEPT);

        //一个服务端对应多个客户端
        while(true){
            // 选择出已经注册的通道
            selec.select();
            // 获取这次选择出来的通道的事件类型
            Set<SelectionKey> keys = selec.selectedKeys();
            Iterator<SelectionKey> its = keys.iterator();
            while (its.hasNext()){
                SelectionKey key = its.next();
                // 连接事件
                if(key.isAcceptable()){
                    // 获取通道
                    ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
                    // 接收连接
                    SocketChannel sc = sscx.accept();
                    sc.configureBlocking(false);
                    // 将改通道注册到选择器并指定事件类型为“读和写”
                    sc.register(selec, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
                }
                //读事件
                if(key.isReadable()){
                    // 获取通道
                    SocketChannel sc = (SocketChannel) key.channel();
                    // 读取数据
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    sc.read(buffer);
                    System.out.println(new String(buffer.array(), 0, buffer.position()));
                    // 将此通道上的读事件注销
                    sc.register(selec, key.interestOps() ^ SelectionKey.OP_READ);
                }
                //写事件
                if(key.isWritable()){
                    SocketChannel sc = (SocketChannel) key.channel();
                    // 写出数据,向客户端响应
                    sc.write(ByteBuffer.wrap("hi client".getBytes()));
                    // 将此通道上的写事件注销
                    sc.register(selec, key.interestOps() ^ SelectionKey.OP_WRITE);
                }
                its.remove();
            }
        }
    }
}
/**
 * @author zxj
 * @date 2020/3/17 8:46
 */
public class Client {
    public static void main(String[] args) throws IOException {
        SocketChannel sc = SocketChannel.open();
//        sc.configureBlocking(false);
        sc.connect(new InetSocketAddress("localhost", 8070));
        System.out.println("客户端已连接");
        // 向服务器发送数据
        sc.write(ByteBuffer.wrap("hello server".getBytes()));
        // 读取服务器的响应数据
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        sc.read(buffer);
        System.out.println(new String(buffer.array(), 0, buffer.position()));
        sc.close();
    }
}

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值