Java网络编程中的IO(Input/Output)模型是管理计算机对外部数据读取和写入操作的重要机制。Java提供了多种IO模型来满足不同的网络通信需求。
一、阻塞IO(BIO,Blocking I/O)
- 概念:
- 阻塞IO是最简单和直观的一种IO模型。在BIO模型中,当用户线程发起系统调用时,内核会一直等待,直到有数据可读或可写,才会返回结果。
- 特点:
- 同步阻塞:服务器实现模式为一个连接一个线程,即客户端有连接请求时,服务器端就需要启动一个线程进行处理。如果这个连接不做任何事情,会造成不必要的线程开销。
- 简单易用:BIO模型的编程流程相对简单,容易理解和实现。
- 资源消耗大:在高并发情况下,每个线程都会阻塞,导致系统性能急剧下降,且对服务器资源要求较高。
- 适用场景:
- 适用于连接数目比较小且固定的架构。
- 常用于文件操作或较少并发连接的网络服务。
- 示例:Java传统Socket模型,线程在读写时会被阻塞。
// 客户端 try (Socket socket = new Socket("localhost", 8080); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) { // 阻塞直到数据到达 String response = in.readLine(); System.out.println("Response: " + response); } // 服务端 try (ServerSocket serverSocket = new ServerSocket(8080); Socket clientSocket = serverSocket.accept(); // 阻塞等待连接 PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { out.println("Hello from blocking server!"); }
二、非阻塞IO(NIO,Non-blocking I/O)
- 概念:
- NIO模型允许在不阻塞当前线程的情况下发起IO操作。如果操作无法立即完成,方法会返回特定值(如0或特定错误码),而不是阻塞。
- 特点:
- 非阻塞模式:线程在等待IO操作完成时不会被阻塞,可以继续执行其他任务。
- 高效利用资源:通过减少线程的阻塞等待时间,提高了系统资源的利用率。
- 需要轮询:由于非阻塞IO不会主动通知线程操作完成,因此需要线程反复检查或轮询IO操作的状态。
- 核心组件:
- Channel(通道):表示打开到IO设备(如文件、套接字)的连接,支持非阻塞读取和写入。
- Buffer(缓冲区):用于存取数据的内存块,提供了方便访问该块内存的方法。
- Selector(选择器):能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。
- 适用场景:
- 适用于连接数目多且连接时间较短的架构,如聊天服务器、弹幕系统等。
- 示例:多人聊天室
- 服务端
package com.example.helloword; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; import java.util.Set; /** * 服务端功能 * 监听新连接:通过 ServerSocketChannel 接受客户端连接,并注册到 Selector。 * 处理消息:读取客户端消息并广播给其他用户。 * 上下线通知:当客户端连接或断开时,广播通知所有用户。 * 异常处理:检测客户端异常断开并清理资源。 */ public class GroupChatServer { private static final int PORT = 6667; private ServerSocketChannel serverSocketChannel; private Selector selector; public GroupChatServer() { try { selector = Selector.open(); serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(PORT)); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("服务器启动,监听端口:" + PORT); } catch (IOException e) { e.printStackTrace(); } } public void listen() { try { while (true) { selector.select(); Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); // 防止重复处理 if (key.isAcceptable()) { handleAccept(key); } else if (key.isReadable()) { handleRead(key); } } } } catch (IOException e) { e.printStackTrace(); } } private void handleAccept(SelectionKey key) throws IOException { SocketChannel clientChannel = serverSocketChannel.accept(); clientChannel.configureBlocking(false); clientChannel.register(selector, SelectionKey.OP_READ); String msg = "[" + clientChannel.getRemoteAddress() + "] 上线了"; System.out.println(msg); broadcast(msg, clientChannel); // 广播上线通知 } private void handleRead(SelectionKey key) { SocketChannel clientChannel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); try { int len = clientChannel.read(buffer); if (len > 0) { String msg = new String(buffer.array(), 0, len).trim(); System.out.println("收到消息: " + msg); broadcast(msg, clientChannel); // 广播消息 } else if (len == -1) { // 客户端正常关闭 handleDisconnect(clientChannel, "下线"); } } catch (IOException e) { handleDisconnect(clientChannel, "异常断开"); } } private void handleDisconnect(SocketChannel clientChannel, String reason) { try { String msg = "[" + clientChannel.getRemoteAddress() + "] " + reason; System.out.println(msg); broadcast(msg, clientChannel); clientChannel.close(); } catch (IOException ex) { ex.printStackTrace(); } } private void broadcast(String msg, SocketChannel excludeChannel) throws IOException { for (SelectionKey key : selector.keys()) { Channel targetChannel = key.channel(); if (targetChannel instanceof SocketChannel && targetChannel != excludeChannel) { SocketChannel dest = (SocketChannel) targetChannel; ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes()); dest.write(buffer); } } } public static void main(String[] args) { new GroupChatServer().listen(); } }
- 客户端
package com.example.helloword; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Scanner; import java.util.Set; /** * 客户端功能 * 连接服务器:初始化时自动连接服务器。 * 发送消息:读取用户输入并发送到服务器。 * 接收消息:监听服务器广播的消息并实时显示。 * 退出机制:输入 exit 退出群聊并关闭连接。 */ public class GroupChatClient { private static final String HOST = "127.0.0.1"; private static final int PORT = 6667; private SocketChannel socketChannel; private Selector selector; private String username; public GroupChatClient() { try { selector = Selector.open(); socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT)); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); username = "用户" + socketChannel.getLocalAddress().toString().substring(1).replaceAll("[:.]", ""); System.out.println(username + " 已连接到服务器"); } catch (IOException e) { e.printStackTrace(); } } public void sendMsg(String msg) { if ("exit".equalsIgnoreCase(msg)) { try { socketChannel.close(); selector.close(); System.out.println("已退出群聊"); System.exit(0); } catch (IOException e) { e.printStackTrace(); } return; } try { String formattedMsg = username + ": " + msg; ByteBuffer buffer = ByteBuffer.wrap(formattedMsg.getBytes()); socketChannel.write(buffer); } catch (IOException e) { e.printStackTrace(); } } public void readMsg() { try { while (true) { selector.select(); System.out.println("==> readMsg"); Set<SelectionKey> keys = selector.selectedKeys(); for (SelectionKey key : keys) { if (key.isReadable()) { SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); int len = channel.read(buffer); if (len > 0) { String msg = new String(buffer.array(), 0, len).trim(); System.out.println(msg); } else if (len == -1) { System.out.println("服务端已关闭连接"); key.cancel(); // 取消 SelectionKey 的注册 channel.close(); // 关闭通道 System.exit(0); } } } keys.clear(); } } catch (IOException e) { System.out.println("与服务器断开连接"); System.exit(0); } finally { // 关闭资源 try { if (socketChannel != null) socketChannel.close(); if (selector != null) selector.close(); } catch (IOException ex) { ex.printStackTrace(); } } } public static void main(String[] args) { GroupChatClient client = new GroupChatClient(); // 启动线程监听服务器消息 new Thread(client::readMsg).start(); // 读取用户输入并发送 Scanner scanner = new Scanner(System.in); while (true) { String msg = scanner.nextLine(); client.sendMsg(msg); } } }
- 允许开启多个客户端:(IDEA 2024)
Edit Confihurations
Modify option
-->Allow multiple instance
- 服务端
三、异步IO(AIO,Asynchronous I/O)
- 概念:
- AIO模型允许在发起IO操作后不阻塞线程,并且在操作完成时通过回调或未来(Future)来通知应用程序。
- 特点:
- 异步操作:发起IO操作后立即返回,操作在后台进行,不会阻塞线程。
- 回调通知:操作完成后,通过回调或Future通知应用程序,进一步减少了线程的阻塞。
- 编程复杂:相比BIO和NIO,AIO的编程模型更为复杂。
适用场景: - 适用于连接数目多且连接时间较长的架构,如相册服务器等。
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel client, Void attachment) {
server.accept(null, this); // 继续接收新连接
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
// 处理读取的数据
}
});
}
});
四、总结
模型 | BIO | NIO | AIO |
---|---|---|---|
阻塞性 | 同步阻塞 | 同步非阻塞 | 异步非阻塞 |
线程模型 | 1 连接 1 线程 | 多路复用(单线程/线程池) | 回调驱动 |
复杂度 | 低 | 中高 | 高 |
适用场景 | 低并发、简单应用 | 高并发、实时响应(如 Netty) | 特定场景(如文件操作) |
接口 | Socket ServerSocket |
SocketChanne ServerSocketChannel |
AsynchronousSocketChannel AsynchronousServerSocketChannel |
实践建议:
- BIO:仅用于教学或低负载场景。
- NIO:大多数高并发场景的首选,结合 Netty 等框架简化开发。
- AIO:谨慎使用,需评估操作系统支持与业务需求。
为什么主流框架(如 Netty)选择 NIO 而非 AIO?
- NIO 的成熟度高,跨平台兼容性好。
- Linux 的 AIO 实现不完全(如文件 AIO 稳定,网络 AIO 依赖线程池)。
- NIO 通过优化(如 epoll 边缘触发)已能支撑百万级并发。