《从零开始搭建游戏服务器》 Protobuf读取Excel表格数据

前言
在之前的博客中,我们已经尝试过了在C#环境下,使用Protobuf来序列化Excel表格,然后再代码中反序列化出表格数据【Unity3D —— protobuf 导excel表格数据】,如今我们在服务器假如要共用一套配置表格,当需要一个类似的导表和读表过程,接下来我就在原来博客的基础上稍加修改,完成这个需求。

准备工作:

为了方便起见,我直接复制了之前的一些工具,假如不了解的也可以直接在此处下载资源包:Protobuf导Excel表C#版,然后跟着我的步骤来进行修改。

修改导表过程:

在之前我们的导表脚本中,例如:

::---------------------------------------------------
::第一步,将xls经过xls_deploy_tool转成data和proto
::---------------------------------------------------
call python xls_deploy_tool.py PERSON xls/person.xls
::---------------------------------------------------
::第二步:把proto翻译成cs
::---------------------------------------------------
call protoc tnt_deploy_person.proto --descriptor_set_out=person.protodesc
call ProtoGen\protogen -i:person.protodesc -o:person.cs
pause

上面是之前C#版本导表的过程,在将.proto转成.cs之前,先转为.protodesc格式的中间状态,然后才能得到最终的.cs脚本。而在java版本中,我们可以跳过中间状态的,直接将.proto转化为.javaJavaBean脚本,修改之后如下:

::---------------------------------------------------
::第一步,将xls经过xls_deploy_tool转成data和proto
::---------------------------------------------------
call python xls_deploy_tool.py PERSON xls/person.xls
::---------------------------------------------------
::第二步:把proto翻译成java
::---------------------------------------------------
call protoc --java_out=java/ tnt_deploy_person.proto
::---------------------------------------------------
::第三步:清除中间文件
::---------------------------------------------------
@echo off
echo TRY TO DELETE TEMP FILES:
del *_pb2.py
del *_pb2.pyc
del *.proto
del *.data
del *.log
del *.txt
::---------------------------------------------------
::第四步,关闭窗口
::---------------------------------------------------
@echo on

因为省去了一个步骤,文件夹中ProtoGen文件目录下的工具我们也用不到了,可以直接删掉,并且这里我们也加入了中间文件的清理指令,我们还可以在批处理脚本中将文件直接拷贝到我们项目指定目录下。

改造表格思路:

为了方便策划进行表格的管理和修改,通常在开发过程中,客户端和服务器使用的是同一套表格,但是这就不免会有冗余的数据,因为有些数据列是客户端使用而对于服务端无用的。
所以,为了减少这些冗余数量,我们可以在表格的第一行加入一个标签,标志三种状态:C->客户端用,S->服务端用,CS->客户端和服务端都用到,如此,前端和后端都只导出需要用到的数据属性列,而跳过不用的列,表格修改如下:

为此对xls_deploy_tool.py这个导表的脚本也需要进行相应的修改,而且对于本在.xls表格中,数据页的命名只支持大写字母这一点,也需要进行优化改造,由于修改量还是比较大,这里不做细说,只是提供此优化思路。

优化脚本:

为了简化脚本内容,我们可以为每个表格创建一个.bat批处理脚本,但里面只是用来指定当前表格的名称,要导出数据的数据Sheet名称这两项信息,而真正的导表操作交给一个通用的脚本xlss_java.bat去处理,这里通用脚本修改后如下:

@echo off
::表格名称
set XLS_NAME=%1
::数据页名称
set SHEET_NAME=%2
::目标文件夹名称
set DATA_DEST=%3
::表格所在目录
set STEP1_XLS2PROTO_PATH = xls
::当前正在导的表格名称
echo.
echo =========Compilation of %XLS_NAME%.xls=========

@echo on
cd %STEP1_XLS2PROTO_PATH%
::---------------------------------------------------
::第一步,将xls经过xls_deploy_tool转成data和proto
::---------------------------------------------------
call python xls_deploy_tool.py %SHEET_NAME% xls\%XLS_NAME%.xls s
::---------------------------------------------------
::第二步:把proto翻译成java
::---------------------------------------------------
::生成表格列表
dir .\*.proto /b  > protolist.txt
@echo on
for /f "delims=." %%i in (protolist.txt) do protoc --java_out=. dataconfig_%SHEET_NAME%.proto
pause
::---------------------------------------------------
::第三步:把data和java复制到指定目录
::---------------------------------------------------
@echo off
set OUT_PATH=..\res\
set DATA_DEST=com\tw\login\protobufdata
set JAVA_DEST=..\src\
copy .\dataconfig_%SHEET_NAME%.data %OUT_PATH%\%DATA_DEST%\dataconfig_%SHEET_NAME%.bytes
pause
::---------------------------------------------------
::第三步:清除中间文件
::---------------------------------------------------
@echo off
echo TRY TO DELETE TEMP FILES:
del *_pb2.py
del *_pb2.pyc
del *.proto
del *.data
del *.log
del *.txt
::---------------------------------------------------
::第四步,关闭窗口
::---------------------------------------------------
@echo on

