Java NIO

一、概述

如果不想看理论,直接看使用案例,可以跳到第十节

BufferChannelSelector,NIO三大件。
从最终实现上来看,我们可以将IO简单分为两大类:File IO和Socket Stream IO,分别用于操作文件和网络套接字。
我们也按照这个大方向,先介绍File IO相关操作,后续再介绍Socket Stream IO相关操作。

前言:

NIO,是一个比较高级的知识点。平时的代码开发中,我们一般很少直接使用NIO相关知识点。但是,其却是各种通信框架的基础知识。如NettyMina等,就是基于NIO来进行开发的。

IO,我们知道,是Input/Output,输入输出,可以是网络流的IO,也可以是文件的IO。通常这是一种BIO

到这里,我们引入了BIONIO,如果了解的多一点的话还有AIO。那么它们之间有什么关系呢?

本文先简单介绍上述三者之间的区别,并且拆分NIO的知识点,后面会对NIO的各个知识点进行更详细的说明。

1、同步异步、阻塞非阻塞

可以结合下面这篇文章一起看,下面这篇文章中还介绍了多路复用。

10分钟看懂, Java NIO 底层原理
https://www.cnblogs.com/crazymakercircle/p/10225159.html

1.1 同步与异步

区分一个请求是同步还是异步的话,主要看请求在调用过来时候,是等待直到执行结果完成,还是及时返回结果,后续通过异步通知或回调的方式来告诉调用方。

  • 同步请求:调用方发起调用后,会一直等待,直到被调用方返回一个响应结果为止。
  • 异步请求:调用方发起调用后,被调用方立即返回一个响应,后续被调用方通过异步通知或者回调的方式来告知调用方其结果。

1.2 阻塞与非阻塞

阻塞与非阻塞主要是关注程序在等待执行结果时的状态

  • 阻塞:当结果返回之前,线程会被挂起(BLOCK状态)
  • 非阻塞:当结果返回之前,线程不会被挂起(非BLOCK状态)

2、操作系统视角下的BIO、NIO和AIO

2.1 BIO

Blocking IO(阻塞IO),操作系统下BIO整个过程如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KGbNJVo5-1655364759818)(Java%20NIO.assets/image_1.png)]

当应用程序发起系统调用时;

  1. 操作系统首先需要先将数据拷贝到系统内核缓冲区
  2. 然后再将内核缓冲区的数据拷贝到应用程序的进程空间(JVM就是堆内存等)

BIO的情况下,应用程序发起系统调用后,会一直等待操作系统执行完上述的数据拷贝之后,才结束调用。(此时该请求线程会被BLOCK

2.2 NIO

None-Blocking IO,操作系统视图下NIO调用过程如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gbwRPINs-1655364759820)(Java%20NIO.assets/image_2.png)]

相比较BIO而言,发起系统调用后,应用程序线程不是一直在阻塞等待数据返回,而是在不停的轮询查询操作系统是否将数据准备好,当操作系统准备好数据之后,后续的从内核空间拷贝数据到用户空间的过程与BIO相同。

所以,BIO是上述两个阶段都是阻塞的,而NIO第一个阶段非阻塞,第二个阶段阻塞。

另:有关于非阻塞IO,还有一个非常重要的概念,叫做多路复用模型。该模型共包含三种解决方案:selectpollepoll。应用程序使用这些IO函数同时监听多个IO通道的状态变更,可以更好的支持更大量级的连接。

有关于多路复用模型,会在下面单独说明。

2.3 AIO

Asynchronous IO,异步IO在操作系统视角下的调用过程如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KSLr5ZW9-1655364759820)(Java%20NIO.assets/image_3.png)]

应用程序线程发起一个系统调用后,会立即返回,调用事件注册到操作系统上,操作系统准备完成数据并将数据拷贝到用户空间后,会通知应用程序数据已经可用。

在上述两个过程中,AIO均为非阻塞状态

需要说明的是:Java中的BIO NIOAIOjava对操作系统的各种IO模型的封装。

IO类型一阶段(数据拷贝至操作系统内核空间)二阶段(内核空间数据拷贝至应用程序)
BIO同步阻塞同步阻塞
NIO同步非阻塞同步阻塞
AIO异步非阻塞异步非阻塞

二、Buffer 简介

前言:

java.nio包下的Buffer抽象类及其相关实现类,本质上是作为一个固定数量的容器来使用的。

不同于InputStreamOutputStream时的数据容器byte[]Buffer相关实现类容器可以存储不同基础类型的数据,同时可以对容器中的数据进行检索,反复的操作。

Buffer(缓冲区)的工作与Channel(通道)紧密相连。ChannelIO发生时的通过的入口(或出口,channel是双向的),而Buffer是这些数据传输的目标(或来源)。

1、Buffer 基本属性

// Invariants: mark <= position <= limit <= capacity
// 标记地址,与reset搭配使用
private int mark = -1;
// 下一个要被读或写的元素的索引
private int position = 0;
// 容器现存元素的计数
private int limit;
// 容器总容量大小,在Buffer创建时被设定
private int capacity;

对于如下一段代码

// position=mark=0
// limit=capacity=10
ByteBuffer buffer = ByteBuffer.allocate(10);

正是通过以上四个属性,实现了数据的反复操作。

2、Buffer的创建

Buffer的实现类中,使用最广泛的还是ByteBuffer,所以以下示例都是基于ByteBuffer更具体说是HeapByteBuffer)来说明的,后续会专门来说明其他基本类型的使用及实现。

根据ByteBufferAPI,我们可以看到以下四种创建方式:

// 1.直接分配capacity大小的Buffer,具体实现类型为HeapByteBuffer
public static ByteBuffer allocate(int capacity)
    
// 2.直接分配capacity大小的Buffer,具体实现类型为DirectByteBuffer
public static ByteBuffer allocateDirect(int capacity)
        
// 3.直接使用array作为底层数据
public static ByteBuffer wrap(byte[] array)
    
// 4.直接使用array作为底层数据,并且指定offset和length
public static ByteBuffer wrap(byte[] array,int offset, int length)

2.1 allocate创建方式

ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put((byte)'f');
buffer.put((byte)'i');
buffer.put((byte)'r');
buffer.put((byte)'e');

具体存储图如下:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BlbmtWkF-1655364759821)(Java%20NIO.assets/image_4.png)]

2.2 wrap创建方式

// wrap(byte[] array,int offset, int length)
// position=offset
// limit=position+length
// capacity=array.length()
ByteBuffer byteBuffer = ByteBuffer.wrap("fire".getBytes(), 1, 2);

在这里插入图片描述

3、Buffer的基本操作方法

3.1 添加数据

ByteBuffer buffer = ByteBuffer.allocate(10);
// 1.逐字节存放 ByteBuffer put(byte b)
buffer.put((byte)'h');
 
// 2.字节存放到对应index ByteBuffer put(int index, byte b);
buffer.put(0,(byte)'h');
 
// 3.添加字节数组 ByteBuffer put(byte[] src)
byte[] bytes = {'h','e','l','l','o'};
buffer.put(bytes);
 
// 4.添加其他基础类型 ByteBuffer putInt(int x) ...
buffer.putInt(1);

3.2 获取数据

ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("hello".getBytes());
 
// 1.获取index下的数据
byte b = buffer.get(0);
 
// 2.逐个获取(需要先flip下,将position置为0)
buffer.flip();
for (int i = 0; i < buffer.remaining(); i++) {
    byte b1 = buffer.get();
}
 
// 3.将数据传输到bytes中
buffer.flip();
byte[] bytes = new byte[5];
ByteBuffer byteBuffer = buffer.get(bytes);

3.3 缓冲区翻转

3.3.1 flip 方法

flip是一个比较重要也比较简单的方法。

当我们使用put方法将Buffer填充满之后,此时调用get来获取Buffer中的数据时,会获取不到数据,由于get是从当前position来获取数据的,故需要先调用flip来将position置为0

// flip源码如下
// 我们也可以手动设置 buffer.limit(buffer.position()).position(0);
public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}
3.3.2 rewind 方法

rewind 相对于flip方法而言,rewind也可以重复读取数据,唯一区别就是没有重新设置limit参数。

// 源码如下
public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}
3.3.3 案例

上述两者之间有何不同呢,通过下面的2个示例来说明下。

ByteBuffer buffer = ByteBuffer.allocate(10);
// 执行完成执行position为4
buffer.put("fire".getBytes());
 
// 为了测试flip与rewind的不同,重新设置为3
buffer.position(3);
 
// flip后 pos=0 lim=3 cap=10
buffer.flip();
while (buffer.remaining() > 0) {
    byte b1 = buffer.get();
    System.out.println(b1);
}

结果:

102
105
114
ByteBuffer buffer = ByteBuffer.allocate(10);
// 执行完成执行position为4
buffer.put("fire".getBytes());
 
// 为了测试flip与rewind的不同,重新设置为3
buffer.position(3);

// 注意:需要单独测试,注释掉上述的flip相关代码
// rewind后 pos=0 limit=cap=10
buffer.rewind();
while (buffer.remaining() > 0) {
    byte b1 = buffer.get();
    System.out.println(b1);
}

结果:

102
105
114
101
0
0
0
0
0
0

总结:

针对flip而言,flip之后的Buffer数据操作上限就是上次操作到的位置position
rewind,上限依旧是limit,可以重新操作全部数据

3.4 缓冲区压缩

有时我们需要从缓冲区中释放已经操作过的数据,然后重新填充数据(针对未操作过的数据,我们是需要保留的)。

我们可以将未操作过的数据(也就是position-limit之间的数据),重新拷贝到0位置,即可实现上述需求。而Buffer中已经针对这种场景实现了具体方法,也就是compact方法

