Netty教程 / 第 10 节

第1章:Netty 简介与环境搭建

1.1 什么是 Netty

1.1.1 Netty 的定义

Netty 是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能协议服务器和客户端。它是基于 Java NIO 的客户端/服务器框架,极大地简化了网络编程,如 TCP 和 UDP 套接字服务器的开发。

1.1.2 Netty 的核心特点

  1. 设计优雅

    • 统一的 API,支持多种传输类型(阻塞和非阻塞)
    • 简单而强大的线程模型
    • 真正的无连接数据报套接字支持
  2. 易于使用

    • 详细的 Javadoc 和大量示例
    • 不需要额外的依赖,JDK 5+ 即可
  3. 高性能

    • 吞吐量更高,延迟更低
    • 减少资源消耗
    • 最小化不必要的内存复制
  4. 安全

    • 完整的 SSL/TLS 和 StartTLS 支持
    • 可在 Applet 与 Android 的限制环境运行
  5. 社区活跃

    • 持续更新和维护
    • 大量成功的商业/开源项目使用

1.1.3 Netty 的发展历史

版本发布时间主要特性
Netty 3.x2008年首个稳定版本,奠定基础架构
Netty 4.x2013年重大重构,性能大幅提升,目前主流版本
Netty 5.x2015年(已废弃)实验性版本,后被官方放弃

注意:目前推荐使用 Netty 4.1.x 系列,这是最稳定和广泛使用的版本。


1.2 Netty 的应用场景

1.2.1 互联网行业

  1. RPC 框架

    • Dubbo:阿里巴巴的分布式服务框架
    • gRPC:Google 的高性能 RPC 框架
    • Apache Thrift
  2. 消息中间件

    • RocketMQ:阿里巴巴的消息中间件
    • Kafka:部分网络层使用 Netty
  3. 即时通讯

    • 微信、QQ 等 IM 系统
    • 实时聊天应用
    • 推送服务
  4. 游戏服务器

    • 网络游戏服务端
    • 实时对战游戏
    • 棋牌游戏平台

1.2.2 大数据领域

  1. Hadoop 生态

    • Apache Spark:使用 Netty 进行节点间通信
    • Apache Flink:网络层基于 Netty
    • Apache Storm
  2. 数据库

    • Cassandra:分布式数据库
    • HBase:使用 Netty 进行 RPC 通信

1.2.3 其他应用

  • API 网关:如 Spring Cloud Gateway
  • 代理服务器:HTTP/SOCKS 代理
  • 物联网:设备通信、数据采集
  • 流媒体:视频直播、音频传输

1.3 Netty 的核心优势

1.3.1 相比传统 Java NIO 的优势

特性Java NIONetty
API 复杂度复杂,需要深入理解简单易用,封装良好
线程模型需要自己实现内置 Reactor 线程模型
ByteBuffer功能有限,API 不友好ByteBuf 功能强大
粘包/拆包需要手动处理提供多种解码器
断线重连需要自己实现提供现成方案
内存泄漏容易出现引用计数机制
性能优化需要深入优化已经高度优化

1.3.2 技术优势详解

1. 零拷贝(Zero-Copy)

传统方式:
硬盘 → 内核缓冲区 → 用户缓冲区 → Socket缓冲区 → 网卡
       (拷贝1)      (拷贝2)      (拷贝3)

Netty零拷贝:
硬盘 → 内核缓冲区 → 网卡
       (拷贝1)

2. 内存池技术

  • 减少 GC 压力
  • 提高内存分配效率
  • 支持堆内和堆外内存

3. 高效的 Reactor 线程模型

Boss线程组(接收连接)
    ↓
Worker线程组(处理I/O)
    ↓
业务线程池(处理业务逻辑)

4. 无锁化串行设计

  • 每个 Channel 绑定到一个 EventLoop
  • 避免多线程竞争
  • 提高性能

1.4 开发环境准备

1.4.1 JDK 安装

推荐版本:JDK 8 / JDK 11 / JDK 17

验证安装:

java -version
javac -version

预期输出:

java version "1.8.0_301"
Java(TM) SE Runtime Environment (build 1.8.0_301-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.301-b09, mixed mode)

1.4.2 Maven 配置

1. 创建 Maven 项目

项目结构:

netty-demo/
├── pom.xml
└── src/
    ├── main/
    │   ├── java/
    │   └── resources/
    └── test/
        └── java/

