JAVA NIO 原理解析以及使用案例

1,017 阅读42分钟

一、概述

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

Buffer 、 Channel 、 Selector ,NIO三大件。

从最终实现上来看,我们可以将IO简单分为两大类:File IO和Socket Stream IO,分别用于操作文件和网络套接字。

我们也按照这个大方向,先介绍File IO相关操作,后续再介绍Socket Stream IO相关操作。

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

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

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

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

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

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

10分钟看懂, Java NIO 底层原理

www.cnblogs.com/crazymakerc…

1.1 同步与异步

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

同步请求
异步请求

1.2 阻塞与非阻塞

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

阻塞
非阻塞

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

2.1 BIO

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

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

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

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

2.2 NIO

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

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

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

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

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

2.3 AIO

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

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

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

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

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

二、Buffer 简介

前言:

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

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

Buffer (缓冲区)的工作与 Channel (通道)紧密相连。 Channel 是 IO 发生时的通过的入口(或出口, 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 )来说明的,后续会专门来说明其他基本类型的使用及实现。

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

// 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');

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);
}

结果:

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);
}

结果:

总结:

针对 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:

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

3.5 标记与重置

mark 和 reset 方法, 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();

复制后的 duplicate 与 buffer 共享源数据数组 ,只是拥有不同的 position 、 limit

总结:

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

三、HeapBuffer与DirectBuffer

blog.csdn.net/qq_26323323…

前言:

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

本文中会重点介绍其四种实现类, HeapByteBuffer 、 HeapByteBufferR 、 DirectByteBuffer 、 DirectByteBufferR 。

而关于 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 本质上是一个字节数组 ,通过 position 、 limit 的控制,来反复的操作该字节数组。

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还有各个基础类型的实现类,如下图

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

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

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

推荐阅读:

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

四、Channel 简介

1、Channel 基本定义

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

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

  1. 通过通道,程序既可以读数据又可以写数据;流的读写则是单向的(比如 InputStream 就是读, OutputStream 就是写)
  2. 通道可以进行数据的异步读写;而流的读写一般都是阻塞的同步的;
  3. 通道的数据读写需要通过 Buffer , Buffer 的操作比较灵活;而流的话直接读写在 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;
}

可以看到,这两个就是新增了对 ByteBuffer 的 read 和 write 操作

2.4 ByteChannel

public interface ByteChannel
    extends ReadableByteChannel, WritableByteChannel
{
 
    
}

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

ByteChannel 继承了 ReadableByteChannel 和 WritableByteChannel ,

实现 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 方法因为将文件直接映射到内存中,因此其读写性能相比 FileInputStream 和 FileOutputStream 来说要好一些,但是资源消耗代价也会大些,因此比较适合大文件的读写;
  • 可以利用 transferTo()/transferFrom() 来将 bytes 数组在两个通道之间来回传递,该性能相对来较快,可以快速实现文件复制,因为 FileChannel 是将通过 JNI (本地方法接口)将文件读取到 native 堆即堆外内存中,通过 DirectrByteBuffer 来引用这些数据,这样在实现文件复制或传输时,无需将文件从堆外内存拷贝到 java 堆中,本质上这就是减少了内核内存和用户内存之间的数据拷贝,从而提升性能;

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

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

  • 可以通过 open() 方法开启一个通道,同时也可以通过 FileInputStream 或者 FileOutputStream , RandomAccessFile 调用方法 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.getChannel 、 FileOutputStream.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 提供了 sync 、 fsync 、 msync 、 fdatasync 、 sync_file_range 5个函数。

open 函数的 O_SYNC 和 O_DSYNC 参数有着和 fsync 及 fdatasync 类似的含义:使每次 write 都会阻塞到磁盘 IO 完成。

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

O_DSYNC 和 O_SYNC 标志有微妙的区别:

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

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

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

(来自zhuanlan.zhihu.com/p/104994838…

正是由于内存和磁盘之间的读写速度差异,所以才有了 write 方法只是将数据写入 pageCache 的优化做法,同时操作系统也提供了 O_SYNC 和 O_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() 等。

参考:

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 校验和计算。

再稍微讲讲 mmap 和 sendFile 的区别。

linux下的mmap和零拷贝技术 - 简书

mmap与sendfile() - 简书

七、SocketChannel

前言:

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

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

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

本文就来介绍下 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 接口继承了 ReadableByteChannel 和 WritableByteChannel ,实现了对数据的读写。

上文中的示例里, 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 上)的通道,用来连接 socket ( client-server )。

而 SocketChannel.socket 方法,则返回通道对应的 Socket 。

总结:虽然每个 SocketChannel 通道都有一个关联的 Socket 对象,但并非所有的 socket 都有一个关联的 SocketChannel 。

如果我们使用传统的方式来 new Socket ,那么其不会有关联的 SocketChannel

参考:

非阻塞式socket_一个菜鸟的博客-CSDN博客_非阻塞socket

SocketChannel—各种注意点_billluffy的博客-CSDN博客_socketchannel NIO相关的坑,大家可以借鉴下

八、ServerSocketChannel

前言:

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

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

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 中我们描述了三种情况下( connect 、 read 、 write )阻塞和非阻塞 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 。

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

九、Selector

前言:

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

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 ,之前提到的 SocketChannel 和 ServerSocketChannel 都是 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使用案例

文件内容:

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

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

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

代码:

/**
     * 写出
     *
     * @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

blog.csdn.net/qq_26323323…

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的子类。