// 示例如下
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("fire".getBytes());

// position=0,获取一个数据
buffer.flip();
buffer.get();

// 进行数据压缩
buffer.compact();

//新添加的数据,会从position=3开始进行覆盖。
buffer.put("hello".getBytes());

// flip后 position=0 limit=3 capacity=10
buffer.flip();
while (buffer.remaining() > 0) {
    byte b1 = buffer.get();
    System.out.println((char)b1);
}

压缩前的buffer:

在这里插入图片描述

压缩后的buffer:

在这里插入图片描述

相比较而言:将原positionlimit之间的数据(1-4,也就是i r e)拷贝到index=0位置,position也就是3,后续新写入数据直接覆盖原position=3的位置数据。

3.5 标记与重置

markreset方法,mark用来做标记,reset用来调回到做标记的位置。

ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("fire".getBytes());
// 直接将position置为2
buffer.position(2);
// 做标记,在position=2位置做标记
buffer.mark();
// 获取position=2的数据,
System.out.println((char) buffer.get()); // r
// 执行reset,然后重新获取数据,发现是同一个数据
buffer.reset();
System.out.println((char) buffer.get()); // r

经过reset操作后,position重新回到2,也就是mark时的position,故两次get方法获取的是同一个position的值

3.6 复制

Buffer还提供了快速复制一个Buffer的功能

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

// 复制buffer
ByteBuffer duplicate = buffer.duplicate();

复制后的duplicatebuffer共享源数据数组,只是拥有不同的positionlimit

在这里插入图片描述

总结:

Buffer作为数据存储的容器,其有很多的实现类和API,本文中对其基本API进行了分析,后续我们继续对其实现类进行分析。

三、HeapBuffer与DirectBuffer

https://blog.csdn.net/qq_26323323/article/details/120145394

前言:

Buffer简介中,在测试ByteBuffer API时,一直在使用HeapByteBuffer在测试。实际ByteBuffer作为一个抽象类,还有其他实现类。如下图所示:

在这里插入图片描述

本文中会重点介绍其四种实现类,HeapByteBufferHeapByteBufferRDirectByteBufferDirectByteBufferR
而关于MappedByteBuffer,后续会单独来介绍。

1、HeapByteBuffer

Heap代表堆空间,顾名思义,这个Buffer是分配在JVM堆上的,该区域受JVM管理,回收也由GC来负责。
通过查看其源码可以看到其分配操作过程

class HeapByteBuffer extends ByteBuffer {
    // 构造方法
    protected HeapByteBuffer(byte[] buf, int mark, int pos, int lim, int cap, int off) {
        super(mark, pos, lim, cap, buf, off);
    }
}
 
// ByteBuffer
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>
{
    final byte[] hb;                  // Non-null only for heap buffers
    final int offset;
    boolean isReadOnly;                 // Valid only for heap buffers
 
    ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
                 byte[] hb, int offset)
    {
        super(mark, pos, lim, cap);
        this.hb = hb;
        this.offset = offset;
    }
}

通过源码可以很清楚的看到,HeapByteBuffer本质上是一个字节数组,通过positionlimit的控制,来反复的操作该字节数组。

2、HeapByteBufferR

那么该类与HeapByteBuffer有什么区别呢?直接看源码

class HeapByteBufferR extends HeapByteBuffer {
    protected HeapByteBufferR(byte[] buf, int mark, int pos, int lim, int cap, int off) {
        // 直接调用HeapByteBuffer的赋值方法
        super(buf, mark, pos, lim, cap, off);
        // 设置为只读
        this.isReadOnly = true;
    }
}

可以看到,该类与HeapByteBuffer几乎没有区别,除了属性isReadOnly之外,该属性是属于ByteBuffer的属性。

那么设置isReadOnly=true,将HeapByteBufferR设置为只读后,具体有哪些限制呢?继续看源码

    public ByteBuffer put(byte x) {
        throw new ReadOnlyBufferException();
    }
 
    public ByteBuffer put(int i, byte x) {
        throw new ReadOnlyBufferException();
    }
...

所有的put方法都抛出了异常,不允许对数组中的值进行添加操作了。
那么问题来了,既然不允许以put方式来对HeapByteBufferR进行赋值操作,那要怎样才能赋值呢,看下面的示例

ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("risk".getBytes());
 
// 直接对原HeapByteBuffer进行操作,会生成一个与原HeapByteBuffer一样的buffer,且只读
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();

总结:每一个类型的ByteBuffer都有只读和非只读类型的实现类,只读实现类默认以R结尾。

3、DirectByteBuffer

字面意思是直接缓冲区。何为直接缓冲呢?
实际就是堆外内存,该内存块不属于JVM的Heap堆,而是操作系统的其他内存块(本质上就是C语言用malloc进行分配所得到的内存块)。
通过源码我们可以看到其与HeapByteBuffer分配时的不同

// ByteBuffer
public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}
 
// DirectByteBuffer
DirectByteBuffer(int cap) {
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);
 
    long base = 0;
    try {
        // 通过unsafe来分配内存
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        // 最终赋值给address来获取内存地址引用
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}
 
// Unsafe
public native long allocateMemory(long var1);
    Unsafe.allocateMemory是一个native方法,会调用到本操作系统相关的实现方法。(也就是上述所说的,C语言直接调用malloc创建的内存块)。而关于其put get等方法,都是通过Unsafe来控制的
// DirectByteBuffer
public ByteBuffer put(byte x) {
    unsafe.putByte(ix(nextPutIndex()), ((x)));
    return this;
}
 
public byte get() {
    return ((unsafe.getByte(ix(nextGetIndex()))));
}

4、DirectByteBuffer与HeapByteBuffer异同

Q:既然我们已经有了HeapByteBuffer,那为什么还需要DirectByteBuffer呢?
A:是由于操作系统没法直接访问JVM内存。

细细想来,这个答案有明显的不合理处,操作系统作为底层设施,所有的进程都运行在其上,内存都由其来分配,怎么可能无法操作JVM的内存呢?

理论上来说,操作系统是可以访问JVM内存空间的,但是由于JVM需要进行GC,如果当IO设备直接和JVM堆内存数据直接交互,此时JVM进行了GC操作,原来IO设备操作的字节被移动到其他区域,那IO设备便无法正确的获取到该字节数据。

而DirectByteBuffer,是由操作系统直接分配的,位置不会变动,是可以与IO设置直接进行交互的。所以实际上当IO设备与HeapByteBuffer进行交互时,会先将HeapByteBuffer中的数据临时拷贝到DirectByteBuffer(临时创建的,使用后销毁),然后再从DirectByteBuffer拷贝到IO设置内存空间(一般就是内核空间)

1)源码释疑

我们可以通过源码来验证上面这段话的正确性(会涉及到后面Channel的知识点)

// 我们通过FileChannel打开一个文件,然后将HeapByteBuffer中的数据写入到该文件中
RandomAccessFile file = new RandomAccessFile(new File("C:\\Users\\lucky\\Desktop\\filetest.txt"), "rwd");
FileChannel channel = file.getChannel();
 
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put((byte) 'h').put((byte) 'e').put((byte) 'l').put((byte) 'l').put((byte) 'o');
 
buffer.position(0);
// 通过channel将buffer中的数据写入
channel.write(buffer);

观察FileChannel.write

// FileChannelImpl
public int write(ByteBuffer var1) throws IOException {
        this.ensureOpen();
        if (!this.writable) {
            throw new NonWritableChannelException();
        } else {
            synchronized(this.positionLock) {
                int var3 = 0;
                int var4 = -1;
 
                try {
                    this.begin();
                    var4 = this.threads.add();
                    if (!this.isOpen()) {
                        byte var12 = 0;
                        return var12;
                    } else {
                        do {
                            // 通过IOUtil来写入
                            var3 = IOUtil.write(this.fd, var1, -1L, this.nd);
                        } while(var3 == -3 && this.isOpen());
 
                        int var5 = IOStatus.normalize(var3);
                        return var5;
                    }
                } finally {
                    this.threads.remove(var4);
                    this.end(var3 > 0);
 
                    assert IOStatus.check(var3);
 
                }
            }
        }
    }
 
// IOUtil.java
static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
        if (var1 instanceof DirectBuffer) {
            // 如果使用的就是DirectByteBuffer,则直接写入
            return writeFromNativeBuffer(var0, var1, var2, var4);
        } else {
            int var5 = var1.position();
            int var6 = var1.limit();
 
            assert var5 <= var6;
 
            int var7 = var5 <= var6 ? var6 - var5 : 0;
            // 创建DirectByteBuffer
            ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7);
 
            int var10;
            try {
                // 将HeapByteBuffer中的数据全部写入到DirectByteBuffer
                var8.put(var1);
                var8.flip();
                var1.position(var5);
                // 将DirectByteBuffer中的数据写入到文件中
                int var9 = writeFromNativeBuffer(var0, var8, var2, var4);
                if (var9 > 0) {
                    var1.position(var5 + var9);
                }
 
                var10 = var9;
            } finally {
                Util.offerFirstTemporaryDirectBuffer(var8);
            }
 
            return var10;
        }
    }

通过以上源码的分析,正好验证了我们上述结果。

2)优缺点比较

HeapByteBuffer,上面我们已经说了,在于IO设备进行交互时,会多一次拷贝,DirectByteBuffer则不会。
HeapByteBuffer的内存回收归JVM操作,使用GC即可,使用时不需要担心内存泄露;而DirectByteBuffer的内存分配和回收都需要使用方来自行解决,操作难度相对会大一些,更容易内存泄露。

3)JVM参数设置

