一、Spring Boot + Netty 的定位:用 <span style="color:red;">自定義協議</span>把“長連接能力”產品化 🚀
在業務側(例如邊緣節點控制通道、內網 RPC、設備網關、推送/回源協商),HTTP 往往不是最優解。Netty 適合把網絡層能力做成“可治理”的服務:<span style="color:red;">高併發</span>、<span style="color:red;">低延遲</span>、<span style="color:red;">長連接</span>、<span style="color:red;">可觀測</span>、<span style="color:red;">可演進</span>。
版本建議:Spring Boot 當前主線已進入 4.0.x(Java 17 基線)。(Home) Netty 官方下載頁在 2025-12-15 標註 4.2.9.Final 為 Stable/Recommended。(Netty)
二、協議先定“邊界”:用長度字段解決 <span style="color:red;">粘包/拆包</span> 🔧
協議幀結構(Header 16B + Body)
| 字段 | 長度 | 作用 | 治理價值 |
| --------- | -: | --------------------- | --------- |
| magic | 2 | 魔數(如 0xCAFE) | 過濾亂入流量/誤連 |
| version | 1 | 協議版本 | 平滑升級 |
| type | 1 | 消息類型(請求/響應/心跳) | 統一路由 |
| requestId | 8 | 請求標識 | 追蹤、冪等、對賬 |
| bodyLen | 4 | Body 字節長度 | 拆包核心 |
| body | N | 業務數據(JSON/Protobuf 等) | 可替換 |
長度計算公式: frameLen = 16 + bodyLen
這也是 LengthFieldBasedFrameDecoder 能穩定拆包的前提。
三、核心代碼:拆包 + 編解碼 + Pipeline(最小可用) ✅
1)Maven 依賴(示例)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.2.9.Final</version>
</dependency>
</dependencies>
解釋(務實版):
spring-boot-starter:提供容器、配置、生命週期管理,讓 Netty 變成“服務組件”。netty-all:快速集成,適合教程/PoC;正式環境更建議按模塊引入並統一版本治理,避免依賴衝突(這點就像 CDN 節點的組件版本漂移會引發“玄學故障”一樣)。
2)消息模型(協議承載體)
public record Msg(short magic, byte version, byte type, long requestId, byte[] body) {}
解釋:
record讓消息對象不可變,減少併發場景下的狀態污染。type/requestId是後續做“鏈路追蹤、超時控制、重試冪等”的基礎資產。
3)Decoder / Encoder(把 ByteBuf 變成業務消息)
public final class MsgDecoder extends io.netty.handler.codec.ByteToMessageDecoder {
@Override protected void decode(io.netty.channel.ChannelHandlerContext ctx,
io.netty.buffer.ByteBuf in,
java.util.List<Object> out) {
short magic = in.readShort();
byte version = in.readByte();
byte type = in.readByte();
long requestId = in.readLong();
int bodyLen = in.readInt();
byte[] body = new byte[bodyLen];
in.readBytes(body);
out.add(new Msg(magic, version, type, requestId, body));
}
}
public final class MsgEncoder extends io.netty.handler.codec.MessageToByteEncoder<Msg> {
@Override protected void encode(io.netty.channel.ChannelHandlerContext ctx, Msg msg,
io.netty.buffer.ByteBuf out) {
out.writeShort(msg.magic());
out.writeByte(msg.version());
out.writeByte(msg.type());
out.writeLong(msg.requestId());
out.writeInt(msg.body().length);
out.writeBytes(msg.body());
}
}
解釋(抓重點):
- Decoder/Encoder 只做“協議層”轉換,不做業務解析,這是 <span style="color:red;">分層治理</span>。
bodyLen決定讀取多少字節,避免讀多/讀少導致的錯位。- 這裏假設上游已經完成拆包(下一段會做)。
4)Netty Pipeline(拆包器是關鍵)
public final class ServerInit extends io.netty.channel.ChannelInitializer<io.netty.channel.socket.SocketChannel> {
@Override protected void initChannel(io.netty.channel.socket.SocketChannel ch) {
var p = ch.pipeline();
// maxFrame=1MB;lengthFieldOffset=12(magic2+ver1+type1+requestId8)
p.addLast(new io.netty.handler.codec.LengthFieldBasedFrameDecoder(
1024 * 1024, 12, 4, 0, 0));
p.addLast(new io.netty.handler.timeout.IdleStateHandler(60, 0, 0));
p.addLast(new MsgDecoder());
p.addLast(new MsgEncoder());
p.addLast(new BizHandler());
}
}
解釋:
LengthFieldBasedFrameDecoder解決 <span style="color:red;">粘包/半包</span>:只要長度字段可信,就能穩定切幀。12/4/0/0的含義:長度字段在第 12 字節開始、長度 4 字節,frameLen = length + 16(因為 lengthFieldEndOffset=16)。IdleStateHandler用於心跳與連接保活治理,避免“死連接佔坑”。🙂
四、Spring Boot 託管 Netty:自動啓動 + <span style="color:red;">優雅停機</span> 🧯
@org.springframework.stereotype.Component
public class NettyServer implements org.springframework.context.SmartLifecycle {
private io.netty.channel.Channel serverCh;
private io.netty.channel.EventLoopGroup boss, worker;
private volatile boolean running = false;
@org.springframework.beans.factory.annotation.Value("${netty.port:19090}")
private int port;
@Override public void start() {
boss = new io.netty.channel.nio.NioEventLoopGroup(1);
worker = new io.netty.channel.nio.NioEventLoopGroup();
try {
var b = new io.netty.bootstrap.ServerBootstrap()
.group(boss, worker)
.channel(io.netty.channel.socket.nio.NioServerSocketChannel.class)
.childHandler(new ServerInit());
serverCh = b.bind(port).syncUninterruptibly().channel();
running = true;
} catch (Exception e) {
stop(); // 啓動失敗也走同一套釋放邏輯
throw e;
}
}
@Override public void stop() {
running = false;
if (serverCh != null) serverCh.close().syncUninterruptibly();
if (worker != null) worker.shutdownGracefully().syncUninterruptibly();
if (boss != null) boss.shutdownGracefully().syncUninterruptibly();
}
@Override public boolean isRunning() { return running; }
@Override public boolean isAutoStartup() { return true; }
}
解釋:
SmartLifecycle讓 Netty 跟隨 Spring 容器啓動/停止,避免“應用停了端口還佔着”的尷尬。shutdownGracefully()是 <span style="color:red;">穩定性底線</span>:給 in-flight 請求收尾時間,減少連接重置與數據丟失。start()裏 try/catch 後stop():屬於“故障自愈式資源回收”,能顯著降低線上殘留線程與 FD 泄漏風險。
五、運行驗證(命令)與解釋 ✅
mvn -DskipTests package
java -jar target/app.jar
ss -lntp | grep 19090
解釋:
mvn -DskipTests package:先打包跑通鏈路,減少測試阻塞(後續再補協議/編解碼單測)。java -jar:以生產方式啓動,驗證生命週期託管是否生效。ss -lntp:確認端口監聽與進程綁定,定位“端口不通/啓動未生效”最快。
工作流程圖(vditor/Markdown 友好)
如果你準備把它用於“邊緣節點到控制面的長連接”,下一步建議把 <span style="color:red;">鑑權</span>、<span style="color:red;">心跳</span>、<span style="color:red;">限流</span>、<span style="color:red;">黑白名單</span> 做成 Pipeline 可插拔策略,否則線上會被各種異常連接教做人。