速查表
NMS Network & Netty Pipeline 速查表
NMS Network Reference
適用版本:Paper 1.21.11 / 26.2(Mojang 官方命名;26.1 起原版不再混淆)。兩版寫法不同處以 // @1.21.11: 行尾標註或 // @only <版本> 區塊區分
套件根:net.minecraft.network, io.netty.channel
封包攔截用法見
Skills/nms/nms-packet-interceptor/SKILL.md執行緒規則見Skills/_shared/nms-threading.md
Channel Pipeline 結構
Paper 26.x 玩家連線的 Netty pipeline(從網路 wire 到 ServerPlayer):
Network Wire (TCP)
│
▼
┌──────────────────┐
│ frame_decoder │ Varint 長度前綴拆幀(LengthFieldBasedFrameDecoder)
├──────────────────┤
│ prepender │ (Outbound) 寫入 Varint 長度前綴
├──────────────────┤
│ decompress │ Zlib 解壓縮(threshold 以上才啟用)
├──────────────────┤
│ compress │ (Outbound) Zlib 壓縮
├──────────────────┤
│ decrypt │ AES-CFB8 解密(登入後啟用)
├──────────────────┤
│ encrypt │ (Outbound) AES-CFB8 加密
├──────────────────┤
│ decoder │ 封包 ID + FriendlyByteBuf → Packet<?> 物件
├──────────────────┤
│ encoder │ (Outbound) Packet<?> 物件 → 位元組
├──────────────────┤
│ packet_handler │ ServerGamePacketListenerImpl(vanilla 處理點)
└──────────────────┘
│
▼
ServerPlayer(Bukkit 事件從這裡觸發)
注入位置建議
// 在 packet_handler 前注入,先於 vanilla 處理
channel.pipeline().addBefore("packet_handler", "my_handler", myHandler);
⚠️ 注意:
decoder之前注入的 handler 看到的是原始 byte,不是 Packet 物件。 若要讀取/修改 Packet 物件,務必加在decoder之後、packet_handler之前。
執行緒模型圖 / Threading Model
Netty Boss Group Netty Worker Group Bukkit Main Thread
(accept 連線) (IO 讀寫 / pipeline) (tick 迴圈)
│ │ │
│ channel accept │ │
├──────────────────►│ │
│ │ channelRead() │
│ │ (所有 handler 的 read)│
│ │ │
│ │ 需要存取 Entity/World │
│ │──────────────────────►│
│ │ Bukkit.getScheduler() │
│ │ .runTask() │
│ │ │
│ │ connection.send() │
│ │◄──────────────────────│
│ │ (排入 Netty write queue)
│ │ │
核心規則:
| 操作 | 執行緒 |
|---|---|
channelRead() handler |
Netty IO thread |
write() handler |
Netty IO thread |
connection.send(packet) |
Any(Netty 排入 write queue) |
Bukkit.broadcastMessage() |
Main thread only |
player.teleport() |
Main thread only |
Level.getBlockState() |
Main thread only |
Connection 類
net.minecraft.network.Connection
| 成員 | 類型 | 說明 |
|---|---|---|
channel |
Channel |
底層 Netty channel |
packetListener |
PacketListener |
當前協議階段的監聽器 |
address |
SocketAddress |
遠端 IP 位址 |
disconnected |
boolean |
連線是否已斷開 |
取得 Connection
import net.minecraft.network.Connection;
import net.minecraft.server.level.ServerPlayer;
import org.bukkit.craftbukkit.entity.CraftPlayer;
ServerPlayer nms = ((CraftPlayer) player).getHandle();
// nms.connection 是 ServerGamePacketListenerImpl
Connection connection = nms.connection.connection; // 底層 Connection 物件
Channel channel = connection.channel;
ChannelDuplexHandler 生命週期
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
public class MyHandler extends ChannelDuplexHandler {
/** 連線建立(通常在 pipeline 注入時已建立,不常用) */
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
}
/** Serverbound:從 wire 讀入的封包(客戶端 → 伺服器)
* 在 Netty IO thread 執行。
* 呼叫 super.channelRead() 讓封包繼續往下傳。
* 回傳 null 或不呼叫 super = 取消封包。 */
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof Packet<?> packet) {
// 處理邏輯
}
super.channelRead(ctx, msg); // 繼續傳遞
}
/** Clientbound:準備寫出到 wire 的封包(伺服器 → 客戶端)
* 在 Netty IO thread 執行。
* promise 為 ChannelFuture,可監聽寫入完成。 */
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof Packet<?> packet) {
// 處理邏輯
}
super.write(ctx, msg, promise); // 繼續往 wire 送
}
/** 連線關閉(玩家離線或超時)
* 在此清理資源,不要再呼叫 connection.send()。 */
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// 清理
super.channelInactive(ctx);
}
/** 異常處理(DecoderException, ClosedChannelException 等) */
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (cause instanceof io.netty.handler.codec.DecoderException) {
// 封包解析失敗,通常直接關閉連線
ctx.close();
return;
}
super.exceptionCaught(ctx, cause);
}
}
EventLoop 執行緒操作
移除 handler 必須在 EventLoop 上執行,避免 pipeline 競爭:
import io.netty.channel.Channel;
// 安全移除 handler
Channel channel = connection.channel;
if (channel.isOpen()) {
channel.eventLoop().execute(() -> {
if (channel.pipeline().get("my_handler") != null) {
channel.pipeline().remove("my_handler");
}
});
}
EventLoop 常用方法
| 方法 | 說明 |
|---|---|
channel.eventLoop().execute(Runnable) |
在 IO thread 執行一次 |
channel.eventLoop().inEventLoop() |
檢查當前是否在 EventLoop |
channel.eventLoop().schedule(Runnable, delay, TimeUnit) |
延遲執行 |
channel.isActive() |
連線是否仍開啟 |
channel.isOpen() |
Channel 是否未關閉 |
Pipeline 操作速查
import io.netty.channel.ChannelPipeline;
ChannelPipeline pipeline = channel.pipeline();
// 插入 handler
pipeline.addBefore("packet_handler", "my_handler", handler); // 常用
pipeline.addAfter("decoder", "my_handler", handler);
pipeline.addFirst("my_handler", handler);
pipeline.addLast("my_handler", handler);
// 移除 handler
pipeline.remove("my_handler");
pipeline.remove(handler);
// 查詢 handler
ChannelHandler h = pipeline.get("my_handler"); // 不存在回傳 null
boolean exists = pipeline.get("my_handler") != null;
// 替換 handler
pipeline.replace("my_handler", "new_handler", newHandler);
// 列出所有 handler 名稱(除錯用)
pipeline.names().forEach(System.out::println);
常見 Netty 異常與處理
| 異常類 | 觸發時機 | 建議處理 |
|---|---|---|
ClosedChannelException |
向已關閉的 channel 寫入 | 在 send 前檢查 channel.isActive() |
DecoderException |
封包格式錯誤(非法 VarInt 等) | 在 exceptionCaught 關閉連線 |
ReadTimeoutException |
連線超時(KeepAlive 未收到) | Paper 自動處理,踢出玩家 |
IllegalStateException: Handler already added |
重複注入同名 handler | 注入前先檢查 pipeline.get(name) == null |
NullPointerException: connection |
玩家已離線 | 發送前檢查 nms.connection != null |
ServerGamePacketListenerImpl 常用操作
net.minecraft.server.network.ServerGamePacketListenerImpl(即 ServerPlayer.connection)
import net.minecraft.server.network.ServerGamePacketListenerImpl;
ServerGamePacketListenerImpl conn = nmsPlayer.connection;
// 發送封包
conn.send(packet);
conn.send(packet, PacketSendListener.thenRun(() -> {
// 封包送達後的回調(在 IO thread)
}));
// 踢出玩家
conn.disconnect(Component.literal("你被踢出了"));
// 延遲踢出(在主執行緒)
Bukkit.getScheduler().runTask(plugin, () ->
player.kick(net.kyori.adventure.text.Component.text("踢出原因")));
Protocol 狀態枚舉
玩家連線在不同登入階段使用不同的封包協議:
| 階段 | 監聽器類 | 封包類型 |
|---|---|---|
HANDSHAKING |
握手 | 僅 Handshake 封包 |
LOGIN |
ServerLoginPacketListenerImpl |
Login 封包 |
PLAY |
ServerGamePacketListenerImpl |
Game 封包 |
CONFIGURATION |
ServerConfigurationPacketListenerImpl |
Config(1.20.2+) |
Plugin 通常只接觸 PLAY 階段(
PlayerJoinEvent之後)。
相關技能
Skills/nms/nms-packet-interceptor/SKILL.md— Netty pipeline 注入完整實作Skills/nms/nms-packet-sender/SKILL.md— 封包發送Skills/_shared/nms-threading.md— 執行緒安全詳解docs/paper-nms/packets.md— Clientbound/Serverbound 封包目錄