SpringBoot 集成 Netty
当 SpringBoot 自带的 HTTP 服务满足不了长连接和高并发的需求时,Netty 是一个值得考虑的选择。本文记录了将 Netty 集成到 SpringBoot 项目中的完整过程,包括服务端搭建、客户端通信、心跳机制和自定义协议。
为什么需要 Netty?
做后端开发,迟早会遇到一个问题:SpringBoot 自带的 Tomcat 在处理 HTTP 请求时表现不错,但当你需要长连接、自定义协议、或者极致的网络性能时,它就显得力不从心了。这时候,Netty 就该登场了。
Netty 是一个基于 NIO 的异步非阻塞网络框架,简单来说,它能让你用很少的线程处理大量的并发连接。它的核心优势在于:
- 基于 NIO 的异步非阻塞 I/O 模型,天然适合高并发场景
- 支持
HTTP、WebSocket等多种协议,扩展性极强 - 提供了清晰的编程模型和
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 | 处理入站/出站事件的业务逻辑 | 具体办事员 |
ChannelPipeline | Handler 的有序链表,数据流经每个 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-allMaven 依赖 - 在
application.yml中配置 Netty 服务器端口和地址 - 实现
NettyServer启动类,使用@PostConstruct/@PreDestroy管理生命周期 - 实现
NettyServerHandler处理器,处理连接、断开、消息读取等事件 - 实现客户端
NettyClient和NettyClientHandler - 添加
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
-
心跳间隔的选择取决于具体业务场景。移动端网络不稳定,建议缩短心跳间隔(5–10 秒);内网环境相对稳定,可适当放宽(15–30 秒)。服务端读超时通常设为客户端心跳间隔的 2–3 倍,以容忍偶尔的网络抖动。参考 Netty IdleStateHandler 文档。 ↩