《从零开始搭建游戏服务器》 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());
运行结果: