《从零开始搭建游戏服务器》 Protobuf读取Excel表格数据
本篇文章会和大家介绍下如何用Protobuf读取Excel表格数据,有不了解的开发者可以看看下面的分享,会对你们用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转化为.java的JavaBean脚本,修改之后如下:
::--------------------------------------------------- ::第一步,将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%\?TA_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资源网进行搜索:- 然而,在经过我的测试,返现这个类只对通过protobuf数据类在代码中创建的protobuf数据有效,但是对于通过表格定义然后导表所得到的表格数据,显然还存在着缺陷,特别是当前表格定义的是一行一条对应的结构数据,导出来的二进制文件解析出来是对应数据结构的列表,例如这里每行数据是一个 - Person数据,那么整个表导出来就是一个- PersonArray,但是使用- ProtoStuffSerializerUtil解析这种数据结构会出现解析报错。
<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原生的反序列化方式来定义一个解析表格数据的函数,为了使其更为通用,我使用泛型来定义此函数,- 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;
    }
}
总结优化:
假如有多张表格,我们可以在代码中构建一个管理表格配置数据的数据中心,在服务器启动时将表格都读取到内存中,表格数据反序列之后存在易用的数据结构中,这里我简单写了一个类似的工具类:
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());
运行结果: 