而指定表格的脚本,例如person.xls表格的Person也需要进行导表:

set ExcelFileName=person
set ExcelSheetName_1=Person

call xlss_java.bat %ExcelFileName% %ExcelSheetName_1%
pause

如此每次添加一张新表,不必复制大量的命令行内容,只需简短的几行声明即可。


项目实战:

通过上述的步骤,我们得到了表格数据序列化之后的.bytes二进制数据文件,还有对应每张表结构的JavaBean解析类,那么接下来我们要做的步骤大概如下:

  • 在java中读取二进制文件,获得byte[]字节数组;
  • 创建一个序列化和反序列化的工具类,用来实现将byte[]反序列化为对应的结构化数据还有将protobuf数据序列化为byte[]字节流。
读取二进制文件:

首先,关于二进制文件的读取操作,网上检索一下发现有一大堆方法,首先,我们在当前项目的根目录下创建一个res文件夹,把之前导表得到的所有.bytes二进制文件都拷贝到此文件目录下:

创建一个读取二进制文件,并转化为二进制数组返回给调用函数:

package com.tw.login.tools;
import java.io.BufferedInputStream;    
import java.io.BufferedOutputStream;    
import java.io.FileInputStream;    
import java.io.FileOutputStream;    
import java.io.IOException;    

/**  
 * 此工具类用于二进制文件的读写  
 *   
 * @author linsh
 */    
public class BinaryFile {    
    //读取res目录下的二进制文件
    public static byte[] readResFile(String filePath){
        return read("res/"+filePath+".bytes");
    }

    // 把二进制文件读入字节数组,如果没有内容,字节数组为null     
    public static byte[] read(String filePath) {    
        byte[] data = null;    
        try {    
            BufferedInputStream in = new BufferedInputStream(    
                    new FileInputStream(filePath));    
            try {    
                data = new byte[in.available()];    
                in.read(data);    
            } finally {    
                in.close();    
            }    
        } catch (IOException e) {    
            e.printStackTrace();    
        }    
        return data;    
    }    

    // 把字节数组为写入二进制文件,数组为null时直接返回     
    public static void write(String filePath, byte[] data) {    
        if (data == null)    
            return;    
        try {    
            BufferedOutputStream out = new BufferedOutputStream(    
                    new FileOutputStream(filePath));    
            try {    
                out.write(data);    
            } finally {    
                out.close();    
            }    
        } catch (IOException e) {    
            e.printStackTrace();    
        }    
    }    
}    

调用的方式很简单,例如这里我们要读取刚刚导入到res目录下的dataconfig_Person.bytes二进制文件:

byte[] bs = BinaryFile.readResFile("dataconfig_Person");
定义解析类:

