开发日常小结(2):如何使用Google提供的Protocol buffer协议实现序列化呢?--游戏公告序列化实例详解

2018年4月6日

一般的商业项目,其前后端之间的数据交互要求使用更加高效的方式,google就提供了这种解决办法,那就是 序列化框架protocol buffer;这原本是Google内部开发的技术,后来开源给大众使用(佩服佩服!!);现在做的游戏项目也使用了该框架,因此做个总结和归纳。

1、protocol buffer 概念:


1)Protocol Buffers(也称protobuf)是Google公司出口的一种独立于开发语言(混合语言数据标准.),独立于平台的可扩展的结构化数据序列机制。通俗点来讲它跟xml和json是一类。是一种数据交互格式协议。


2)其最大的特点是基于二进制,因此比传统的XML表示高效短小得多。虽然是二进制数据格式,但并没有因此变得复杂,开发人员通过按照一定的语法定义结构化的消息格式,然后送给命令行工具,工具将自动生成相关的类,可以支持php、java、c++、python等语言环境。通过将这些类包含在项目中,可以很轻松的调用相关方法来完成业务消息的序列化与反序列化工作。


2、与其他编码方式比较

序列化设计到编码,目前可以用的技术如下:

1)使用JSON,将java对象转换成JSON结构化字符串。在web应用、移动开发方面等,基于Http协议下,这是常用的,因为JSON的可读性较强。性能稍差。

2)基于XML,和JSON一样,数据在序列化成字节流之前,都转换成字符串。可读性强,性能差,异构系统、open api类型的应用中常用。

3)使用JAVA内置的编码和序列化机制,可移植性强,性能稍差。无法跨平台(语言)。

4)其他开源的序列化/反序列化框架,比如Apache Avro,Apache Thrift,这两个框架和Protobuf相比,性能非常接近,而且设计原理如出一辙;

其中Avro在大数据存储(RPC数据交换,本地存储)时比较常用;

Thrift的亮点在于内置了RPC机制,所以在开发一些RPC交互式应用时,Client和Server端的开发与部署都非常简单。

评价一个序列化框架的优缺点,大概有2个方面:

1)结果数据大小,原则上说,序列化后的数据尺寸越小,传输效率越高。 

2)结构复杂度,这会影响序列化/反序列化的效率,结构越复杂,越耗时。

Protobuf是一个高性能、易扩展的序列化框架,它的性能测试有关数据可以参看官方文档。通常在TCP Socket通讯(RPC调用)相关的应用中使用;它本身非常简单,易于开发,而且结合Netty框架可以非常便捷的实现一个RPC应用程序,同时Netty也为Protobuf解决了有关Socket通讯中“半包、粘包”等问题(反序列化时,字节成帧)。


3、protocol buffer 安装

可查阅其他相关博客,网上资料很多;但是我正在使用的eclipse通过安装插件来使用了protocol buffer,所以没在电脑安装;


4、protocol buffer 使用


第一步, 写一个proto的文件 .定义你需要的数据结构.

每二步, 使用你想要用的语言的proto文件编译器把写的proto文件编译为目标语言的相关类. (目前google提供了 C++、Java、Python 三种语言的 API).

第三步, 把第二步生成的类包含到你写的程序中, 就可以使用它了.


假设要完成游戏项目的一个后台公告功能,即后台将保存在redis的内容(序列化的数据)转为java 对象(反序列化),然后代码才能继续使用;下面为实例

编写proto文件:

WorldChannelCmdProto.proto:

//公告ID状态
message ProclamationStatusProto{
	optional int32 proclamationId = 1;			 
	optional int32 proclamationReadState = 2;	 
}

//用户公告
message RoleProclamationInfoProto{
	repeated ProclamationStatusProto proclamationStatusMap = 1;
}

//公告
message ProclamationInfoProto{
	optional int32 type = 1;				 
	optional int64 postTime = 2;				 
	optional string content = 3;				 
	optional int32 packageInfo = 4;				 
	optional int32 proclamationReadState = 5;		 
	optional int32 proclamationId = 6;			 
}

//公告列表
message ProclamationInfoListProto{
	repeated ProclamationInfoProto proclamationInfoList = 1;
}