2. pom.xml 配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.netty.tutorial</groupId>
    <artifactId>netty-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>Netty Tutorial Demo</name>
    <description>Netty从入门到精通示例代码</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <netty.version>4.1.100.Final</netty.version>
        <slf4j.version>1.7.36</slf4j.version>
        <logback.version>1.2.11</logback.version>
    </properties>

    <dependencies>
        <!-- Netty核心依赖 -->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>${netty.version}</version>
        </dependency>

        <!-- 日志依赖 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
        </dependency>

        <!-- 单元测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

1.4.3 IDE 配置(IntelliJ IDEA)

1. 导入项目

  • File → Open → 选择项目目录
  • 等待 Maven 依赖下载完成

2. 配置 JDK

  • File → Project Structure → Project
  • 设置 Project SDK 为 JDK 8+

3. 配置编码

  • File → Settings → Editor → File Encodings
  • 设置为 UTF-8

1.4.4 日志配置

创建 src/main/resources/logback.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>

    <!-- Netty日志级别 -->
    <logger name="io.netty" level="INFO"/>
</configuration>

1.5 第一个 Netty 程序(Hello World)

1.5.1 服务端实现

package com.netty.tutorial.hello;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Netty Hello World 服务端
 * 功能:接收客户端消息并回复
 */
public class HelloWorldServer {
    
    private static final Logger logger = LoggerFactory.getLogger(HelloWorldServer.class);
    
    private final int port;
    
    public HelloWorldServer(int port) {
        this.port = port;
    }
    
    public void start() throws InterruptedException {
        // 1. 创建两个线程组
        // bossGroup: 负责接收客户端连接
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // workerGroup: 负责处理已建立连接的I/O操作
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        
        try {
            // 2. 创建服务端启动引导类
            ServerBootstrap bootstrap = new ServerBootstrap();
            
            // 3. 配置启动参数
            bootstrap.group(bossGroup, workerGroup)
                    // 指定使用NIO传输Channel
                    .channel(NioServerSocketChannel.class)
                    // 设置服务端连接队列大小
                    .option(ChannelOption.SO_BACKLOG, 128)
                    // 设置保持连接
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    // 配置Channel初始化器
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            // 获取Pipeline
                            ChannelPipeline pipeline = ch.pipeline();
                            
                            // 添加字符串解码器
                            pipeline.addLast("decoder", new StringDecoder());
                            // 添加字符串编码器
                            pipeline.addLast("encoder", new StringEncoder());
                            // 添加业务处理器
                            pipeline.addLast("handler", new HelloWorldServerHandler());
                        }
                    });
            
            // 4. 绑定端口,开始接收连接
            ChannelFuture future = bootstrap.bind(port).sync();
            logger.info("Netty服务器启动成功,监听端口: {}", port);
            
            // 5. 等待服务器socket关闭
            future.channel().closeFuture().sync();
            
        } finally {
            // 6. 优雅关闭线程组
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
            logger.info("Netty服务器已关闭");
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        new HelloWorldServer(8080).start();
    }
}

服务端业务处理器:

package com.netty.tutorial.hello;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 服务端业务处理器
 */
public class HelloWorldServerHandler extends SimpleChannelInboundHandler<String> {
    
    private static final Logger logger = LoggerFactory.getLogger(HelloWorldServerHandler.class);
    
    /**
     * 连接建立时调用
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        logger.info("客户端连接成功: {}", ctx.channel().remoteAddress());
    }
    
    /**
     * 接收到消息时调用
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        logger.info("收到客户端消息: {}", msg);
        
        // 回复客户端
        String response = "服务器收到: " + msg;
        ctx.writeAndFlush(response);
        
        logger.info("回复客户端: {}", response);
    }
    
    /**
     * 连接断开时调用
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        logger.info("客户端断开连接: {}", ctx.channel().remoteAddress());
    }
    
    /**
     * 异常处理
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        logger.error("发生异常: ", cause);
        ctx.close();
    }
}

1.5.2 客户端实现

package com.netty.tutorial.hello;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Scanner;

/**
 * Netty Hello World 客户端
 * 功能:连接服务器并发送消息
 */
public class HelloWorldClient {
    
    private static final Logger logger = LoggerFactory.getLogger(HelloWorldClient.class);
    
    private final String host;
    private final int port;
    
    public HelloWorldClient(String host, int port) {
        this.host = host;
        this.port = port;
    }
    
