IO之NIO入门

一、IO流

1.什么是IO?

在我们的电脑操作系统中,有许许多多文件例如.txt、.html、.exe、.rar、.class、.java...等等,那么什么是文件呢?文件可以看作是一串二进制流储存在计算机中,不管是socket、管道、终端、对我们来说一切都是文件一切都是流。在信息交换的过程中、我们都是对这些流进行数据的收发操作、简称为I/O操作(input and output),往流中读出数据系统调用read,写入数据系统调用write。 我们在平常开发过程中接触最多的就是 磁盘 IO(读写文件) 和 网络 IO(网络请求和相应) 。

从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。

当应用程序发起 IO 调用后,会经历两个步骤:

  1. 内核等待 IO 设备准备好数据
  2. 内核将数据从内核空间拷贝到用户空间。

image.png

在我学习c++的时候,是我初识IO流,在c++代码中能够非常生动形象的告诉你IO是怎么样操作的。其中cin代表inputstream,cout代表outputstream,使用>>或者<<指向需要操作的对象。

#include <iostream> 
using namespace std; 
int main( ) { 
    char str[50]; 
    cin >> str;
    cout << "Value of str is : " << str << endl; 
}
复制代码

2.IO分类

Java中的IO的分类:

  1. 按流的方向可以分为输出流和输入流。
  2. 按操作的单元来分可以分为字符流和字节流。
  3. 按流的操作类可以分为处理流和节点流。

其中JAVA的IO中有4个输入输出流的基类:

  1. 输入字节流——InputStream

image.png

  1. 输入字符流——Reader

image.png 3. 输出字节流——OutputStream

image.png 4. 输出字符流——Writer

image.png

按操作方式分类结构图: image.png

@Component
public class OrderFB implements FallbackProvider {
    @Override
    public String getRoute() {
        return "order-service";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.INTERNAL_SERVER_ERROR;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return HttpStatus.INTERNAL_SERVER_ERROR.value();
            }

            @Override
            public String getStatusText() throws IOException {
                return HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase();
            }

            @Override
            public void close() {

            }

            @Override
            public InputStream getBody() throws IOException {
                /*InputStream inputStream =
                        new BufferedInputStream(new FileInputStream(new File("D:\1.txt")));*/
                String json = JsonResult.err().code(500).msg("系统错误").toString();
                return new ByteArrayInputStream(json.getBytes("utf-8"));
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders h = new HttpHeaders();
                h.add("Content-Type", "application/json;charset=UTF-8");
                return h;
            }
        };
    }
}
复制代码

二、NIO

1. 什么是NIO?

我们知道常见三种IO模型有BIO、NIO、AIO,他们分别对应的是同步堵塞IO、同步非堵塞IO和异步非堵塞IO。 Java 中的 NIO 于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel , SelectorBuffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 IO是面向流的,NIO是面向缓冲区的,IO流是阻塞的,NIO流是不阻塞的,NIO有选择器,而IO没有。

BIO模型

image.png

NIO模型

image.png

image.png

相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。

下面来简单介绍一下NIO中的三大核心:选择器Selector、缓冲区Buffer、通道Channel。

2. Buffer

public abstract class Buffer {

    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
复制代码

用于特定原始类型的数据的容器。java NIO 中的buffer用于NIO通道进行交互,数据可以从通道读入缓冲区中,也可以从缓冲区写入到通道中。缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。在NIO库中所有的数据都是由缓冲区处理的。

已知直接子类:

NIO中所有的缓冲区类型都继承于抽象类Buffer其中最为常用的是ByteBuffer这个实现类,在通道中读写字节数据。

buffer是一个容器,存在于内存中,是一个连续的数组。其中的成员变量有4个capacity、position、limit、mark。

2.1 Buffer中成员

capacity : 此缓冲区的容量,缓冲区能容纳的元素数量。

position : 当前位置,是在缓冲区中下一次发生读取或者写入的索引。

limit : 界限,是缓冲区有效索引的下一个位置,当在buffer的写入操作时limit与capacity同一个位置,当在buffer的读取操作时limit代表缓冲区不为null的有效数据界限。

mark : 标记,相当于缓冲区的备忘录,当使用mark()方法时标记一个索引,当使用reset()方法时position将恢复到这个位置。

从源码可以看出来:0 <= mark <= position <= limit <= capacity

image.png

