从零开始搭建游戏服务器 第三节 Protobuf的引入并使用

目录

上一节问题答案公布

上一节我们创建了ConnectActor,并且使用ConnectActorManager和connectId将其管理起来。

并且我们在收到客户端上行数据时,对指定的ConnectActor发送了一条BaseMsg消息。

上一节笔者留下来的作业答案在此公布,应该不困难,步骤如下:

  1. 修改BaseActor.java
java 复制代码
	@Override
   public Receive<BaseMsg> createReceive() {
       ReceiveBuilder<BaseMsg> builder = newReceiveBuilder();
       onCreateReceive(builder);
       builder.onMessage(BaseMsg.class, this::onBaseMsg);
       return builder.build();
   }

   protected void onCreateReceive(ReceiveBuilder<BaseMsg> builder){}
复制代码
添加了一个onCreateReceive方法用于各个Actor自己注册消息回调方法。
  1. 创建ClientUpMsg和ConnectClosedMsg
java 复制代码
/**
* 客户端上行数据
*/
public class ClientUpMsg extends BaseMsg {

   private final byte[] data;

   public ClientUpMsg(byte[] data) {
       this.data = data;
   }

   public byte[] getData() {
       return data;
   }
}

/**
1. 连接断开信息
*/
public class ConnectClosedMsg extends BaseMsg {
}
  1. 修改ConnectActor重写onCreateReceive方法
java 复制代码
   @Override
   protected void onCreateReceive(ReceiveBuilder<BaseMsg> builder) {
       builder.onMessage(ClientUpMsg.class, this::onClientUpMsg);
       builder.onMessage(ConnectClosedMsg.class, this::onConnectClosedMsg);
   }

   /**
    * 客户端上行数据
    */
   private Behavior<BaseMsg> onClientUpMsg(ClientUpMsg msg) {
       log.info("receive client up msg. {}", new String(msg.getData()));
       return this;
   }

   /**
    * 连接关闭
    * 移除connectActor
    */
   private Behavior<BaseMsg> onConnectClosedMsg(ConnectClosedMsg msg) {
       log.info("receive connect closed msg.");
       ConnectActorManager.getInstance().removeConnectActor(connectId);
       return this;
   }
  1. 修改LoginNettyHandler使其在不同的情况下发送不同的消息给ConnectActor
java 复制代码
   /**
    * 收到协议数据
    */
   @Override
   protected void channelRead0(ChannelHandlerContext ctx, byte[] msg) throws Exception {
       HashMap<String, Object> context = this.getContextAttrMap(ctx);
       long connectId = (long)context.get("connectId");
       ConnectActorManager actorManager = ConnectActorManager.getInstance();
       ActorRef<BaseMsg> connectActor = actorManager.getConnectActor(connectId);
       if (connectActor == null) {
           connectActor = actorManager.createConnectActor(connectId, ctx);
       }
       ClientUpMsg clientUpMsg = new ClientUpMsg(msg);
       connectActor.tell(clientUpMsg);
   }
   /**
    * 连接断开
    */
   @Override
   public void channelInactive(ChannelHandlerContext ctx) throws Exception {
       HashMap<String, Object> contextAttrMap = this.getContextAttrMap(ctx);
       long connectId = (long) contextAttrMap.get("connectId");
       ActorRef<BaseMsg> actorRef = ConnectActorManager.getInstance().getConnectActor(connectId);
       if (actorRef != null) {
           actorRef.tell(new ConnectClosedMsg());
       } else {
           log.info("onClose时 connectActor不存在,直接跳过了。 connectId={}", connectId);
       }
       log.info("连接断开, connectId={}", connectId);
   }

测试一下:

启动LoginServer和Client,等待连接完成后在Client端控制台分别输入test和stop。

本节内容

本节我们将引入protobuf, 并使用protobuf生成对应的java类, 然后在Client中将protobuf消息发送到LoginServer.

Protobuf介绍

Protobuf是Google公司开发的一种灵活,高效,自动化地序列化结构数据的方法,类似于XML、JSON、YAML等。

但是它比上述格式更小、更快、更灵活。

我们可以编写.proto文件定义数据的结构,然后用其提供的工具生成对应语言的代码。

正文

在build.gradle引入protobuf

groovy 复制代码
        // protobuf
        implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '3.25.3'

创建几个目录用于保存protobuf文件 common模块下添加org.protobuf包, 与commmon区分开的原因是减少spring扫描的文件加快启动速度.

在根目录下创建protobuf目录用于保存proto源文件, 然后生成的java代码放到common下的org.protobuf包

编写proto并生成

在protobuf目录新建PlayerMsg.proto和ProtoEnumMsg.proto

分别用于存放玩家相关协议结构和协议号定义

java 复制代码
syntax = "proto3";

option java_outer_classname = "PlayerMsg";
option java_package = "org.protobuf";