    public void start() throws InterruptedException {
        // 1. 创建客户端线程组
        EventLoopGroup group = new NioEventLoopGroup();
        
        try {
            // 2. 创建客户端启动引导类
            Bootstrap bootstrap = new Bootstrap();
            
            // 3. 配置启动参数
            bootstrap.group(group)
                    // 指定使用NIO传输Channel
                    .channel(NioSocketChannel.class)
                    // 配置Channel初始化器
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ChannelPipeline pipeline = ch.pipeline();
                            
                            // 添加字符串解码器
                            pipeline.addLast("decoder", new StringDecoder());
                            // 添加字符串编码器
                            pipeline.addLast("encoder", new StringEncoder());
                            // 添加业务处理器
                            pipeline.addLast("handler", new HelloWorldClientHandler());
                        }
                    });
            
            // 4. 连接服务器
            ChannelFuture future = bootstrap.connect(host, port).sync();
            logger.info("连接服务器成功: {}:{}", host, port);
            
            Channel channel = future.channel();
            
            // 5. 发送消息
            Scanner scanner = new Scanner(System.in);
            logger.info("请输入消息(输入'quit'退出):");
            
            while (scanner.hasNextLine()) {
                String line = scanner.nextLine();
                if ("quit".equalsIgnoreCase(line)) {
                    break;
                }
                
                // 发送消息到服务器
                channel.writeAndFlush(line);
            }
            
            // 6. 关闭连接
            channel.close().sync();
            
        } finally {
            // 7. 优雅关闭线程组
            group.shutdownGracefully();
            logger.info("客户端已关闭");
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        new HelloWorldClient("localhost", 8080).start();
    }
}

客户端业务处理器:

package com.netty.tutorial.hello;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 客户端业务处理器
 */
public class HelloWorldClientHandler extends SimpleChannelInboundHandler<String> {
    
    private static final Logger logger = LoggerFactory.getLogger(HelloWorldClientHandler.class);
    
    /**
     * 连接建立时调用
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        logger.info("成功连接到服务器: {}", ctx.channel().remoteAddress());
    }
    
    /**
     * 接收到消息时调用
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        logger.info("收到服务器响应: {}", msg);
    }
    
    /**
     * 异常处理
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        logger.error("发生异常: ", cause);
        ctx.close();
    }
}

1.5.3 运行测试

1. 启动服务端

运行 HelloWorldServer.main()
控制台输出:
15:30:00.123 [main] INFO  HelloWorldServer - Netty服务器启动成功,监听端口: 8080

2. 启动客户端

运行 HelloWorldClient.main()
控制台输出:
15:30:05.456 [main] INFO  HelloWorldClient - 连接服务器成功: localhost:8080
15:30:05.457 [nioEventLoopGroup-2-1] INFO  HelloWorldClientHandler - 成功连接到服务器: localhost/127.0.0.1:8080
请输入消息(输入'quit'退出):

3. 发送消息

客户端输入:Hello Netty!

客户端输出:
15:30:10.789 [nioEventLoopGroup-2-1] INFO  HelloWorldClientHandler - 收到服务器响应: 服务器收到: Hello Netty!

服务端输出:
15:30:10.788 [nioEventLoopGroup-3-1] INFO  HelloWorldServerHandler - 收到客户端消息: Hello Netty!
15:30:10.789 [nioEventLoopGroup-3-1] INFO  HelloWorldServerHandler - 回复客户端: 服务器收到: Hello Netty!

1.6 项目代码:简单的 Echo 服务器

Echo 服务器是一个经典的网络编程示例,它会将接收到的数据原封不动地发送回客户端。

1.6.1 Echo 服务端

package com.netty.tutorial.echo;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Echo服务器 - 回显服务器
 * 将接收到的数据原封不动地发送回客户端
 */
public class EchoServer {
    
    private static final Logger logger = LoggerFactory.getLogger(EchoServer.class);
    
    private final int port;
    
    public EchoServer(int port) {
        this.port = port;
    }
    
    public void start() throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(new EchoServerHandler());
                        }
                    });
            
            ChannelFuture future = bootstrap.bind(port).sync();
            logger.info("Echo服务器启动成功,监听端口: {}", port);
            
            future.channel().closeFuture().sync();
            
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
            logger.info("Echo服务器已关闭");
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        new EchoServer(8888).start();
    }
}

/**
 * Echo服务器处理器
 */
class EchoServerHandler extends ChannelInboundHandlerAdapter {
    
