返回

SpringBoot 集成 Netty

当 SpringBoot 自带的 HTTP 服务满足不了长连接和高并发的需求时,Netty 是一个值得考虑的选择。本文记录了将 Netty 集成到 SpringBoot 项目中的完整过程,包括服务端搭建、客户端通信、心跳机制和自定义协议。

为什么需要 Netty?

做后端开发,迟早会遇到一个问题:SpringBoot 自带的 Tomcat 在处理 HTTP 请求时表现不错,但当你需要长连接、自定义协议、或者极致的网络性能时,它就显得力不从心了。这时候,Netty 就该登场了。

Netty 是一个基于 NIO 的异步非阻塞网络框架,简单来说,它能让你用很少的线程处理大量的并发连接。它的核心优势在于:

  • 基于 NIO 的异步非阻塞 I/O 模型,天然适合高并发场景
  • 支持 HTTPWebSocket 等多种协议,扩展性极强
  • 提供了清晰的编程模型和 API,上手门槛比直接用 NIO 低得多

那么问题来了——如何把 Netty 优雅地集成到 SpringBoot 项目里,让两者各司其职?接下来我们一步步来实现。

graph TB
    subgraph SpringBoot
        A[HTTP 请求 :8080] --> B[Tomcat]
        B --> C[Controller / Service]
    end
    subgraph Netty
        D[TCP 连接 :8090] --> E[BossGroup\n接收连接]
        E --> F[WorkerGroup\n处理 I/O]
        F --> G[ChannelHandler\n业务逻辑]
    end
    C -.-> G
    style SpringBoot fill:transparent,stroke:#d97757
    style Netty fill:transparent,stroke:#3b82f6

Netty 核心组件速览

在开始写代码之前,先快速了解 Netty 的核心概念,后面的代码会反复用到这些组件:

组件职责类比
Channel网络连接的抽象,代表一个打开的连接电话线路
EventLoopGroup线程池,处理 I/O 事件接线员团队
BossGroup专门负责接收新连接前台接待
WorkerGroup负责已建立连接的读写操作后台处理人员
ChannelHandler处理入站/出站事件的业务逻辑具体办事员
ChannelPipelineHandler 的有序链表,数据流经每个 Handler流水线
Bootstrap / ServerBootstrap启动辅助类,简化配置启动器

第一步:添加依赖

先把基础的依赖加上,这没什么好说的:

<!-- Netty依赖 -->
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.76.Final</version>
</dependency>

<!-- SpringBoot依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

第二步:配置 Netty 服务器参数

把端口和地址抽到配置文件里,方便不同环境切换。在 application.yml 中添加:

netty:
  server:
    port: 8090 # Netty服务器端口
    host: 127.0.0.1 # Netty服务器地址

注意这里 Netty 监听的是 8090 端口,和 SpringBoot 的 8080 互不冲突。两个服务各跑各的,互不干扰。

第三步:搭建服务端

服务端是整个架构的核心。Netty 的服务端由两部分组成:启动类负责配置和启动,处理器负责处理具体的业务逻辑。

服务器启动类

这里有个关键设计:用 @PostConstruct 让 Netty 服务器随 SpringBoot 一起启动,用 @PreDestroy 确保优雅关闭。bossGroup 负责接收连接,workerGroup 负责处理 I/O 事件——这是 Netty 经典的 Reactor 模式。

NettyServer.java
@Component
public class NettyServer {

    private static final Logger logger = LoggerFactory.getLogger(NettyServer.class);

    @Value("${netty.server.port}")
    private int port;

    private ServerBootstrap serverBootstrap;
    private EventLoopGroup bossGroup;
    private EventLoopGroup workerGroup;
    private ChannelFuture channelFuture;

    @PostConstruct
    public void start() throws InterruptedException {
        bossGroup = new NioEventLoopGroup(1);
        workerGroup = new NioEventLoopGroup();

        try {
            serverBootstrap = new ServerBootstrap();
            serverBootstrap.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) {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast(new StringDecoder());
                        pipeline.addLast(new StringEncoder());
                        pipeline.addLast(new NettyServerHandler());
                    }
                });

            channelFuture = serverBootstrap.bind(port).sync();
            logger.info("Netty服务器启动成功,监听端口: {}", port);
        } catch (Exception e) {
            logger.error("Netty服务器启动异常", e);
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    @PreDestroy
    public void destroy() {
        logger.info("Netty服务器关闭");
        if (channelFuture != null) {
            channelFuture.channel().close();
        }
        if (bossGroup != null) {
            bossGroup.shutdownGracefully();
        }
        if (workerGroup != null) {
            workerGroup.shutdownGracefully();
        }
    }
}

服务器处理器

处理器是真正干活的地方。ChannelGroup 用来管理所有连接的客户端,方便后续做广播推送之类的功能。每个生命周期回调都有明确的职责:连接时加入分组,断开时移除,收到消息时处理业务逻辑。