关于解析类,其实就是将序列化后的表格数据,以二进制流读取,存在byte[]字节数组中,然后再讲数组中的数据转化为对应的protobuf数据结构类型。

  • protostuff插件:
    看到网上很多基于protostuff(这是一个基于protobuf开发的工具)定义一个自己的序列化和反序列化的工具类ProtoStuffSerializerUtil,由于反序列化需要使用到protostuff的一下API,需要在pom.xml中添加两个依赖,添加依赖时,假如不清楚如何定义版本和目录,可以在Maven资源网进行搜索:

    <dependency>
        <groupId>io.protostuff</groupId>
        <artifactId>protostuff-runtime-registry</artifactId>
        <version>1.5.3</version>
    </dependency>
    <dependency>
        <groupId>io.protostuff</groupId>
        <artifactId>protostuff-core</artifactId>
        <version>1.5.3</version>
    </dependency>

    然而,在经过我的测试,返现这个类只对通过protobuf数据类在代码中创建的protobuf数据有效,但是对于通过表格定义然后导表所得到的表格数据,显然还存在着缺陷,特别是当前表格定义的是一行一条对应的结构数据,导出来的二进制文件解析出来是对应数据结构的列表,例如这里每行数据是一个Person数据,那么整个表导出来就是一个PersonArray,但是使用ProtoStuffSerializerUtil解析这种数据结构会出现解析报错。

  • protobuf反序列化:
    由于我们一般对于表格数据都是只读不写,所以这里我还是使用protobuf原生的反序列化方式来定义一个解析表格数据的函数,为了使其更为通用,我使用泛型来定义此函数,MessageLite是所有protobuf数据对象的父类,弱化了传入数据的类型限制,达到通用的效果:

    package com.tw.login.protobufdata;
    
    import java.io.Serializable;
    import com.google.protobuf.InvalidProtocolBufferException;
    import com.google.protobuf.MessageLite;
    /**
     * 表格二进制数据反序列化接口
     * @author linsh
     *
     */
    @SuppressWarnings("serial")
    public class ProtobufSerializerUtil implements Serializable {
    
        @SuppressWarnings({ "unchecked"})
        public static <T> T deserialize(byte[] bs, MessageLite prototype) {
            MessageLite msg = null;
            try {
                msg = prototype.getDefaultInstanceForType().getParserForType().parseFrom(bs);
            } catch (InvalidProtocolBufferException e) {
                e.printStackTrace();
            }
            return (T) msg;
        }
    }

    结合上述读取二进制文件的接口,完整解析表格数据的调用方式如下:

PersonArray persons = ProtobufSerializerUtil.deserialize(BinaryFile.readResFile("dataconfig_Person"), PersonArray.getDefaultInstance());

总结优化:

假如有多张表格,我们可以在代码中构建一个管理表格配置数据的数据中心,在服务器启动时将表格都读取到内存中,表格数据反序列之后存在易用的数据结构中,这里我简单写了一个类似的工具类:

package com.tw.login.protobufdata;

import java.util.HashMap;
import java.util.Map;

import com.google.protobuf.MessageLite;
import com.tw.login.protobufdata.DataconfigPerson.Person;
import com.tw.login.protobufdata.DataconfigPerson.PersonArray;
import com.tw.login.tools.BinaryFile;
/**
 * 表格读取到内存,表格配置数据中心
 * @author linsh
 *
 */
public class ProtobufDataConfigCenter {
    private static ProtobufDataConfigCenter _Instance = null;
    //数据.bytes文件与对应的解析类绑定
    private static Map<String, MessageLite> file_map;
    //数据与结构体绑定
    private static Map<String, MessageLite> data_map;
    //指定表的数据结构
    private PersonArray persons;
    /**
     * 单例模式
     * @return
     */
    public static ProtobufDataConfigCenter Instance(){
        if(_Instance == null){
            _Instance = new ProtobufDataConfigCenter();
            Initialize();
        }
        return _Instance;
    }
    /**
     * 初始化
     */
    private static void Initialize() {
        data_map = new HashMap<String, MessageLite>();
        file_map = new HashMap<String, MessageLite>();
        //人物信息表
        file_map.put("dataconfig_Person", PersonArray.getDefaultInstance());
        LoadDataConfigs();
    }

    private static void LoadDataConfigs(){
        for(String _key:file_map.keySet()){
            MessageLite data = ProtobufSerializerUtil.deserialize(BinaryFile.readResFile(_key), _Instance.GetInstanceByFileName(_key));
            data_map.put(_key, data);
        }
    }
    /**
     * 通过数据文件名称获取对应的解析类
     * @param fileName
     * @return
     */
    public MessageLite GetInstanceByFileName(String fileName) {
        return file_map.get(fileName);
    }

    /**
     * 通过id获取玩家信息表的数据
     * @param _id
     * @return
     */
    public Person GetPersonDataById(int _id) {
        Person result = null;
        if(data_map.containsKey("dataconfig_Person")){
            persons = (PersonArray) data_map.get("dataconfig_Person");
            for(int i=0;i<persons.getItemsCount();i++){
                if(persons.getItems(i).getId() == _id){
                    return persons.getItems(i);
                }
            }
        }
        return result;
    }
}

这里public Person GetPersonDataById(int _id)是特定表格的外部调用接口,具体的导表和反序列化过程调用方无需了解,只需调用此接口即可得到想要的数据,测试一下:

Person person = ProtobufDataConfigCenter.Instance().GetPersonDataById(1);
logger.info("获取表格数据:----------------"+person.getUsername());

运行结果:

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值