实战篇:搭建游戏服务器骨架工程
概述
本篇把一个可运行的游戏服务器骨架完整搭建起来:多模块 Maven 工程 + Netty 服务器启动 + 基础配置。最终产物是一个能接受客户端连接、完成简单消息收发的可运行工程,后续所有功能都在这套骨架上生长。
一、工程总览
1.1 目标
本篇目标:
可运行的多模块 Maven 工程
Netty 服务器能启动并接受连接
Protobuf 协议模块可编译
基础配置加载与日志就绪
一条最小消息链路打通最终工程结构:
game-server-parent(父 POM)
├── game-server-common 公共模块
├── game-server-protocol 协议模块
├── game-server-gateway 网关(Netty)
├── game-server-logic 逻辑模块
└── game-server-start 启动模块1.2 环境准备
| 依赖 | 版本建议 |
|---|---|
| JDK | 17+ |
| Maven | 3.8+ |
| IDE | IDEA 2023+ |
| Protobuf 插件 | protobuf-maven-plugin |
二、父工程创建
2.1 父 POM
xml
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.game</groupId>
<artifactId>game-server-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>game-server-common</module>
<module>game-server-protocol</module>
<module>game-server-gateway</module>
<module>game-server-logic</module>
<module>game-server-start</module>
</modules>
<dependencyManagement>
<dependencies>
<!-- 统一版本:Netty/Protobuf/Spring 等 -->
</dependencies>
</dependencyManagement>
</project>要点:
packaging=pom(聚合工程)
子模块在 <modules> 中注册
版本统一在 dependencyManagement 管理三、公共模块 common
3.1 职责与内容
game-server-common 包含:
通用工具类(时间/随机/日志)
常量定义
通用异常
消息响应包装java
package com.game.common;
public final class GameConstant {
public static final int MAGIC_NUMBER = 0xABCD;
public static final int VERSION = 1;
public static final int MAX_FRAME_LENGTH = 1024 * 1024;
}java
package com.game.common;
public class GameException extends RuntimeException {
private final int code;
public GameException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() { return code; }
}四、协议模块 protocol
4.1 定义 Protobuf 消息
game-server-protocol/src/main/proto/login.proto:proto
syntax = "proto3";
package game.login;
message C2SLogin {
string account = 1;
string token = 2;
}
message S2CLogin {
int32 code = 1;
int64 playerId = 2;
string message = 3;
}4.2 配置 protobuf 插件
xml
<build>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.25.1</protocArtifact>
</configuration>
<executions>
<execution>
<goals><goal>compile</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>4.3 Opcode 常量
java
package com.game.protocol;
public interface Opcode {
// 登录模块 0x1000
int C2S_LOGIN = 0x1001;
int S2C_LOGIN = 0x1002;
// 心跳
int C2S_HEARTBEAT = 0x0001;
int S2C_HEARTBEAT = 0x0002;
}运行 mvn compile 生成 Protobuf Java 类
协议模块独立编译(前后端共享)五、网关模块 gateway(Netty 核心)
5.1 Netty Server 启动
java
package com.game.gateway;
@Component
public class GameNettyServer {
private final EventLoopGroup bossGroup = new NioEventLoopGroup(1);
private final EventLoopGroup workerGroup = new NioEventLoopGroup();
@Value("${game.server.port:8888}")
private int port;
public void start() throws InterruptedException {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new LengthFieldBasedFrameDecoder(1024*1024, 0, 4, 0, 4))
.addLast(new MessageDecoder())
.addLast(new MessageEncoder())
.addLast(new HeartbeatHandler())
.addLast(new DispatchHandler());
}
});
ChannelFuture future = bootstrap.bind(port).sync();
log.info("Game server started on port {}", port);
future.channel().closeFuture().sync();
}
public void shutdown() {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}要点:
bossGroup/workerGroup(Reactor 模型)
LengthFieldBasedFrameDecoder 拆包
自定义编解码器 + 心跳 + 分发5.2 编解码器
java
// 消息体:opcode(2) + bodyLength(4) + body
public class MessageDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
if (in.readableBytes() < 6) return;
short opcode = in.readShort();
int bodyLength = in.readInt();
if (in.readableBytes() < bodyLength) {
// 数据不足,回退等待
in.resetReaderIndex();
return;
}
byte[] body = new byte[bodyLength];
in.readBytes(body);
out.add(new GameMessage(opcode, body));
}
}java
public class MessageEncoder extends MessageToByteEncoder<GameMessage> {
@Override
protected void encode(ChannelHandlerContext ctx, GameMessage msg, ByteBuf out) {
out.writeShort(msg.getOpcode());
out.writeInt(msg.getBody().length);
out.writeBytes(msg.getBody());
}
}5.3 心跳处理
java
public class HeartbeatHandler extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent idleEvent) {
if (idleEvent.state() == IdleState.READER_IDLE) {
// 读超时,判定离线
ctx.close();
log.info("connection idle timeout, close: {}", ctx.channel().remoteAddress());
}
} else {
super.userEventTriggered(ctx, evt);
}
}
}5.4 消息分发
java
@Component
public class MessageDispatcher {
private final Map<Integer, MessageHandler> handlers = new ConcurrentHashMap<>();
public void register(int opcode, MessageHandler handler) {
handlers.put(opcode, handler);
}
public void dispatch(ChannelHandlerContext ctx, GameMessage msg) {
MessageHandler handler = handlers.get(msg.getOpcode());
if (handler == null) {
log.warn("unknown opcode: {}", msg.getOpcode());
return;
}
handler.handle(ctx, msg);
}
}说明:
网关只做接入与路由
具体业务在 logic 模块注册 Handler
网关与逻辑通过接口解耦六、逻辑模块 logic
6.1 消息处理接口
java
package com.game.logic.handler;
public interface MessageHandler {
int opcode();
void handle(ChannelHandlerContext ctx, GameMessage msg);
}6.2 登录处理器
java
@Component
public class LoginHandler implements MessageHandler {
@Override
public int opcode() { return Opcode.C2S_LOGIN; }
@Override
public void handle(ChannelHandlerContext ctx, GameMessage msg) {
try {
LoginProto.C2SLogin req =
LoginProto.C2SLogin.parseFrom(msg.getBody());
log.info("player login: {}", req.getAccount());
LoginProto.S2CLogin resp = LoginProto.S2CLogin.newBuilder()
.setCode(0)
.setPlayerId(10001L)
.setMessage("welcome")
.build();
ctx.writeAndFlush(new GameMessage(Opcode.S2C_LOGIN, resp.toByteArray()));
} catch (Exception e) {
log.error("login failed", e);
}
}
}6.3 处理器注册
java
@Configuration
public class HandlerRegistry {
@Autowired
private List<MessageHandler> handlers;
@Autowired
private MessageDispatcher dispatcher;
@PostConstruct
public void init() {
for (MessageHandler handler : handlers) {
dispatcher.register(handler.opcode(), handler);
log.info("register handler opcode: {}", handler.opcode());
}
}
}Spring 自动注入所有 MessageHandler 实现
(基于接口实现自动发现)七、启动模块 start
7.1 启动类
java
@SpringBootApplication(scanBasePackages = "com.game")
public class GameServerApplication {
public static void main(String[] args) throws InterruptedException {
ConfigurableApplicationContext context =
SpringApplication.run(GameServerApplication.class, args);
GameNettyServer server = context.getBean(GameNettyServer.class);
server.start();
}
}7.2 优雅关闭
java
@PreDestroy
public void shutdown() {
log.info("shutting down game server...");
nettyServer.shutdown();
}7.3 基础配置 application.yml
yaml
server:
port: 8080 # Spring 管理端口
game:
server:
port: 8888 # Netty 游戏端口
spring:
profiles:
active: dev说明:
Spring Boot 管业务与健康检查
Netty 管游戏长连接
端口分离,互不干扰八、验证与运行
8.1 构建运行
构建:
mvn clean install
mvn -pl game-server-start -am package
运行:
java -jar game-server-start/target/game-server-start-1.0.0.jar8.2 验证链路
验证方式:
1. 日志出现 "Game server started on port 8888"
2. 用 telnet/nc 连接端口
3. 发送一条登录消息 → 收到响应
4. 观察心跳与断线日志
命令行快速验证:
telnet localhost 8888
(后续用自研客户端/压测工具验证协议)8.3 骨架验收清单
| 项 | 状态 |
|---|---|
| 多模块 Maven 构建通过 | ✅ |
| Protobuf 生成成功 | ✅ |
| Netty 启动监听端口 | ✅ |
| 编解码链路可用 | ✅ |
| 心跳/断线处理 | ✅ |
| 消息分发到 Handler | ✅ |
| 配置多环境加载 | ✅ |
验收方式:
启动成功 + 日志正常
最小消息链路打通
骨架即最小可运行系统九、常见问题
9.1 Netty 启动报端口占用
解决:
检查端口是否被占用(netstat)
修改配置端口
避免与 Spring 端口冲突9.2 Protobuf 类没生成
解决:
检查 proto 文件路径(src/main/proto)
protobuf-maven-plugin 配置正确
执行 mvn compile 生成
检查依赖:protobuf-java9.3 收不到消息
排查:
粘包拆包是否正确(长度字段)
编码/解码字段顺序一致
Opcode 是否匹配
日志定位到哪一层
经验:
先打日志,逐层验证
确认字节序(大端)十、小结
本篇产出一个可运行的六模块骨架:common(公共)+ protocol(协议)+ gateway(Netty 接入)+ logic(业务)+ start(装配)。核心链路是"Netty 启动 → 拆包解码 → 心跳检测 → 消息分发 → Handler 处理 → 编码回写"。这个骨架就是后续所有功能的起点:往里加房间系统、玩家系统、对战逻辑,都是在同一套接入与分发框架上扩展。