  • 新创建的缓冲区始终具有零位置和未定义的标记。 初始限制可以为零,或者可以是取决于缓冲器的类型和构造方式的某些其他值。 新分配的缓冲区的每个元素被初始化为零。

2.2 Buffer其中重要方法

2.2.1 写入数据到Buffer

//创建一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//第一种方法写入
buffer.put("Hello World".getBytes());
//第二中方法写入  其中rw代表读写模式
RandomAccessFile rw = new RandomAccessFile("D:\1.txt", "rw");
FileChannel channel = rw.getChannel(); //获取通道
//读取通道中的数据向缓冲区写入
channel.read(buffer);
复制代码

2.2.2 从Buffer中读取数据

//创建一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello World".getBytes());
//第一种方法读取
buffer.get();
//第二中方法读取  其中rw代表读写模式
RandomAccessFile rw = new RandomAccessFile("D:\1.txt", "rw");
FileChannel channel = rw.getChannel(); //获取通道
//读取通道中的数据向缓冲区写入
channel.write(buffer);
复制代码

2.2.3 flip()方法

flip()方法在JDK手册中的解释为翻转此缓冲区。

当向缓冲区写入数据时,调用flip()可以使position回到0的位置,使limit来到position写入最后一刻的位置,这时可以将此缓冲区看作为可以读取的状态,简称读取转换。

2.2.4 获取成员的位置

//创建一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//获取容量
buffer.capacity();
//获取position的位置
buffer.position();
//设置position位置
buffer.position(10);
//获取limit位置
buffer.limit();
//设置limit位置
buffer.limit(buffer.position());
//设置mark的位置到当前position位置
buffer.mark();
复制代码

2.2.5 rewind()

rewind()将position设回0,所以可以重读Buffer中的所有数据,且limit保持不变。

2.2.6 clear() 和 compact()

如果调用clear(),position会被设回为0,limit被设为capacity的值,缓冲区中的数据将被清空。 如果调用compact(),会将所有未读取的数据拷贝到buffer的起始处,将position设为未读取数据的后面,将limit设为capacity的值。如果写入数据的话,不会去覆盖原来未读取的数据。

2.2.7 remaining()

public final int remaining() {
    return limit - position;
}
复制代码

读取数据时若返回>0则缓冲区中还有数据可以继续读取。

2.3 缓冲区的操作

2.3.1 缓冲区分片

在NIO中,除了可以创建一个缓冲区外,可以在此缓冲区中创建一个子缓冲区,既在现有的缓冲区中切除一个新缓冲区,但是现有的缓冲区与子缓冲区在底层的数组上是数据共享的。

//创建一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
//向buffer写入值
for (int i = 0; i < buffer.capacity(); i++) {
    buffer.put((byte) i);
}
//设置position和limit位置
buffer.position(3);
buffer.limit(7);
//创建子缓冲区
ByteBuffer slice = buffer.slice();
//向子缓冲区写入值
for (int i = 0; i < slice.capacity(); i++) {
    byte b = slice.get(i);
    b *= 10;
    slice.put(i,b);
}
//重新设回0
buffer.rewind();
buffer.limit(buffer.capacity());
//读取数据
while (buffer.remaining() > 0){
    System.out.print(buffer.get()+",");
}
复制代码

返回值为:0,1,2,30,40,50,60,7,8,9,

2.3.2 只读缓冲区

只读缓冲区,顾名思义就是此缓冲区只能读取不能写入数据。调用asReadOnlyBuffer(),将任何常规缓冲区转换为与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据。

//创建一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
//创建只读缓冲区
ByteBuffer byteBuffer = buffer.asReadOnlyBuffer();
复制代码

2.3.3 直接缓冲区

直接缓冲区是为了加快IO速度,使用一种特殊方式为其分配内存的缓冲区,JDK文档中:给定一个直接字节缓冲区,jvm将尽最大努力直接对它执行本机IO操作。也就是说每次调用本机IO操作时,将尝试不将数据拷贝到中间的缓冲区中或者从内核缓冲区中拷贝数据。

//创建一个直接缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(10);
复制代码

2.3.4 内存映射文件IO

内存映射文件IO是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的IO快的多。它是通过使文件中的数据出现为内存数组的内容来完成的,一般来说只有文件中实际读取或写入的部分才会映射到内存中。

//创建一个文件通道
RandomAccessFile rw = new RandomAccessFile("D:\1.txt", "rw");
FileChannel channel = rw.getChannel();
//创建内存映射文件IO
MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
map.put(0,(byte)97);
channel.close();
复制代码

3 Channel

Channel使一个通道,可以通过它读取和写入数据,网络数据也可以通过Channel进行读写操作,不同于流,Channel使双向的而流使单向的,所以Channel是可以比流更好的映射底层操作系统的API。NIO通过Channel封装对数据源的操作,通过Channel我们可以操作数据且不用关心数据的物理结构。Channel用于字节缓冲区与另一端的实体之间有效的传输数据。但是注意Channel本身并不直接写入和读取数据,而是需要依赖Buffer来进行操作。

Channel (Java Platform SE 6)