NettyServerHandler.java
public class NettyServerHandler extends SimpleChannelInboundHandler<String> {

    private static final Logger logger = LoggerFactory.getLogger(NettyServerHandler.class);

    // 管理所有连接的客户端,支持广播
    private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        Channel channel = ctx.channel();
        logger.info("客户端连接成功:{}", channel.remoteAddress());
        channelGroup.add(channel);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        Channel channel = ctx.channel();
        logger.info("客户端断开连接:{}", channel.remoteAddress());
        channelGroup.remove(channel);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        Channel channel = ctx.channel();
        logger.info("收到客户端{}消息: {}", channel.remoteAddress(), msg);
        channel.writeAndFlush("服务器已收到消息: " + msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        logger.error("Netty服务器异常", cause);
        ctx.close();
    }
}

第四步:搭建客户端

有了服务端,自然需要客户端来验证通信。客户端的结构和服务端类似,只不过用的是 Bootstrap 而不是 ServerBootstrap,并且它主动发起连接而不是监听端口。

NettyClient.java
@Component
public class NettyClient {

    private static final Logger logger = LoggerFactory.getLogger(NettyClient.class);

    @Value("${netty.server.host}")
    private String host;

    @Value("${netty.server.port}")
    private int port;

    private Bootstrap bootstrap;
    private EventLoopGroup group;
    private Channel channel;

    @PostConstruct
    public void start() {
        group = new NioEventLoopGroup();

        try {
            bootstrap = new Bootstrap();
            bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast(new StringDecoder());
                        pipeline.addLast(new StringEncoder());
                        pipeline.addLast(new NettyClientHandler());
                    }
                });

            ChannelFuture future = bootstrap.connect(host, port).sync();
            channel = future.channel();
            logger.info("Netty客户端启动成功,连接到服务器: {}:{}", host, port);
        } catch (Exception e) {
            logger.error("Netty客户端启动异常", e);
            group.shutdownGracefully();
        }
    }

    public void sendMessage(String message) {
        if (channel != null && channel.isActive()) {
            channel.writeAndFlush(message);
        }
    }

    @PreDestroy
    public void destroy() {
        logger.info("Netty客户端关闭");
        if (channel != null) {
            channel.close();
        }
        if (group != null) {
            group.shutdownGracefully();
        }
    }
}
NettyClientHandler.java
public class NettyClientHandler extends SimpleChannelInboundHandler<String> {

    private static final Logger logger = LoggerFactory.getLogger(NettyClientHandler.class);

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        logger.info("连接到服务器成功");
        ctx.writeAndFlush("Hello, Netty Server!");
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        logger.info("收到服务器消息: {}", msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        logger.error("Netty客户端异常", cause);
        ctx.close();
    }
}

进阶:心跳机制

基础通信跑通之后,下一个必须解决的问题就是心跳检测。在长连接场景下,如果不做心跳,你根本不知道对面是断了还是只是没说话。Netty 内置了 IdleStateHandler,省去了自己写定时器的麻烦。

IdleStateHandler 参数说明

IdleStateHandler 的构造函数接收三个时间参数,分别对应不同的空闲检测策略:

参数含义适用场景
readerIdleTime读空闲超时时间,超过该时间没有收到数据则触发事件服务端检测客户端是否存活
writerIdleTime写空闲超时时间,超过该时间没有发送数据则触发事件客户端定期向服务端发送心跳
allIdleTime读写都空闲的超时时间通用场景,连接完全无活动时触发

[!TIP] 心跳间隔的选择需要权衡。太短(如 1 秒)会产生大量无意义的网络流量;太长(如 60 秒)则无法及时发现断连。一般建议:客户端心跳间隔设为 5–15 秒,服务端读超时设为客户端心跳间隔的 2–3 倍(给网络波动留余量)。

关于心跳间隔的详细选择建议,参见文末脚注1

ChannelInitializer 中添加心跳检测:

// 5秒没有读写操作就触发事件
pipeline.addLast(new IdleStateHandler(0, 0, 5, TimeUnit.SECONDS));
pipeline.addLast(new HeartbeatHandler());

然后实现一个简单的心跳处理器,在空闲超时时发送心跳包:

public class HeartbeatHandler extends ChannelInboundHandlerAdapter {

    private static final Logger logger = LoggerFactory.getLogger(HeartbeatHandler.class);

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent event) {
            if (event.state() == IdleState.ALL_IDLE) {
                logger.info("发送心跳包");
                ctx.writeAndFlush("HEARTBEAT")
                   .addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }
}

[!WARNING] CLOSE_ON_FAILURE 这个监听器很重要——如果心跳包发送失败(说明连接已经断了),会自动关闭 Channel,避免资源泄漏。在生产环境中,还应该配合重连机制使用,否则连接断了就再也连不上了。

进阶:自定义协议

