1. NIO 基本介绍
Java NIO 是同步非阻塞的。NIO 相关的类放在 java.nio
包及子包下面,并且对原生的 IO 进行了很多类的改写。
NIO 是面向缓冲区或者是面向块编程的:数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中移动,这就增加了处理过程中的灵活性。
它有三大核心组件:
- Channel:通道
- Buffer:缓冲区
- Selector:选择器
其核心组件结构如下图:
由上面的图可以看出:
- 一个 Channel 对应一个 Buffer
- 一个 Selector 对应一个 Thread,但对应多个 Channel
- Selector 会根据不同事件在各个通道上切换
- 数据的读取/写入是通过 Buffer,这个跟 BIO 不同。BIO 是直接与通道打交道的
- NIO 中的 Buffer 是双向的(既可以读又可以写),但需要
flip()
方法切换;BIO 中不是双向流,要么是一个单独的输入流,要么就是一个输出流 - Channel 也是双向的
Client 不直接与 Channel 交互,而是通过中间媒介 Buffer 进行交互。
2. Buffer
Buffer 本质上是一个可以读、写数据的内存块,可以理解为一个容器对象。
Java 中的基本数据类型除了 boolean
类型外,其余的都有与之对应的 Buffer 类型。
下面以 IntBuffer
为例:
Buffer 使用示例:
public class BufferDemo {
public static void main(String[] args) {
// 1.创建 Buffer
IntBuffer intBuffer = IntBuffer.allocate(5);
for (int i = 0; i < intBuffer.capacity(); i++) {
intBuffer.put(i);
}
// 2.Buffer 转换。写 --> 读
intBuffer.flip();
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
}
}
这里创建了一个 IntBuffer
,大小为 5。然后,往其里面 put
了 5 个整数(Buffer 写)。由于 Buffer 既可以写又可以读。所以,在进行读取之前,进行 Buffer 切换 intBuffer.flip()
3. Channel
NIO 的通道类似于流,但有如下区别:
- 通道可以同时进行读、写,而流只能进行读或者写
- 通道可以实现异步读、写数据
- 通道可以从缓冲区读数据,也可以写数据到缓冲区
Channel
是一个接口:
public interface Channel extends Closeable {
// ...
}
常用的 Channel 类有:
- FileChannle:用于文件的数据的读、写
- DatagramChannel:用于 UDP 的数据的读、写
- ServerSocketChannel:用于 TCP 的数据的读、写
- SocketChannel:用于 TCP 的数据的读、写
示例一:本地文件写数据
使用 ByteBuffer 和 FileChannel 将 “hello,JAVA” 写入到某个磁盘文件
public static void main(String[] args) throws Exception {
String str = "hello, JAVA";
FileOutputStream out = new FileOutputStream("E:\\temp.txt");
FileChannel fileChannel = out.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(str.getBytes());
byteBuffer.flip();
fileChannel.write(byteBuffer);
out.close();
}
运行上述代码后,便会在 E 盘中生成一个 temp.txt 文件,并且,其里面有内容。
示例二:本地文件读数据
使用 ByteBuffer 和 FileChannel 将 temp.txt 文件中的内容读取出来
public static void main(String[] args) throws Exception {
File file = new File("E:\\temp.txt");
FileInputStream in = new FileInputStream(file);
FileChannel fileChannel = in.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
fileChannel.read(byteBuffer);
// 将字节转化为字符串
String result = new String(byteBuffer.array());
System.out.println(result);
in.close();
}
示例三:本地文件读、写数据
通过 FileChannel 和一个 Buffer 完成某个文件的拷贝
public class FileChannelRwDemo {
public static void main(String[] args) throws Exception{
FileInputStream in = new FileInputStream("E:\\temp.txt");
FileChannel inFileChannel = in.getChannel();
FileOutputStream out = new FileOutputStream("temp.txt");
FileChannel outFileChannel = out.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while (true) {
// 此行代码是重点
byteBuffer.clear();
int read = inFileChannel.read(byteBuffer);
if (-1 == read) {
break;
}
byteBuffer.flip();
outFileChannel.write(byteBuffer);
}
in.close();
out.close();
}
}
4. Selector
Selector 能够检测多个注册的通道上是否有事件发生。如果有事件发生,便获取事件,然后针对每个事件进行相应的处理。
只有在连接真正地有读、写事件发生时,才会进行读、写。这就大大地减少了系统的开销,并且,不必为每个连接都创建一个线程,不用去维护多个线程。
Selector 工作流程:
- 当客户端连接时,会通过
ServerSocketChannel
得到SocketChannel
- 将
SocketChannel
注册到 Selector 上(SelectableChannel#register()) - 注册后返回一个 SelectionKey,会和该 Selector 关联
- Selector 通过
select()
方法进行监听。该方法返回有事件发生的通道数 - 进一步可得到 SelectionKey
- SelectionKey 通过
channel()
方法反向获取SocketChannel
,然后,可以进行操作
示例:通过 NIO,进行服务端与客户端数据通讯
服务端:
public class NioServer {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
// 设置非阻塞
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
// 将ServerSocketChannel注册到Selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
if (selector.select(1000) == 0) {
System.out.println("服务器等待了1秒,无连接");
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 如果有客户端连接
if (selectionKey.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端连接成功,生成了一个 socketChannel :" + socketChannel.hashCode());
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
// 获取与Channel关联的Buffer
ByteBuffer byteBuffer = (ByteBuffer)selectionKey.attachment();
socketChannel.read(byteBuffer);
System.out.println("服务端收到了:" + new String(byteBuffer.array()));
}
iterator.remove();
}
}
}
}
客户端:
public class NioClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
// (正在)连接服务端
if (!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("因为连接需要时间,客户端不用阻塞,可以做其它工作...");
}
}
// 连接成功
String str = "Hello Java";
ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
socketChannel.write(byteBuffer);
System.in.read();
}
}
先运行服务端,再运行客户端,服务端打印出如下信息: