目录
Java NIO 基本介绍
- Java NIO 全称 java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的
- NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。【基本案例】
- NIO 有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
- NIO是 面向缓冲区 ,或者面向 块 编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
- Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。【后面有案例说明】
- 通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个。
- HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。
NIO和BIO的比较
- BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多
- BIO 是阻塞的,NIO 则是非阻塞的
- BIO基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
NIO三大核心原理示意图
一张图描述NIO 的 Selector 、 Channel 和 Buffer 的关系
Selector 、 Channel 和 Buffer 的关系图(简单版)
- 关系图的说明:
- 每个channel 都会对应一个Buffer
- Selector 对应一个线程, 一个线程对应多个channel(连接)
- 该图反应了有三个channel 注册到 该selector //程序
- 程序切换到哪个channel 是有事件决定的, Event 就是一个重要的概念
- Selector 会根据不同的事件,在各个通道上切换
- Buffer 就是一个内存块 , 底层是有一个数组
- 数据的读取写入是通过Buffer, 这个和BIO , BIO 中要么是输入流,或者是输出流, 不能双向,但是NIO的Buffer 是可以读也可以写, 需要 flip 方法切换
- channel 是双向的, 可以返回底层操作系统的情况, 比如Linux , 底层的操作系统通道就是双向的.
缓冲区(Buffer)
基本介绍
- 缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer,如图: 【后面举例说明】
Buffer类及其子类
- 在 NIO 中,Buffer 是一个顶层父类,它是一个抽象类, 类的层级关系图:
常用Buffer子类一览:
-
- ByteBuffer,存储字节数据到缓冲区
- ShortBuffer,存储字符串数据到缓冲区
- CharBuffer,存储字符数据到缓冲区
- IntBuffer,存储整数数据到缓冲区
- LongBuffer,存储长整型数据到缓冲区
- DoubleBuffer,存储小数到缓冲区
- FloatBuffer,存储小数到缓冲区
- Buffer类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息:
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
属性 | 描述 |
Capacity | 容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变 |
Limit | 表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的 |
Position | 位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值,为下次读写作准备 |
Mark | 标记 |
public class BufferDemo {
public static void main(String[] args) {
IntBuffer intBuffer = IntBuffer.allocate(5);
for (int i = 0; i < 5; i++) {
intBuffer.put(i * 2);
}
intBuffer.flip();
intBuffer.position(1);
intBuffer.limit(3); // <=limit
while (intBuffer.hasRemaining()){
System.out.println(intBuffer.get());
}
}
}
Buffer类相关方法一览
public abstract class Buffer {
//JDK1.4时,引入的api
public final int capacity( )//返回此缓冲区的容量
public final int position( )//返回此缓冲区的位置
public final Buffer position (int newPositio)//设置此缓冲区的位置
public final int limit( )//返回此缓冲区的限制
public final Buffer limit (int newLimit)//设置此缓冲区的限制
public final Buffer mark( )//在此缓冲区的位置设置标记
public final Buffer reset( )//将此缓冲区的位置重置为以前标记的位置
public final Buffer clear( )//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并没有真正擦除, 后面操作会覆盖
public final Buffer flip( )//反转此缓冲区
public final Buffer rewind( )//重绕此缓冲区
public final int remaining( )//返回当前位置与限制之间的元素数
public final boolean hasRemaining( )//告知在当前位置和限制之间是否有元素
public abstract boolean isReadOnly( );//告知此缓冲区是否为只读缓冲区
//JDK1.6时引入的api
public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组
public abstract Object array();//返回此缓冲区的底层实现数组
public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区
}
ByteBuffer
从前面可以看出对于 Java 中的基本数据类型(boolean除外),都有一个 Buffer 类型与之相对应,最常用的自然是ByteBuffer 类(二进制数据),该类的主要方法如下:
public abstract class ByteBuffer {
//缓冲区创建相关api
public static ByteBuffer allocateDirect(int capacity)//创建直接缓冲区
public static ByteBuffer allocate(int capacity)//设置缓冲区的初始容量
public static ByteBuffer wrap(byte[] array)//把一个数组放到缓冲区中使用
//构造初始化位置offset和上界length的缓冲区
public static ByteBuffer wrap(byte[] array,int offset, int length)
//缓存区存取相关API
public abstract byte get( );//从当前位置position上get,get之后,position会自动+1
public abstract byte get (int index);//从绝对位置get
public abstract ByteBuffer put (byte b);//从当前位置上添加,put之后,position会自动+1
public abstract ByteBuffer put (int index, byte b);//从绝对位置上put
}
通道(Channel)
基本介绍
- BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)是双向的,可以读操作,也可以写操作。
- Channel在NIO中是一个接口 public interface Channel extends Closeable{}
- 常用的 Channel 类有:FileChannel、DatagramChannel、ServerSocketChannel 和 SocketChannel。【ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket】
- FileChannel 用于文件的数据读写,DatagramChannel 用于 UDP 的数据读写,ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。
FileChannel 类
FileChannel主要用来对本地文件进行 IO 操作,常见的方法有
public int read(ByteBuffer dst) ,从通道读取数据并放到缓冲区中
public int write(ByteBuffer src) ,把缓冲区的数据写到通道中
public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道
public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道
应用实例1-本地文件写数据
实例要求:
- 使用前面学习后的ByteBuffer(缓冲) 和 FileChannel(通道), 将 "hello,Channel" 写入到file01.txt 中
- 文件不存在就创建
- 代码演示
public class NIOFileChannel01 {
public static void main(String[] args) throws IOException {
String str = "Hello, Channel";
// 创建一个输出流
FileOutputStream fileOutputStream = new FileOutputStream("file.txt");
// 通过 fileOutputStream 获取 对应的 FileChannel,类型是FileChannelImpl
FileChannel fileChannel = fileOutputStream.getChannel();
// 创建一个缓存区ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 将str 放入byteBuffer
byteBuffer.put(str.getBytes(StandardCharsets.UTF_8));
// 将byteBuffer 数据写入到 FileChannel
byteBuffer.flip();
fileChannel.write(byteBuffer);
fileChannel.close();
}
}
应用实例2-本地文件读数据
实例要求:
- 使用前面学习后的ByteBuffer(缓冲) 和 FileChannel(通道), 将 file01.txt 中的数据读入到程序,并显示在控制台屏幕
- 假定文件已经存在
- 代码演示
public class NIOFileChannel02 {
public static void main(String[] args) throws IOException {
// 创建文件的输入流
File file = new File("file.txt");
FileInputStream fileInputStream = new FileInputStream(file);
// 通过 fileInputStream 获取对应的FileChannel,实际为 FileChannelImpl
FileChannel fileChannel = fileInputStream.getChannel();
// 创建缓存区
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
// 将通道的数据读入buffer
fileChannel.read(byteBuffer);
byteBuffer.flip();
// 将byteBuffer中的字节数据 转换为字符串
System.out.println(new String(byteBuffer.array()));
fileInputStream.close();
}
}
应用实例3-使用一个Buffer完成文件读取
public class NIOFileChannel03 {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("1.txt");
FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
FileChannel readChannel = fileInputStream.getChannel();
FileChannel writeChannel = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while(true){// 循环读取
/**
* public final Buffer clear() {
* position = 0;
* limit = capacity;
* mark = -1;
* return this;
* }
*/
// clear 一定不能忘,因为如果读至文件末尾,也即buffer 缓存区已经满了,下次是不能存放任何内容的,就会导致read == 0,
// 从而不能跳出循环
byteBuffer.clear();
int read = readChannel.read(byteBuffer);
if(read == -1){
break;
}
// 将缓存去区中的数据写入到 输出流中
byteBuffer.flip();
writeChannel.write(byteBuffer);
}
}
}
应用实例4-拷贝文件transferFrom 方法
实例要求:
- 使用 FileChannel(通道) 和 方法 transferFrom ,完成文件的拷贝
- 拷贝一张图片
- 代码演示
public class NIOFileChannel04 {
public static void main(String[] args) throws IOException {
// 创建相关流
FileInputStream fileInputStream = new FileInputStream("a.jfif");
FileOutputStream fileOutputStream = new FileOutputStream("b.jpg");
// 创建相关通道
FileChannel readChannel = fileInputStream.getChannel();
FileChannel writeChannel = fileOutputStream.getChannel();
// 拷贝
writeChannel.transferFrom(readChannel,0,readChannel.size());
// 拷贝完成,关闭相关资源
readChannel.close();
writeChannel.close();
fileOutputStream.close();
fileInputStream.close();
}
}
关于Buffer 和 Channel的注意事项和细节
- ByteBuffer 支持类型化的put 和 get, put 放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有 BufferUnderflowException 异常。[示例1]
- 可以将一个普通Buffer 转成只读Buffer [示例2]
- NIO 还提供了 MappedByteBuffer, 可以让文件直接在内存(堆外的内存)中进行修改, 而如何同步到文件由NIO 来完成. [示例3]
- 前面我们讲的读写操作,都是通过一个Buffer 完成的,NIO 还支持 通过多个Buffer (即 Buffer 数组) 完成读写操作,即 Scattering 和 Gathering 【示例4】
示例1
public class NIOByteBufferPutGet {
public static void main(String[] args) {
// 创建一个buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(64);
byteBuffer.putInt(10);
byteBuffer.putLong(5);
byteBuffer.putChar('a');
byteBuffer.putShort((short) 2);
byteBuffer.flip();
System.out.println(byteBuffer.getInt());
System.out.println(byteBuffer.getLong());
System.out.println(byteBuffer.getChar());
System.out.println(byteBuffer.getShort());
}
}
示例2
public class NIOByteBufferPutGet {
public static void main(String[] args) {
// 创建一个buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(64);
byteBuffer.putInt(10);
byteBuffer.putLong(5);
byteBuffer.putChar('a');
byteBuffer.putShort((short) 2);
byteBuffer.flip();
System.out.println(byteBuffer.getInt());
System.out.println(byteBuffer.getLong());
System.out.println(byteBuffer.getChar());
System.out.println(byteBuffer.getShort());
}
}
示例3
/**
* MappedBuffer 可以直接在内存(堆外内存)中修改,而不需要操作系统再拷贝一次
*/
public class MappedByteBufferDemo {
public static void main(String[] args) throws IOException {
RandomAccessFile randomAccess = new RandomAccessFile("1.txt","rw");
// 获取对应的通道
FileChannel fileChannel = randomAccess.getChannel();
// 参数1 : FileChannel.MapMode 读写模式
// 参数2 : 可以直接修改的起始位置
// 参数3 : 是映射到内存的大小(不是索引位置),即将 1.txt 的 多少个字节映射到内存中
// 可以修改的范围就是0-5
// mappedByteBuffer 实际类型就是 DirectByteBuffer
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
mappedByteBuffer.put(0, (byte) 'W');
mappedByteBuffer.put(1, (byte) '9');
// mappedByteBuffer.put(5, (byte) 'K'); // java.lang.IndexOutOfBoundsException
randomAccess.close();
System.out.println("~修改成功~");
}
}
示例4
/**
* Scatting: 将数据写入到Buffer时,可以采用buffer数组,依次写入 [分散]
* Gathering: 从 Buffer 读取数据时,可以采用Buffer数组依次读,[收集]
*/
public class ScatteringAndGatheringDemo {
public static void main(String[] args) throws IOException {
// 使用 ServerSocketChannel 和 SocketChannel 网络
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7777);
// 绑定端口到Socket, 并启动
serverSocketChannel.socket().bind(inetSocketAddress);
// 创建Buffer数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(5);
byteBuffers[1] = ByteBuffer.allocate(3);
// 等客户端连接(telnet)
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.stream(byteBuffers).map(byteBuffer -> "position:" + byteBuffer.position() + ",limit:" + byteBuffer.limit()).forEach(System.out::println);
}
// 将所有的buffer 进行 flip
Arrays.asList(byteBuffers).forEach(Buffer::flip);
// 将数据 传输到客户端
int byteWrite = 0;
while (byteWrite < messageLength) {
long write = socketChannel.write(byteBuffers);
byteWrite += write;
}
// 将所有的buffer 进行 clear
Arrays.asList(byteBuffers).forEach(Buffer::clear);
System.out.println("byteRead:"+byteRead+",byteWrite:"+byteWrite+",messageLength"+messageLength);
}
}
}
声明:内容来源于网络,目的是为了学习和技术交流,如有侵犯,请联系删除