什么是 NIO ?
NIO,全称 New IO(新IO)或 Non-Blocking IO(非阻塞IO),使得当前程序在处理IO事务时不会影响其他程序的运行,而且可以在不使用多线程的情况下满足IO操作的需求。NIO包含三个核心部分:
通道(Channel): 用于文件操作和网络数据传递的通道。它提供了一种更灵活、高效的IO操作方式。
缓冲(Buffer): 缓冲的使用能够提高IO操作效率,减少不必要的读写次数,是NIO中的重要组成部分。
选择器(Selector): 是NIO的真正核心,用于管理多个通道的IO事件,实现非阻塞IO的核心机制。
通道(Channel)和 缓冲(Buffer)
Java NIO(New I/O)中的 Buffer
缓冲区和 Channel
通道是进行数据传输的重要组件。
在 Buffer
中,ByteBuffer
是最常用的字节缓冲区,同时还有其他基本数据类型对应的缓冲区,如 ShortBuffer
、IntBuffer
、CharBuffer
、FloatBuffer
、DoubleBuffer
等。以 ByteBuffer
为例:
ByteBuffer
常用方法:
allocate(int capacity):
分配指定容量的字节缓冲区。get()
: 从缓冲区中读取一个 byte。flip()
: 翻转缓冲区,将限制设置为当前位置,然后将位置设置为 0,用于读取之前写入的数据。wrap(byte[] arr)
: 将字节数组包装为ByteBuffer
。put(byte[] b)
: 将字节数组存入缓冲区。
在 Channel
接口中,常见的通道有 FileChannel
、DatagramChannel
、ServerSocketChannel
、SocketChannel
等。以 FileChannel
为例:
FileChannel
常用方法:
read(ByteBuffer buffer)
: 从通道中读取数据到ByteBuffer
中。write(ByteBuffer buffer)
: 将数据从ByteBuffer
写入通道。transferFrom(ReadableByteChannel src, long position, long count)
: 从指定的srcChannel
中读取指定位置开始的count
个元素到当前通道中,实现文件复制操作。transferTo(long position, long count, WritableByteChannel target)
: 将当前通道中的数据写入到指定的target
通道中,从当前通道的position
位置开始,写入count
个元素。
一个简单的例子
java
package com.qfedu.b_niofile;
import org.junit.Test;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* NIO文件操作测试类
*/
public class FileNioTest {
/**
* 通过NIO写入数据到文件中的操作
*/
@Test
public void testNioFileWrite() throws IOException {
// 使用 try-with-resources 自动关闭资源
try (FileOutputStream fos = new FileOutputStream("D:/aaa/1.txt");
FileChannel foc = fos.getChannel()) {
// 分配4KB缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024 * 4);
// 准备数据,放入缓冲区
String str = "测试NIO";
buffer.put(str.getBytes());
// 翻转缓冲区,准备写入操作
buffer.flip();
// 缓冲区数据写入到通道中
foc.write(buffer);
}
}
/**
* 通过NIO从文件中读取数据的操作
*/
@Test
public void testNioFileRead() throws IOException {
// 使用 try-with-resources 自动关闭资源
try (FileInputStream fis = new FileInputStream("D:/aaa/1.txt");
FileChannel fic = fis.getChannel()) {
// 分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 从通道读取数据保存到缓冲区中
int read = fic.read(buffer);
System.out.println("Bytes read: " + read);
// 输出缓冲区中的数据
System.out.println("Content: " + new String(buffer.array(), 0, read));
}
}
/**
* 使用NIO进行文件复制的操作
*/
@Test
public void testCopyFile() throws IOException {
long start = System.currentTimeMillis();
// 使用 try-with-resources 自动关闭资源
try (FileInputStream fis = new FileInputStream("D:/aaa/1.mp4");
FileOutputStream fos = new FileOutputStream("D:/aaa/2.mp4");
FileChannel srcChannel = fis.getChannel();
FileChannel dstChannel = fos.getChannel()) {
// 将数据从源通道传输到目标通道
srcChannel.transferTo(0, srcChannel.size(), dstChannel);
}
long end = System.currentTimeMillis();
System.out.println("Time:" + (end - start));
}
/**
* 使用BufferedInputStream和BufferedOutputStream进行文件复制的操作
*/
@Test
public void testCopyUseBuffer() throws IOException {
long start = System.currentTimeMillis();
// 使用 try-with-resources 自动关闭资源
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:/aaa/1.mp4"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:/aaa/3.mp4"))) {
int length;
byte[] buf = new byte[4 * 1024];
// 读取数据到缓冲区,再写入目标文件
while ((length = bis.read(buf)) != -1) {
bos.write(buf, 0, length);
}
}
long end = System.currentTimeMillis();
System.out.println("Time:" + (end - start));
}
}
选择器(Selector)
Selector 是 Java NIO 中的一个关键组件。它允许一个单独的线程来管理多个通道(Channel),监控这些通道上的事件,当事件发生时,可以选择性地对这些事件进行响应。Selector 的主要作用是实现多路复用(Multiplexing),使得一个线程可以同时处理多个通道的 I/O 事件。
Selector常用方法
java
// 获取一个选择器对象
public static Selector open();
// 监听所有注册通道,存在IO流操作时,将对应的信息SelectionKey存入内部集合
// 参数是一个超时时间,0表示无限期等待
public int select(long timeout);
// 返回当前Selector内部集合中保存的所有SelectionKey
public Set<SelectionKey> selectionKeys();
SelectionKey
java
// 表示Selector和网络通道之间的关系
public interface SelectionKey {
int OP_ACCEPT; // 16 需要连接
int OP_CONNECT; // 8 已经连接
int OP_READ; // 1 读取操作
int OP_WRITE; // 4 写入操作
// 获取与之关联的 Selector 对象
public abstract Selector selector();
// 获取与之关联的通道
public abstract SelectableChannel channel();
// 获取与之关联的共享数据
public final Object attachment();
// 设置或改变监听事件
public abstract SelectionKey interestOps(int ops);
// 是否可以 accept
public final boolean isAcceptable();
// 是否可以读
public final boolean isReadable();
// 是否可以写
public final boolean isWritable();
}
网络编程 与 NIO
网络编程的特点
- 通信:得满足通信能力,实现数据的传输与接收。
- 异步:支持异步操作,即不需要等待一个操作完成才能执行下一个操作。
- 多连接:能够处理多个连接,支持同时与多个客户端或服务端建立连接。
- 并发性:具备处理多个任务的能力,能够同时处理多个请求或连接,提高系统的并发性。
传统 BIO 与 NIO 的对比
-
阻塞与非阻塞:
- BIO: 阻塞I/O模型,每个I/O操作(读、写)都会阻塞线程,直到数据准备好或写入完成。
- NIO: 非阻塞I/O模型,允许一个线程同时管理多个通道,通过选择器(Selector)实现,可以实现一个线程处理多个I/O操作。
-
连接数处理能力:
- BIO: 对于每个连接都需要独立的线程,当连接数较多时,会导致线程数增加,资源消耗大。
- NIO: 通过一个线程管理多个连接,避免了为每个连接创建独立线程,提高了连接数处理的能力。
-
缓冲:
- BIO: 数据从流中读取或写入时,需要通过缓冲区进行。
- NIO: 缓冲区的使用更加灵活,可以直接读写缓冲区,减少了数据从缓冲区到流的复制操作。
-
选择器(Selector):
- BIO: 没有选择器概念,每个通道需要独立的线程。
- NIO: 通过选择器,一个线程可以管理多个通道,实现了多路复用。
-
适用场景:
- BIO: 适用于连接数较少且相对稳定的情况,例如传统的同步阻塞Socket。
- NIO: 适用于连接数较多、连接时间短、通信频繁的情况,例如Web应用中的高并发处理
网络编程中使用的 通道 和 缓冲
-
服务端Socket程序对应的Channel通道
javaServerSocketChannel // 开启服务器ServerSocketChannel通道,等于开始服务器程序 public static ServerSocketChannel open(); // 设置服务器端端口号 public final ServerSocketChannel bind(SocketAddress local); // 设置阻塞或非阻塞模式, 取值 false 表示采用非阻塞模式 public final SelectableChannel configureBlocking(boolean block); // [非阻塞] 获取一个客户端连接,并且得到对应的操作通道 public SocketChannel accept(); // [重点方法] 注册当前选择器,并且选择监听什么事件 public final SelectionKey register(Selector sel, int ops);
-
客户端Socket程序对应的Channel通道。
javaSocketChannel // 打开一个Socket客户端Channel对象 public static SocketChannel open(); // 这里可以设置是阻塞状态,还是非阻塞状态,false表示非阻塞 public final SelectableChannel configureBlocking(boolean block); // 连接服务器 public boolean connect(SocketAddress remote); // 如果connect连接失败,可以通过finishConnect继续连接 public boolean finishConnect(); // 写入数据到缓冲流中 public int write(ByteBuffer buf); // 从缓冲流中读取数据 public int read(ByteBuffer buf); // 注册当前SocketChannel,选择对应的监听操作,并且可以带有Object attachment参数 public final SelectionKey register(Selector sel, int ops, Object attachment); // 关闭SocketChannel public final void close();
NIO完成一个TCP聊天室
NIO TCP聊天室客户端
java
/**
* NIO 非阻塞状态的TCP聊天室客户端核心代码
*/
public class ChatClient {
private static final String HOST = "192.168.31.154";
private static final int PORT = 8848;
private SocketChannel socket;
private String userName;
/**
* 客户端构造方法,创建客户端对象
*
* @param userName 指定的用户名
*/
public ChatClient(String userName) throws IOException, InterruptedException {
// 1. 打开SocketChannel
socket = SocketChannel.open();
// 2. 设置非阻塞状态
socket.configureBlocking(false);
// 3. 根据指定的HOST IP地址和对应PORT端口号创建对应的 InetSocketAddress
InetSocketAddress address = new InetSocketAddress(HOST, PORT);
// 4. 连接服务器
if (!socket.connect(address)) {
// 如果没有连接到服务器,保持请求连接的状态
while (!socket.finishConnect()) {
System.out.println("服务器请求连接失败,等待2s继续请求连接...");
Thread.sleep(2000);
}
}
this.userName = userName;
System.out.println("客户端 " + userName + " 准备就绪");
}
/**
* 发送数据到服务器,用于广播消息,群聊
*
* @param message 指定的消息
*/
public void sendMsg(String message) throws IOException {
// 断开服务器连接 close
if ("close".equals(message)) {
socket.close();
return;
}
message = userName + ":" + message;
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
socket.write(buffer);
}
/**
* 接收服务器发送的数据
*/
public void receiveMsg() throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int length = socket.read(buffer);
if (length > 0) {
System.out.println(new String(buffer.array()));
}
}
}
NIO TCP聊天室服务端
java
/**
* NIO 非阻塞状态的TCP聊天室服务端核心代码
*
* @author Anonymous
*/
public class ChatServer {
private ServerSocketChannel serverSocket;
private Selector selector;
private static final int PORT = 8848;
/**
* 服务器构造方法,开启ServerSocketChannel,同时开启Selector,注册操作
*
* @throws IOException 异常
*/
public ChatServer() throws IOException {
serverSocket = ServerSocketChannel.open();
selector = Selector.open();
serverSocket.bind(new InetSocketAddress(PORT));
serverSocket.configureBlocking(false);
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
}
/**
* 服务器干活方法,指定客户端绑定,数据接收和转发
*/
public void start() {
try {
while (true) {
if (0 == selector.select(2000)) {
System.out.println("服务器默默的等待连接,无人访问...");
continue;
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 1. 连接
if (key.isAcceptable()) {
SocketChannel socket = serverSocket.accept();
socket.configureBlocking(false);
socket.register(selector, SelectionKey.OP_READ);
broadcast(socket, socket.getRemoteAddress().toString() + "上线了");
}
// 2. 接收数据转发
if (key.isReadable()) {
readMsg(key);
}
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 从指定的SelectionKey中读取数据
*
* @param key 符合OP_READ 要求的SelectionKey
*/
public void readMsg(SelectionKey key) throws IOException {
SocketChannel socket = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int length = socket.read(buffer);
if (length > 0) {
String message = new String(buffer.array());
broadcast(socket, message);
}
}
/**
* 广播方法,该方法是群发消息,但是不要发给自己
*
* @param self 当前发送数据的客户端
* @param message 消息
*/
public void broadcast(SocketChannel self, String message) throws IOException {
Set<SelectionKey> keys = selector.keys();
for (SelectionKey key : keys) {
SelectableChannel channel = key.channel();
if (channel instanceof SocketChannel && !channel.equals(self)) {
SocketChannel socketChannel = (SocketChannel) channel;
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
socketChannel.write(buffer);
}
}
}
public static void main(String[] args) throws IOException {
new ChatServer().start();
}
}
NIO TCP聊天室客户端线程开启
java
import java.io.IOException;
import java.util.Scanner;
public class ChatClientThread {
public static void main(String[] args) throws IOException, InterruptedException {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入用户名:");
String userName = scanner.nextLine();
if (userName.length() == 0) {
return;
}
ChatClient chatClient = new ChatClient(userName);
// 接收消息
new Thread(() -> {
while (true) {
try {
chatClient.receiveMsg();
Thread.sleep(2000);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 发送消息
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
chatClient.sendMsg(msg);
}
}
}
搞定 ૮(˶ᵔ ᵕ ᵔ˶)ა
总结
一定要多思考,如果人永远待在舒适圈的话,人永远不会成长。共勉
觉得作者写的不错的,值得你们借鉴的话,就请点一个免费的赞吧!这个对我来说真的很重要。૮(˶ᵔ ᵕ ᵔ˶)ა