用字符串直接传输在 demo 阶段没问题,但真实项目里你一定需要自定义协议。原因很简单:你需要处理粘包拆包问题,需要传输结构化数据,需要区分不同类型的消息。

下面是一个基于「长度前缀 + JSON 消息体」的简单协议实现。协议设计的更多思路可参考 Netty 官方文档中的 Codec 章节

graph LR
    A[Message 对象] --> B[MessageEncoder]
    B --> C["[4字节长度][JSON字节流]"]
    C --> D[TCP 传输]
    D --> E["[4字节长度][JSON字节流]"]
    E --> F[MessageDecoder]
    F --> G[Message 对象]

消息实体

@Data
public class Message {
    private String id;
    private String type;
    private String content;
    private Date createTime;
}

这里的 Message 就是在客户端和服务端之间传输的 DTO

编码器

编码器的职责很明确:把 Java 对象序列化成字节流。这里先写入 4 字节的长度字段,再写入 JSON 内容,接收端就能准确知道一条消息在哪里结束。

public class MessageEncoder extends MessageToByteEncoder<Message> {

    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
        String json = new ObjectMapper().writeValueAsString(msg);
        byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
        out.writeInt(bytes.length);
        out.writeBytes(bytes);
    }
}

解码器

解码器是编码器的逆过程,但要多考虑一件事:TCP 是流式协议,数据可能分多次到达。所以需要先检查可读字节数是否足够,不够就等下一次数据到来再处理。

public class MessageDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (in.readableBytes() < 4) {
            return; // 长度字段不完整,等待更多数据
        }

        in.markReaderIndex();
        int length = in.readInt();

        if (in.readableBytes() < length) {
            in.resetReaderIndex(); // 消息体不完整,重置读指针
            return;
        }

        byte[] bytes = new byte[length];
        in.readBytes(bytes);
        String json = new String(bytes, StandardCharsets.UTF_8);
        Message message = new ObjectMapper().readValue(json, Message.class);
        out.add(message);
    }
}

[!NOTE] 这里的 markReaderIndex()resetReaderIndex() 是处理半包问题的关键。如果消息体还没完全到达,我们需要把读指针退回到长度字段之前,等下次数据到齐了再统一处理。这是 Netty 解码器的标准写法。

运行效果

一切就绪后,启动项目,你会依次看到这样的日志输出:

# 服务器启动
Netty服务器启动成功,监听端口: 8090

# 客户端连接
客户端连接成功:/127.0.0.1:54321

# 消息交互
收到客户端/127.0.0.1:54321消息: Hello, Netty Server!
收到服务器消息: 服务器已收到消息: Hello, Netty Server!

# 心跳检测(5秒无活动后)
发送心跳包

Netty 集成步骤清单

将 Netty 集成到 SpringBoot 项目的关键步骤:

  • 添加 netty-all Maven 依赖
  • application.yml 中配置 Netty 服务器端口和地址
  • 实现 NettyServer 启动类,使用 @PostConstruct / @PreDestroy 管理生命周期
  • 实现 NettyServerHandler 处理器,处理连接、断开、消息读取等事件
  • 实现客户端 NettyClientNettyClientHandler
  • 添加 IdleStateHandler 心跳检测机制
  • 实现自定义协议的编解码器(MessageEncoder / MessageDecoder
  • 添加断线重连机制
  • 生产环境压测,确认线程池大小和连接数配置合理

常见问题排查

[!WARNING] 端口冲突: 如果启动时报 Address already in use,检查 8090 端口是否被占用。可以用 lsof -i :8090 排查,或者在配置文件中换一个端口。

[!WARNING] 连接被拒绝: 客户端报 Connection refused,通常是服务端还没启动完成客户端就尝试连接了。可以在客户端的 @PostConstruct 中加一个短暂的延迟,或者使用 @DependsOn 注解控制 Bean 的初始化顺序。

[!WARNING] 内存泄漏: 如果 ChannelGroup 中的连接数持续增长但不减少,说明断开连接时没有正确移除。检查 channelInactive 是否被正确调用,以及是否有异常导致该方法被跳过。

回顾

整个集成过程走下来,核心思路其实很清晰:让 SpringBoot 管理 Netty 的生命周期,让 Netty 专注于网络通信。@PostConstruct@PreDestroy 是衔接两者的关键。

在实际项目中,你可能还需要考虑重连机制、消息确认、序列化性能优化等问题,但本文的基础架构已经足够作为起点。掌握了这套模式,后续不管是做 IM 系统、物联网网关还是游戏服务器,都能快速搭起骨架来。更多 Netty 特性和最佳实践可参考 Netty 官方文档GitHub 仓库

Footnotes

  1. 心跳间隔的选择取决于具体业务场景。移动端网络不稳定,建议缩短心跳间隔(5–10 秒);内网环境相对稳定,可适当放宽(15–30 秒)。服务端读超时通常设为客户端心跳间隔的 2–3 倍,以容忍偶尔的网络抖动。参考 Netty IdleStateHandler 文档