# 游戏服务器与客户端Protobuf通信与排行榜实战
## 目录
1. [项目概述](#1-项目概述)
2. [Protobuf基础](#2-protobuf基础)
3. [协议定义](#3-协议定义)
4. [游戏客户端实现](#4-游戏客户端实现)
5. [游戏服务器实现](#5-游戏服务器实现)
6. [排行榜系统实现](#6-排行榜系统实现)
7. [完整运行示例](#7-完整运行示例)
8. [扩展与优化](#8-扩展与优化)
---
## 1. 项目概述
### 1.1 项目背景
本项目实现了一个完整的游戏服务器与客户端通信系统,使用Protobuf作为序列化协议,实现以下核心功能:
- **通信框架**:基于TCP长连接的双向通信
- **协议设计**:使用Protobuf定义通信协议
- **排行榜系统**:实时更新的玩家分数排行榜
- **心跳机制**:保证连接的稳定性
- **重连机制**:客户端断线自动重连
### 1.2 技术栈
```
技术栈:
├── 通信协议:Protobuf 3.x
├── 通信方式:TCP Socket
├── 序列化:Protobuf二进制序列化
├── 客户端语言:Java
├── 服务器语言:Java
└── 排行榜实现:TreeMap + Redis(可选)
```
### 1.3 项目结构
```
game-rank-system/
├── proto/ # Protobuf协议定义
│ └── game.proto # 游戏协议定义
├── client/ # 客户端代码
│ ├── GameClient.java # 客户端主类
│ ├── PacketEncoder.java # 编码器
│ ├── PacketDecoder.java # 解码器
│ └── ClientHandler.java # 客户端处理器
├── server/ # 服务器代码
│ ├── GameServer.java # 服务器主类
│ ├── PacketEncoder.java # 编码器
│ ├── PacketDecoder.java # 解码器
│ ├── ServerHandler.java # 服务器处理器
│ └── RankManager.java # 排行榜管理器
└── model/ # 数据模型
├── Player.java # 玩家模型
└── RankItem.java # 排行榜项
```
---
## 2. Protobuf基础
### 2.1 什么是Protobuf
Protocol Buffers(简称Protobuf)是Google开发的一种语言无关、平台无关、可扩展的序列化结构数据的方法。
**优点**:
- 序列化后体积小(比JSON小3-10倍)
- 序列化/反序列化速度快(比JSON快20-100倍)
- 向后兼容性好
- 支持多种语言
- 自描述性强
**缺点**:
- 二进制格式,不可读
- 需要预定义.proto文件
### 2.2 Protobuf vs JSON
```java
/**
* Protobuf vs JSON 对比
*/
public class ProtobufVsJson {
public static void main(String[] args) {
System.out.println("┌─────────────────────────────────────────────────────────────────┐");
System.out.println("│ 对比项 │ Protobuf │ JSON │");
System.out.println("├─────────────────────────────────────────────────────────────────┤");
System.out.println("│ 序列化大小 │ 小(二进制) │ 大(文本) │");
System.out.println("├─────────────────────────────────────────────────────────────────┤");
System.out.println("│ 序列化速度 │ 快 │ 慢 │");
System.out.println("├─────────────────────────────────────────────────────────────────┤");
System.out.println("│ 可读性 │ 差(二进制) │ 好(文本) │");
System.out.println("├─────────────────────────────────────────────────────────────────┤");
System.out.println("│ 跨语言 │ 支持 │ 支持 │");
System.out.println("├─────────────────────────────────────────────────────────────────┤");
System.out.println("│ 版本兼容 │ 好 │ 一般 │");
System.out.println("├─────────────────────────────────────────────────────────────────┤");
System.out.println("│ 使用场景 │ 游戏通信、RPC │ Web API、配置文件 │");
System.out.println("└─────────────────────────────────────────────────────────────────┘");
}
}
```
---
## 3. 协议定义
### 3.1 Protobuf文件定义
```protobuf
// game.proto
syntax = "proto3";
package game;
// 选项设置
option java_package = "com.game.protocol";
option java_outer_classname = "GameProtocol";
// 消息类型枚举
enum MsgType {
UNKNOWN = 0; // 未知消息
HEARTBEAT = 1; // 心跳消息
HEARTBEAT_ACK = 2; // 心跳响应
// 登录相关
LOGIN_REQ = 10; // 登录请求
LOGIN_RESP = 11; // 登录响应
// 排行榜相关
GET_RANK_REQ = 20; // 获取排行榜请求
GET_RANK_RESP = 21; // 获取排行榜响应
UPDATE_SCORE_REQ = 22; // 更新分数请求
UPDATE_SCORE_RESP = 23; // 更新分数响应
RANK_CHANGE_NOTIFY = 24; // 排行榜变化通知
}
// 心跳消息
message HeartBeat {
int64 timestamp = 1;
}
// 登录请求
message LoginReq {
string player_id = 1;
string player_name = 2;
int32 level = 3;
}
// 登录响应
message LoginResp {
int32 code = 1;
string message = 2;
int64 player_id = 3;
}
// 排行榜项
message RankItem {
int32 rank = 1; // 排名
string player_id = 2; // 玩家ID
string player_name = 3; // 玩家名称
int64 score = 4; // 分数
int32 level = 5; // 等级
}
// 获取排行榜请求
message GetRankReq {
int32 top_n = 1; // 获取前N名
}
// 获取排行榜响应
message GetRankResp {
int32 code = 1;
string message = 2;
repeated RankItem rank_list = 3; // 排行榜列表
int32 my_rank = 4; // 我的排名
}
// 更新分数请求
message UpdateScoreReq {
int64 score = 1; // 新分数
string reason = 2; // 更新原因
}
// 更新分数响应
message UpdateScoreResp {
int32 code = 1;
string message = 2;
int32 my_rank = 3; // 更新后的排名
int64 new_score = 4; // 新分数
}
// 排行榜变化通知
message RankChangeNotify {
repeated RankItem top_ranks = 1; // 前N名变化
}
// 游戏消息包装
message GameMessage {
MsgType msg_type = 1; // 消息类型
bytes data = 2; // 消息数据(具体消息的序列化结果)
}
```
### 3.2 编译Protobuf文件
```bash
# 编译Proto文件
protoc --java_out=./src/main/java ./proto/game.proto
# 编译后生成的文件结构:
# com/game/protocol/GameProtocol.java
```
### 3.3 生成的Java类使用
```java
/**
* Protobuf生成的Java类使用示例
*/
public class ProtobufUsage {
public void example() {
// 创建登录请求
GameProtocol.LoginReq loginReq = GameProtocol.LoginReq.newBuilder()
.setPlayerId("player_001")
.setPlayerName("玩家001")
.setLevel(10)
.build();
// 序列化为字节数组
byte[] data = loginReq.toByteArray();
// 反序列化
try {
GameProtocol.LoginReq parsed = GameProtocol.LoginReq.parseFrom(data);
System.out.println("Player ID: " + parsed.getPlayerId());
System.out.println("Player Name: " + parsed.getPlayerName());
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
}
}
```
---
## 4. 游戏客户端实现
### 4.1 客户端主类
```java
package com.game.client;
import com.game.protocol.GameProtocol;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
/**
* 游戏客户端
*/
public class GameClient {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 8080;
private EventLoopGroup group;
private Channel channel;
private String playerId;
private String playerName;
public static void main(String[] args) {
GameClient client = new GameClient();
client.start();
}
/**
* 启动客户端
*/
public void start() {
group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer
() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// 编码器
pipeline.addLast("encoder", new PacketEncoder());
// 解码器
pipeline.addLast("decoder", new PacketDecoder());
// 业务处理器
pipeline.addLast("handler", new ClientHandler(GameClient.this));
}
});
// 连接服务器
ChannelFuture future = bootstrap.connect(SERVER_HOST, SERVER_PORT).sync();
channel = future.channel();
System.out.println("========================================");
System.out.println(" 游戏客户端已启动");
System.out.println(" 服务器: " + SERVER_HOST + ":" + SERVER_PORT);
System.out.println("========================================");
// 启动心跳线程
startHeartbeat();
// 等待连接建立
Thread.sleep(1000);
// 处理用户输入
handleUserInput();
// 等待连接关闭
channel.closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
shutdown();
}
}
/**
* 处理用户输入
*/
private void handleUserInput() {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("\n========================================");
System.out.println("请选择操作:");
System.out.println("1. 登录");
System.out.println("2. 获取排行榜");
System.out.println("3. 更新分数");
System.out.println("4. 退出");
System.out.println("========================================");
System.out.print("请输入选项: ");
String choice = scanner.nextLine();
switch (choice) {
case "1":
login(scanner);
break;
case "2":
getRank(scanner);
break;
case "3":
updateScore(scanner);
break;
case "4":
System.out.println("正在退出...");
shutdown();
return;
default:
System.out.println("无效的选项!");
}
}
}
/**
* 登录
*/
private void login(Scanner scanner) {
System.out.print("请输入玩家ID: ");
playerId = scanner.nextLine();
System.out.print("请输入玩家名称: ");
playerName = scanner.nextLine();
System.out.print("请输入玩家等级: ");
int level = Integer.parseInt(scanner.nextLine());
// 构建登录请求
GameProtocol.LoginReq loginReq = GameProtocol.LoginReq.newBuilder()
.setPlayerId(playerId)
.setPlayerName(playerName)
.setLevel(level)
.build();
// 发送登录请求
sendMsg(GameProtocol.MsgType.LOGIN_REQ, loginReq);
System.out.println("发送登录请求...");
}
/**
* 获取排行榜
*/
private void getRank(Scanner scanner) {
if (playerId == null) {
System.out.println("请先登录!");
return;
}
System.out.print("请输入获取前N名: ");
int topN = Integer.parseInt(scanner.nextLine());
// 构建获取排行榜请求
GameProtocol.GetRankReq getRankReq = GameProtocol.GetRankReq.newBuilder()
.setTopN(topN)
.build();
// 发送获取排行榜请求
sendMsg(GameProtocol.MsgType.GET_RANK_REQ, getRankReq);
System.out.println("发送获取排行榜请求...");
}
/**
* 更新分数
*/
private void updateScore(Scanner scanner) {
if (playerId == null) {
System.out.println("请先登录!");
return;
}
System.out.print("请输入新分数: ");
long score = Long.parseLong(scanner.nextLine());
System.out.print("请输入更新原因: ");
String reason = scanner.nextLine();
// 构建更新分数请求
GameProtocol.UpdateScoreReq updateScoreReq = GameProtocol.UpdateScoreReq.newBuilder()
.setScore(score)
.setReason(reason)
.build();
// 发送更新分数请求
sendMsg(GameProtocol.MsgType.UPDATE_SCORE_REQ, updateScoreReq);
System.out.println("发送更新分数请求...");
}
/**
* 发送消息
*/
public void sendMsg(GameProtocol.MsgType msgType, com.google.protobuf.GeneratedMessageV3 message) {
if (channel == null || !channel.isActive()) {
System.out.println("连接未建立或已断开!");
return;
}
// 构建游戏消息
GameProtocol.GameMessage gameMsg = GameProtocol.GameMessage.newBuilder()
.setMsgType(msgType)
.setData(message.toByteArray())
.build();
// 发送消息
channel.writeAndFlush(gameMsg);
}
/**
* 启动心跳
*/
private void startHeartbeat() {
channel.eventLoop().scheduleAtFixedRate(() -> {
if (channel != null && channel.isActive()) {
// 发送心跳
GameProtocol.HeartBeat heartBeat = GameProtocol.HeartBeat.newBuilder()
.setTimestamp(System.currentTimeMillis())
.build();
sendMsg(GameProtocol.MsgType.HEARTBEAT, heartBeat);
}
}, 10, 10, TimeUnit.SECONDS);
}
/**
* 关闭客户端
*/
public void shutdown() {
if (channel != null) {
channel.close();
}
if (group != null) {
group.shutdownGracefully();
}
System.exit(0);
}
public void setPlayerId(String playerId) {
this.playerId = playerId;
}
public void setPlayerName(String playerName) {
this.playerName = playerName;
}
}
```
### 4.2 客户端编码器
```java
package com.game.client;
import com.game.protocol.GameProtocol;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
/**
* 客户端编码器
* 将Protobuf消息编码为字节流
*/
public class PacketEncoder extends MessageToByteEncoder {
@Override
protected void encode(ChannelHandlerContext ctx, GameProtocol.GameMessage msg, ByteBuf out) throws Exception {
// 获取消息的字节数组
byte[] data = msg.toByteArray();
// 写入数据长度(4字节)
out.writeInt(data.length);
// 写入数据
out.writeBytes(data);
}
}
```
### 4.3 客户端解码器
```java
package com.game.client;
import com.game.protocol.GameProtocol;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
/**
* 客户端解码器
* 将字节流解码为Protobuf消息
*/
public class PacketDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List