  • public interface Channel
    extends Closeable
    复制代码

用于 I/O 操作的连接。

通道表示到实体,如硬件设备、文件、网络套接字或可以执行一个或多个不同 I/O 操作(如读取或写入)的程序组件的开放的连接。

通道可处于打开或关闭状态。创建通道时它处于打开状态,一旦将其关闭,则保持关闭状态。一旦关闭了某个通道,试图对其调用 I/O 操作就会导致 ClosedChannelException 被抛出。通过调用通道的 isOpen 方法可测试通道是否处于打开状态。

正如扩展和实现此接口的各个接口和类规范中所描述的,一般情况下通道对于多线程的访问是安全的。

从以下版本开始:1.4

3.1 FileChannel

FileChannel文件通道,可以实现read,write以及scatter/gatter操作,有许多的关于文件的操作。

3.1.1 读写数据

//创建一个文件通道
RandomAccessFile rw1 = new RandomAccessFile("D:\1.txt", "rw");
FileChannel channel1 = rw1.getChannel();
RandomAccessFile rw2 = new RandomAccessFile("D:\2.txt", "rw");
FileChannel channel2 = rw2.getChannel();
//创建缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//读取
int a = channel1.read(buf);
while (a != -1){
    buf.flip();
    //写入
    channel2.write(buf);
    buf.clear();
    a = channel1.read(buf);
}
channel1.close();
channel2.close();
复制代码

3.1.2 transferTO 和 transferFrom

如果是两个通道中有一个是FileChannel,那么可以将数据从一个通道传输到另外一个通道。 注意channel1可能来不及准备好数据,所以可能不能把所有数据也就是count字节全部传输。

//创建一个文件通道
RandomAccessFile rw1 = new RandomAccessFile("D:\1.txt", "rw");
FileChannel channel1 = rw1.getChannel();
RandomAccessFile rw2 = new RandomAccessFile("D:\2.txt", "rw");
FileChannel channel2 = rw2.getChannel();
//当前位置
long position = 0;
//总量
long count = channel1.size();
//传输
channel1.transferTo(position,count,channel2);

channel1.transferFrom(channel2, position, count);

channel1.close();
channel2.close();
复制代码

3.1.3 Scatter/Gatter

NIO支持scatter和gatter用于描述从一个通道读写到另一个通道的操作。

scatter,分散,从一个通道读取数据时将数据写入多个其他buffer中。

gatter,聚集,将多个buffer的数据写入同一个通道中。

ByteBuffer buf1 = ByteBuffer.allocate(128);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
//创建一个缓冲区数组,多个buffer
ByteBuffer[] array = {buf1,buf2};
//创建一个通道
RandomAccessFile rw = new RandomAccessFile("D:\1.txt", "rw");
FileChannel channel = rw.getChannel();

//分散
channel.read(array);

//聚集
channel.write(array);
复制代码

注意read方法是按照数组的顺序进行写入到buffer的,当第一个buffer满了时,才写入到下一个buffer。write方法也是按照数组的顺序进行写入到channel的,但是只会写入position到limit之间的有效数据。

3.1.4 其他方法

FileChannnel.position(),在特定的位置进行读写操作。

FileChannnel.truncate(),截取指定长度的文件,后面的部分将被删除。

FileChannnel.force(),将通道里为被写入磁盘的数据强制写到磁盘中,出于对性能的考虑操作系统会将数据缓存到内存中,所以无法保证数据一定会及时写到磁盘中,如果要保证这一点需要调用此方法。

3.2 Socket通道

新的socket通道类可以运行非阻塞模式并且是可以选择的,这样可以使程序有更大的灵活性和伸缩性。借助NIO,可以使用一个或几个线程就可以管理成百上千的活动socket连接并且大大的减少了性能的损耗。socket与socket通道,通道是连接一个IO服务和与之交互的方法,而socket不会再次实现与之对应的通道协议API,在java.net中已经存在的socket通道都可以被大多数协议操作重复使用。像Socket、ServerSocket、DatagramSocket都有getChannel()方法。socket通道实现了AbstractSelectableChannel,就绪选择是一种可以用来查询通道的机制,可以判断到通道是否准备好执行目标,只需要调用configureBlocking()方法,设值为false则为非堵塞模式,true为堵塞模式。

public final SelectableChannel configureBlocking(boolean block)
    throws IOException
{
    synchronized (regLock) {
        if (!isOpen())
            throw new ClosedChannelException();
        if (blocking == block)
            return this;
        if (block && haveValidKeys())
            throw new IllegalBlockingModeException();
        implConfigureBlocking(block);
        blocking = block;
    }
    return this;
}
复制代码

3.2.1 ServerSocketChannel

public abstract class ServerSocketChannel
    extends AbstractSelectableChannel
    implements NetworkChannel
复制代码

ServerSocketChannel并没有实现定义读和写的功能,所以ServerSocketChannel使负责监听传入的连接和创建新的SocketChannel对象,它本身使不传入数据的。

//获取serverSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定一个端口号
serverSocketChannel.bind(new InetSocketAddress(8080));
//设置非堵塞
serverSocketChannel.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(1024);
//监听连接
while (true){
    //获取socketChannel 如果是非堵塞模式将会在这里堵塞
    SocketChannel accept = serverSocketChannel.accept();
    if (ObjectUtils.isEmpty(accept)){
        Thread.currentThread().sleep(2000);
    }else {
        System.out.println("connection from" + accept.socket().getRemoteSocketAddress());
        buf.rewind();
        //写入数据
        accept.write(buf);
        accept.close();
    }
}
复制代码

3.2.2 SocketChannel

public abstract class SocketChannel
    extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel
复制代码

SocketChannel是一个连接到TCP网络套接字的通道。可以看出是用来连接Socket,主要用来处理网络中的IO,是基于TCP连接传输的,实现了可选通道可以被多路复用。 注意的是,已经存在的Socket不能创建SocketChannel,如果未进行连接的SocketChennl执行IO操作会抛出异常。支持设定参数。

/**SO_SNDBUF 发送缓冲区大小
 *  SO_RCVBUF 接受缓冲区大小
 *  SO_KEEPALIVE 保活连接
 *  O_REUSEADDR 复用地址
 *  SO_LINGER   有数据传输是延迟关闭通道  只有非堵塞时有用
 *  TCP_NODELAY 禁用Nagle算法
 * */
//创建SocketChannel
SocketChannel socketChannel = SocketChannel.open();
//设置参数
socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
//获取发送缓冲区参数
socketChannel.getOption(StandardSocketOptions.SO_SNDBUF);
socketChannel.bind(new InetSocketAddress(8080));
socketChannel.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(1024);
//向缓冲区写入
socketChannel.read(buf);
//读取缓冲区
socketChannel.write(buf);
socketChannel.close();
复制代码

3.2.3 DatagramChannel

DatagramChannel是模拟包导向无连接协议UDP,DatagramChannel是无连接的,每个数据报都是一个自包含的实体,拥有它自己的目的地址及不依赖其他数据包的数据负载。DatagramChannel可以发送单独的数据报给不同的目的地址,同样DatagramChannel也可以接受任意地址的数据报。

//创建一个DatagramChannel
DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.bind(new InetSocketAddress(8080));
datagramChannel.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(1024);
//接受一个udp包
buf.clear();
datagramChannel.receive(buf);
//发送udp包
datagramChannel.send(buf, new InetSocketAddress("www.baidu.com",80));
//连接  因为udp不存在正真意义上的连接,这里代表向特定的地址用read和write
//在建立连接以后调用read和write才不会抛出NotYetConnectedException
datagramChannel.connect(new InetSocketAddress("www.baidu.com",80));
//发送
datagramChannel.read(buf);
//接受
datagramChannel.write(buf);
datagramChannel.close();
复制代码

4. Selector

