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 → 继续轮询

核心组件

  1. Channel(通道):数据传输的管道
  2. Buffer(缓冲区):数据的容器
  3. 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 三种模型对比总结

特性BIONIOAIO
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),但有以下区别:

StreamChannel
单向(输入流或输出流)双向(可读可写)
阻塞可以非阻塞
直接读写数据通过 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 对比总结

特性传统NIONetty
编程复杂度
Buffer管理手动自动
粘包/拆包手动处理内置解码器
线程模型需要自己实现内置Reactor模型
异常处理容易遗漏完善的异常处理
性能优化需要深入优化已经高度优化
内存泄漏容易发生引用计数机制
扩展性一般优秀(Pipeline)

2.5 项目代码:传统 NIO vs Netty 对比示例

本章配套代码包含:

  1. BIO 服务器:传统阻塞式服务器
  2. NIO 服务器:基于 Selector 的服务器
  3. Netty 服务器:使用 Netty 实现的服务器
  4. 性能测试:对比三种实现的性能

详细代码请参考:项目代码/chapter02-nio-basics


2.6 本章小结

本章我们学习了:

BIO、NIO、AIO 三种 I/O 模型
Java NIO 核心组件:Channel、Buffer、Selector
传统 Socket 编程的痛点
Netty 如何优雅地解决这些问题

关键要点

  1. BIO:一个连接一个线程,适合连接数少的场景
  2. NIO:一个线程处理多个连接,适合高并发场景
  3. Netty 基于 NIO,但大大简化了编程复杂度
  4. Netty 提供了完善的解决方案:线程模型、ByteBuf、编解码器等

下一章预告

下一章我们将深入学习 Netty 核心组件,包括:

  • Channel 详解
  • EventLoop 详解
  • ChannelFuture 详解
  • ChannelHandler 详解
  • ChannelPipeline 详解

练习题

  1. 基础题:编写一个 NIO 服务器,支持多客户端连接,并回显客户端消息
  2. 进阶题:对比 BIO、NIO、Netty 三种实现,测试在 1000 个并发连接下的性能
  3. 挑战题:使用原生 NIO 实现一个简单的 HTTP 服务器

上一章第1章:Netty简介与环境搭建
下一章第3章:Netty核心组件