Java NIO - Buffer & Channel
Java NIO 为java non-blocking IO,从JDK1.4开始,Java提供了一系列改进的输入/输出的新特性,是同步非阻塞的
NIO三大核心部分:
- Channel(通道)
- Buffer(缓冲区)
- Selector(选择器)
NIO是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直到数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情
通俗的理解:NIO是可以做到一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的BIO,非得分配10000个线程
NIO整个流程的示意图如下:
- 每个channel都对应一个buffer
- selector会对应一个线程,一个线程对应多个channel
- 程序切换到哪个channel,是由事件决定的
- selector会根据不同的事件,在各个通道上切换
- buffer就是一个内存块,底层是有一个数组的
- 数据的读取写入,是通过buffer,这个和BIO有本质的不同的
- BIO中要么是输入流,或者是输出流,不能双向。但是NIO的buffer是可以读也可以写,需要flip方法切换
- channel是双向的,可以返回底层操作系统的情况,比如Linux,底层的操作系统通道就是双向的
NIO和BIO的比较
1.BIO以流的方式处理数据,NIO以块的方式处理数据,块IO的效率比流IO高很多
2.BIO是阻塞的,NIO则是非阻塞的
3.BIO基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector用于去监听多个通道的事件(比如:连接请求、数据到达等),因此使用单个线程就可以监听多个客户单通道
Buffer
Buffer(缓冲区):缓冲区本质上是一个可以读写数据的内存块,可以理解为一个容器对象(含数组),该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。
Channel提供了从文件、网络读取数据的渠道,但是读取或写入的数据必须经过Buffer
与Java基本类型相对应,NIO提供了多种 Buffer 类型,如ByteBuffer
、CharBuffer
、IntBuffer
等
Buffer
的类的层级如下:
Buffer中有4个很重要的变量,它们是理解Buffer工作机制的关键,分别是
- capacity - 总容量
- position - 指针当前位置
- limit - 读/写边界位置
- mark - 标记
Buffer的工作方式跟C语言里的字符数组非常的像,类比一下,capacity就是数组的总长度,position就是我们读/写字符的下标变量,limit就是结束符的位置。Buffer初始时3个变量的情况如下图
如下使用IntBuffer
的简单例子
public class BasicBuffer {
public static void main(String[] args) {
//创建一个Buffer,大小为5,即可以存放5个int
IntBuffer intBuffer = IntBuffer.allocate(5);
//存放数据
// intBuffer.put(10);
// intBuffer.put(11);
// intBuffer.put(12);
// intBuffer.put(13);
// intBuffer.put(14);
for (int i = 0; i < intBuffer.capacity(); i++) {
intBuffer.put(i * 2);
}
//取数据
//将buffer转换,读写切换
intBuffer.flip();
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
}
}
输出结果为:
0
2
4
6
8
打个断点,调试看下:
1.在IntBuffer intBuffer = IntBuffer.allocate(5);
之后
2.在for循环运行之后
3.intBuffer.flip();
之后
4.读取第一个后
所以,也可以通过设置position
和limit
来限制读取的范围,在上面例子的基础上,如果设置:
//取数据
//将buffer转换,读写切换
intBuffer.flip();
intBuffer.position(1);
intBuffer.limit(3);
此时输出的结果为(可见,不能超过limit):
2
4
其它方法
1.clear()
clear()
- 清除此缓冲区,即将各个标记恢复到初始状态,但是数据并没有真正擦除
2.hasRemaining()
hasRemaining()
- 告知此缓冲区是否具有可访问的底层实现数组
3.isReadOnly()
isReadOnly()
- 告知此缓冲区是否为只读缓冲区
4.hasArray()
hasArray()
- 告知此缓冲区师傅具有可访问的底层实现数组
5.array()
array()
- 返回此缓冲区底层实现数组
Channel
NIO的通过类似于流,但有如下的区别
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读取数据,也可以写数据到缓冲
Channel是一个接口
public interface Channel extends Closeable {
/**
* Tells whether or not this channel is open.
*
* @return {@code true} if, and only if, this channel is open
*/
public boolean isOpen();
/**
* Closes this channel.
*
* <p> After a channel is closed, any further attempt to invoke I/O
* operations upon it will cause a {@link ClosedChannelException} to be
* thrown.
*
* <p> If this channel is already closed then invoking this method has no
* effect.
*
* <p> This method may be invoked at any time. If some other thread has
* already invoked it, however, then another invocation will block until
* the first invocation is complete, after which it will return without
* effect. </p>
*
* @throws IOException If an I/O error occurs
*/
public void close() throws IOException;
}
Java NIO中最常用的通道实现是如下几个,可以看出跟传统的 I/O 操作类是一一对应的。
FileChannel
:读写文件DatagramChannel
: UDP协议网络通信SocketChannel
:TCP协议网络通信ServerSocketChannel
:监听TCP连接
FileChannel
FileChannel
也是一个抽象类,真实实现为FileChannelImpl
FileChannel
主要用来对本地文件进行IO操作,常见的方法有:
1.int read(ByteBuffer dst)
- 从通道读取数据并放到缓冲区中
2.int write(ByteBuffer src)
- 把缓冲区的数据写到通道中
3.long transferFrom(ReadableByteChannel src, long position, long count)
- 从目标通道中复制数据到当前通道
4.long transferTo(long position, long count, WritableByteChannel target)
- 把数据从当前通道复制给目标通道
本地文件写数据
本地文件写数据的流程,可以用图表示如下:
public class NIOFileChannel {
public static void main(String[] args) throws Exception {
String str = "Hello, FileChannel";
//创建一个输出流
FileOutputStream fileOutputStream = new FileOutputStream("file01.txt");
//通过fileOutputStream获取对应的channel
FileChannel channel = fileOutputStream.getChannel();
//创建一个缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将 str 放入到buffer中
byteBuffer.put(str.getBytes());
//byteBuffer进行反转
byteBuffer.flip();
//将bytebuffer的数据,写入到fileOutputStream
channel.write(byteBuffer);
//关闭
fileOutputStream.close();
}
}
本地文件读数据
本地文件读数据的流程如下:
public class NIOFileChannel01 {
public static void main(String[] args) throws Exception {
File file = new File("file01.txt");
//创建一个输入流
FileInputStream fileInputStream = new FileInputStream(file);
//通过FileInputStream获取对应的channel
FileChannel channel = fileInputStream.getChannel();
//创建一个缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
//将通道的数据读入到buffer
channel.read(byteBuffer);
//将bytebuffer转为string
System.out.println(new String(byteBuffer.array()));
//关闭
fileInputStream.close();
}
}
使用一个Buffer完成文件的读写
流程如下:
public class NIOFileChannel03 {
public static void main(String[] args) throws Exception {
//创建一个输入流
FileInputStream fileInputStream = new FileInputStream("file01.txt");
//通过FileInputStream获取对应的channel
FileChannel channel01 = fileInputStream.getChannel();
//创建一个输出流
FileOutputStream fileOutputStream = new FileOutputStream("file02.txt");
//通过fileOutputStream获取对应的channel
FileChannel channel02 = fileOutputStream.getChannel();
//创建一个缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while (true) {//循环读取
byteBuffer.clear();//重置
int read = channel01.read(byteBuffer);
if (read == -1) {//表示读完
break;
}
//buffer中的数据放入到channel02
byteBuffer.flip();
channel02.write(byteBuffer);
}
//关闭
fileInputStream.close();
fileOutputStream.close();
}
}
拷贝文件
使用transferFrom
拷贝文件
public class NIOFileChannel04 {
public static void main(String[] args) throws Exception {
//创建一个输入流
FileInputStream fileInputStream = new FileInputStream("avatar.png");
//通过FileInputStream获取对应的channel
FileChannel sourceChannel = fileInputStream.getChannel();
//创建一个输出流
FileOutputStream fileOutputStream = new FileOutputStream("avatar_copy.png");
//通过fileOutputStream获取对应的channel
FileChannel destChannel = fileOutputStream.getChannel();
//使用transferFrom完成拷贝
destChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
//关闭
sourceChannel.close();
destChannel.close();
fileInputStream.close();
fileOutputStream.close();
}
}
Buffer和Channel的注意事项和细节
1.ByteBuffer支持类型化的put和get,put放入的是什么类型,get就应该使用相应的类型来取出,否则可能会有BufferUnderflowException
异常
如下的例子,没有按放入的类型取数据,抛出了异常:
public class NIOFileChannel05 {
public static void main(String[] args) throws Exception {
ByteBuffer buffer = ByteBuffer.allocate(64);
buffer.putInt(100);
buffer.putLong(9);
buffer.putChar('王');
buffer.putShort((short) 4);
//取出
buffer.flip();
System.out.println(buffer.getChar());
System.out.println(buffer.getShort());
System.out.println(buffer.getLong());
System.out.println(buffer.getLong());
}
}
2.可以将一个普通的Buffer转为只读Buffer
如下例子,对一个只读的Buffer,写入数据,会抛出异常
public class NIOFileChannel06 {
public static void main(String[] args) throws Exception {
ByteBuffer buffer = ByteBuffer.allocate(64);
for (int i = 0; i < 64; i++) {
buffer.put((byte) i);
}
buffer.flip();
//得到一个只读的buffer
ByteBuffer readonlyBuffer = buffer.asReadOnlyBuffer();
System.out.println(readonlyBuffer.getClass()); //java.nio.HeapByteBufferR
//读取
while (readonlyBuffer.hasRemaining()) {
System.out.println(readonlyBuffer.get());
}
//readonlyBuffer.put((byte) 1); //抛出异常
}
}
异常如下:
3.MappedByteBuffer
MappedByteBuffer
可以让文件直接在内存(堆外的内存)中进行修改,操作系统不需要拷贝一次,同步到文件则是由NIO来完成。
MappedByteBuffer
继承自ByteBuffer
如下的例子:
public class MappedByteBufferDemo {
public static void main(String[] args) throws Exception {
RandomAccessFile randomAccessFile = new RandomAccessFile("file01.txt", "rw");
FileChannel fileChannel = randomAccessFile.getChannel();
/**
* 参数1: 使用的读写模式
* 参数2:修改的起始位置
* 参数3:映射到内存的大小,即多个字节映射到内存,即可以修改的范围
*/
MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
map.put(0, (byte) '1');
map.put(1, (byte) '2');
}
}
原文件内容为Hello, FileChannel
,修改后,变成如下的形式:
Scattering & Gathering
前面的例子,都是通过一个Buffer完成的,NIO还支持多个Buffer(即Buffer数组)完成读写操作,即Scattering
和Gathering
Scattering - 将数据写入到Buffer时,可以采用buffer数组,依次写入(分散)
Gathering - 从Buffer读取数据时,可以采用buffer素组,依次读入(聚合)
如下使用Buffer数组的例子:
public class ScatteringAndGatheringTest {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
//绑定端口到socket并启动
serverSocketChannel.socket().bind(inetSocketAddress);
//创建Buffer数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(5);
byteBuffers[1] = ByteBuffer.allocate(3);
//等待客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
int messageLength = 8;//从客户端接收8个字节
//循环读取
while (true) {
int byteRead = 0;
while (byteRead < messageLength) {
long read = socketChannel.read(byteBuffers);
byteRead += read;
System.out.println("byteRead = " + byteRead);
//流打印,看当前buffer的position 和 limit
Arrays.asList(byteBuffers).stream()
.map(buffer -> "position = " + buffer.position() + ", limit = " + buffer.limit())
.forEach(System.out::println);
//将所有的buffer进行flip
Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());
//将数据读出显示到客户端
long byteWrite = 0;
while (byteWrite < messageLength) {
long write = socketChannel.write(byteBuffers);
byteWrite += write;
}
//将所有的buffer clear
Arrays.asList(byteBuffers).forEach(buffer -> buffer.clear());
System.out.println("byteWrite = " + byteWrite);
}
}
}
}