  • public abstract class Selector
    extends Object
    复制代码

SelectableChannel 对象的多路复用器。

可通过调用此类的 open 方法创建选择器,该方法将使用系统的默认选择器提供者创建新的选择器。也可通过调用自定义选择器提供者的 openSelector 方法来创建选择器。通过选择器的 close 方法关闭选择器之前,它一直保持打开状态。

Selector称之为选择器,可以叫多路复用器,是NIO中核心组件之一,是用来检查一个或者多个NIO Channel的状态是否准备好进行读和写的操作,就绪的状态是一旦通道具备完成某个操作的条件,则表示该通道的某个操作就已经就绪。所以说Selector可以实现单线程来管理多个通道,也就是多个网络连接。这样做的好处是,使用更少的线程来管理通道,相比使用多个线程,这样避免了线程上下文切换带来的开销。

4.1 可选择通道

SelectableChannel特点: 1.不是所有的channel都可以被selector复用,这里有个前提是需要继承SelectableChannel抽象类。 2.SelectableChannel类提供了实现通道的可选择性所需要的公共方法,它是所有支持就绪检查的通道的父类。 3.SelectableChannel注册到selector对象上,在注册时需要指定通道的那些操作时selector感兴趣的。

4.2 SelectionKey

1.通道注册后,并且通道处于就绪状态,就可以被选择器查询到,使用select()方法去完成。 2.selector可以不断的查询通道中发生操作的就绪状态,并且挑选感兴趣的操作就绪状态,一旦通道达到就绪状态,就会被selector选中,将selectorKey放入选择键集合中。 3.一个选择键,首先包含了注册在selector通道的操作类型,也包含了通道与选择器之间的注册关系。

4.3 Channel注册到Selector

/** 注册的状态
 * SelectionKey.OP_READ     可读
 * SelectionKey.OP_WRITE    可写
 * SelectionKey.OP_CONNECT  连接
 * SelectionKey.OP_ACCEPT   接受
 */
//创建一个Selector
Selector selector = Selector.open();
//注册channel到selector
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8080));
//查看支持的注册状态  通道不一定要支持所有的4种操作,例如socketChannel不支持accept
serverSocketChannel.validOps();
//注册
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
复制代码