HeapByteBuffer内存分配在JVM堆空间,则通过-XX:Xmx可以设置其最大值;

DirectByteBuffer分配在堆外空间,则通过-XX:MaxDirectMemorySize来设置其最大值

5.Buffer扩展

有关于Buffer的使用,我们最常用的就是上述的ByteBuffer。实际上除了这些,Buffer还有各个基础类型的实现类,如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jrss4A6j-1655364759823)(Java%20NIO.assets/image_10.png)]

这些基础类型的Buffer实现与ByteBuffer在使用上差不多,只不过操作的不再是以字节为单位,而是以对应基础类型为单位。

上述展示的也是Buffer的抽象类实现,比如IntBuffer也是一个abstract类,而具体的实现同ByteBuffer一样,也是有Heap和Direct两种,IntBuffer具体实现如下:

在这里插入图片描述

关于这些,笔者不再详细说明,大家可以简单了解使用即可。

推荐阅读:

有关于DirectByteBuffer的更深入介绍(笔者的操作系统相关知识实在太薄弱了,没法这么深入),可以参考大神文章: https://blog.csdn.net/wangwei19871103/article/details/104235590

四、Channel 简介

1、Channel 基本定义

Channel,翻译过来就是通道。通道表示打开到IO设备(文件、套接字)的连接。

通道有点类似于流的概念,就是InputStreamOutputStream,都是可以用来传输数据的。但是两者之间又有本质的不同,不同点如下

  1. 通过通道,程序既可以读数据又可以写数据;流的读写则是单向的(比如InputStream就是读,OutputStream就是写)

  2. 通道可以进行数据的异步读写;而流的读写一般都是阻塞的同步的;

  3. 通道的数据读写需要通过BufferBuffer的操作比较灵活;而流的话直接读写在byte[]中;

2、Channel 的类图

​ 我们从几个层级来展示下Channel由上至下的接口实现

在这里插入图片描述

2.1 Channel 接口

public interface Channel extends Closeable {
    // 当前通道是否打开
    public boolean isOpen();
    // 关闭通道
    public void close() throws IOException;
}

一个打开的通道即代表是一个特定的IO服务的特定连接。

当通道关闭后,该连接会丢失。对于已经关闭的通道进行读写操作都会抛出ClosedChannelException

调用close方法来关闭通道时,可能会导致通道在关闭底层的IO服务的过程中线程暂时阻塞。通道关闭的阻塞行为取决于操作系统或对应文件系统

2.2 InterruptibleChannel

只是一个标记接口,表示当前通道是可被中断的。

实现该接口的通道有以下特性:如果一个线程在一个通道上被阻塞并且同时被中断,那么当前通道则会被关闭,同时该阻塞线程也会产生一个 ClosedByInterruptException。

2.3 ReadableByteChannel和WritableByteChannel

// WritableByteChannel.java

public interface WritableByteChannel extends Channel {
	public int write(ByteBuffer src) throws IOException;
}

// ReadableByteChannel.java
public interface ReadableByteChannel extends Channel {
    public int read(ByteBuffer dst) throws IOException;
}

可以看到,这两个就是新增了对ByteBufferreadwrite操作

2.4 ByteChannel

public interface ByteChannel
    extends ReadableByteChannel, WritableByteChannel
{
    
}

只实现其中一个接口(ReadableByteChannelWritableByteChannel)则只能实现单向的读或写,数据只能在一个方向上传输。

ByteChannel继承了ReadableByteChannelWritableByteChannel

实现ByteChannel的类可以同时进行读和写,实现数据的双向传输。

2.5 SelectableChannel

/** A channel that can be multiplexed via a {@link Selector}. */
public abstract class SelectableChannel
    extends AbstractInterruptibleChannel
    implements Channel
{
	public abstract SelectorProvider provider();
    public abstract SelectionKey keyFor(Selector sel);
    public abstract SelectionKey register(Selector sel, int ops, Object att)
        throws ClosedChannelException;
    ...
}

可以看到,SelectableChannel不再是单打独斗的Channel了,而是与Selector进行了结合。

从它的注释中我们能看到,这种Channel是一种通过Selector进行多路复用的Channel。

看其的实现类SocketChannel、ServerSocketChannel,我们也知道,这些都是使用多路复用的最佳场景。

2.6 FileChannel

/** A channel for reading, writing, mapping, and manipulating a file. */
public abstract class FileChannel
    extends AbstractInterruptibleChannel
    implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel
{
	...
}

FileChannel看其注释,就会了解到,这是一个操作文件的Channel。我们通过FileChannel可以读、写、mapping(映射)、操作一个文件。

五、FileChannel 简介

1、FileChannel 基本定义

FileChannel是一个连接到文件的通道,我们可以通过FileChannel读写文件,映射文件。

常用功能:

  • position 指向文件内容的绝对位置。该绝对位置可以通过position()查询和position(long)进行修改。
  • truncate() 裁剪特定大小文件
  • force() 强制把内存中的数据刷新到硬盘中
  • lock() 对通道上锁

FileChannel 特性

  • 可以利用read(ByteBuffer,position)或者write(ByteBuffer,position)来在文件的绝对位置上读取或者写入,但是不会改变通道本身的position

  • 可以利用map(MapMode,position,size)方法将文件映射到内存中,其中position指的是通道的绝对位置,size映射大小,映射方式有三种:

    • MapMode.READ_ONLY:只读的方式映射到内存,修改文件将抛出ReadOnlyBufferException;
    • MapMode.READ_WRITE:读写的方式映射到内存,修改后的内存可以通过force()方法写入内存,但是对其他关联到该文件进程可见性是不确定的,可能会出现并发性问题,同时在该模式下,通道必须以rw的方式打开;
    • MapMode.PRIVATE:私有方式,可以修改映射到内存的文件,但是该修改不会写入内存,同时对其他进程也是不可见的
      另外该map中的数据只能等到gc的时候才能清理,同时map一旦创建,将和FileChannel无关,FileChannel关闭也不会对其有影响;
      map方法因为将文件直接映射到内存中,因此其读写性能相比FileInputStreamFileOutputStream来说要好一些,但是资源消耗代价也会大些,因此比较适合大文件的读写;
  • 可以利用transferTo()/transferFrom()来将bytes数组在两个通道之间来回传递,该性能相对来较快,可以快速实现文件复制,因为FileChannel是将通过JNI(本地方法接口)将文件读取到native堆即堆外内存中,通过DirectrByteBuffer来引用这些数据,这样在实现文件复制或传输时,无需将文件从堆外内存拷贝到java堆中,本质上这就是减少了内核内存和用户内存之间的数据拷贝,从而提升性能;

  • 可以利用lock(position,size,isShared)方法实现对指定文件区域进行加锁,加锁的方式分为共享互斥,有些操作系统不支持共享锁,因此可通过isShared()方式来判断是否能进行互斥操作;

  • FileChannel是线程安全的,对于多线程操作,只有一个线程能对该通道所在文件进行修改,

  • 可以通过open()方法开启一个通道,同时也可以通过FileInputStream或者FileOutputStreamRandomAccessFile调用方法getChannel()来获取;

RandomAccessFile accessfile = new RandomAccessFile(new java.io.File("C:\\Users\\Administrator\\git\\javabase\\JavaBase\\resources\\text.txt"), "rw");
FileChannel fileChannel = accessfile.getChannel();
MappedByteBuffer map = fileChannel.map(MapMode.READ_WRITE, 0, fileChannel.size());

Charset charset=Charset.forName("utf-8");
CharBuffer decode = charset.decode(map.asReadOnlyBuffer());
System.out.println(decode.toString());//读取测试
byte[] chars = "hao hi yo".getBytes();

map.put(chars,0,chars.length);//写入测试,写入位置和position有关
map.force();
fileChannel.close();

3、FileChannel的基本结构

在这里插入图片描述

通过它的类结构图我们可以看到,FileChannel实现了对文件的读写操作,还被设置为可中断。下面来具体了解下其API。

2、FileChannel API

2.1 FileChannel 创建

File file = new File("D:\\test.txt");
 
// 1.通过RandomAccessFile创建
RandomAccessFile raFile = new RandomAccessFile(file, "rwd");
FileChannel channel = raFile.getChannel();
 
// 2.通过FileInputStream创建
FileInputStream fileInputStream = new FileInputStream(file);
FileChannel inputStreamChannel = fileInputStream.getChannel();
 
// 3.通过FileOutputStream创建
FileOutputStream fileOutputStream = new FileOutputStream(file);
FileChannel outputStreamChannel = fileOutputStream.getChannel();

通过这三种方式创建的FileChannel有什么具体的区别呢?我们通过源码来比对下

// RandomAccessFile.getChannel()
channel = FileChannelImpl.open(fd, path, true, rw, this);
 
// FileInputStream.getChannel()
channel = FileChannelImpl.open(fd, path, true, false, this);
 
// FileOutputStream.getChannel()
channel = FileChannelImpl.open(fd, path, false, true, append, this);
 
// FileChannelImpl构造方法
private FileChannelImpl(FileDescriptor var1, String var2, boolean var3, boolean var4, boolean var5, Object var6) {
    this.fd = var1;
    this.readable = var3;
    this.writable = var4;
    this.append = var5;
    this.parent = var6;
    this.path = var2;
    this.nd = new FileDispatcherImpl(var5);
}

通过FileChannelImpl的私有构造方法我们可以了解到var3参数对应的是是否可读,var4对应的是是否可写。
再结合FileInputStream.getChannelFileOutputStream.getChannel时传入FileChannelImplement的参数,可以得到以下结果:

