Netty教程 / 第 20 节
第2章:网络编程基础
本章导读
在深入学习 Netty 之前,我们需要理解网络编程的基础知识。本章将介绍 BIO、NIO、AIO 三种 I/O 模型,深入讲解 Java NIO 的核心组件,分析传统 Socket 编程的痛点,并展示 Netty 如何优雅地解决这些问题。
2.1 BIO、NIO、AIO 概念对比
2.1.1 BIO (Blocking I/O) - 同步阻塞 I/O
定义:传统的阻塞式 I/O 模型,一个线程处理一个连接。
工作原理:
客户端连接 → 服务端创建线程 → 线程阻塞等待数据 → 处理数据 → 返回结果
代码示例:
// 传统BIO服务器
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
// accept()会阻塞,直到有客户端连接
Socket socket = serverSocket.accept();
// 为每个连接创建一个线程
new Thread(() -> {
try {
InputStream in = socket.getInputStream();
byte[] buffer = new byte[1024];
// read()会阻塞,直到有数据到达
int len = in.read(buffer);
// 处理数据...
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
特点:
| 优点 | 缺点 |
|---|---|
| 编程模型简单 | 一个连接一个线程,资源消耗大 |
| 易于理解和实现 | 线程上下文切换开销大 |
| 连接数受限于线程数 | |
| 大量线程处于阻塞状态,浪费资源 |
适用场景:
- 连接数较少且固定的场景
- 服务器资源充足
- 对并发要求不高的应用
2.1.2 NIO (Non-blocking I/O) - 同步非阻塞 I/O
定义:基于事件驱动的 I/O 模型,一个线程可以处理多个连接。
工作原理:
多个客户端连接 → Selector轮询 → 检测就绪事件 → 处理就绪的Channel → 继续轮询
核心组件:
- Channel(通道):数据传输的管道
- Buffer(缓冲区):数据的容器
- Selector(选择器):事件监听和分发
代码示例:
// NIO服务器
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// select()会阻塞,但可以监听多个Channel
selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
keys.remove();
if (key.isAcceptable()) {
// 处理连接事件
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 处理读事件
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer);
// 处理数据...
}
}
}
特点:
| 优点 | 缺点 |
|---|---|
| 一个线程处理多个连接 | 编程复杂度高 |
| 资源利用率高 | 需要处理半包、粘包问题 |
| 适合高并发场景 | Selector 在 Linux 上基于 epoll,性能好 |
| 减少线程上下文切换 | 但在 Windows 上性能一般 |
适用场景:
- 高并发、连接数多的场景
- 连接时间长但数据交互少
- 聊天服务器、推送服务器
2.1.3 AIO (Asynchronous I/O) - 异步非阻塞 I/O
定义:真正的异步 I/O,操作完成后会主动通知应用程序。
工作原理:
发起异步操作 → 立即返回 → 继续其他工作 → 操作完成后回调通知
代码示例:
// AIO服务器
AsynchronousServerSocketChannel serverChannel =
AsynchronousServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel client, Object attachment) {
// 继续接受下一个连接
serverChannel.accept(null, this);
// 处理当前连接
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
// 读取完成,处理数据
attachment.flip();
// 处理数据...
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
// 处理失败
}
});
}
@Override
public void failed(Throwable exc, Object attachment) {
// 处理失败
}
});
特点:
| 优点 | 缺点 |
|---|---|
| 真正的异步 I/O | 编程复杂度最高 |
| 性能理论上最好 | 在 Linux 上实现不成熟 |
| 充分利用 OS 支持 | 调试困难 |
适用场景:
- 理论上适合所有高并发场景
- 但实际应用较少,因为 NIO 已经足够好
2.1.4 三种模型对比总结
| 特性 | BIO | NIO | AIO |
|---|---|---|---|
| I/O模型 | 同步阻塞 | 同步非阻塞 | 异步非阻塞 |
| 编程复杂度 | 简单 | 复杂 | 非常复杂 |
| 可靠性 | 好 | 好 | 一般 |
| 吞吐量 | 低 | 高 | 高 |
| 线程模型 | 一连接一线程 | 一线程多连接 | 一线程多连接 |
| 适用场景 | 连接数少 | 连接数多 | 连接数多 |
| JDK支持 | JDK 1.0+ | JDK 1.4+ | JDK 1.7+ |
Netty 的选择:
- Netty 主要基于 NIO 实现
- 也支持 BIO(OIO)和 AIO,但不推荐使用
- NIO 在实际应用中性能和稳定性最佳
2.2 Java NIO 核心组件
2.2.1 Channel(通道)
定义:Channel 是数据传输的管道,类似于流(Stream),但有以下区别:
| Stream | Channel |
|---|---|
| 单向(输入流或输出流) | 双向(可读可写) |
| 阻塞 | 可以非阻塞 |
| 直接读写数据 | 通过 Buffer 读写 |
主要实现类:
// 文件操作
FileChannel fileChannel = new RandomAccessFile("file.txt", "rw").getChannel();
// 网络操作
ServerSocketChannel serverChannel = ServerSocketChannel.open(); // 服务端
SocketChannel socketChannel = SocketChannel.open(); // 客户端
DatagramChannel datagramChannel = DatagramChannel.open(); // UDP
核心方法:
// 读取数据到Buffer
int bytesRead = channel.read(buffer);
// 从Buffer写入数据
int bytesWritten = channel.write(buffer);
// 设置非阻塞模式
channel.configureBlocking(false);
// 注册到Selector
channel.register(selector, SelectionKey.OP_READ);
// 关闭Channel
channel.close();
示例代码:
public class ChannelDemo {
public static void main(String[] args) throws IOException {
// 创建ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
System.out.println("服务器启动,监听端口: 8080");
while (true) {
// 非阻塞模式下,accept()立即返回
SocketChannel clientChannel = serverChannel.accept();
if (clientChannel != null) {
System.out.println("客户端连接: " + clientChannel.getRemoteAddress());
// 读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("接收到: " + new String(data));
}
clientChannel.close();
}
// 避免CPU空转
Thread.sleep(1000);
}
}
}
2.2.2 Buffer(缓冲区)
定义:Buffer 是一个数据容器,本质上是一个数组,用于存储不同类型的数据。
Buffer 类型:
ByteBuffer // 最常用
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
核心属性:
capacity // 容量:Buffer的最大数据容量
position // 位置:下一个要读/写的元素索引
limit // 限制:第一个不应该读/写的元素索引
mark // 标记:用于记录当前position的位置
属性关系:
0 <= mark <= position <= limit <= capacity
核心方法:
// 创建Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024); // 堆缓冲
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024); // 直接缓冲
// 写入数据
buffer.put((byte) 1);
buffer.put("Hello".getBytes());
// 切换到读模式
buffer.flip(); // limit = position; position = 0;
// 读取数据
byte b = buffer.get();
buffer.get(bytes);
// 清空Buffer
buffer.clear(); // position = 0; limit = capacity;
// 压缩Buffer(保留未读数据)
buffer.compact();
// 重置position到mark
buffer.mark();
buffer.reset();
// 倒带(重新读取)
buffer.rewind(); // position = 0;
Buffer 使用流程:
// 1. 创建Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 2. 写入数据到Buffer
channel.read(buffer);
// 或
buffer.put("Hello".getBytes());
// 3. 切换到读模式
buffer.flip();
// 4. 从Buffer读取数据
while (buffer.hasRemaining()) {
byte b = buffer.get();
System.out.print((char) b);
}
// 5. 清空Buffer
buffer.clear();
示例代码:
public class BufferDemo {
public static void main(String[] args) {
// 创建Buffer
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println("初始状态: " + bufferStatus(buffer));
// 写入数据
buffer.put("Hello".getBytes());
System.out.println("写入后: " + bufferStatus(buffer));
// 切换到读模式
buffer.flip();
System.out.println("flip后: " + bufferStatus(buffer));
// 读取数据
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
System.out.println();
System.out.println("读取后: " + bufferStatus(buffer));
// 清空Buffer
buffer.clear();
System.out.println("clear后: " + bufferStatus(buffer));
}
private static String bufferStatus(ByteBuffer buffer) {
return String.format("position=%d, limit=%d, capacity=%d",
buffer.position(), buffer.limit(), buffer.capacity());
}
}
输出:
初始状态: position=0, limit=10, capacity=10
写入后: position=5, limit=10, capacity=10
flip后: position=0, limit=5, capacity=10
Hello
读取后: position=5, limit=5, capacity=10
clear后: position=0, limit=10, capacity=10
2.2.3 Selector(选择器)
定义:Selector 是 NIO 的核心组件,用于监听多个 Channel 的事件(如连接、读、写)。
核心概念:
- 一个 Selector 可以监听多个 Channel
- 一个线程使用一个 Selector 就可以处理多个 Channel
- 避免了多线程的上下文切换开销
事件类型:
SelectionKey.OP_ACCEPT // 接受连接事件(ServerSocketChannel)
SelectionKey.OP_CONNECT // 连接就绪事件(SocketChannel)
SelectionKey.OP_READ // 读就绪事件
SelectionKey.OP_WRITE // 写就绪事件
使用流程:
// 1. 创建Selector
Selector selector = Selector.open();
// 2. 创建Channel并设置为非阻塞
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(8080));
// 3. 注册Channel到Selector
SelectionKey key = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 4. 轮询事件
while (true) {
// 阻塞直到有事件发生
int readyChannels = selector.select();
if (readyChannels == 0) {
continue;
}
// 5. 获取就绪的事件
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey selectionKey = keyIterator.next();
// 6. 处理事件
if (selectionKey.isAcceptable()) {
// 处理连接事件
} else if (selectionKey.isReadable()) {
// 处理读事件
} else if (selectionKey.isWritable()) {
// 处理写事件
}
// 7. 移除已处理的事件
keyIterator.remove();
}
}
完整示例:
public class SelectorDemo {
public static void main(String[] args) throws IOException {
// 创建Selector
Selector selector = Selector.open();
// 创建ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
// 注册ACCEPT事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器启动,监听端口: 8080");
while (true) {
// 阻塞等待事件
selector.select();
// 获取就绪事件
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
keys.remove();
if (key.isAcceptable()) {
// 处理连接事件
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
// 注册READ事件
client.register(selector, SelectionKey.OP_READ);
System.out.println("客户端连接: " + client.getRemoteAddress());
} else if (key.isReadable()) {
// 处理读事件
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = client.read(buffer);
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("接收到: " + new String(data));
// 回显数据
buffer.rewind();
client.write(buffer);
} else if (bytesRead < 0) {
// 客户端断开连接
System.out.println("客户端断开: " + client.getRemoteAddress());
key.cancel();
client.close();
}
}
}
}
}
}
2.3 传统 Socket 编程的痛点
2.3.1 BIO 的问题
1. 资源消耗大
// 每个连接一个线程
while (true) {
Socket socket = serverSocket.accept();
new Thread(() -> {
// 处理连接...
}).start();
}
问题:
- 1000个连接 = 1000个线程
- 每个线程占用 1MB 栈空间
- 大量线程导致频繁的上下文切换
2. 阻塞导致资源浪费
// read()会阻塞
int len = inputStream.read(buffer);
问题:
- 线程大部分时间在等待
- CPU 利用率低
- 无法充分利用系统资源
3. 扩展性差
// C10K问题:无法支持10000个并发连接
// 原因:线程数量限制
2.3.2 NIO 的问题
1. 编程复杂
// 需要手动管理Buffer
buffer.flip();
buffer.clear();
buffer.compact();
// 需要处理半包、粘包
// 需要自己实现协议解析
2. Selector 空轮询 Bug
// JDK的epoll空轮询Bug
// 导致CPU 100%占用
selector.select(); // 可能立即返回,而不是阻塞
3. 需要处理各种边界情况
// 连接断开
// 网络异常
// 半包、粘包
// 内存泄漏
2.3.3 代码对比
传统 BIO 服务器:
public class BIOServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("服务器启动...");
while (true) {
// 阻塞等待连接
Socket socket = serverSocket.accept();
// 每个连接创建一个线程
new Thread(() -> {
try {
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
byte[] buffer = new byte[1024];
int len;
// 阻塞读取数据
while ((len = in.read(buffer)) != -1) {
String msg = new String(buffer, 0, len);
System.out.println("收到: " + msg);
// 回显
out.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
问题:
- ❌ 每个连接一个线程
- ❌ 线程阻塞等待数据
- ❌ 无法支持大量并发连接
传统 NIO 服务器:
public class NIOServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器启动...");
while (true) {
selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
keys.remove();
try {
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = client.read(buffer);
if (len > 0) {
buffer.flip();
// 需要处理半包、粘包问题
// 需要手动管理Buffer
client.write(buffer);
} else if (len < 0) {
key.cancel();
client.close();
}
}
} catch (IOException e) {
key.cancel();
key.channel().close();
}
}
}
}
}
问题:
- ❌ 代码复杂,难以维护
- ❌ 需要处理各种边界情况
- ❌ 需要手动处理半包、粘包
- ❌ 容易出现内存泄漏
2.4 Netty 如何解决这些问题
2.4.1 简化 NIO 编程
Netty 服务器:
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new EchoHandler());
}
});
ChannelFuture future = bootstrap.bind(8080).sync();
System.out.println("服务器启动...");
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
class EchoHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// Netty自动处理Buffer
ctx.write(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
}
优势:
- ✅ 代码简洁,易于理解
- ✅ 自动管理 Buffer
- ✅ 内置线程模型
- ✅ 异常处理完善
2.4.2 Netty 的核心解决方案
1. 优雅的线程模型
Boss线程组(1个线程)
↓ 接收连接
Worker线程组(多个线程)
↓ 处理I/O
业务线程池(可选)
↓ 处理业务逻辑
2. 强大的 ByteBuf
// 自动扩容
ByteBuf buf = Unpooled.buffer();
buf.writeBytes("Hello".getBytes());
buf.writeBytes(" World".getBytes());
// 引用计数,自动释放
buf.release();
// 零拷贝
CompositeByteBuf composite = Unpooled.compositeBuffer();
composite.addComponents(buf1, buf2);
3. 丰富的编解码器
// 解决粘包/拆包
pipeline.addLast(new LineBasedFrameDecoder(1024));
pipeline.addLast(new StringDecoder());
// 自定义协议
pipeline.addLast(new LengthFieldBasedFrameDecoder(...));
4. 解决 Selector 空轮询 Bug
// Netty内部检测并重建Selector
if (空轮询次数 > 512) {
重建Selector;
迁移所有Channel;
}
5. 内存池技术
// 池化ByteBuf,减少GC
PooledByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT;
ByteBuf buf = allocator.buffer();
2.4.3 对比总结
| 特性 | 传统NIO | Netty |
|---|---|---|
| 编程复杂度 | 高 | 低 |
| Buffer管理 | 手动 | 自动 |
| 粘包/拆包 | 手动处理 | 内置解码器 |
| 线程模型 | 需要自己实现 | 内置Reactor模型 |
| 异常处理 | 容易遗漏 | 完善的异常处理 |
| 性能优化 | 需要深入优化 | 已经高度优化 |
| 内存泄漏 | 容易发生 | 引用计数机制 |
| 扩展性 | 一般 | 优秀(Pipeline) |
2.5 项目代码:传统 NIO vs Netty 对比示例
本章配套代码包含:
- BIO 服务器:传统阻塞式服务器
- NIO 服务器:基于 Selector 的服务器
- Netty 服务器:使用 Netty 实现的服务器
- 性能测试:对比三种实现的性能
详细代码请参考:项目代码/chapter02-nio-basics
2.6 本章小结
本章我们学习了:
✅ BIO、NIO、AIO 三种 I/O 模型
✅ Java NIO 核心组件:Channel、Buffer、Selector
✅ 传统 Socket 编程的痛点
✅ Netty 如何优雅地解决这些问题
关键要点
- BIO:一个连接一个线程,适合连接数少的场景
- NIO:一个线程处理多个连接,适合高并发场景
- Netty 基于 NIO,但大大简化了编程复杂度
- Netty 提供了完善的解决方案:线程模型、ByteBuf、编解码器等
下一章预告
下一章我们将深入学习 Netty 核心组件,包括:
- Channel 详解
- EventLoop 详解
- ChannelFuture 详解
- ChannelHandler 详解
- ChannelPipeline 详解
练习题
- 基础题:编写一个 NIO 服务器,支持多客户端连接,并回显客户端消息
- 进阶题:对比 BIO、NIO、Netty 三种实现,测试在 1000 个并发连接下的性能
- 挑战题:使用原生 NIO 实现一个简单的 HTTP 服务器
上一章:第1章:Netty简介与环境搭建
下一章:第3章:Netty核心组件