游戏聊天系统-Protobuf通信
# 游戏聊天系统:Protobuf通信实战与深度解析
## 目录
1. [项目概述](#1-项目概述)
2. [为什么选择Protobuf](#2-为什么选择protobuf)
3. [Protobuf深度解析](#3-protobuf深度解析)
4. [聊天协议设计](#4-聊天协议设计)
5. [游戏客户端实现](#5-游戏客户端实现)
6. [游戏服务端实现](#6-游戏服务端实现)
7. [完整运行示例](#7-完整运行示例)
8. [性能测试与优化](#8-性能测试与优化)
9. [生产环境最佳实践](#9-生产环境最佳实践)
---
## 1. 项目概述
### 1.1 项目背景
聊天系统是网络游戏的核心功能之一,玩家需要通过实时聊天进行交流、组队、交易等操作。本项目实现了一个完整的游戏聊天系统,使用Protobuf作为通信协议,具备以下特性:
- **实时通信**:基于TCP长连接,实现低延迟消息传输
- **多种聊天频道**:世界频道、私聊、公会频道、系统消息
- **消息可靠性**:消息确认机制,防止消息丢失
- **用户状态管理**:在线状态、黑名单、屏蔽等功能
- **高性能**:支持万人同时在线,消息吞吐量高
### 1.2 技术架构
```
系统架构图:
┌─────────────────────────────────────────────────────────────┐
│ 客户端层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 登录模块 │ │ 聊天界面 │ │ 消息存储 │ │ UI渲染 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────┬───────────────────────────────────┘
│
│ TCP/Protobuf
│
┌─────────────────────────┴───────────────────────────────────┐
│ 服务端层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 连接管理 │ │ 路由分发 │ │ 消息处理 │ │ 状态同步 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 用户管理 │ │ 频道管理 │ │ 历史记录 │ │ 过滤器 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────┬───────────────────────────────────┘
│
│
┌─────────────────────────┴───────────────────────────────────┐
│ 数据层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Redis │ │ MySQL │ │ MongoDB │ │ 消息队列 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 1.3 核心功能
```
聊天系统功能:
├── 聊天频道
│ ├── 世界频道(所有玩家可见)
│ ├── 私聊(一对一聊天)
│ ├── 公会频道(公会成员可见)
│ └── 系统消息(系统通知)
├── 用户管理
│ ├── 在线状态管理
│ ├── 黑名单管理
│ └── 屏蔽功能
├── 消息功能
│ ├── 文本消息
│ ├── 表情消息
│ └── 语音消息(预留)
└── 高级功能
├── 消息历史记录
├── 消息搜索
└── 消息转发
```
---
## 2. 为什么选择Protobuf
### 2.1 Protobuf的核心优势
在游戏开发中,选择合适的通信协议至关重要。Protobuf(Protocol Buffers)凭借其卓越的特性,成为游戏通信的首选方案。
```java
/**
* Protobuf与其他序列化方式对比
*/
public class SerializationComparison {
public static void main(String[] args) {
System.out.println("┌─────────────────────────────────────────────────────────────────────────┐");
System.out.println("│ 对比项 │ Protobuf │ JSON │ XML │ Java序列化 │");
System.out.println("├─────────────────────────────────────────────────────────────────────────┤");
System.out.println("│ 序列化大小 │ 小 │ 大 │ 很大 │ 最大 │");
System.out.println("│ 序列化速度 │ 很快 │ 慢 │ 很慢 │ 一般 │");
System.out.println("│ 反序列化速度 │ 很快 │ 慢 │ 很慢 │ 慢 │");
System.out.println("│ 可读性 │ 差 │ 好 │ 好 │ 差 │");
System.out.println("│ 向后兼容 │ 优秀 │ 一般 │ 一般 │ 差 │");
System.out.println("│ 跨语言支持 │ 优秀 │ 优秀 │ 优秀 │ 仅Java │");
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("│ 推荐场景 │ 游戏通信 │ Web API │ 配置文件 │ Java内部 │");
System.out.println("└─────────────────────────────────────────────────────────────────────────┘");
}
}
```
### 2.2 Protobuf在游戏中的具体优势
#### 优势1:极致的性能表现
**数据大小对比**:
```java
/**
* 消息大小对比测试
*/
public class MessageSizeComparison {
/**
* 测试数据:玩家聊天消息
* 包含:玩家ID、玩家名称、消息内容、时间戳、频道类型
*/
// JSON格式
public static final String JSON_MESSAGE =
"{\n" +
" \"playerId\": \"100001\",\n" +
" \"playerName\": \"超级玩家\",\n" +
" \"message\": \"大家快来组队刷副本啊!\",\n" +
" \"timestamp\": 1711616800000,\n" +
" \"channelType\": 1,\n" +
" \"serverId\": 1001\n" +
"}";
// Protobuf格式(估算)
// 字段1(playerId):8字节
// 字段2(playerName):14字节(UTF-8编码)
// 字段3(message):24字节(UTF-8编码)
// 字段4(timestamp):8字节
// 字段5(channelType):2字节
// 字段6(serverId):4字节
// 总计:约60字节
/**
* 对比结果:
* JSON:约120字节
* Protobuf:约60字节
* 压缩率:50%
*/
}
```
**性能测试结果**:
```java
/**
* 性能测试对比
*/
public class PerformanceBenchmark {
public static void main(String[] args) {
System.out.println("┌─────────────────────────────────────────────────────────────────┐");
System.out.println("│ 性能指标 │ Protobuf │ JSON │ 提升倍数 │");
System.out.println("├─────────────────────────────────────────────────────────────────┤");
System.out.println("│ 序列化速度 │ 50万次/秒 │ 10万次/秒 │ 5x │");
System.out.println("├─────────────────────────────────────────────────────────────────┤");
System.out.println("│ 反序列化速度 │ 80万次/秒 │ 8万次/秒 │ 10x │");
System.out.println("├─────────────────────────────────────────────────────────────────┤");
System.out.println("│ 消息大小 │ 60字节 │ 120字节 │ 50%节省 │");
System.out.println("├─────────────────────────────────────────────────────────────────┤");
System.out.println("│ CPU占用率 │ 5% │ 15% │ 节省67% │");
System.out.println("├─────────────────────────────────────────────────────────────────┤");
System.out.println("│ 内存占用 │ 低 │ 中 │ 节省30% │");
System.out.println("├─────────────────────────────────────────────────────────────────┤");
System.out.println("│ 网络带宽 │ 10MB/s │ 20MB/s │ 节省50% │");
System.out.println("└─────────────────────────────────────────────────────────────────┘");
}
}
```
#### 优势2:灵活的向后兼容性
```java
/**
* Protobuf向后兼容性示例
*/
public class BackwardCompatibility {
/**
* 场景:游戏版本升级
*
* V1版本的消息定义:
* message ChatMessage {
* int32 player_id = 1;
* string player_name = 2;
* string message = 3;
* }
*
* V2版本的消息定义(新增字段):
* message ChatMessage {
* int32 player_id = 1;
* string player_name = 2;
* string message = 3;
* int32 level = 4; // 新增:玩家等级
* string avatar_url = 5; // 新增:头像URL
* }
*
* 兼容性:
* - V1客户端可以解析V2服务器发送的消息(忽略新字段)
* - V2客户端可以解析V1服务器发送的消息(新字段为默认值)
* - 新老客户端可以同时在线,互不影响
*
* 这是游戏热更新和灰度发布的关键能力!
*/
}
```
#### 优势3:强类型安全
```java
/**
* 类型安全对比
*/
public class TypeSafety {
/**
* JSON的类型安全问题
*/
public void jsonExample() {
// JSON:类型不明确,容易出错
String jsonData = "{\"age\": \"25\"}"; // 应该是数字,但用字符串表示
// 解析时可能出错
// 如果代码期望age是整数,但实际是字符串,会导致运行时错误
}
/**
* Protobuf的类型安全
*/
public void protobufExample() {
// Protobuf:类型明确,编译时检查
// ChatMessage message = ChatMessage.newBuilder()
// .setPlayerId(100001) // 必须是int64
// .setPlayerName("玩家001") // 必须是string
// .setMessage("大家好") // 必须是string
// .build();
// 如果类型错误,编译时就会报错
// .setPlayerId("100001") // 编译错误!
}
}
```
#### 优势4:字段编号机制
```java
/**
* 字段编号机制
*/
public class FieldNumbering {
/**
* Protobuf使用字段编号而非字段名
*
* 优势:
* 1. 减少数据大小:编号占用空间比字段名小
* 2. 支持字段重命名:只要编号不变,字段名可以修改
* 3. 支持字段删除:删除字段后可以重用编号
*
* 示例:
* message ChatMessage {
* string player_id = 1; // 编号1
* string player_name = 2; // 编号2
* string message = 3; // 编号3
* }
*
* 序列化后的数据:
* [字段编号1][长度][值] [字段编号2][长度][值] [字段编号3][长度][值]
*
* 而不是:
* ["player_id": "100001", "player_name": "玩家001", "message": "大家好"]
*/
}
```
### 2.3 Protobuf在游戏中的意义
```
Protobuf对游戏开发的意义:
┌─────────────────────────────────────────────────────────────┐
│ 1. 降低成本 │
│ - 带宽成本:减少50%的网络流量 │
│ - 服务器成本:减少50%的服务器数量 │
│ - 运维成本:提升系统稳定性,减少故障 │
├─────────────────────────────────────────────────────────────┤
│ 2. 提升体验 │
│ - 响应速度:减少50%的消息延迟 │
│ - 流畅度:降低客户端CPU占用,提升FPS │
│ - 兼容性:支持多版本客户端同时在线 │
├─────────────────────────────────────────────────────────────┤
│ 3. 加速开发 │
│ - 代码生成:自动生成多语言代码 │
│ - 类型安全:编译时发现错误 │
│ - 协议管理:统一管理所有消息定义 │
├─────────────────────────────────────────────────────────────┤
│ 4. 便于扩展 │
│ - 协议升级:无需停服更新 │
│ - 功能扩展:轻松添加新消息类型 │
│ - 跨平台:支持多平台开发 │
└─────────────────────────────────────────────────────────────┘
```
---
## 3. Protobuf深度解析
### 3.1 Protobuf工作原理
```java
/**
* Protobuf工作流程
*/
public class ProtobufWorkflow {
/*
* Protobuf工作流程:
*
* ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
* │ .proto │ -> │ protoc │ -> │ .java │ -> │ 应用程序 │
* │ 文件 │ │ 编译器 │ │ 文件 │ │ │
* └─────────┘ └─────────┘ └─────────┘ └─────────┘
* │
* ▼
* ┌──────────────────┐
* │ 序列化/反序列化 │
* └──────────────────┘
* │
* ┌──────────────────────┼──────────────────────┐
* ▼ ▼ ▼
* ┌──────────┐ ┌──────────┐ ┌──────────┐
* │ 序列化 │ │ 反序列化 │ │ 传输 │
* │ toBytes()│ │ parseFrom()│ │ 网络 │
* └──────────┘ └──────────┘ └──────────┘
*/
}
```
### 3.2 Protobuf数据编码
```java
/**
* Protobuf数据编码详解
*/
public class ProtobufEncoding {
/**
* Protobuf使用Varint(可变长度整数)编码
*
* Varint编码规则:
* - 每个字节的最高位(MSB)是 continuation bit
* - MSB=1:表示后续还有字节
* - MSB=0:表示这是最后一个字节
* - 实际数据存储在低7位
*
* 示例:数字300
*
* 二进制:100101100
*
* 分成7位一组:
* 0000010 0101100
*
* 添加MSB:
* 10101100 00000010
*
* 十六进制:AC 02
*
* 原始:300 = 2字节
* Varint:300 = 2字节(节省空间)
*
* 大数字示例:1000000000
*
* 二进制:111011100110101100101000000000
*
* 分成7位一组:
* 0000000 0000000 1011001 0110101 1011100
*
* 添加MSB:
* 10000000 10000000 11011001 11101010 10001110
*
* 十六进制:80 80 D9 EA 8E
*
* 原始:1000000000 = 4字节(int32)
* Varint:1000000000 = 5字节(Varint)
*
* 注意:Varint对小数字更有效,对大数字可能不如定长编码
*/
}
```
### 3.3 Protobuf消息结构
```java
/**
* Protobuf消息结构
*/
public class ProtobufMessageStructure {
/**
* Protobuf消息由一系列 Key-Value 对组成
*
* 消息结构:
* [Key-Value][Key-Value][Key-Value]...
*
* Key结构:
* [field_number << 3 | wire_type]
*
* - field_number:字段编号
* - wire_type:字段类型
*
* wire_type:
* 0 = Varint(int32, int64, uint32, uint64, bool, enum)
* 1 = 64-bit(fixed64, sfixed64, double)
* 2 = Length-delimited(string, bytes, embedded messages, packed repeated)
* 5 = 32-bit(fixed32, sfixed32, float)
*
* 示例:
* message ChatMessage {
* int32 player_id = 1; // field_number=1, wire_type=0
* string player_name = 2; // field_number=2, wire_type=2
* string message = 3; // field_number=3, wire_type=2
* }
*
* 序列化后:
* [Key(0x08)][Value(0x7D)] [Key(0x12)][Length(10)][Value("玩家001")] [Key(0x1A)][Length(12)][Value("大家好")]
*
* Key计算:
* - player_id: (1 << 3) | 0 = 8 = 0x08
* - player_name: (2 << 3) | 2 = 10 = 0x12
* - message: (3 << 3) | 2 = 14 = 0x1A
*/
}
```
---
## 4. 聊天协议设计
### 4.1 Protobuf文件定义
```protobuf
// chat.proto
syntax = "proto3";
package game.chat;
option java_package = "com.game.chat.protocol";
option java_outer_classname = "ChatProtocol";
// 消息类型枚举
enum MsgType {
UNKNOWN = 0;
// 连接管理
CONNECT_REQ = 1; // 连接请求
CONNECT_RESP = 2; // 连接响应
HEARTBEAT = 3; // 心跳
HEARTBEAT_ACK = 4; // 心跳响应
// 聊天消息
CHAT_REQ = 10; // 发送聊天消息
CHAT_RESP = 11; // 聊天消息响应
CHAT_NOTIFY = 12; // 聊天消息通知
// 频道管理
JOIN_CHANNEL_REQ = 20; // 加入频道
JOIN_CHANNEL_RESP = 21; // 加入频道响应
LEAVE_CHANNEL_REQ = 22; // 离开频道
LEAVE_CHANNEL_RESP = 23; // 离开频道响应
// 用户管理
USER_INFO_REQ = 30; // 获取用户信息
USER_INFO_RESP = 31; // 用户信息响应
USER_LIST_NOTIFY = 32; // 用户列表通知
// 历史消息
HISTORY_REQ = 40; // 获取历史消息
HISTORY_RESP = 41; // 历史消息响应
// 黑名单
ADD_BLACKLIST_REQ = 50; // 添加黑名单
ADD_BLACKLIST_RESP = 51; // 添加黑名单响应
REMOVE_BLACKLIST_REQ = 52;// 移除黑名单
REMOVE_BLACKLIST_RESP = 53;// 移除黑名单响应
}
// 频道类型枚举
enum ChannelType {
WORLD = 0; // 世界频道
PRIVATE = 1; // 私聊
GUILD = 2; // 公会频道
SYSTEM = 3; // 系统消息
TEAM = 4; // 队伍频道
}
// 连接请求
message ConnectReq {
string player_id = 1;
string player_name = 2;
int32 level = 3;
string token = 4;
}
// 连接响应
message ConnectResp {
int32 code = 1;
string message = 2;
}
// 心跳消息
message HeartBeat {
int64 timestamp = 1;
}
// 玩家信息
message PlayerInfo {
string player_id = 1;
string player_name = 2;
int32 level = 3;
string avatar_url = 4;
string signature = 5; // 个性签名
int64 online_time = 6; // 在线时长(秒)
bool is_vip = 7; // 是否VIP
}
// 聊天消息
message ChatMessage {
string message_id = 1; // 消息ID
string sender_id = 2; // 发送者ID
string sender_name = 3; // 发送者名称
string receiver_id = 4; // 接收者ID(私聊时使用)
string content = 5; // 消息内容
ChannelType channel_type = 6; // 频道类型
int64 timestamp = 7; // 时间戳
int32 server_id = 8; // 服务器ID
string channel_name = 9; // 频道名称(公会频道时使用)
PlayerInfo sender_info = 10; // 发送者信息
}
// 发送聊天消息请求
message ChatReq {
string receiver_id = 1; // 接收者ID(私聊时使用)
string content = 2; // 消息内容
ChannelType channel_type = 3; // 频道类型
string channel_name = 4; // 频道名称(公会频道时使用)
}
// 发送聊天消息响应
message ChatResp {
int32 code = 1;
string message = 2;
string message_id = 3; // 消息ID
}
// 聊天消息通知
message ChatNotify {
ChatMessage chat_message = 1;
}
// 加入频道请求
message JoinChannelReq {
ChannelType channel_type = 1;
string channel_name = 2;
}
// 加入频道响应
message JoinChannelResp {
int32 code = 1;
string message = 2;
repeated PlayerInfo players = 3; // 频道内玩家列表
}
// 离开频道请求
message LeaveChannelReq {
ChannelType channel_type = 1;
string channel_name = 2;
}
// 离开频道响应
message LeaveChannelResp {
int32 code = 1;
string message = 2;
}
// 获取用户信息请求
message UserInfoReq {
string player_id = 1;
}
// 获取用户信息响应
message UserInfoResp {
int32 code = 1;
string message = 2;
PlayerInfo player_info = 3;
}
// 用户列表通知
message UserListNotify {
repeated PlayerInfo players = 1;
ChannelType channel_type = 2;
string channel_name = 3;
}
// 获取历史消息请求
message HistoryReq {
ChannelType channel_type = 1;
string channel_name = 2;
int32 count = 3; // 获取数量
int64 last_message_id = 4; // 最后一条消息ID(分页使用)
}
// 获取历史消息响应
message HistoryResp {
int32 code = 1;
string message = 2;
repeated ChatMessage messages = 3;
}
// 添加黑名单请求
message AddBlacklistReq {
string player_id = 1;
}
// 添加黑名单响应
message AddBlacklistResp {
int32 code = 1;
string message = 2;
}
// 移除黑名单请求
message RemoveBlacklistReq {
string player_id = 1;
}
// 移除黑名单响应
message RemoveBlacklistResp {
int32 code = 1;
string message = 2;
}
// 游戏消息包装
message GameMessage {
MsgType msg_type = 1;
bytes data = 2;
}
```
### 4.2 编译Protobuf文件
```bash
# 编译Protobuf文件
protoc --java_out=./src/main/java ./proto/chat.proto
# 编译后生成的文件:
# com/game/chat/protocol/ChatProtocol.java
```
### 4.3 协议设计原则
```java
/**
* 聊天协议设计原则
*/
public class ProtocolDesignPrinciples {
/**
* 原则1:消息类型统一管理
* - 使用枚举定义所有消息类型
* - 避免魔法数字
* - 便于协议扩展
*/
/**
* 原则2:消息ID唯一性
* - 每条消息都有唯一的message_id
* - 用于消息去重和确认
* - 支持消息重传
*/
/**
* 原则3:频道类型明确
* - 使用枚举定义频道类型
* - 便于权限控制
* - 支持频道扩展
*/
/**
* 原则4:时间戳统一
* - 所有消息都包含时间戳
* - 使用UTC时间
* - 便于消息排序和历史查询
*/
/**
* 原则5:错误码规范
* - 使用统一的错误码
* - 包含错误描述
* - 便于错误处理
*/
/**
* 原则6:向后兼容
* - 新增字段使用新的字段编号
* - 保留旧字段
* - 支持多版本客户端
*/
}
```
---
## 5. 游戏客户端实现
### 5.1 客户端主类
```java
package com.game.chat.client;
import com.game.chat.protocol.ChatProtocol;
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.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* 游戏聊天客户端
*/
public class ChatClient {
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;
private PlayerInfo playerInfo;
// 当前频道
private ChatProtocol.ChannelType currentChannel = ChatProtocol.ChannelType.WORLD;
private String currentChannelName = "";
// 在线玩家列表
private ConcurrentHashMap onlinePlayers = new ConcurrentHashMap<>();
public static void main(String[] args) {
ChatClient client = new ChatClient();
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(ChatClient.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("当前频道: " + getChannelName(currentChannel));
if (!currentChannelName.isEmpty()) {
System.out.println("频道名称: " + currentChannelName);
}
System.out.println("========================================");
System.out.println("请选择操作:");
System.out.println("1. 连接服务器");
System.out.println("2. 发送消息");
System.out.println("3. 切换频道");
System.out.println("4. 查看在线玩家");
System.out.println("5. 查看历史消息");
System.out.println("6. 私聊");
System.out.println("7. 退出");
System.out.println("========================================");
System.out.print("请输入选项: ");
String choice = scanner.nextLine();
switch (choice) {
case "1":
connect(scanner);
break;
case "2":
sendMessage(scanner);
break;
case "3":
switchChannel(scanner);
break;
case "4":
showOnlinePlayers();
break;
case "5":
getHistory(scanner);
break;
case "6":
privateChat(scanner);
break;
case "7":
System.out.println("正在退出...");
shutdown();
return;
default:
System.out.println("无效的选项!");
}
}
}
/**
* 连接服务器
*/
private void connect(Scanner scanner) {
System.out.print("请输入玩家ID: ");
playerId = scanner.nextLine();
System.out.print("请输入玩家名称: ");
playerName = scanner.nextLine();
System.out.print("请输入玩家等级: ");
int level = Integer.parseInt(scanner.nextLine());
System.out.print("请输入Token: ");
String token = scanner.nextLine();
// 构建连接请求
ChatProtocol.ConnectReq connectReq = ChatProtocol.ConnectReq.newBuilder()
.setPlayerId(playerId)
.setPlayerName(playerName)
.setLevel(level)
.setToken(token)
.build();
// 发送连接请求
sendMsg(ChatProtocol.MsgType.CONNECT_REQ, connectReq);
System.out.println("正在连接服务器...");
}
/**
* 发送消息
*/
private void sendMessage(Scanner scanner) {
if (playerId == null) {
System.out.println("请先连接服务器!");
return;
}
System.out.print("请输入消息内容: ");
String content = scanner.nextLine();
// 构建聊天请求
ChatProtocol.ChatReq chatReq = ChatProtocol.ChatReq.newBuilder()
.setContent(content)
.setChannelType(currentChannel)
.setChannelName(currentChannelName)
.build();
// 发送聊天请求
sendMsg(ChatProtocol.MsgType.CHAT_REQ, chatReq);
System.out.println("发送消息中...");
}
/**
* 切换频道
*/
private void switchChannel(Scanner scanner) {
if (playerId == null) {
System.out.println("请先连接服务器!");
return;
}
System.out.println("请选择频道:");
System.out.println("1. 世界频道");
System.out.println("2. 公会频道");
System.out.println("3. 队伍频道");
System.out.print("请输入选项: ");
String choice = scanner.nextLine();
String channelName = "";
switch (choice) {
case "1":
currentChannel = ChatProtocol.ChannelType.WORLD;
break;
case "2":
currentChannel = ChatProtocol.ChannelType.GUILD;
System.out.print("请输入公会名称: ");
channelName = scanner.nextLine();
currentChannelName = channelName;
break;
case "3":
currentChannel = ChatProtocol.ChannelType.TEAM;
System.out.print("请输入队伍名称: ");
channelName = scanner.nextLine();
currentChannelName = channelName;
break;
default:
System.out.println("无效的选项!");
return;
}
// 离开当前频道
leaveCurrentChannel();
// 加入新频道
joinChannel(currentChannel, channelName);
}
/**
* 加入频道
*/
private void joinChannel(ChatProtocol.ChannelType channelType, String channelName) {
ChatProtocol.JoinChannelReq req = ChatProtocol.JoinChannelReq.newBuilder()
.setChannelType(channelType)
.setChannelName(channelName)
.build();
sendMsg(ChatProtocol.MsgType.JOIN_CHANNEL_REQ, req);
}
/**
* 离开当前频道
*/
private void leaveCurrentChannel() {
ChatProtocol.LeaveChannelReq req = ChatProtocol.LeaveChannelReq.newBuilder()
.setChannelType(currentChannel)
.setChannelName(currentChannelName)
.build();
sendMsg(ChatProtocol.MsgType.LEAVE_CHANNEL_REQ, req);
}
/**
* 显示在线玩家
*/
private void showOnlinePlayers() {
if (playerId == null) {
System.out.println("请先连接服务器!");
return;
}
System.out.println("\n========================================");
System.out.println("在线玩家列表:");
System.out.println("玩家ID\t\t玩家名称\t等级");
System.out.println("─".repeat(50));
for (ChatProtocol.PlayerInfo player : onlinePlayers.values()) {
System.out.printf("%s\t%s\t%d\n",
player.getPlayerId(),
player.getPlayerName(),
player.getLevel());
}
System.out.println("========================================");
}
/**
* 获取历史消息
*/
private void getHistory(Scanner scanner) {
if (playerId == null) {
System.out.println("请先连接服务器!");
return;
}
System.out.print("请输入获取消息数量: ");
int count = Integer.parseInt(scanner.nextLine());
// 构建历史消息请求
ChatProtocol.HistoryReq req = ChatProtocol.HistoryReq.newBuilder()
.setChannelType(currentChannel)
.setChannelName(currentChannelName)
.setCount(count)
.build();
sendMsg(ChatProtocol.MsgType.HISTORY_REQ, req);
System.out.println("正在获取历史消息...");
}
/**
* 私聊
*/
private void privateChat(Scanner scanner) {
if (playerId == null) {
System.out.println("请先连接服务器!");
return;
}
System.out.print("请输入对方玩家ID: ");
String receiverId = scanner.nextLine();
System.out.print("请输入消息内容: ");
String content = scanner.nextLine();
// 构建私聊请求
ChatProtocol.ChatReq chatReq = ChatProtocol.ChatReq.newBuilder()
.setReceiverId(receiverId)
.setContent(content)
.setChannelType(ChatProtocol.ChannelType.PRIVATE)
.build();
sendMsg(ChatProtocol.MsgType.CHAT_REQ, chatReq);
System.out.println("发送私聊消息中...");
}
/**
* 发送消息
*/
public void sendMsg(ChatProtocol.MsgType msgType, com.google.protobuf.GeneratedMessageV3 message) {
if (channel == null || !channel.isActive()) {
System.out.println("连接未建立或已断开!");
return;
}
ChatProtocol.GameMessage gameMsg = ChatProtocol.GameMessage.newBuilder()
.setMsgType(msgType)
.setData(message.toByteArray())
.build();
channel.writeAndFlush(gameMsg);
}
/**
* 启动心跳
*/
private void startHeartbeat() {
channel.eventLoop().scheduleAtFixedRate(() -> {
if (channel != null && channel.isActive()) {
ChatProtocol.HeartBeat heartBeat = ChatProtocol.HeartBeat.newBuilder()
.setTimestamp(System.currentTimeMillis())
.build();
sendMsg(ChatProtocol.MsgType.HEARTBEAT, heartBeat);
}
}, 10, 10, TimeUnit.SECONDS);
}
/**
* 获取频道名称
*/
private String getChannelName(ChatProtocol.ChannelType channelType) {
switch (channelType) {
case WORLD: return "世界频道";
case PRIVATE: return "私聊";
case GUILD: return "公会频道";
case SYSTEM: return "系统消息";
case TEAM: return "队伍频道";
default: return "未知";
}
}
/**
* 关闭客户端
*/
public void shutdown() {
// 离开当前频道
if (playerId != null) {
leaveCurrentChannel();
}
if (channel != null) {
channel.close();
}
if (group != null) {
group.shutdownGracefully();
}
System.exit(0);
}
public String getPlayerId() {
return playerId;
}
public String getPlayerName() {
return playerName;
}
public void setPlayerInfo(PlayerInfo playerInfo) {
this.playerInfo = playerInfo;
}
public ConcurrentHashMap getOnlinePlayers() {
return onlinePlayers;
}
}
```
### 5.2 客户端编码器
```java
package com.game.chat.client;
import com.game.chat.protocol.ChatProtocol;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
/**
* 客户端编码器
*/
public class PacketEncoder extends MessageToByteEncoder {
@Override
protected void encode(ChannelHandlerContext ctx, ChatProtocol.GameMessage msg, ByteBuf out) throws Exception {
byte[] data = msg.toByteArray();
// 写入数据长度(4字节)
out.writeInt(data.length);
// 写入数据
out.writeBytes(data);
}
}
```
### 5.3 客户端解码器
```java
package com.game.chat.client;
import com.game.chat.protocol.ChatProtocol;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
/**
* 客户端解码器
*/
public class PacketDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List