获取方式是否有文件读写权限
RandomAccessFile.getChannel可读,是否可写根据传入mode来判断
FileInputStream.getChannel可读,不可写
FileOutputStream.getChannel可写,不可读
另:FileChannel还提供了一个open()的static方法,也可以通过该方式来获取,只不过这种方式不太常用,笔者不再详述。

2.2 RandomAccessFile的mode

RandomAccessFile的构造方法中有两个参数,分别对应file引用和mode(模式)。

mode具体有哪些值呢?我们直接看源码

public RandomAccessFile(File file, String mode)
    throws FileNotFoundException {
    
    String name = (file != null ? file.getPath() : null);
    int imode = -1;
    // read 只读模式
    if (mode.equals("r"))
        imode = O_RDONLY;
    // rw read and write 读写模式
    else if (mode.startsWith("rw")) {
        imode = O_RDWR;
        rw = true;
        if (mode.length() > 2) {
            // 还有s和d,分别对应于O_SYNC O_DSYNC
            if (mode.equals("rws"))
                imode |= O_SYNC;
            else if (mode.equals("rwd"))
                imode |= O_DSYNC;
            else
                imode = -1;
        }
    }
    ...
    fd = new FileDescriptor();
    fd.attach(this);
    path = name;
    open(name, imode);
}

O_SYNC O_DSYNC这两个分别代表什么呢?

由于内存比磁盘读写速度快了好几个数量级,为了弥补磁盘IO性能低,Linux内核引入了页面高速缓存(PageCache)。我们通过Linux系统调用(open--->write)写文件时,内核会先将数据从用户态缓冲区拷贝到PageCache便直接返回成功,然后由内核按照一定的策略把脏页Flush到磁盘上,我们称之为write back

write写入的数据是在内存的PageCache中的,一旦内核发生Crash或者机器Down掉,就会发生数据丢失,对于分布式存储来说,数据的可靠性是至关重要的,所以我们需要在write结束后,调用fsync或者fdatasync将数据持久化到磁盘上。
write back减少了磁盘的写入次数,但却降低了文件磁盘数据的更新速度,会有丢失更新数据的风险。为了保证磁盘文件数据和PageCache数据的一致性,Linux提供了syncfsyncmsyncfdatasyncsync_file_range 5个函数。
open函数的O_SYNCO_DSYNC参数有着和fsyncfdatasync类似的含义:使每次write都会阻塞到磁盘IO完成。

  • O_SYNC:使每次write操作阻塞等待磁盘IO完成,文件数据和文件属性都更新。
  • O_DSYNC:使每次write操作阻塞等待磁盘IO完成,但是如果该写操作并不影响读取刚写入的数据,则不需等待文件属性被更新。

O_DSYNCO_SYNC标志有微妙的区别:

文件以O_SYNC标志打开时,数据和属性总是同步更新。对于该文件的每一次write都将在write返回前更新文件时间,这与是否改写现有字节或追加文件无关。相对于fsync/fdatasync,这样的设置不够灵活,应该很少使用。

文件以O_DSYNC标志打开时,仅当文件属性需要更新以反映文件数据变化(例如,更新文件大小以反映文件中包含了更多数据)时,标志才影响文件属性。在重写其现有的部分内容时,文件时间属性不会同步更新。

实际上:LinuxO_SYNCO_DSYNC做了相同处理,没有满足POSIX的要求,而是都实现了fdatasync的语义。

(来自https://zhuanlan.zhihu.com/p/104994838)

正是由于内存和磁盘之间的读写速度差异,所以才有了write方法只是将数据写入pageCache的优化做法,同时操作系统也提供了O_SYNCO_DSYNC来保证数据刷入磁盘。

2.3 write 写相关方法

// 1.将单个ByteBuffer写入FileChannel
public abstract int write(ByteBuffer src) throws IOException;
 
// 2.写入批量ByteBuffer,offset即ByteBuffer的offset
public abstract long write(ByteBuffer[] srcs, int offset, int length)
        throws IOException;
 
// 3.同2,offset为0
public final long write(ByteBuffer[] srcs) throws IOException {
    return write(srcs, 0, srcs.length);
}

标准写入方式:

File file = new File("D:\\test.txt");
 
// 1.通过RandomAccessFile创建
RandomAccessFile raFile = new RandomAccessFile(file, "rwd");
FileChannel channel = raFile.getChannel();
 
ByteBuffer byteBuffer = ByteBuffer.allocate(100);
 
String text = "When grace is lost from life, come with a burst of song";
byteBuffer.put(text.getBytes());
 
byteBuffer.flip();
// 写入数据
while (byteBuffer.hasRemaining()) {
    channel.write(byteBuffer);
}

注意:write方法是在while循环中做的,因为无法保证一次write方法向FileChannel中写入多少字节

2.4 read 读相关方法

// 1.将文件内容读取到单个ByteBuffer
public abstract int read(ByteBuffer dst) throws IOException;
 
// 2.将文件内容读取到ByteBuffer[]中,ByteBuffer的offset为指定值
public abstract long read(ByteBuffer[] dsts, int offset, int length)
        throws IOException;
 
// 3.同2
public final long read(ByteBuffer[] dsts) throws IOException {
    return read(dsts, 0, dsts.length);
}

标准读取方式:

File file = new File("D:\\test.txt");
 
// 1.通过RandomAccessFile创建
RandomAccessFile raFile = new RandomAccessFile(file, "rwd");
FileChannel channel = raFile.getChannel();
 
ByteBuffer byteBuffer = ByteBuffer.allocate(100);
// 真正读取到readCount个字节
int readCount = channel.read(byteBuffer);
 
byteBuffer.flip();
byte[] array = byteBuffer.array();
// 将读取到的内容写入到String
String s = new String(array);
// 结果就是刚才2.3 write方法中写入的值
System.out.println(s);

2.5 force方法

public abstract void force(boolean metaData) throws IOException;

之前2.2说过,write方法写入文件可能只是写入了PageCache,如果此时系统崩溃,那么只存在于PageCache而没有刷入磁盘的数据就有可能丢失。使用force方法,我们就可以强制将文件内容和元数据信息(参数boolean metaData就是用来决定是否将元数据也写入磁盘)写入磁盘。该方法对一些关键性的操作,比如事务操作,就是非常关键的,使用force方法可以保证数据的完整性和可靠恢复。

2.6 lock相关方法

// 1.从file的position位置开始,锁定长度为size,锁定类别共享锁(true)或独占锁(false)
public abstract FileLock lock(long position, long size, boolean shared)
        throws IOException;
 
// 2.同1,基本独占全文件
public final FileLock lock() throws IOException {
    return lock(0L, Long.MAX_VALUE, false);
}
 
// 3.同1,尝试进行文件锁定
public abstract FileLock tryLock(long position, long size, boolean shared)
        throws IOException;
 
// 4.同2,尝试进行文件锁定
public final FileLock tryLock() throws IOException {
    return tryLock(0L, Long.MAX_VALUE, false);
}

首先,我们需要明白的是:锁定针对的是文件本身,而不是Channel或者线程
FileLock可以是共享的,也可以是独占的。
锁的实现很大程度上依赖于本地的操作系统实现。当操作系统不支持共享锁时,则会主动升级共享锁为独占锁。

// 通过两个进程来测试下FileLock
FileLock lock = null;
try {
    File file = new File("D:\\test.txt");
 
    // 1.通过RandomAccessFile创建
    RandomAccessFile raFile = new RandomAccessFile(file, "rwd");
    FileChannel channel = raFile.getChannel();
 
    // 主动设置独占锁或共享锁
    lock = channel.lock(0, Integer.MAX_VALUE, true);
    System.out.println(lock);
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        // 需要主动release
        lock.release();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

笔者基于使用Windows机器测试结果: 支持两个进程对同一文件的共享锁; 不支持两个进程对同一文件的独占锁(一个独占一个共享也不可以

总结:

本文主要介绍了下FileChannel的常用API。基于FileChannel,我们可以实现对文件的读写操作。
FileChannel还有些比较高级的API,比如map()transferTo()transferFrom()等。

参考:

https://zhuanlan.zhihu.com/p/104994838

六、map和transferTo、transferFrom

前言:

上文我们介绍了下FileChannel的基本API使用。本文中,我们就一起看下FileChannel中的高阶API。
说是高阶,还真的就是,这些知识点大量利用了操作系统的对文件传输映射的高级玩法,极大的提高了我们操作文件的效率。我们熟知的kafka、rocketMQ等也是用了这些高阶API,才有如此的高效率。
我们提出一个需求,描述如下:提供一个对外的socket服务,该服务就是获取指定文件目录下的文件,并写出到socket中,最终展现在client端。

1、传统的文件网络传输过程

按照此需求,常规方式,我们使用如下代码来完成:

File file = new File("D:\\test.txt");
Long size = file.length();
byte[] arr = new byte[size.intValue()];
 
try {
    // 1.将test.txt文件内容读取到arr中
    FileInputStream fileInputStream = new FileInputStream(file);
    fileInputStream.read(arr);
 
    // 2.提供对外服务
    Socket socket = new ServerSocket(9999).accept();
 
    // 3.传输到客户端
    socket.getOutputStream().write(arr);
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

以上是一个最简单版本的实现。
那么从操作系统的角度,以上传输经历了哪些过程呢?

在这里插入图片描述

这中间的过程我们可以分为以下几步:

fileInputStream.read方法对应于:

1)第一次复制:**read方法调用,用户态切换到内核态。**数据从硬盘拷贝到内核缓冲区,基于DMA自动操作,不需要CPU支持

2)第二次复制:从内核缓冲区拷贝到用户缓冲区(也就是byte[] arr中)。read方法返回,用内核态到用户态的转换。
socket.getOutputStream().write(arr)对应于:

3)第三次复制:从用户缓冲区拷贝数据到socket的内核缓冲区。write方法调用,用户态切换到内核态。

4)数据从socket内核缓冲区,使用DMA拷贝到网络协议引擎。write方法返回,内核态切换到用户态。