1)上面声明了四个message:ProclamationStatusProto、RoleProclamationInfoProto、 ProclamationInfoProto、 ProclamationInfoListProto;


2)在消息定义中,我们需要确定三个问题:一、确定消息命名,二、给消息取一个有意义的名字。

ProclamationInfoProto 指定了6个字段,它们的字段类型都是optional,标量类型有int32/int64/string,字段编号为1 -- 6,

在Protocol Buffers中,字段的编号非常重要,字段名仅仅是作为参考和生成代码用

同理,

ProclamationInfoListProto指定了1个字段,字段类型是repeated,标量类型有ProclamationInfoProto ,字段编号为1 ,

在Protocol Buffers中,可以指定字段的类型为其他message类型

ProclamationStatusProto 指定了2个字段,RoleProclamationInfoProto 指定 ProclamationStatusProto 字段;

在Protocol Buffers中,可以同时定义多个message类型,生成代码时根据生成代码的目标语言不同,处理的方式不太一样,如Java会针对每个message类型生成一个.java文件


3)



运行脚本编译proto为protobuffer类


1)里面的内容是:

protoc -I=../../main/java/com/xs/fun/base/proto --java_out=../../main/java/ ../../main/java/com/xs/fun/base/proto/*.proto 
pause

言简意赅,输入为proto文件 (路径),输出为java文件(路径);


2)双击运行,结果如下:



PS,机器生成的代码很糟糕(各方面都是。。。),所以不展示了;


要如何使用才能使用Protocol Buffer呢?

回归根源,为什么要序列化呢?发送数据?存储数据?是的,数据流动所需。项目中,往往要保存数据到redis里面,而这个过程就是先将java对象序列化然后再保存到redis;一开始我还是懵逼的,但是慢慢的研究下去,便柳暗花明又一村了。比如我要保存一个数据对象,就是上面提到的worldChannelProto中定义的message:ProclamationInfoListProto,将它存入Redis 数据库。

没错,道理和java内置的序列化一样,要继承ProtobufSerializable接口!

第一步,新建 ProclamationInfoList.java,导入jar包; 

ProclamationInfoList.java:

public class ProclamationInfoList implements ProtobufSerializable{}


第二步,先看一下ProtobufSerializable接口:

ProtobufSerializable.java:

package com.xs.fun.base.dao.redis.base;

import com.google.protobuf.GeneratedMessage;

public interface ProtobufSerializable {
	
	void copyFrom(GeneratedMessage message);
	
	GeneratedMessage copyTo();
	
	void parseFrom(byte[] bytes);

	byte[] toByteArray();
}


个人对上面四个方法的理解:

1、copyFrom:无返回值,但传入参数是proto文件的message,顾名思义,就是从message获取信息,赋值到java数据结构;

2、copyTo:无传参,但是有返回参数GeneratedMessage ,顾名思义,就是从java数据结构获取信息,赋值到message,然后返回值;

3、parseFrom,即传入了二进制数组,然后将它“格式刷”成 需要的protoco message;

4、toByteArray,即将protoco message转化为二进制数组;


第三步、将需要实现接口的方法填充

上面的四个方法实现,是有“玄机”在里头的:

proto文件:


1、message 包含了另一个message 对象,所以,当你要序列化ProclamationInfoListProto,就要先序列化ProclamationInfoProto,将序列化的ProclamationInfoProto,一个个存入列表ProclamationInfoListProto里面,然后整体保存进redis。。

2、这说明,ProclamationInfoProto也要有对应的java数据结构(说了句废话,,)


第四步、新建 ProclamationInfo.java:

package com.xs.fun.base.bo.proclamation;

import com.google.protobuf.GeneratedMessage;
import *******.WorldChannelProtoBuffer.ProclamationInfoProto;

public class ProclamationInfo {
	private int type;
	
	private long postTime;
	
	private String content;
	
	private int packageInfo;
	
	private int proclamationReadState;

	private int proclamationId;
	
	public ProclamationInfo() {}
	
	public ProclamationInfo(ProclamationInfoProto proto){	//特殊的构造方法
		this.type = proto.getType();
		this.postTime = proto.getPostTime();
		this.content = proto.getContent();
		this.packageInfo = proto.getPackageInfo();
		this.proclamationReadState = proto.getProclamationReadState();
		this.proclamationId = proto.getProclamationId();
	}
	
	public void copyFrom(GeneratedMessage message){
		ProclamationInfoProto proto = (ProclamationInfoProto)message;
		this.type = proto.getType();
		this.postTime = proto.getPostTime();
		this.content = proto.getContent();
		this.packageInfo = proto.getPackageInfo();
		this.proclamationReadState = proto.getProclamationReadState();
		this.proclamationId = proto.getProclamationId();
	}
	
	public ProclamationInfoProto copyTo(){
		ProclamationInfoProto.Builder builder = ProclamationInfoProto.newBuilder();
		builder.setType(this.type);
		builder.setPostTime(this.postTime);
		builder.setContent(this.content);
		builder.setPackageInfo(this.packageInfo);
		builder.setProclamationReadState(this.proclamationReadState);
		builder.setProclamationId(this.proclamationId);
		return builder.build();
	}

	***这里省略一大堆的 getter和setter***
	
}

研究上面的代码,发现了啥?没错,有一个特殊的构造方法,它的作用就是:直接导入了message protobuffer(自动生成的java类:WorldChannelProtoBuffer.ProclamationInfoProto),然后使用赋值。因次也解答了这部分的疑惑, 要如何使用才能使用Protocol Buffer呢


第五步,回到 ProclamationInfoList.java,附完整代码:

package com.xs.fun.base.bo.proclamation;

import java.util.LinkedList;

import com.google.protobuf.GeneratedMessage;
import com.google.protobuf.InvalidProtocolBufferException;
import com.xs.fun.base.dao.redis.base.ProtobufSerializable;
import com.xs.fun.base.proto.service.WorldChannelProtoBuffer.ProclamationInfoListProto;
import com.xs.fun.base.proto.service.WorldChannelProtoBuffer.ProclamationInfoProto;


public class ProclamationInfoList implements ProtobufSerializable{
	private LinkedList<ProclamationInfo> proclamationInfoList = new LinkedList<>();
	
	private final int LIST_SIZE_LIMIT = 200;
	
	@Override
	public void copyFrom(GeneratedMessage message) {
		ProclamationInfoListProto proto = (ProclamationInfoListProto)message;
		for(ProclamationInfoProto itemProto : proto.getProclamationInfoListList()){
			proclamationInfoList.add(new ProclamationInfo(itemProto));
		}
	}

	@Override
	public ProclamationInfoListProto copyTo() {
		ProclamationInfoListProto.Builder builder = ProclamationInfoListProto.newBuilder();
		for (ProclamationInfo info : proclamationInfoList) {
			builder.addProclamationInfoList(info.copyTo());
		}
		
		return builder.build();
	}

	@Override
	public void parseFrom(byte[] bytes) {
		try {
			ProclamationInfoListProto proto = ProclamationInfoListProto.parseFrom(bytes);
			copyFrom(proto);
		} catch (InvalidProtocolBufferException e) {
			e.printStackTrace();
		}
	}

	@Override
	public byte[] toByteArray() {
		return copyTo().toByteArray();
	}

	public LinkedList<ProclamationInfo> getProclamationInfoList() {
		return proclamationInfoList;
	}

	public void setProclamationInfoList(
			LinkedList<ProclamationInfo> ProclamationInfoList) {
		this.proclamationInfoList = proclamationInfoList;
	}
	
	public void addProclamationInfo(ProclamationInfo info){
		proclamationInfoList.addFirst(info);
		if (proclamationInfoList.size() > LIST_SIZE_LIMIT) {
			proclamationInfoList.remove(LIST_SIZE_LIMIT);
		}
	}
}

是的,这里面就是所有的序列化和反序列化操作了;对于包含其他message的复杂结构的message proto,序列化是不能跳过子 message的;同理,一个java数据对象的序列化,其实是调用了 执行脚本自动生成protobuffer类的方法(api)来完成。


文末,一张图作总结:




猜你喜欢

转载自blog.csdn.net/qq_29166327/article/details/79832613
今日推荐