    private static final Logger logger = LoggerFactory.getLogger(EchoServerHandler.class);
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf in = (ByteBuf) msg;
        
        // 读取接收到的数据
        String received = in.toString(CharsetUtil.UTF_8);
        logger.info("服务器接收到: {}", received);
        
        // 将数据原封不动地写回客户端
        ctx.write(in);
    }
    
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        // 将缓冲区的数据刷新到网络
        ctx.flush();
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        logger.error("发生异常: ", cause);
        ctx.close();
    }
}

1.6.2 Echo 客户端

package com.netty.tutorial.echo;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Echo客户端
 */
public class EchoClient {
    
    private static final Logger logger = LoggerFactory.getLogger(EchoClient.class);
    
    private final String host;
    private final int port;
    
    public EchoClient(String host, int port) {
        this.host = host;
        this.port = port;
    }
    
    public void start() throws InterruptedException {
        EventLoopGroup group = new NioEventLoopGroup();
        
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(new EchoClientHandler());
                        }
                    });
            
            ChannelFuture future = bootstrap.connect(host, port).sync();
            logger.info("Echo客户端连接成功: {}:{}", host, port);
            
            future.channel().closeFuture().sync();
            
        } finally {
            group.shutdownGracefully();
            logger.info("Echo客户端已关闭");
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        new EchoClient("localhost", 8888).start();
    }
}

/**
 * Echo客户端处理器
 */
class EchoClientHandler extends ChannelInboundHandlerAdapter {
    
    private static final Logger logger = LoggerFactory.getLogger(EchoClientHandler.class);
    
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        // 连接建立后,发送消息
        String message = "Hello, Echo Server! 你好,回显服务器!";
        ByteBuf buf = Unpooled.copiedBuffer(message, CharsetUtil.UTF_8);
        ctx.writeAndFlush(buf);
        logger.info("发送消息: {}", message);
    }
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf in = (ByteBuf) msg;
        String received = in.toString(CharsetUtil.UTF_8);
        logger.info("接收到回显: {}", received);
        
        // 释放ByteBuf
        in.release();
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        logger.error("发生异常: ", cause);
        ctx.close();
    }
}

1.7 代码解析

1.7.1 核心组件说明

组件说明
EventLoopGroup事件循环组,管理多个EventLoop
ServerBootstrap服务端启动引导类
Bootstrap客户端启动引导类
Channel网络通道,代表一个连接
ChannelHandler处理器,处理I/O事件
ChannelPipeline处理器链,管理多个Handler
ByteBuf字节缓冲区,Netty的数据容器

1.7.2 执行流程

服务端启动流程:

1. 创建EventLoopGroup(bossGroup和workerGroup)
2. 创建ServerBootstrap
3. 配置Channel类型和参数
4. 设置ChannelHandler
5. 绑定端口
6. 等待连接

客户端连接流程:

1. 创建EventLoopGroup
2. 创建Bootstrap
3. 配置Channel类型和参数
4. 设置ChannelHandler
5. 连接服务器
6. 发送/接收数据

1.8 本章小结

本章我们学习了:

✅ Netty 的基本概念:异步事件驱动的网络框架
✅ Netty 的应用场景:RPC、消息中间件、IM、游戏等
✅ Netty 的核心优势:高性能、易用、零拷贝、内存池
✅ 开发环境搭建:JDK、Maven、IDE配置
✅ 第一个程序:Hello World 示例
✅ Echo 服务器:经典的回显服务器实现

关键要点

  1. Netty 是基于 Java NIO 的高性能网络框架
  2. 使用 Reactor 线程模型,分离连接接收和I/O处理
  3. 通过 ChannelPipeline 实现责任链模式
  4. ByteBuf 是 Netty 的核心数据容器

下一章预告

下一章我们将深入学习网络编程基础,包括:

  • BIO、NIO、AIO 的区别
  • Java NIO 核心组件
  • 传统 Socket 编程的问题
  • Netty 如何优雅地解决这些问题

练习题

  1. 基础题:修改 Hello World 示例,让服务器在回复时加上时间戳
  2. 进阶题:实现一个简单的计算器服务器,客户端发送表达式(如"1+2"),服务器返回计算结果
  3. 挑战题:实现一个支持多客户端的聊天室,客户端发送的消息会广播给所有连接的客户端

提示:可以使用 ChannelGroup 来管理多个客户端连接。


下一章:第2章:网络编程基础