从上面的过程我们可以发现,数据发生了四次拷贝,四次上下文切换。

那么还有没有优化方式呢?答案是肯定的,我们接着往下看。

2、mmap优化

mmap通过内存映射,将文件直接映射到内存中。此时,用户空间和内核空间可以共享这段内存空间的内容。用户对内存内容的修改可以直接反馈到磁盘文件上。

FileChannel提供了map方法来实现mmap功能

File file = new File("D:\\test.txt");
Long size = file.length();
byte[] arr = new byte[size.intValue()];
 
try {
    // 1.将test.txt文件内容读取到arr中
    RandomAccessFile raFile = new RandomAccessFile(file, "rwd");
    FileChannel channel = raFile.getChannel();
    MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, size);
 
    // 2.提供对外服务
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
 
    serverSocketChannel.socket().bind(new InetSocketAddress(9999));
    serverSocketChannel.configureBlocking(false);
 
    while(true){
        SocketChannel socketChannel =
            serverSocketChannel.accept();
 
        if(socketChannel != null){
            // 3.传输到客户端
            socketChannel.write(mappedByteBuffer);
        }
    }
 
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

我们直接将file的内容映射到mappedByteBuffer,然后直接将mappedByteBuffer的内容传递出去。
那么从操作系统的角度,以上传输经历了哪些过程呢?

在这里插入图片描述

参考1中的四个步骤,少了一次内存拷贝,就是将文件从内核缓冲区拷贝到用户进程缓冲区这一步;但是上下文切换并没有减少。

3、sendFile优化(Linux2.1版本)

Linux2.1版本提供了sendFile函数,该函数对本例有哪些优化呢?

就是可以将数据不经过用户态,直接从内核文件缓冲区传输到Socket缓冲区

FileChannel提供transferTo(和transferFrom)方法来实现sendFile功能

File file = new File("D:\\test.txt");
Long size = file.length();
 
