一、概述
如果不想看理论,直接看使用案例,可以跳到第十节
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 底层原理
https://www.cnblogs.com/crazymakercircle/p/10225159.html
1.1 同步与异步
区分一个请求是同步还是异步的话,主要看请求在调用过来时候,是等待直到执行结果完成,还是及时返回结果,后续通过异步通知或回调的方式来告诉调用方。
同步请求
异步请求
1.2 阻塞与非阻塞
阻塞与非阻塞主要是关注程序在等待执行结果时的状态
阻塞
非阻塞
2、操作系统视角下的BIO、NIO和AIO
2.1 BIO
即 Blocking IO (阻塞IO),操作系统下 BIO 整个过程如下所示:
当应用程序发起系统调用时;
- 操作系统首先需要先将数据拷贝到系统内核缓冲区 ,
- 然后再将内核缓冲区的数据拷贝到应用程序的进程空间 (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中的BIONIO和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
https://blog.csdn.net/qq_26323323/article/details/120145394
前言:
在 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
{
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 heapbytebufferdirectbytebuffer var8.putvar1 var8.flip var1.positionvar5 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的更深入介绍(笔者的操作系统相关知识实在太薄弱了,没法这么深入),可以参考大神文章:
https://blog.csdn.net/wangwei19871103/article/details/104235590
四、Channel 简介
1、Channel 基本定义
Channel ,翻译过来就是通道。通道表示打开到IO设备(文件、套接字)的连接。
通道有点类似于流的概念,就是 InputStream 和 OutputStream ,都是可以用来传输数据的。但是两者之间又有本质的不同, 不同点如下 :
- 通过通道,程序既可以读数据又可以写数据;流的读写则是单向的(比如 InputStream 就是读, OutputStream 就是写)
- 通道可以进行数据的异步读写;而流的读写一般都是阻塞的同步的;
- 通道的数据读写需要通过 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_SYNCO_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 的语义。
(来自
https://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() 等。
参考:
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方式 | 4 | 4 |
mmap方式 | 4 | 3 |
sendFile(Linux2.1) | 2 | 3 |
sendFile(Linux2.4) | 2 | 2 |
实际,以上 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 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属性
NetworkChannel setOption(SocketOption 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 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 null 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() {
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.
*
* 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.
*
*
Invoking this method more than once between two successive selection
* operations has the same effect as invoking it just once.
*
* @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 keys();
// Returns this selector's selected-key set.
public abstract Set 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();
}
注意:
- 一个 channel 可以注册到多个 Selector 上
- 每个 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 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的子类。