上一篇文章我们使用Netty搭建了最简单的游戏服务器,并且接收到了前端的信息,那这些信息如何处理呢,本篇文章我们会使用ProtoBuf来处理这些信息。
消息协议
与使用http协议传输数据不用,在游戏服务器中我们需要自定义消息协议,这里我们定义的消息包含三部分,消息的长度,消息的编号(是入场消息、移动消息、或者其他消息),消息体,其中消息的长度占2字节,消息编号占2字节,剩余的则为消息体。
Protobuf 命令行工具
- https://github.com/protocolbuffers/protobuf
- 到 releases 页面中找到下载链接;
- 解压缩后设置 path 环境变量;
- 执行命令 protoc;
- protoc.exe --java_out=${目标目录} .\GameMsgProtocol.proto
- 使用Protobuf 命令行工具可根据GameMsgProtocol.proto定义的消息为我们自动生成对应的java代码,或者其他语言的代码,让我们可以较为灵活快速的定义各种消息,比如在本项目中我们定义的进场消息或者谁在场消息等
GameMsgProtocol.proto和使用命令行工具生成后的GameMsgProtocol.java见最下方附录,大家也可以直接粘贴使用
1.创建package:com.tk.tinygame.herostory.msg,将GameMsgProtocol.java放入(文章最后有GameMsgProtocol.java链接)
2.根据消息协议的内容创建解码器GameMsgDecoder.java,用于解码客户端发来的消息
package com.tk.tinygame.herostory;
import com.google.protobuf.GeneratedMessageV3;
import com.tk.tinygame.herostory.msg.GameMsgProtocol;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 自定义的消息解码器
*/
public class GameMsgDecoder extends ChannelInboundHandlerAdapter {
/**
* 日志对象
*/
static private final Logger LOGGER = LoggerFactory.getLogger(GameMsgDecoder.class);
/**
* @param ctx
* @param msg
* @throws Exception
* @deprecated 用于解码消息
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (null == ctx || null == msg) {
return;
}
if (!(msg instanceof BinaryWebSocketFrame)) {
return;
}
try {
/**
* 读取消息,获取消息长度、编号和消息体
*/
BinaryWebSocketFrame inputFrame = (BinaryWebSocketFrame)msg;
ByteBuf byteBuf = inputFrame.content();
byteBuf.readShort(); // 读取消息的长度
int msgCode = byteBuf.readShort(); // 读取消息编号
byte[] msgBody = new byte[byteBuf.readableBytes()]; //拿到消息体
byteBuf.readBytes(msgBody);
GeneratedMessageV3 cmd = null;
/**
* 根据msgCode消息编号进行不同类型的解码
*/
switch (msgCode) {
//用户入场消息,解码为UserEntryCmd类型
case GameMsgProtocol.MsgCode.USER_ENTRY_CMD_VALUE:
cmd = GameMsgProtocol.UserEntryCmd.parseFrom(msgBody);
break;
//获取谁还在场消息,解码为WhoElseIsHereCmd类型
case GameMsgProtocol.MsgCode.WHO_ELSE_IS_HERE_CMD_VALUE:
cmd = GameMsgProtocol.WhoElseIsHereCmd.parseFrom(msgBody);
break;
default:
break;
}
if (null != cmd) {
ctx.fireChannelRead(cmd);
}
}catch (Exception ex){
// 记录错误日志
LOGGER.error(ex.getMessage(),ex);
}
}
}
3.根据消息协议的内容创建编码器GameMsgEncoder.java,用于编码消息
package com.tk.tinygame.herostory;
import com.google.protobuf.GeneratedMessageV3;
import com.tk.tinygame.herostory.msg.GameMsgProtocol;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 游戏消息编码器
*/
public class GameMsgEncoder extends ChannelOutboundHandlerAdapter {
/**
* 日志对象
*/
static private final Logger LOGGER = LoggerFactory.getLogger(GameMsgEncoder.class);
/**
* @param ctx
* @param msg
* @param promise
* @throws Exception
* @deprecated 编码数据
*/
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (null == ctx || null == msg) {
return;
}
if (!(msg instanceof GeneratedMessageV3)) {
super.write(ctx, msg, promise);
return;
}
try{
// 消息编码
int msgCode = -1;
/**
* 根据消息类型获取消息编号
*/
if (msg instanceof GameMsgProtocol.UserEntryResult) {
msgCode = GameMsgProtocol.MsgCode.USER_ENTRY_RESULT_VALUE;
} else if (msg instanceof GameMsgProtocol.WhoElseIsHereResult) {
msgCode = GameMsgProtocol.MsgCode.WHO_ELSE_IS_HERE_RESULT_VALUE;
} else {
LOGGER.error(
"无法识别的消息类型, msgClazz = {}",
msg.getClass().getSimpleName()
);
super.write(ctx, msg, promise);
return;
}
// 消息体
byte[] msgBody = ((GeneratedMessageV3) msg).toByteArray();
ByteBuf byteBuf = ctx.alloc().buffer();
byteBuf.writeShort((short) msgBody.length); // 消息的长度
byteBuf.writeShort((short) msgCode); // 消息编号
byteBuf.writeBytes(msgBody); // 消息体
// 写出 ByteBuf
BinaryWebSocketFrame outputFrame = new BinaryWebSocketFrame(byteBuf);
super.write(ctx, outputFrame, promise);
}catch (Exception ex){
LOGGER.error(ex.getMessage(),ex);
}
}
}
4.将GameMsgDecoder、GameMsgEncoder加入ServerMain中的pipline,相当于把编解码器加入我们的工作流程中,之前只有消息处理类GameMsgHandler
package com.tk.tinygame.herostory;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 服务器入口类
*/
public class ServerMain {
/**
* 日志对象
*/
static private final Logger LOGGER = LoggerFactory.getLogger(ServerMain.class);
/**
* 应用主函数
* @param args 参数数组
*/
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup(); //拉客的group
EventLoopGroup workGroup = new NioEventLoopGroup(); //干活的group
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,workGroup);
b.channel(NioServerSocketChannel.class); //服务器信道的处理方式
b.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
new HttpServerCodec(), // Http 服务器编解码器
new HttpObjectAggregator(65535), // 内容长度限制
new WebSocketServerProtocolHandler("/websocket"), // WebSocket 协议处理器, 在这里处理握手、ping、pong 等消息
new GameMsgDecoder(),
new GameMsgEncoder(),
new GameMsgHandler() // 自定义的消息处理器
);
}
});
b.option(ChannelOption.SO_BACKLOG,128);
b.childOption(ChannelOption.SO_KEEPALIVE,true);
try {
// 绑定 12345 端口,
// 注意: 实际项目中会使用 argArray 中的参数来指定端口号
ChannelFuture f = b.bind(12345).sync();
if (f.isSuccess()) {
LOGGER.info("服务器启动成功!");
}
// 等待服务器信道关闭,
// 也就是不要立即退出应用程序, 让应用程序可以一直提供服务
f.channel().closeFuture().sync();
} catch (Exception ex) {
//关闭服务器
workGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
LOGGER.error(ex.getMessage(),ex);
}
}
}
5.最后我们需要修改我们的消息处理类,对对应的消息做出响应
- 创建信道组,用于群发消息
static private final ChannelGroup _channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
- 创建用户字典,用于记录当前登录的用户
static private final Map<Integer, User> _userMap = new HashMap<>();
- 当用户登录时,需要将用户加入到信道组中,使得需要广播的消息可以发到每一个用户
@Override
public void channelActive(ChannelHandlerContext ctx) {
if (null == ctx) {
return;
}
try {
super.channelActive(ctx);
//建立长连接后,将信道添加到信道组
_channelGroup.add(ctx.channel());
} catch (Exception ex) {
// 记录错误日志
LOGGER.error(ex.getMessage(), ex);
}
}
- 在channelRead0中处理用户进场逻辑
当用户进场时,会先将登录的用户加入用户字典,然后构建用户信息并广播给信道组中的所有用户 - 在channelRead0中处理谁在场逻辑
当用户进场时,会发出谁在场的消息,服务端接收到消息后,遍历用户字典,把用户字典中的所有用户返回给当前用户
package com.tk.tinygame.herostory;
import com.tk.tinygame.herostory.msg.GameMsgProtocol;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义消息处理器
*/
public class GameMsgHandler extends SimpleChannelInboundHandler<Object> {
/**
* 日志对象
*/
static private final Logger LOGGER = LoggerFactory.getLogger(GameMsgHandler.class);
/**
* 信道组, 注意这里一定要用 static,
* 否则无法实现群发
*/
static private final ChannelGroup _channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
/**
* 用户字典
*/
static private final Map<Integer, User> _userMap = new HashMap<>();
/**
* 信道组
* @param ctx
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
if (null == ctx) {
return;
}
try {
super.channelActive(ctx);
//建立长连接后,将信道添加到信道组
_channelGroup.add(ctx.channel());
} catch (Exception ex) {
// 记录错误日志
LOGGER.error(ex.getMessage(), ex);
}
}
/**
* @param ctx
* @param msg
* @throws Exception
* @deprecated 处理用户消息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
if (null == ctx || null == msg) {
return;
}
LOGGER.info("收到客户端消息, msgClzz={},msgBody = {}",
msg.getClass().getName(),msg);
/**
* 根据消息类型作对应处理
*/
try {
if (msg instanceof GameMsgProtocol.UserEntryCmd) {
//
// 处理用户入场消息
//
GameMsgProtocol.UserEntryCmd cmd = (GameMsgProtocol.UserEntryCmd) msg;
int userId = cmd.getUserId(); //用户id
String heroAvatar = cmd.getHeroAvatar(); //英雄形象
//将登录的用户加入用户字典
User newUser = new User();
newUser.userId = userId;
newUser.heroAvatar = heroAvatar;
_userMap.putIfAbsent(userId, newUser);
GameMsgProtocol.UserEntryResult.Builder resultBuilder = GameMsgProtocol.UserEntryResult.newBuilder();
resultBuilder.setUserId(userId);
resultBuilder.setHeroAvatar(heroAvatar);
// 构建结果并广播
GameMsgProtocol.UserEntryResult newResult = resultBuilder.build();
_channelGroup.writeAndFlush(newResult);
} else if (msg instanceof GameMsgProtocol.WhoElseIsHereCmd) {
//
// 处理还有谁在场消息
//
GameMsgProtocol.WhoElseIsHereResult.Builder resultBuilder = GameMsgProtocol.WhoElseIsHereResult.newBuilder();
//遍历用户字典
for (User currUser : _userMap.values()) {
if (null == currUser) {
continue;
}
GameMsgProtocol.WhoElseIsHereResult.UserInfo.Builder userInfoBuilder = GameMsgProtocol.WhoElseIsHereResult.UserInfo.newBuilder();
userInfoBuilder.setUserId(currUser.userId);
userInfoBuilder.setHeroAvatar(currUser.heroAvatar);
resultBuilder.addUserInfo(userInfoBuilder);
}
//把用户字典用的用户广播
GameMsgProtocol.WhoElseIsHereResult newResult = resultBuilder.build();
ctx.writeAndFlush(newResult);
}
} catch (Exception ex) {
// 记录错误日志
LOGGER.error(ex.getMessage(), ex);
}
}
}
测试服务器代码:
1.登录第一个用户:http://cdn0001.afrxvk.cn/hero_story/demo/step010/index.html?serverAddr=127.0.0.1:12345&userId=1
登录后服务器显示收到了第一个用户的进场信息和谁在场信息
2.登录第二个用户:[http://cdn0001.afrxvk.cn/hero_story/demo/step010/index.html?serverAddr=127.0.0.1:12345&userId=2]
同样输出了类似的信息,此时前端效果如下:可以看出,两个人物同时在线
GameMsgProtocol.proto地址:https://www.jianshu.com/p/42e6018a8f5a
因为GameMsgProtocol.java内容过长无法直接发送,如有需要GameMsgProtocol.java的朋友也可以私信我