try {
    // 1.将test.txt文件内容读取到arr中
    RandomAccessFile raFile = new RandomAccessFile(file, "rwd");
    FileChannel channel = raFile.getChannel();
 
    // 2.提供对外服务
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
 
    serverSocketChannel.socket().bind(new InetSocketAddress(9999));
    serverSocketChannel.configureBlocking(false);
 
    while(true){
        SocketChannel socketChannel =
            serverSocketChannel.accept();
 
        if(socketChannel != null){
            // 3.使用transferTo方法将文件数据传输到客户端
            channel.transferTo(0, size, socketChannel);
        }
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

同2中的代码,只是在最后一步将文件内容传输到socket时,使用了不一样的方法,本例中使用了FileChannel.transferTo方法来传递数据。

那么从操作系统的角度,以上传输经历了哪些过程呢?

在这里插入图片描述

参照1中的4个过程,少了用户空间的参与,那么就不存在用户态与内核态的切换。

所以,总结下来,就是减少了两次上下文切换,同时,减少了一次数据拷贝。

**注意:**剩下的是哪两次上下文切换呢?用户进程调用transferTo方法,用户态切换到内核态;调用方法返回,内核态切换到用户态。

4、sendFile优化(Linux2.4版本)

Linux2.4版本,sendFile做了一些优化,避免了从内核文件缓冲区拷贝到Socket缓冲区的操作,直接拷贝到网卡,再次减少了一次拷贝。

代码同3,只是具体实现时的操作系统不太一样而已。

那么从操作系统的角度,其传输经历了哪些过程呢?

参照1中的4个操作过程,同样少了用户空间的参与,也不存在用户态与内核态的切换。
所以总结下来,就是两次数据拷贝,两次上下文切换(相比较3就是减少了内核文件缓冲区到内核socket缓冲区的拷贝)

总结:

下面我们通过一个图表来展示下以上四种传输方式的异同

传输方式上下文切换次数数据拷贝次数
传统IO方式44
mmap方式43
sendFile(Linux2.1)23
sendFile(Linux2.4)22

实际,以上sendFile的数据传输方式就是我们常说的零拷贝

可能会有些疑问,哪怕Linux2.4版本的sendFile函数不也是有两次数据拷贝嘛,为什么会说是零拷贝呢?

笔者拷贝了一段话,解释的蛮有意思的:

首先我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据,
sendFile 2.1 版本实际上有 2 份数据,算不上零拷贝)。例如我们刚开始的例子,内核缓存区和 Socket 缓冲区的数据就是重复的。

而零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。

再稍微讲讲 mmapsendFile 的区别。

参考:

linux下的mmap和零拷贝技术 - 简书
mmap与sendfile() - 简书

七、SocketChannel

前言:

SocketChannel作为网络套接字的通道,与之前我们学习到的FileChannel有很多不同之处(就是两个大类别的通道)。

没有SocketChannel之前,我们创建网络连接一般都是通过SocketServerSocket,这些都是BIO类别,性能的扩展会受到影响。

借助NIO相关实现SocketChannelServerSocketChannel,我们可以管理大量连接并且实现更小的性能损失。

本文就来介绍下SocketChannel的相关使用。

我们来给定一个需求:就是创建一个简易的对话框,使客户端和服务端可以接收到彼此的对话,并予以响应。(本篇专注于client端,也就是Socket和SocketChannel,下一篇会继续将server端的补上)。

1、基于Socket的客户端

public class BIOClientSocket {
 
    private String address;
    private int port;
 
    public BIOClientSocket(String address, int port) {
        this.address = address;
        this.port = port;
    }
 
    public void connectToServer() {
        Socket socket = new Socket();
        try {
            socket.connect(new InetSocketAddress(address, port));
 
            // 写数据
            new ClientWriteThread(socket).start();
            // 读数据
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String msg = "";
            while ((msg = bufferedReader.readLine()) != null) {
                System.out.println("receive msg: " + msg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
    public static void main(String[] args) {
        String address = "localhost";
        int port = 9999;
 
        BIOClientSocket bioClientSocket = new BIOClientSocket(address, port);
        bioClientSocket.connectToServer();
    }
}
 
/**
 * 客户端发送请求线程
 */
class ClientWriteThread extends Thread {
    private Socket socket;
    private PrintWriter writer;
    private Scanner scanner;
 
    public ClientWriteThread(Socket socket) throws IOException {
        this.socket = socket;
        this.scanner = new Scanner(System.in);
        this.writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
    }
 
    @Override
    public void run() {
        String msg = "";
 
        try {
            // 通过获取对话框里的消息,不断发送到server端
            while ((msg = scanner.nextLine()) != null) {
                if (msg.equals("bye")) {
                    break;
                }
                writer.println(msg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

以上就是标准的Socket客户端与服务端交互的代码,也比较简单,笔者不再详述

2、基于SocketChannel的客户端

public class NIOClientSocket {
 
    private String address;
    private int port;
    private Selector selector;
 
    private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    private ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
 
    private Scanner scanner = new Scanner(System.in);
 
 
    public NIOClientSocket(String address, int port) throws IOException {
        this.address = address;
        this.port = port;
        this.selector = Selector.open();
    }
 
    public void connectToServer() {
        try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
 
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
            socketChannel.connect(new InetSocketAddress(address, port));
 
            connect();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
    private void connect() {
        while (true) {
            try {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                for (SelectionKey key : selectionKeys) {
                    if (key.isConnectable()) {
                        SocketChannel clientChannel = (SocketChannel) key.channel();
                        if (clientChannel.isConnectionPending()) {
                            clientChannel.finishConnect();
                            System.out.println("client connect success...");
                        }
 
                        clientChannel.register(selector, SelectionKey.OP_WRITE);
                    } else if (key.isReadable()) {
                        SocketChannel clientChannel = (SocketChannel) key.channel();
                        StringBuffer sb = new StringBuffer("receive msg: ");
 
                        readBuffer.clear();
                        while (clientChannel.read(readBuffer) > 0) {
                            readBuffer.flip();
                            sb.append(new String(readBuffer.array(), 0, readBuffer.limit()));
                        }
 
                        System.out.println(sb.toString());
                        clientChannel.register(selector, SelectionKey.OP_WRITE);
                    } else if (key.isWritable()) {
                        SocketChannel clientChannel = (SocketChannel) key.channel();
                        String msg = scanner.nextLine();
                        writeBuffer.clear();
 
                        writeBuffer.put(msg.getBytes());
                        writeBuffer.flip();
                        clientChannel.write(writeBuffer);
                        clientChannel.register(selector, SelectionKey.OP_READ);
                    }
                }
 
                selectionKeys.clear();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
 
    public static void main(String[] args) {
        String address = "localhost";
        int port = 9999;
 
        try {
            new NIOClientSocket(address, port).connectToServer();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

借助Selector,我们将想要监听的事件注册到Selector上。

client端默认先进行写,故在连接建立完成之后,直接注册了写事件;

写的事件会阻塞到Scanner上,等待用户输入,输入后传输给Server端,然后注册读事件;

通过这样的读写事件来回注册,就可以实现类似对话框的效果。(当然,必须是一问一答)。

3、SocketChannel API

我们先来看下其类结构图

在这里插入图片描述

可以看到,其可读可写(实现了ByteChannel);可通过Selector进行事件注册(继承了SelectableChannel);可进行端口绑定,Socket属性设置(实现了NetworkChannel)。

3.1 非阻塞模式

SocketChannel提供configureBlocking方法(本质上是AbstractSelectableChannel提供的),来描述通道的阻塞状态。我们可以将SocketChannel设置为非阻塞状态。

同时其还提供了isBlocking方法来查询其阻塞状态。

传统的Socket其阻塞性是影响系统可伸缩性的重要约束。而这种非阻塞的SocketChannel则是许多高性能程序构建的基础。

延伸:阻塞socket与非阻塞socket两者之间有哪些具体区别呢?

1)输入操作

进程A调用阻塞socket.read方法时,若该socket的接收缓冲区没有数据可读,则该进程A被阻塞,操作系统将进程A睡眠,直到有数据到达;
进程A调用非阻塞socket.read方法时,若该socket的接收缓冲区没有数据可读,则进程A收到一个EWOULDBLOCK错误提示,表示无可读数据,read方法立即返回,进程A可针对错误提示进行后续操作。

2)输出操作

进程A调用阻塞socket.write方法时,若该socket的发送缓冲区没有多余空间,则进程A被阻塞,操作系统将进程A睡眠,直到有空间为止;

进程A调用非阻塞socket.write方法时,若该socket的发送缓冲区没有多余空间,则进程A收到一个EWOULDBLOCK错误提示,表示无多余空间,write方法立即返回,进程A可针对错误提供进行后续操作。

3)连接操作

对于阻塞型的socket而言,调用socket.connect方法创建连接时,会有一个三次握手的过程,每次需要等到三次握手完成之后(ESTABLISHED 状态),connect方法才会返回,这意味着其调用进程需要至少阻塞一个RTT时间。

对于非阻塞的SocketChannel而言,调用connect方法创建连接时,当三次握手可以立即建立时(一般发生在客户端和服务端在一个主机上时),connect方法会立即返回;而对于握手需要阻塞RTT时间的,非阻塞的SocketChannel.connect方法也能照常发起连接,同时会立即返回一个EINPROGRESS(在处理中的错误)。

正如上述2中的代码:

// SocketChannel直接建立连接,当前进程并没有阻塞
socketChannel.connect(new InetSocketAddress(address, port));
 
// 后续通过注册的Selector来获取连接状态
// 当selector检测到SocketChannel已经完成连接或连接报错,则会添加OP_CONNECT到key的就绪列表中
if (key.isConnectable()) {
    SocketChannel clientChannel = (SocketChannel) key.channel();
    // 此时需要判断连接是否成功
    if (clientChannel.isConnectionPending()) {
        clientChannel.finishConnect();
        System.out.println("client connect success...");
    }

3.2 NetworkChannel(网络连接相关方法)

SocketChannel实现了NetworkChannel接口的相关方法,来完成ip:port的绑定,socket属性的设置。

// 使当前channel绑定到具体地址
NetworkChannel bind(SocketAddress local) throws IOException;
 
// 设置socket属性
<T> NetworkChannel setOption(SocketOption<T> name, T value) throws IOException;

3.3 AbstractSelectableChannel(绑定Selector相关方法)

SocketChannel继承了AbstractSelectableChannel抽象类,来完成Selector的注册,多路复用功能。

// 将当前通道注册到Selector上
public abstract SelectionKey register(Selector sel, int ops, Object att)
        throws ClosedChannelException;
 
// 获取当前selector上可执行的操作(OP_READ OP_WRITE...)
public final SelectionKey keyFor(Selector sel)

3.4 ByteChannel(数据的读写)

SocketChannel实现ByteChannel接口,这个接口我们之前了解过,ByteChannel接口继承了ReadableByteChannelWritableByteChannel,实现了对数据的读写。

上文中的示例里,clientChannel.read()clientChannel.write()方法就是对其的使用。

4.Socket与SocketChannel

通过以上的介绍,我们会使用了SocketChannel,也会使用Socket来创建对服务端的连接。那么这两者之间有什么关系吗?

// A socket is an endpoint for communication between two machines
public class Socket implements java.io.Closeable {
    /** A socket will have a channel if, and only if, the channel itself was
     * created via the{@link java.nio.channels.SocketChannel#open
     * SocketChannel.open} or {@link
     * java.nio.channels.ServerSocketChannel#accept ServerSocketChannel.accept} */
    public SocketChannel getChannel() {
        return null;
    }
}

根据其类上面的注释,我们可以看到,Socket是一个端点,用于连接两个机器

而直接使用socket.getChannel方法来获取其对应的通道时,则返回了null,同时给出提示:我们只能通过SocketChannel.open或者ServerSocketChannel.accept方法来获取通道。

// A selectable channel for stream-oriented connecting sockets
public abstract class SocketChannel
    extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel
{
    // Retrieves a socket associated with this channel.
    public abstract Socket socket();
}

同样看注释,SocketChannel被描述为一个可选择(注册到Selector上)的通道,用来连接socketclient-server)。
SocketChannel.socket方法,则返回通道对应的Socket

总结:虽然每个SocketChannel通道都有一个关联的Socket对象,但并非所有的socket都有一个关联的SocketChannel
如果我们使用传统的方式来new Socket,那么其不会有关联的SocketChannel

参考:

非阻塞式socket_一个菜鸟的博客-CSDN博客_非阻塞socket
SocketChannel—各种注意点_billluffy的博客-CSDN博客_socketchannel NIO相关的坑,大家可以借鉴下

八、ServerSocketChannel

前言:

上一章节中探讨了关于SocketSocketChannel的使用,都是基于客户端的视角来分析的。本文中我们分析下服务端,也就是ServerSocketServerSocketChannel

同样的需求,实现一个可简单对话的服务端即可。

1、基于ServerSocket的服务端

public class BIOServerSocket {
 
    private String address;
    private int port;
 
    public BIOServerSocket(String address, int port) {
        this.address = address;
        this.port = port;
    }
 
    public void startServer() {
        try {
            ServerSocket serverSocket = new ServerSocket();
            serverSocket.bind(new InetSocketAddress(address, port));
            System.out.println("bio server start...");
 
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("client connect...");
 
                // 写入 thread
                ServerWriteThread serverWriteThread = new ServerWriteThread(clientSocket);
                serverWriteThread.start();
 
                // 读取数据
                read(clientSocket);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
    private void read(Socket clientSocket) {
        try {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            String msg = "";
            while ((msg = bufferedReader.readLine()) != null) {
                System.out.println("receive msg: " + msg);
            }
        } catch (IOException e) {
            try {
                clientSocket.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            e.printStackTrace();
        }
    }
 
 
    public static void main(String[] args) {
        String address = "localhost";
        int port = 9999;
 
        BIOServerSocket bioServerSocket = new BIOServerSocket(address, port);
        bioServerSocket.startServer();
    }
}
 
/**
 * 从Scanner获取输入信息,并写回到client
 */
class ServerWriteThread extends Thread {
    private Socket socket;
    private PrintWriter writer;
    private Scanner scanner;
 
    public ServerWriteThread(Socket socket) throws IOException{
        this.socket = socket;
        scanner = new Scanner(System.in);
        this.writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
    }
 
    @Override
    public void run() {
        String msg = "";
        try {
            while ((msg = scanner.nextLine()) != null) {
                if (msg.equals("bye")) {
                    socket.close();
                    break;
                }
                writer.println(msg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

与上篇中的Socket类似,ServerSocket的使用也是比较简单的,笔者不再详述。

2、基于ServerSocketChannel的服务端

public class NIOServerSocket {
 
    private String address;
    private int port;
    private Selector selector;
 
    private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    private ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
 
    private Scanner scanner = new Scanner(System.in);
 
    public NIOServerSocket(String address, int port) throws IOException {
        this.address = address;
        this.port = port;
        this.selector = Selector.open();
    }
 
    public void startServer() {
        try {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            ServerSocket serverSocket = serverSocketChannel.socket();
 
            serverSocket.bind(new InetSocketAddress(address, port));
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("nio server start...");
        } catch (IOException e) {
            System.out.println("nio server start error...");
            e.printStackTrace();
        }
 
        // 监听连接
        acceptClient();
    }
 
    private void acceptClient() {
        while (true) {
            try {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
 
                for (SelectionKey key : selectionKeys) {
                    if (key.isAcceptable()) {
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
 
                        SocketChannel clientChannel = server.accept();
                        clientChannel.configureBlocking(false);
                        clientChannel.register(selector, SelectionKey.OP_READ);
                        System.out.println("client connect...");
                    } else if (key.isReadable()) {
                        SocketChannel clientChannel = (SocketChannel) key.channel();
 
                        readBuffer.clear();
                        StringBuffer sb = new StringBuffer("receive msg: ");
                        while (clientChannel.read(readBuffer) > 0) {
                            readBuffer.flip();
                            sb.append(new String(readBuffer.array(), 0, readBuffer.limit()));
                        }
 
                        System.out.println(sb.toString());
                        clientChannel.register(selector, SelectionKey.OP_WRITE);
                    } else if (key.isWritable()) {
                        SocketChannel clientChannel = (SocketChannel) key.channel();
                        String msg = scanner.nextLine();
 
                        writeBuffer.clear();
                        writeBuffer.put(msg.getBytes());
                        writeBuffer.flip();
 
                        clientChannel.write(writeBuffer);
                        clientChannel.register(selector, SelectionKey.OP_READ);
                    }
                }
 
                selectionKeys.clear();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
 
    public static void main(String[] args) {
        String address = "localhost";
        int port = 9999;
 
        try {
            new NIOServerSocket(address, port).startServer();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

SocketChannel一样,我们也借助了Selector来实现连接事件的监听、接收客户端请求(read)事件的监听、发送响应(write)事件的监听。

当服务端接收到客户端请求后,先获取对应的SocketChannel,然后将SocketChannel注册Selector 读事件(默认客户端先发送请求)。

读取到客户端请求信息后,然后将SocketChannel注册Selector 写事件,将获取控制台(Scanner)的输出信息,并发送给客户端,之后注册读事件。(同样,也是与客户端一问一答

3、ServerSocketChannel API

先来看下其类结构图

在这里插入图片描述

可以看到,其可进行端口绑定,socket属性设置(实现了NetworkChannel);可通过Selector进行事件注册(继承了SelectableChannel);

比较奇怪的是,没有像SocketChannel一样,实现了ByteChannel,没有读写操作。
ServerSocketChannel不支持读写操作,所有的读写都是基于SocketChannel来实现的。

3.1 非阻塞模式

ServerSocketChannel同样可以设置非阻塞模式。在之前的SocketChannel中我们描述了三种情况下(connectreadwrite)阻塞和非阻塞socket的区别。

ServerSocketChannel的非阻塞模式主要用于接收客户端连接上(accept

1)接收客户端连接

A进程调用阻塞ServerSocket.accept方法,当尚无新的连接到达时,进程A则被阻塞,直到有新的连接达到;

A进程调用非阻塞ServerSocketChannel.accept方法,当尚无新的连接到达时,则方法返回一个EWOULDBLOCK报错,并立即返回。
其他**AbstractSelectableChannel NetworkChannel的继承实现则与SocketChannel中一样的使用方式,笔者不再赘述。**

4、ServerSocket与ServerSocketChannel

还是通过类注释,来分析下其不同之处(发现JDK的注释真是个好东西,之前没有好好看过,基本所有的文章分析来源都是这些注释,建议大家可以好好看看)

// A server socket waits for requests to come in over the network.
public
class ServerSocket implements java.io.Closeable {
    // Listens for a connection to be made to this socket and accepts 
    // it. The method blocks until a connection is made.
    public Socket accept() throws IOException {}
    
    public ServerSocketChannel getChannel() {
        return null;
    }
}

ServerSocket作为一个服务端点,其主要工作就是等待客户端的连接;

accept方法用于监听连接的到来,当连接未建立成功时,accept方法会被阻塞。

当通过ServerSocket.getChannel方法来获取对应ServerSocketChannel时,直接返回一个null,说明对于手动创建的ServerSocket而言,没法获取其对应的channel,只能通过ServerSocketChannel.open方法来获取

// A selectable channel for stream-oriented listening sockets
public abstract class ServerSocketChannel
    extends AbstractSelectableChannel
    implements NetworkChannel
{
    // Accepts a connection made to this channel's socket
    /** If this channel is in non-blocking mode then this method will
     * immediately return <tt>null</tt> if there are no pending connections.
     * Otherwise it will block indefinitely until a new connection is available
     * or an I/O error occurs. */
    public abstract SocketChannel accept() throws IOException;
    
    // Retrieves a server socket associated with this channel.
    public abstract ServerSocket socket();
 
}

ServerSocketChannel作为一个可选择的通道(注册到Selector),用于监听socket连接;

通过accept方法来获取连接到的SocketChannel,看其注释,我们知道,若当前ServerSocketChannel是非阻塞的,且没有客户端连接上来,则直接返回null;若为阻塞类型的,则一直阻塞到有客户端连接上来为止。

ServerSocketChannel.socket方法则返回通道对应的ServerSocket

总结:虽然每个ServerSocketChannel都有一个对应的ServerSocket,但是不是每个ServerSocket都有一个对应的channel

相对非阻塞的ServerSocketChannelServerSocket是阻塞的,通过accept方法就可以明显的区分开。

九、Selector

前言:

有关于Selector的使用,我们在之前的示例中就已经用到过了。当然,基本都是最简单的使用。而有关于Selector的其他APISelectionKeyAPI,我们都没有详细介绍过。故单独列一篇,来说明下其使用。

1、Selector的创建与使用

// 创建,创建比较简单,就是一句话,实际后面做了很多工作。
Selector selector = Selector.open();
 
// Selector.open
public static Selector open() throws IOException {
    return SelectorProvider.provider().openSelector();
}
 
// SelectorProvider.provider()
public static SelectorProvider provider() {
    synchronized (lock) {
        if (provider != null)
            return provider;
        return AccessController.doPrivileged(
            new PrivilegedAction<SelectorProvider>() {
                public SelectorProvider run() {
                    // 加载java.nio.channels.spi.SelectorProvider系统参数配置对应的Provider
                    if (loadProviderFromProperty())
                        return provider;
                    // SPI方式加载SelectorProvider实现类
                    if (loadProviderAsService())
                        return provider;
                    // 以上两种都没有,则返回默认provider。
                    // 笔者Windows系统下直接返回WindowsSelectorProvider
                    // 最终Selector为WindowsSelectorImpl
                    provider = sun.nio.ch.DefaultSelectorProvider.create();
                    return provider;
                }
            });
    }
}

可以看到,Selector还是提供了很多方式来供我们选择Provider的。

1.1 select

之前的文章中,展示了select方法的使用,Selector还有其他select类型方法

// 获取哪些已经准备好的channel数量。非阻塞,方法会立即返回
public abstract int selectNow() throws IOException;
 
// 同selectNow,会一直阻塞到有准备好的channel事件为止
public abstract int select() throws IOException;
 
// 同select(),会阻塞最多timeout毫秒后返回
public abstract int select(long timeout)

我们可以在使用中选择合适的select方法,避免长时间的线程阻塞。

1.2 wakeup

Selector提供wakeup方法来唤醒阻塞在1.1中select方法中的线程。

/**
     * Causes the first selection operation that has not yet returned to return
     * immediately.
     *
     * <p> If another thread is currently blocked in an invocation of the
     * {@link #select()} or {@link #select(long)} methods then that invocation
     * will return immediately.  If no selection operation is currently in
     * progress then the next invocation of one of these methods will return
     * immediately unless the {@link #selectNow()} method is invoked in the
     * meantime.  In any case the value returned by that invocation may be
     * non-zero.  Subsequent invocations of the {@link #select()} or {@link
     * #select(long)} methods will block as usual unless this method is invoked
     * again in the meantime.
     *
     * <p> Invoking this method more than once between two successive selection
     * operations has the same effect as invoking it just once.  </p>
     *
     * @return  This selector
     */
public abstract Selector wakeup();

注解真的很全了。

如果当前线程阻塞在select方法上,则立即返回;

如果当前Selector没有阻塞在select方法上,则本次wakeup调用会在下一次select方法阻塞时生效;

1.3 close

public abstract void close() throws IOException;
public abstract boolean isOpen();

当我们不再使用Selector时,需要调用close方法来释放掉其它占用的资源,并将所有相关的选择键设置为无效。

close后的Selector则不能再使用。

同时提供了isOpen方法来检测Selector是否开启。

1.4 keys & selectedKeys

// Returns this selector's key set.
public abstract Set<SelectionKey> keys();
 
// Returns this selector's selected-key set.
public abstract Set<SelectionKey> selectedKeys();

keys方法返回的是目前注册到Selector上的所有channel以及对应事件;

selectedKeys方法返回的是目前注册到Selector上的所有channel活跃的事件。返回的结果集的每个成员都被判定是已经准备好了。

故,我们之前一直使用的就是selectedKeys方法。

那么SelectionKey是什么呢?接着看。

2.SelectionKey

// A token representing the registration of 
// a {@link SelectableChannel} with a {@link Selector}.
public abstract class SelectionKey {
    
    // 代表我们关注的4种事件
    public static final int OP_READ = 1 << 0;
    public static final int OP_WRITE = 1 << 2;
    public static final int OP_CONNECT = 1 << 3;
    public static final int OP_ACCEPT = 1 << 4;
    
    // SelectionKey本质上就是channel与selector的关联关系
    public abstract SelectableChannel channel();
    public abstract Selector selector();
    
    // channel注册到selector上时,关注的事件
    public abstract int interestOps();
    // 当前channel已经准备好的事件
    public abstract int readyOps();
    
    // 附加信息。我们在channel.register(selector,ops,att)方法时,最后一个参数
    // 即指定了本次注册的附加信息
    public final Object attach(Object ob);
    public final Object attachment();
}

通过上面的注释,我们可以了解到,SelectionKey本质上就是channel注册到selector上后,用于绑定两者关系的一个类。对于channel关注的事件,添加的附件信息在这个类均有所体现。

3、SelectableChannel

最后再来说下SelectableChannel,之前提到的SocketChannelServerSocketChannel都是SelectableChannel的实现类。

public abstract class SelectableChannel
    extends AbstractInterruptibleChannel
    implements Channel
{
 
    // 最重要的两个方法,用于注册channel到selector上,区别就是有没有attachment
    public abstract SelectionKey register(Selector sel, int ops, Object att)
            throws ClosedChannelException;
    public final SelectionKey register(Selector sel, int ops)
            throws ClosedChannelException;
    
    // 检查当前channel是否注册到任何一个selector上
    public abstract boolean isRegistered();
    
    // 当前channel可注册的有效的事件
    public abstract int validOps();
    
    // 阻塞状态,之前的SocketChannel文章中已经详细说过
    public abstract SelectableChannel configureBlocking(boolean block)
        throws IOException;
    // 检查channel阻塞状态
    public abstract boolean isBlocking();
}

注意:

  1. 一个channel可以注册到多个Selector
  2. 每个channel可注册的有效事件不同,如下
// SocketChannel
public final int validOps() {
    return (SelectionKey.OP_READ
            | SelectionKey.OP_WRITE
            | SelectionKey.OP_CONNECT);
}
 
//ServerSocketChannel
public final int validOps() {
    return SelectionKey.OP_ACCEPT;
}

心路旅程:

笔者对于在学习NIO这一阶段时间的感受就是:多去尝试,多看注释。

NIO属于笔者阶段性总结博客的第一站,后续还有更多系列博客要出来。

为什么选择NIO作为第一站呢?因为众多高精尖技术的底层就是NIO。所以弄明白NIO的一系列使用是很有必要的。

之前也知道BIO和NIO的大致写法,基本都是从网上看示例,直接拷贝。但是真让自己来写的话,又是漏洞百出,究其原因,就是对NIO的使用和其原理不甚了解。

通过这一次的总结性输出,对NIO的理解也算是上了一个层次。

十、NIO使用案例

1、读取

文件内容:

ABCDE

代码:

    /**
     * 读取
     *
     * @throws IOException
     */
    private static void fileChannelRead() throws IOException {
        FileInputStream fileInputStream = new FileInputStream("C:\\Users\\jiuhui-4\\Desktop\\t.txt");
        FileChannel channel = fileInputStream.getChannel();
        // 创建一个大小为2的缓冲对象
        ByteBuffer byteBuffer = ByteBuffer.allocate(2);

        // 改变通道位置
        channel.position(1);
        System.out.println("position:" + channel.position());
        //读取2字节内容到byteBuffer中
        channel.read(byteBuffer);

        //类似于flush()函数功能,将buffer里面的数据刷新出去
        byteBuffer.flip();
		System.out.println(new String(byteBuffer.array()));
        
        // 测试从通道中读一个字节
        char ch = (char) byteBuffer.get(0);
        System.out.println(ch);

        // 关闭
        byteBuffer.clear();
        channel.close();
        fileInputStream.close();
    }

结果:

position:1
B

2、写出

下面的代码是将一段字符串写入到输出文件通道中,因为写入的时候并不保证能一次性写入到文件中,所以需要进行判断是否全部写入,如果没有全部写入,需要再次调用写入函数操作。

注意:文件的内容会被覆盖。

代码:

    /**
     * 写出
     *
     * @throws IOException
     */
    private static void fileChannelWrite() throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\jiuhui-4\\Desktop\\t.txt");
        FileChannel fileChannelOut = fileOutputStream.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(2);

        System.out.println("position:" + fileChannelOut.position());
        // 改变通道位置
        fileChannelOut.position(1);

        byteBuffer.put("xy".getBytes(StandardCharsets.UTF_8));
        //类似于flush()函数功能,将buffer里面的数据刷新出去
        byteBuffer.flip();
        fileChannelOut.write(byteBuffer);
        
        //检查是否还有数据未写出
		//while (byteBuffer.hasRemaining()) fileChannelOut.write(byteBuffer);

        // 关闭
        byteBuffer.clear();
        fileChannelOut.close();
        fileOutputStream.close();
    }

结果:

注意:前面是有一个空格的。

 XY

3、截取文件

truncate()方法是截取3字节大小的数据,指定长度后面的部分将被删除。

channel.force(true)将数据强制刷新到硬盘中,因为系统会将数据先保存在内存中,不保证数据会立即写入到硬盘中。

初始文件内容:

ABCDE

代码:

    /**
     * 截取文件
     * 文件内容:ABCDE
     * 截取后文件内容:ABC
     *
     * @throws IOException
     */
    private static void fileChannelTruncate() throws IOException {
        RandomAccessFile randomAccessFile = new RandomAccessFile("C:\\Users\\jiuhui-4\\Desktop\\t.txt", "rw");
        FileChannel channel = randomAccessFile.getChannel();

        //截取内容
        channel.truncate(3);
        //强制刷新数据到硬盘
        channel.force(true);

        // 关闭
        channel.close();
        randomAccessFile.close();
    }

截取后文件内容:

ABC

4、复制文件

(1)流通道

没有使用Map方式复制文件,会覆盖整个目标文件。

# 源文件内容
XYZ
# 目标文件内容
ABCDE

# 复制后的目标文件内容
XYZ
    /**
     * 复制文件,根据流通道。
     *
     * @param src
     * @param dest
     * @throws Exception
     */
    public static void copyByStreamChannel(File src, File dest) throws Exception {
        FileInputStream fileInputStream = new FileInputStream(src);
        FileOutputStream fileOutputStream = new FileOutputStream(dest);
        FileChannel fileChannelIn = fileInputStream.getChannel();// 获取文件通道
        FileChannel fileChannelOut = fileOutputStream.getChannel();

        //转存 方式一
        fileChannelIn.transferTo(0, fileChannelIn.size(), fileChannelOut);

        //转存 方式二
        /*ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        int readSize = fileChannelIn.read(byteBuffer);
        while (readSize != -1) {
            byteBuffer.flip();//类似于flush()函数功能,将buffer里面的数据刷新出去
            fileChannelOut.write(byteBuffer);
            byteBuffer.clear();
            readSize = fileChannelIn.read(byteBuffer);
        }*/

        fileChannelIn.close();
        fileChannelOut.close();
        fileInputStream.close();
        fileOutputStream.close();
    }

(2)随机访问文件

使用 fileChannelOut.map 的方式复制文件,如果目标文件有5字节,而源文件只有3字节,那么仅覆盖前3个字节。

# 源文件内容
XYZ
# 目标文件内容
ABCDE

# 复制后的目标文件内容
XYZDE
    /**
     * 复制文件,使用随机访问文件的方式。
     *
     * @param src
     * @param dest
     */
    public static void copyByRandomAccessFile(File src, File dest) {
        try {
            RandomAccessFile randomAccessFileRead = new RandomAccessFile(src, "r");
            RandomAccessFile randomAccessFileReadWrite = new RandomAccessFile(dest, "rw");

            FileChannel fileChannelIn = randomAccessFileRead.getChannel();
            FileChannel fileChannelOut = randomAccessFileReadWrite.getChannel();

            long size = fileChannelIn.size();
            // 映射内存
            MappedByteBuffer mappedByteBufferIn = fileChannelIn.map(MapMode.READ_ONLY, 0, size);
            MappedByteBuffer mappedByteBufferOut = fileChannelOut.map(MapMode.READ_WRITE, 0, size);

            // 转存 方式一
            mappedByteBufferOut.put(mappedByteBufferIn);

            // 转存 方式二
            /*for (int i = 0; i < size; i++) {
                byte b = mappedByteBufferIn.get(i);
                mappedByteBufferOut.put(i, b);
            }*/

            fileChannelIn.close();
            fileChannelOut.close();
            randomAccessFileRead.close();
            randomAccessFileReadWrite.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

5、合并文件

    /**
     * 合并文件
     */
    public static void mergeFile() {
        try {
            // 源文件
            List<File> srcFileList = new ArrayList();
            srcFileList.add(new File("E:\\t1.txt"));
            srcFileList.add(new File("E:\\t2.txt"));

            // 目标文件
            File dest = new File("E:\\t.txt");
            RandomAccessFile randomAccessFileReadWrite = new RandomAccessFile(dest, "rw");
            FileChannel outChannel = randomAccessFileReadWrite.getChannel();

            // 合并操作 方式一
            /*for (File srcFile : srcFileList) {
                RandomAccessFile randomAccessFileRead = new RandomAccessFile(srcFile, "r");
                FileChannel inChannel = randomAccessFileRead.getChannel();
                outChannel.transferFrom(inChannel, outChannel.size(), inChannel.size());
                inChannel.close();
                randomAccessFileRead.close();
            }*/

            // 合并操作 方式二
            long position = 0;
            for (File srcFile : srcFileList) {
                RandomAccessFile randomAccessFileRead = new RandomAccessFile(srcFile, "r");
                FileChannel inChannel = randomAccessFileRead.getChannel();

                // 映射内存
                MappedByteBuffer mappedByteBufferIn = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
                MappedByteBuffer mappedByteBufferOut = outChannel.map(MapMode.READ_WRITE, position, inChannel.size());
                mappedByteBufferOut.put(mappedByteBufferIn);

                // 记录位置
                position += inChannel.size();

                inChannel.close();
                randomAccessFileRead.close();
            }

            // 关闭
            outChannel.close();
            randomAccessFileReadWrite.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

6、ServerSocket

NIO源码解析-SocketChannel

https://blog.csdn.net/qq_26323323/article/details/120314675

NIO源码解析-FileChannel高阶知识点map和transferTo、transferFrom

NIO源码解析-FileChannel高阶知识点map和transferTo、transferFrom_恐龙弟旺仔的博客-CSDN博客_filechannel map

十一、RandomAccessFile、FileInputStream和FileOutputStream的区别

1、RandomAccessFile

(1)是基于指针形式读写文件数据的,比较灵活。
(2)有两种创建模式:只读模式和读写模式 。

(3)RandomAccessFile不属于InputStream和OutputStream类。

(4)RandomAccessFile使用随机访问的方式,根据文件的hashcode生成一个位置存入文件,取得时候再反过来根据这个固定的位置直接就能找到文件,File就不能。

(5)RandomAccessFile可以提高读取的速度。

(6)注:文件如果很大,可以通过指针的形式分为多个进行下载。最后拼接到一个文件。迅雷下载就是采用这种方式。

2、FileInputStream和FileOutputStream

(1)FileInputStream及FileOutputStream使用的是流式访问的方式。

(2)InputStream类是所有表示输入流的类的父类,System.in就是它的一个对象。OutputStream是所有表示输出流的类的父类,System.out就间接继承了OutputStream类。

(3)FileInputStream是InputStream的子类,FileOutputStream是OutputStream的子类。

转载于:https://my.oschina.net/Clarences/blog/1923093

十二、参考资料

FileChannle 和 MMAP 高性能分析
https://blog.csdn.net/MakeContral/article/details/85380197

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值