// 玩家注册
message C2SPlayerRegister { // 客户端上行包,返回S2CPlayerRegister
    string accountName = 1; // 账号
    string password = 2;    // 密码
}
message S2CPlayerRegister {
    bool success = 1;   // 是否成功
}
java 复制代码
syntax = "proto3";

option java_outer_classname = "ProtoEnumMsg";
option java_package = "org.protobuf";

// 所有协议号
message CMD {
    enum ID {
        DEFAULT = 0;
        // 玩家注册
        PLAYER_REGISTER = 10101;
    }
}

IDEA安装genprotobuf插件

修改一下插件的配置,使其默认生成java类

选中我们刚创建的两个Msg,右键生成protobuf类

然后将生成的文件移动到common模块下的org.protobuf包

至此完成proto文件的编写和生成.

使用生成的proto来进行数据传输

修改clientMain下的handleBackGroundCmd, 当我们输入register时就发送一个C2SPlayerRegister消息到LoginServer.

java 复制代码
	@Override
    protected void handleBackGroundCmd(String cmd) {
        if (cmd.equals("test")) {
            channel.writeAndFlush("test".getBytes());
        } else if (cmd.equals("register")) {
            PlayerMsg.C2SPlayerRegister.Builder builder = PlayerMsg.C2SPlayerRegister.newBuilder();
            builder.setAccountName("clintAccount");
            builder.setPassword("123456");
            Pack pack = new Pack(ProtoEnumMsg.CMD.ID.PLAYER_REGISTER_VALUE, builder.build().toByteArray());
            byte[] data = PackCodec.encode(pack);
            channel.writeAndFlush(data);
        }
    }

当我们输入register时,创建一个PlayerMsg.C2SPlayerRegister.Builder, 往里面的字段赋值, 然后用Pack将其和上面定义的协议号打包, 最后整个协议包编码成byte[]后通过channel通道发送到LoginServer.

接下来修改LoginServer进行协议的接收与解码. 由于我们之前已经将Channel接收到的数据通过ClientUpMsg发送到了ConnectActor,所以我们只需要修改ConnectActor里的消息处理逻辑即可.

java 复制代码
/**
     * 客户端上行数据
     */
    private Behavior<BaseMsg> onClientUpMsg(ClientUpMsg msg) throws InvalidProtocolBufferException {
        Pack decode = PackCodec.decode(msg.getData());
        log.info("receive client up msg. cmdId = {}", decode.getCmdId());
        byte[] data = decode.getData();
        if (decode.getCmdId() == ProtoEnumMsg.CMD.ID.PLAYER_REGISTER_VALUE) {
            // 注册协议
            PlayerMsg.C2SPlayerRegister c2SPlayerRegister = PlayerMsg.C2SPlayerRegister.parseFrom(data);
            log.info("player register, accountName = {}, password = {}", c2SPlayerRegister.getAccountName(), c2SPlayerRegister.getPassword());
        }
        return this;
    }

在上述代码中,我们将byte[]编码成Pack,然后获得协议号, 因为每个协议号对应的协议结构是相同的,所以我们判断协议号为玩家注册后直接对其进行还原, 就能得到客户端上行的数据.

测试一下:

启动LoginServer, 启动Client

Client连接上后控制台输入register发送消息

可以看到LoginServer的控制台打印出了玩家注册日志

总结

本节的讲东西比较简单, 主要是proto文件的编写与生成, 以及如何对protobuf打包与解包. 这些在后续我们多使用就能熟练.

留一个作业, 在PlayerMsg中添加一个PlayerLogin的登录协议, 然后client输入login发送账号密码, LoginServer接收到后进行解包并输出到控制台中.

下一节将开始使用MongoDB进行数据的持久化保存, 为什么使用MongoDB是因为最近的手游公司使用MongoDB的占比越来越多, 一些以前使用MySQL的公司也开始逐渐切换到MongoDB.

相关推荐
Zhansiqi1 天前
day42部分题目
python
小王不爱笑1321 天前
IO 模型
开发语言·python
巨斧空间掌门1 天前
JDK17 下载 windows Linux
linux·运维·服务器
kishu_iOS&AI1 天前
Conda 简要说明与常用指令
python·安全·conda
小陈工1 天前
FastAPI性能优化实战:从每秒100请求到1000的踩坑记录
python·性能优化·django·flask·numpy·pandas·fastapi
知我Deja_Vu1 天前
【避坑指南】ConcurrentHashMap 并发计数优化实战
java·开发语言·python
njidf1 天前
用Python制作一个文字冒险游戏
jvm·数据库·python
江畔何人初1 天前
kube-apiserver、kube-proxy、Calico 关系
运维·服务器·网络·云原生·kubernetes
魔士于安1 天前
unity 圆盘式 太空飞船
游戏·unity·游戏引擎·贴图·模型
呆呆小孩1 天前
Anaconda 被误删抢救手册:从绝望到重生
python·conda