NIO及对其三大组件(Buffer、Channel、Selector)的详解

1、简介

1.1 Java中的IO介绍

  1. BIO:BlockingIO,同步式阻塞式IO,即传统的IO,是java中最早期的流
  2. NIO:Non-BlockingIO,又称New IO,同步式非阻塞IO,是JDK1.4提供的流
  3. AIO:AsynchronousIO,异步是非阻塞IO,可以认为是NIO的二代版本,是JDK1.8提供的流

1.2 概述

  1. NIO是JDK1.4出现的一个新的用于进行数据传输的流
  2. 全称是Non-BlockingIO,是一种同步式非阻塞式的IO,也是一种能供进行多路复用的IO
  3. NIO中有3大组件:Buffer、Channel、Selector
  4. NIO在使用的时候可以基于事件驱动方式来实现

1.3 BIO的缺点

  1. 一对一的连接方式:即每一个连接请求对应一个线程,在请求最大的情况下,会导致服务器端的压力非常大而致整个服务器的效率变低
  2. 阻塞:当线程在进行read或者write的时候,除非读完或者写完,否则在这个过程中不能发生任何操作
  3. 单项传输:数据只能从一端传向另一端,如果需要反向传输需要令创建流对象

1.4 NIO的特点

  1. 一对多的连接方式:利用一个或者少量线程处理大量的连接请求,降低服务器端的压力
  2. 非阻塞:在线程不能进行read或者write方法的时候,立即返回0,等待下一次操作
  3. 双向传输:利用通道可以实现数据的双向

1.5 NIO的缺点

  1. 在请求量比较大的情况下会出现部分请求的响应时间比较长的现象
  2. 不适用于长任务场景,不然会导致其他的请求无法处理

1.6 BIO和NIO的比较

NIO及对其三大组件(Buffer、Channel、Selector)的详解_第1张图片

2. ByteBuffer

2.1 概述

  1. 字节缓冲区,继承了Buffer类
  2. 底层是依靠字节数组来存储数据
  3. 本身是一个抽象类,需要利用其子类创建对象或者是利用其提供的allocate或者wrap方法来创建ByteBuffer对象
  4. 重要位置:capacity >= limit >= position >=mark

2.2 重要位置

  1. capacity:容量位,用于标记该缓冲区的容量,在缓冲区创建好之后就不再改变
  2. limit:限制位,用于限制操作位position所能达到的最大位置。在缓冲区刚创建的时候指向容量位
  3. position:操作位,用于指向要操作的位置,实际意义类似于数组中的下标,在缓冲区刚创建的时候指向0
  4. mark:标记位,用于进行标记,在缓冲区刚创建的时候指向-1,默认不启用

2.3 重要方法

NIO及对其三大组件(Buffer、Channel、Selector)的详解_第2张图片

2.4 示例

Demo01
package cn.tedu.buffer;

import java.nio.ByteBuffer;

public class ByteBufferDemo {

    public static void main(String[] args) {

        // 表示给底层的字节数组来指定大小
        // 缓冲区在给定之后,长度就不能改变了
        ByteBuffer buffer =
                ByteBuffer.allocate(10);
        // 添加数据
        buffer.put("abc".getBytes());
        buffer.put((byte) 0);
        buffer.put("def".getBytes());
        // 将position挪动
        // buffer.position(0);
        // 获取元素
        // 获取的是一个字节
        // byte b = buffer.get();
        // System.out.println(b);
        // byte b2 = buffer.get();
        // System.out.println(b2);

        // 遍历
        // 将limit挪到position的位置上
        // 将position归零
        // buffer.limit(buffer.position());
        // buffer.position(0);
        // 上述两部操作称之为翻转缓冲区
        // 等价于
        buffer.flip();
        // while(buffer.position() < buffer.limit()){
        // 等价
        while(buffer.hasRemaining()){
            byte b = buffer.get();
            System.out.println(b);
        }

    }

}
Demo02
package cn.tedu.buffer;

import java.nio.ByteBuffer;

public class ByteBufferDemo2 {

    public static void main(String[] args) {

        // 适合于数据未知的场景
        // ByteBuffer buffer = ByteBuffer.allocate();
        // 数据已知
        // ByteBuffer buffer =
        //         ByteBuffer.wrap("hello big2002".getBytes());
        // System.out.println(buffer.position());

        ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.put("hello".getBytes());

        // 获取缓冲区底层的字节数组
        byte[] data = buffer.array();
        // System.out.println(new String(data, 0,
        //         buffer.position()));
        buffer.flip();
        System.out.println(new String(data, 0,
                buffer.limit()));

    }

}

3. Channel

3.1 简介

  1. Channel,称之为通道,在NIO中用于完成数据的传输
  2. 在操作的时候是面向缓冲区进行的
  3. 可以实现数据的双向传输
  4. Channel默认是阻塞的,可以手动设置为非阻塞

3.2 FileChannel

3.2.1 概述

  1. FileChannel,顾名思义是面向文件的通道
  2. 可以利用FileChannel完成对文件的读写操作
  3. 利用FileChannel读取文件的时候,是先将文件中的内容映射到虚拟内存中,然后在读取到程序的缓冲区中
  4. FileChannel不能直接创建,可以利用FileInputStream、FileOutPutStream、RandomAccessFile对象中的个体Channel()方法获取
  5. 如果是通过FileInputStream获取FileChannel,那么只能进行读取操作
  6. 如果是通过FileOutputStream获取FileChannel,那么只能进行写入操作
  7. 如果是通过RandomAccessStream获取FileChannel,那么可以进行读写操作

3.2.2 示例

读取过程
@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();
 
}

3.3 UDP

3.3.1 概述

  1. 用于进行UDP收发的通道
  2. 是无连接的网络协议,只能进行发送和接受的操作
  3. 基本类是DatagramChannel,是一个抽象类

