一、BIO(Blocking I/O)
1. BIO 的概念
BIO = 同步阻塞 I/O
特点:
- 每个连接 必须分配一个线程
- 读写操作 会阻塞线程
- 当没有数据时,线程会 一直等待
简单理解:
客户端请求 → 服务器线程处理 → 读数据 → 阻塞等待
2. BIO 工作流程
流程如下:
1 客户端连接服务器
2 服务器 ServerSocket.accept() 阻塞等待连接
3 创建新线程处理客户端
4 线程调用 InputStream.read() 阻塞等待数据
5 处理数据并返回结果
结构示意:
客户端1 ──线程1──┐
客户端2 ──线程2──┤ BIO服务器
客户端3 ──线程3──┘
每个客户端一个线程。
3. BIO 优缺点
优点:
- 编程简单
- 容易理解
- 代码结构清晰
缺点:
- 线程开销大
- 连接数一多就崩
- 不适合高并发
举例:
如果 10000 个连接
需要
10000 个线程
服务器很容易耗尽资源。
4. BIO Java 实现
服务器代码:
java
import java.io.*;
import java.net.*;
public class BIOServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("BIO服务器启动");
while (true) {
Socket socket = serverSocket.accept(); // 阻塞等待连接
System.out.println("客户端连接成功");
new Thread(() -> {
try {
InputStream in = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String msg;
while ((msg = reader.readLine()) != null) { // 阻塞等待数据
System.out.println("收到:" + msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
客户端代码:
java
import java.io.*;
import java.net.*;
public class BIOClient {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 8080);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
out.println("hello BIO");
socket.close();
}
}
二、NIO(Non‑Blocking I/O)
1. NIO 的概念
NIO = 同步非阻塞 I/O
核心特点:
- 一个线程可以处理多个连接
- 使用 Selector(选择器)
- 通过 事件驱动
核心组件:
- Channel(通道)
- Buffer(缓冲区)
- Selector(选择器)
2. NIO 工作机制
传统 BIO:
java
连接 → 创建线程
NIO:
java
连接注册到 Selector
一个线程轮询事件
流程:
1 创建 ServerSocketChannel 2 设置 非阻塞模式 3 注册到 Selector 4 监听事件
- ACCEPT
- READ
- WRITE 5 有事件时处理
结构:
java
┌────客户端1
│
Selector ────┼────客户端2
│
└────客户端3
(一个线程)
3. NIO 三大核心组件
1 Channel(通道)
类似 Socket,但支持非阻塞。
常见:
java
ServerSocketChannel
SocketChannel
FileChannel
2 Buffer(缓冲区)
NIO 必须使用 Buffer
常见:
java
ByteBuffer
IntBuffer
CharBuffer
读取流程:
java
channel → buffer → 程序
写入流程:
java
程序 → buffer → channel
3 Selector(选择器)
Selector 用来 监听多个 Channel 事件
支持事件:
java
SelectionKey.OP_ACCEPT
SelectionKey.OP_CONNECT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
三、NIO Java 实现
1 NIO服务器
java
import java.net.*;
// 导入网络相关类,例如 InetSocketAddress(用于表示IP+端口)
import java.nio.*;
// 导入NIO缓冲区类,例如 ByteBuffer
import java.nio.channels.*;
// 导入NIO通道相关类,例如 ServerSocketChannel、SocketChannel、Selector
import java.util.*;
// 导入集合类,例如 Set、Iterator
public class NIOServer {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 创建一个服务器通道
// 类似 BIO 里的 ServerSocket
// 用于监听客户端连接
serverChannel.socket().bind(new InetSocketAddress(8080));
// 绑定端口 8080
// InetSocketAddress 表示 IP + 端口
// 默认监听本机地址
serverChannel.configureBlocking(false);
// 设置为 **非阻塞模式**
// 如果不设置,默认是阻塞
// NIO 必须使用非阻塞模式
Selector selector = Selector.open();
// 创建一个 Selector(选择器)
// Selector 的作用:
// 一个线程监听多个 Channel 的事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 将 serverChannel 注册到 selector 上
// 并监听 **OP_ACCEPT 事件**
// OP_ACCEPT 表示:有新的客户端连接
System.out.println("NIO服务器启动");
while (true) {
selector.select();
// 阻塞等待事件发生
// 如果有事件(连接、读写)发生就返回
// 没有事件会一直等待
Set<SelectionKey> keys = selector.selectedKeys();
// 获取所有已经发生事件的 key
// 每个 key 代表一个 Channel 的事件
Iterator<SelectionKey> iterator = keys.iterator();
// 创建迭代器,用来遍历所有事件
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 获取当前事件
if (key.isAcceptable()) {
// 判断是否是 **客户端连接事件**
ServerSocketChannel server =
(ServerSocketChannel) key.channel();
// 获取触发事件的 Channel
// 这里就是 serverChannel
SocketChannel channel = server.accept();
// 接收客户端连接
// 返回 SocketChannel
// 类似 BIO 中的 socket = serverSocket.accept()
channel.configureBlocking(false);
// 将客户端 Channel 设置为非阻塞模式
// 所有 NIO Channel 都必须是非阻塞
channel.register(selector, SelectionKey.OP_READ);
// 把客户端 Channel 注册到 selector
// 并监听 **读事件**
// 表示当客户端发送数据时触发
System.out.println("客户端连接");
}
if (key.isReadable()) {
// 判断是否是 **读事件**
// 表示客户端发送了数据
SocketChannel channel =
(SocketChannel) key.channel();
// 获取客户端 Channel
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 创建一个缓冲区
// 大小 1024 字节
// 用来存放读取的数据
int len = channel.read(buffer);
// 从 channel 读取数据到 buffer
// 返回读取的字节数
// 可能返回:
// >0 读取成功
// 0 没有数据
// -1 连接关闭
if (len > 0) {
buffer.flip();
// 切换为 **读模式**
// 因为 buffer 写入完数据后
// 需要 flip 才能读取
byte[] data = new byte[buffer.remaining()];
// 创建字节数组
// remaining() 表示 buffer 中剩余可读数据
buffer.get(data);
// 从 buffer 读取数据到 byte[]
System.out.println("收到:" + new String(data));
// 把字节转换为字符串并打印
}
}
iterator.remove();
// 移除当前事件
// 防止下次循环重复处理
}
}
}
}
2 NIO客户端
java
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
public class NIOClient {
public static void main(String[] args) throws Exception {
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress("127.0.0.1", 8080));
ByteBuffer buffer = ByteBuffer.wrap("hello NIO".getBytes());
channel.write(buffer);
channel.close();
}
}
四、BIO vs NIO 对比
| 特性 | BIO | NIO |
|---|---|---|
| IO模式 | 阻塞 | 非阻塞 |
| 线程模型 | 一连接一线程 | 一个线程处理多个连接 |
| 扩展性 | 差 | 强 |
| 编程复杂度 | 简单 | 较复杂 |
| 适用场景 | 连接数少 | 高并发 |
用几个真实可运行的 Java 示例来演示这段 NIO 服务器代码在不同情况下是如何处理的。每个例子都是一个客户端程序
先启动你的 NIOServer,然后运行下面不同的客户端代码。
示例1:一个客户端发送一条消息
客户端代码:
java
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client1 {
public static void main(String[] args) throws Exception {
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress("127.0.0.1", 8080));
ByteBuffer buffer = ByteBuffer.wrap("hello server".getBytes());
channel.write(buffer);
channel.close();
}
}
运行流程:
1 客户端连接服务器
触发服务器代码:
java
if (key.isAcceptable())
服务器打印:
java
客户端连接
2 客户端发送数据
触发服务器代码:
java
if (key.isReadable())
服务器读取:
java
int len = channel.read(buffer);
服务器输出:
java
收到:hello server
示例2:一个客户端连续发送多条数据
客户端代码:
java
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client2 {
public static void main(String[] args) throws Exception {
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress("127.0.0.1", 8080));
for (int i = 0; i < 5; i++) {
String msg = "message-" + i;
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
channel.write(buffer);
Thread.sleep(1000);
}
channel.close();
}
}
服务器输出可能是:
java
客户端连接
收到:message-0
收到:message-1
收到:message-2
收到:message-3
收到:message-4
这里服务器循环触发:
java
key.isReadable()
每次客户端发送数据都会触发一次。
示例3:多个客户端同时连接
客户端代码(启动多个):
java
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client3 {
public static void main(String[] args) throws Exception {
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress("127.0.0.1", 8080));
String msg = "client message";
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
channel.write(buffer);
Thread.sleep(5000);
channel.close();
}
}
同时运行 3次 Client3。
服务器输出:
java
客户端连接
客户端连接
客户端连接
收到:client message
收到:client message
收到:client message
这里发生的事情:
服务器只有 一个线程
但 Selector 同时管理:
java
client1
client2
client3
代码处理流程:
java
selector.select()
↓
selectedKeys()
↓
遍历每个事件
示例4:客户端发送大数据
客户端代码:
java
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client4 {
public static void main(String[] args) throws Exception {
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress("127.0.0.1", 8080));
StringBuilder sb = new StringBuilder();
for(int i=0;i<500;i++){
sb.append("data");
}
ByteBuffer buffer = ByteBuffer.wrap(sb.toString().getBytes());
channel.write(buffer);
channel.close();
}
}
服务器处理代码:
java
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = channel.read(buffer);
这里服务器只读取 1024字节。
如果数据很大,就会发生:
java
多次 READ 事件
服务器可能输出多次:
java
收到:data...
收到:data...
示例5:客户端断开连接
客户端代码:
java
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
public class Client5 {
public static void main(String[] args) throws Exception {
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress("127.0.0.1", 8080));
channel.close();
}
}
服务器执行:
java
int len = channel.read(buffer);
返回:
java
-1
表示:
java
客户端关闭连接
正确服务器代码通常会写:
java
if(len == -1){
channel.close();
}
一个完整测试组合
可以这样测试:
启动服务器
java
NIOServer
然后运行:
java
Client1
Client2
Client3
Client4
服务器会不断触发:
java
OP_ACCEPT
OP_READ
核心循环:
java
selector.select()
↓
selectedKeys()
↓
处理事件