博客 / 詳情

返回

藍易雲cdn:Springboot整合Netty,自定義協議實現

一、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 友好)

flowchart LR
A[客户端] --> B[Encoder: Msg->ByteBuf]
B --> C[TCP 傳輸]
C --> D[LengthFieldBasedFrameDecoder 拆包]
D --> E[Decoder: ByteBuf->Msg]
E --> F[BizHandler 業務分發]
F --> G[Encoder 響應]
G --> A

如果你準備把它用於“邊緣節點到控制面的長連接”,下一步建議把 <span style="color:red;">鑑權</span>、<span style="color:red;">心跳</span>、<span style="color:red;">限流</span>、<span style="color:red;">黑白名單</span> 做成 Pipeline 可插拔策略,否則線上會被各種異常連接教做人。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.