3.3.2 示例

发送端
@Test
public void send() throws IOException {
	// 开启通道
	DatagramChannel dc = DatagramChannel.open();
	// 准备数据
	ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());
	// 发送数据
	dc.send(buffer, new InetSocketAddress("localhost", 8090));
	// 关闭通道
	dc.close();
}
接收端
@Test
public void recieve() throws IOException {
 
    // 开启通道
    DatagramChannel dc = DatagramChannel.open();
    // 绑定连接地址和端口号
    dc.socket().bind(new InetSocketAddress(8090));
 
    // 准备缓冲区
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    // 接收数据
    dc.receive(buffer);
    System.out.println(new String(buffer.array(), 0, buffer.position()));
 
    // 关闭通道
    dc.close();
 
}

3.4 TCP

3.4.1 概述

  1. 用于进行TCP通信的通道
  2. 需要进行连接的网络协议
  3. 提供了连接、接收、读取、写入操作
  4. 客户端通道是SocketChannel,服务器端通道是ServerSocketChannel

3.4.2 示例

客户端
@Test
public void client() throws IOException {
 
    // 开启客户端通道
    SocketChannel sc = SocketChannel.open();
    // 可以手动设置为非阻塞模式
    sc.configureBlocking(false);
    // 发起连接
    sc.connect(new InetSocketAddress("localhost", 8090));
    // 由于是非阻塞的,所以连接不一定建立了,所以要判断连接是否建立
    while (!sc.isConnected())
        // 如果连接没有建立,则试图重新建立连接
        sc.finishConnect();
    // 写出数据
    sc.write(ByteBuffer.wrap("hello".getBytes()));
    // 关闭通道
    sc.close();
}

服务器端

@Test
public void server() throws IOException {
 
    // 开启服务器端通道
    ServerSocketChannel ssc = ServerSocketChannel.open();
    // 绑定要监听的端口
    ssc.socket().bind(new InetSocketAddress(8090));
    // 手动设置为非阻塞
    ssc.configureBlocking(false);
    // 获取连接过来的通道
    SocketChannel sc = ssc.accept();
    // 判断连接是否真正建立
    while (sc == null)
       // 如果没有建立则重新获取建立
        sc = ssc.accept();
    // 准备缓冲区用于存储数据
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    // 读取数据
    sc.read(buffer);
    System.out.println(new String(buffer.array(), 0, buffer.position()));
    // 关闭通道
    ssc.close();
 
}

4. Selector

4.1 概述

  1. Selector,称之为多路复用选择器
  2. 对通道进行选择,需要基于事件进行驱动
  3. 针对了四类事件:connect、accept、read、write,四类事件定义在SelectionKey中
  4. 可以实现利用一个或者少量线程处理大量请求
  5. 适用于大量的短任务场景,不适用于长任务场景
  6. Selector针对的必须是非阻塞的通道

4.2 示例

作用:针对通道的指定事件来进行选择
Selector在使用的时候针对非阻塞通道进行操作
NIO及对其三大组件(Buffer、Channel、Selector)的详解_第3张图片

客户端

package cn.tedu.selector;


import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class Client {
    public static void main(String[] args) throws IOException {

        SocketChannel sc = SocketChannel.open();
        sc.connect(new InetSocketAddress("localhost",8090));
        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()));

    }
}

服务器端

package cn.tedu.selector;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class Srever {
    public static void main(String[] args) throws IOException {

        //开启服务器通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //设置非阻塞
        ssc.configureBlocking(false);
        //绑定端口
        ssc.bind(new InetSocketAddress(8090));
        //开启选择器
        Selector selc = Selector.open();
        //将通道注册到选择器上
        ssc.register(selc, SelectionKey.OP_ACCEPT);

        //模拟:服务器开启之后不关闭
        while(true){
            //随着运行时间的延长,接收到的请求会越来越多
            //需要针对这些请求来进行选择,将能触发时间的请求留下
            //将不能触发事件的请求过滤掉
            selc.select();
            //选完之后能够留下来的请求都是有用的请求
            //connect/read/write
            //获取请求的事件类型
            Set<SelectionKey> set = selc.selectedKeys();
            //需要针对请求的不同类型类进行分门别类的处理
            Iterator<SelectionKey> it = set.iterator();
            while(it.hasNext()){
                SelectionKey key = it.next();
                //触发服务器的accept操作 - 说明客户端一定调用了connect方法
                if (key.isAcceptable()){
                    //从事件中获取通道
                    ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
                    //接收连接
                    SocketChannel sc = sscx.accept();
                    sc.configureBlocking(false);
                    //根据需求确定,如果需要读操作,那么就给READ
                    //如果需要写操作,那就给WRITE
                    //如果存在多个register,那么后边的会覆盖前边的
                    sc.register(selc,
                           // SelectionKey.OP_READ + SelectionKey.OP_WRITE);
                           // SelectionKey.OP_READ | SelectionKey.OP_WRITE);
                            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()));
                    //读取完成之后,需要将READ事件从通道身上移除掉
                    //key.interestOps() 获取到所有事件
                    sc.register(selc,
                            key.interestOps() - SelectionKey.OP_READ);
//                               key.interestOps() ^ SelectionKey.OP_READ);
                }
                if (key.isWritable()){
                    SocketChannel sc = (SocketChannel) key.channel();
                    sc.write(ByteBuffer.wrap("收到数据啦~~".getBytes()));
                    sc.register(selc,
                            key.interestOps() - SelectionKey.OP_WRITE);
                }
                //处理完成之后,需要将这一大类时间移除掉
                it.remove();
            }
        }
    }
}

你可能感兴趣的:(NIO及对其三大组件(Buffer、Channel、Selector)的详解)