目录
4.4.2.3.?应用实例3 - 使用一个 Buffer 完成文件读取、写入
4.4.2.4.?应用实例4 - 拷贝文件 transferFrom 方法
4.4.3.?关于 Buffer 和 Channel 的注意事项和细节
?
? ? 在 Java 编程中,IO(Input/Output)操作是非常常见的操作,它涉及到文件读写、网络通信等方面。Java 提供了各种类来支持这些操作。本文将从 IO 的基础知识讲起,逐步深入,介绍 Java IO 的各个方面。
? ? Java IO(Input/Output)是 Java 语言中用于读写数据的 API,它提供了一系列类和接口,用于读取和写入各种类型的数据。下面是 Java IO 发展史的简要介绍:
? ? Java BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
BIO 模型
? ? Java NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理。
NIO 模型
? ? Java AIO(NIO.2):异步非阻塞,AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
? ? BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序简单易理解。
? ? NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4 开始支持。
? ? AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。
1. BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多。
2. BIO 是阻塞的,NIO 则是非阻塞的。
3. BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
4. Buffer和Channel之间的数据流向是双向的。
1. 服务器端启动一个 ServerSocket。
2. 客户端启动 Socket 对服务器进行通信,默认情况下服务器端需要对每个客户端建立一个线程与之通讯。
3. 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝。
4. 如果有响应,客户端线程会等待请求结束后,再继续执行。
Blocking I/O
package com.lm.bio;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BIOServer {
public static void main(String[] args) throws Exception {
// 线程池机制
// 思路
// 1. 创建一个线程池
// 2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
// 创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动了");
while (true) {
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
// 监听,等待客户端连接
System.out.println("等待连接....");
// 会阻塞在accept()
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
// 就创建一个线程,与之通讯(单独写一个方法)
newCachedThreadPool.execute(new Runnable() {
public void run() { // 重写
// 可以和客户端通讯
handler(socket);
}
});
}
}
// 编写一个handler方法,和客户端通讯
public static void handler(Socket socket) {
try {
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
byte[] bytes = new byte[1024];
// 通过socket获取输入流
InputStream inputStream = socket.getInputStream();
// 循环的读取客户端发送的数据
while (true) {
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
System.out.println("read....");
int read = inputStream.read(bytes);
if (read != -1) {
System.out.println(new String(bytes, 0, read)); // 输出客户端发送的数据
} else {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("关闭和client的连接");
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
打开 telnet 客户端,输入 open 127.0.0.1 6666 命令,会一直显示“正在连接127.0.0.1...”,这时其实已经连上,此时按下 Ctrl+] 键连接成功 出现 Microsoft Telnet,就可以用命令 send 发送消息了。
1. 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write。
2. 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
3. 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。
? ? NIO 有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。
? ? NIO 是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
? ? Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
? ? 通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 10000 个请求过来,根据实际情况,可以分配 50 或者 100 个线程来处理。不像之前的阻塞 IO 那样,非得分配 10000 个。
? ? 选择器是 Java NIO 中的一个重要组件,它可以用于同时监控多个通道的读写事件,并在有事件发生时立即做出响应。选择器可以实现单线程监听多个通道的效果,从而提高系统吞吐量和运行效率。
? ? 通道是一个用于读写数据的对象,类似于 Java IO 中的流(Stream)。与流不同的是,通道可以进行非阻塞式的读写操作,并且可以同时进行读写操作。通道分为两种类型:FileChannel 和SocketChannel,分别用于文件和网络通信。
? ? 缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。缓冲区对象包含了一些状态变量,例如容量(capacity)、限制(limit)、位置(position)等,用于控制数据的读写。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer,如图:
在 NIO 中,Buffer 是一个顶层父类,它是一个抽象类,类的层级关系图:
Buffer 类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息:
Buffer 类相关方法一览:
public abstract class Buffer {
// JDK1.4 时引入的 API
public final int capacity(); // 返回此缓冲区的容量
public final int position(); // 返回此缓冲区的位置
public final Buffer position(int newPosition); // 设置此缓冲区的位置
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(); // 告知此缓冲区是否为直接缓冲区
}
? ? 从前面可以看出对于 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); // 把一个数组放到缓冲区中使用
public static ByteBuffer wrap(byte[] array, int offset, int length); // 构造初始化位置 offset 和上界 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,put之后,position会自动+1
public abstract ByteBuffer put(int index, byte b); // 从绝对位置上put
}
NIO 的通道类似于流,但有些区别如下:
? ? 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 主要用来对本地文件进行 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); // 把数据从当前通道复制给目标通道
4.4.2.1. 应用实例1 - 本地文件写数据
使用前面学习后的 ByteBuffer(缓冲)和 FileChannel(通道),将 “hello,流华追梦” 写入到 file01.txt 中:
package com.lm.nio;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel01 {
public static void main(String[] args) throws Exception {
String str = "hello,流华追梦";
// 创建一个输出流 -> channel
FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");
// 通过 fileOutputStream 获取对应的 FileChannel
// 这个 fileChannel 真实类型是 FileChannelImpl
FileChannel fileChannel = fileOutputStream.getChannel();
// 创建一个缓冲区 ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 将 str 放入 byteBuffer
byteBuffer.put(str.getBytes());
// 对 byteBuffer 进行 flip
byteBuffer.flip();
// 将 byteBuffer 数据写入到 fileChannel
fileChannel.write(byteBuffer);
fileOutputStream.close();
}
}
4.4.2.2.?应用实例2 - 本地文件读数据
使用前面学习后的 ByteBuffer(缓冲)和 FileChannel(通道),将 file01.txt 中的数据读入到程序,并显示在控制台屏幕:
package com.lm.nio;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel02 {
public static void main(String[] args) throws Exception {
// 创建文件的输入流
File file = new File("d:\\file01.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 的字节数据转成 String
System.out.println(new String(byteBuffer.array()));
fileInputStream.close();
}
}
4.4.2.3.?应用实例3 - 使用一个 Buffer 完成文件读取、写入
使用 FileChannel(通道)和方法 read、write,完成文件的拷贝:
package com.lm.nio;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel03 {
public static void main(String[] args) throws Exception {
FileInputStream fileInputStream = new FileInputStream("1.txt");
FileChannel fileChannel01 = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
FileChannel fileChannel02 = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while (true) { // 循环读取
// 这里有一个重要的操作,一定不要忘了
/*
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
*/
byteBuffer.clear(); // 清空 buffer
int read = fileChannel01.read(byteBuffer);
System.out.println("read = " + read);
if (read == -1) { // 表示读完
break;
}
// 将 buffer 中的数据写入到 fileChannel02--2.txt
byteBuffer.flip();
fileChannel02.write(byteBuffer);
}
// 关闭相关的流
fileInputStream.close();
fileOutputStream.close();
}
}
4.4.2.4.?应用实例4 - 拷贝文件 transferFrom 方法
使用 FileChannel(通道)和方法 transferFrom,完成文件的拷贝:
package com.lm.nio;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
public class NIOFileChannel04 {
public static void main(String[] args) throws Exception {
// 创建相关流
FileInputStream fileInputStream = new FileInputStream("d:\\a.jpg");
FileOutputStream fileOutputStream = new FileOutputStream("d:\\a2.jpg");
// 获取各个流对应的 FileChannel
FileChannel sourceCh = fileInputStream.getChannel();
FileChannel destCh = fileOutputStream.getChannel();
// 使用 transferForm 完成拷贝
destCh.transferFrom(sourceCh, 0, sourceCh.size());
// 关闭相关通道和流
sourceCh.close();
destCh.close();
fileInputStream.close();
fileOutputStream.close();
}
}
ByteBuffer 支持类型化的 put 和 get,put 放入的是什么数据类型,get 就应该使用相应的数据类型来取出,否则可能有 BufferUnderflowException 异常:
package com.lm.nio;
import java.nio.ByteBuffer;
public class NIOByteBufferPutGet {
public static void main(String[] args) {
// 创建一个 Buffer
ByteBuffer buffer = ByteBuffer.allocate(64);
// 类型化方式放入数据
buffer.putInt(100);
buffer.putLong(9);
buffer.putChar('尚');
buffer.putShort((short) 4);
// 取出
buffer.flip();
System.out.println();
System.out.println(buffer.getInt());
System.out.println(buffer.getLong());
System.out.println(buffer.getChar());
System.out.println(buffer.getShort());
}
}
可以将一个普通 Buffer 转成只读 Buffer:
package com.lm.nio;
import java.nio.ByteBuffer;
public class ReadOnlyBuffer {
public static void main(String[] args) {
// 创建一个 buffer
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());
// 读取
while (readOnlyBuffer.hasRemaining()) {
System.out.println(readOnlyBuffer.get());
}
readOnlyBuffer.put((byte) 100); //ReadOnlyBufferException
}
}
NIO 还提供了 MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由 NIO 来完成:
package com.lm.nio;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
/**
* 说明 1.MappedByteBuffer 可让文件直接在内存(堆外内存)修改,操作系统不需要拷贝一次
*/
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception {
RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
// 获取对应的通道
FileChannel channel = randomAccessFile.getChannel();
/**
* 参数 1:FileChannel.MapMode.READ_WRITE 使用的读写模式
* 参数 2:0:可以直接修改的起始位置
* 参数 3:5: 是映射到内存的大小(不是索引位置),即将 1.txt 的多少个字节映射到内存
* 可以直接修改的范围就是 0-5
* 实际类型 DirectByteBuffer
*/
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
mappedByteBuffer.put(0, (byte) 'H');
mappedByteBuffer.put(3, (byte) '9');
mappedByteBuffer.put(5, (byte) 'Y');//IndexOutOfBoundsException
randomAccessFile.close();
System.out.println("修改成功~~");
}
}
前面我们讲的读写操作,都是通过一个 Buffer 完成的,NIO 还支持通过多个 Buffer(即 Buffer 数组)完成读写操作,即 Scattering 和 Gathering:
package com.lm.nio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
/**
* Scattering:将数据写入到 buffer 时,可以采用 buffer 数组,依次写入 [分散]
* Gathering:从 buffer 读取数据时,可以采用 buffer 数组,依次读
*/
public class ScatteringAndGatheringTest {
public static void main(String[] args) throws Exception {
// 使用 ServerSocketChannel 和 SocketChannel 网络
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);
// 等客户端连接 (telnet)
SocketChannel socketChannel = serverSocketChannel.accept();
int messageLength = 8; // 假定从客户端接收 8 个字节
// 循环的读取
while (true) {
int byteRead = 0;
while (byteRead < messageLength) {
long l = socketChannel.read(byteBuffers);
byteRead += l; // 累计读取的字节数
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 byteWirte = 0;
while (byteWirte < messageLength) {
long l = socketChannel.write(byteBuffers);//
byteWirte += l;
}
// 将所有的buffer进行clear
Arrays.asList(byteBuffers).forEach(buffer -> {
buffer.clear();
});
System.out.println("byteRead = " + byteRead + ", byteWrite = " + byteWirte + ", messagelength = " + messageLength);
}
}
}
Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)。
Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
避免了多线程之间的上下文切换导致的开销。
Nonblocking I/O
Selector 在是一个抽象类,常用方法说明如下:
对上图的说明:
? ? JDK7 引入了 Asynchronous I/O,即 AIO。在进行 I/O 编程中,常用到两种模式:Reactor 和 Proactor。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理。
? ? AIO 即 NIO2.0,叫做异步不阻塞的 IO。AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
? ? AIO 使用了三个核心组件:AsynchronousChannel、CompletionHandler 和 AsynchronousServerSocketChannel。其中,AsynchronousChannel 是读/写数据的通道,CompletionHandler 是 I/O 操作完成时的回调方法,AsynchronousServerSocketChannel 是异步服务器端套接字通道,用于监听客户端的连接请求。
1. 高并发性:Java AIO 采用异步 IO 方式进行数据读写操作,可以实现高并发处理能力。
2. 高吞吐量:Java AIO 支持异步读写操作,可以同时处理多个请求,从而提高了数据读写的效率和吞吐量。
3. 高可靠性:由于Java AIO 采用异步 IO 方式进行数据读写操作,可以避免线程阻塞等待 I/O 操作完成的情况,从而提高程序的可靠性。
4. 简单易用:Java AIO 提供了简单易用的 API,不需要编写复杂的代码就可以实现异步 IO 操作。
? ? Java AIO 适用于需要大量并发连接,但每个连接却很少有数据交互的场景,例如基于消息的应用程序、远程过程调用(RPC)等。在这些应用场景中,AIO 可以大幅度提高程序的性能和并发处理能力,从而满足用户对高吞吐量和低延迟的要求。
? ? 另外,Java AIO 也可以用于开发高性能的网络服务器,例如聊天室服务器、在线游戏服务器等。由于 AIO 支持异步读取输入流和输出流,因此可以同时处理多个客户端请求,有效地提高了服务器的并发处理能力。
BIO、NIO、AIO 对比表:
? | BIO | NIO | AIO |
---|---|---|---|
IO模型 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
编程难度 | 简单 | 复杂 | 复杂 |
可靠性 | 差 | 好 | 好 |
吞吐量 | 低 | 高 | 高 |
?