4.4 其他方法

* select():堵塞到至少有一个通道在你注册的事件上就绪  返回值为int 表示有多少通道就绪
* select(long timeout):最长堵塞时间为timeout毫秒
* selectNow():非堵塞,只要有通道就绪就立刻返回
* wakeup():通过调用此方法让堵塞状态的selector立刻返回
复制代码
//客户端
@Test
public void demo05() throws Exception{
    //创建socketChannel
    SocketChannel socketChannel =
            SocketChannel.open(new InetSocketAddress("127.0.0.1",8080));
    socketChannel.configureBlocking(false);
    //创建缓冲区
    ByteBuffer buf = ByteBuffer.allocate(1024);
    //准备数据
    buf.put(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()).getBytes());
    buf.flip();
    //写入通道
    socketChannel.write(buf);
    buf.clear();
}

//服务器
@Test
public void demo06() throws Exception {
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.configureBlocking(false);
    serverSocketChannel.bind(new InetSocketAddress(8080));
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    //创建selector
    Selector selector = Selector.open();
    //注册
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    //轮询
    while (true) {
        if (selector.select() > 0) {
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                //判断状态
                if (selectionKey.isAcceptable()) {
                    //获取连接
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    while (channel.read(byteBuffer) != -1) {
                        byteBuffer.flip();
                        System.out.println(String.valueOf(byteBuffer.array()));
                        byteBuffer.clear();
                    }

                }
            }
            iterator.remove();
        }
    }
}
复制代码

猜你喜欢

转载自juejin.im/post/7032350137020055588