通讯协议序列化解读(一) Protobuf详解教程
前言:说到JSON可能大家很熟悉,是目前应用最广泛的一种序列化格式,它使用起来简单方便,而且拥有超高的可读性。但是在越来越多的应用场景里,JSON冗长的缺点导致它并不是一种最优的选择。
一、常用序列化格式介绍
目前JAVA常用的序列化有protobuf,json,xml,Serializable,hessian,kryo。他们的优缺点如下:
-
JSON:不多说了,用途广泛,序列化方式还衍生了阿里的fastjson,美团的MSON,谷歌的GSON等更加优秀的转码工具。
优点:使用方便。
缺点:数据冗长,转码性能一般。 -
XML:很久之前的转码方法了,现在用的不多。
优点:暂时没发现。
缺点:数据冗长,转码性能一般。 -
Serialzable:JDK自带的序列化。
优点:使用方便。
缺点:转码性能低下。 -
hessian:基于 binary-RPC实现的远程通讯library,使用二进制传输数据。
优点:数据长度小。
缺点:性能低下。
说了这么多,全是性能低下,MMP一群智障儿?当然不是!kryo就是一款快速、高效的序列化框架,但是它不是我们今天的主角,因为他只能在java中使用,和前端非java语言的通讯就存在极大的隔阂。我们今天的主角是protobuf?emmm,算是吧,但是也不全是,先给大家说下protobuf吧。
- protobuf:谷歌公司出的一款开源项目,性能好,效率高,并且支持多种语言,例如:java,C++,python等。
优点:转码性能高,支持多语言。
缺点:中文文档少,使用相对复杂。
二、protobuf详解
在使用protobuf之前,需要安装protobuf编译器和运行时环境。
由于protobuf是跨平台,跨语言的,所以需要下载和安装对应版本的编译器和运行时依赖。
2.1 proto语法介绍
.proto Type | 说明 | C++ Type | Java Type | Python Type[2] | Go Type |
---|---|---|---|---|---|
double | double | double | float | float64 | |
float | float | float | float | float32 | |
int32 | 使用可变长度编码。对负数进行编码时比较低效 – 如果你的字段要使用负数值,请使用sint32来代替。 | int32 | int | int | int |
int64 | 使用可变长度编码。对负数进行编码时比较低效 – 如果你的字段要使用负数值,请使用sint64来代替。 | int64 | long | int/long[3] | int64 |
uint32 | 使用可变长度编码 | uint32 | int[1] | int/long[3] | uint32 |
uint64 | 使用可变长度编码 | uint64 | long[1] | int/long[3] | uint64 |
详细语法由于篇章太多不在此做介绍,详情点开另一篇博文:http://www.cnblogs.com/tohxyblog/p/8974763.html
1 2 3 4 5 6 | <!-- protobuf-谷歌 --> <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version> 3.5 . 1 </version> </dependency> |
2.2.1下载编译器编译文件
下载地址:https://github.com/google/protobuf/releases
选择对应系统的版本,下载后解压。
可以通过定义好的.proto文件来生成Java代码,需要基于.proto文件运行protocol buffer编译器protoc。如果你没有安装编译器,下载安装包并遵照README安装。
通过如下方式调用protocol编译器:
1 | protoc -I=/Users/rinzz04/Desktop/proto/proto --java_out=/Users/rinzz04/Desktop/proto/ /Users/rinzz04/Desktop/proto/proto/InitGame.proto |
-I=proto文件存放路径
- --java_out=生成的java文件夹目录
- 后面紧跟proto具体文件
proto文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | syntax = "proto3" ; message User { string userId = 1 ; string userName = 2 ; bool sex = 3 ; string openId = 4 ; string createTime = 5 ; string phoneNum = 6 ; string userImg = 7 ; string introduct = 8 ; } |
2.2.3protobuf使用教程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | //以user为例编码成byte[] UserOuterClass.User.Builder userBuild = UserOuterClass.User.newBuilder(); userBuild.setUserId(user.getUserId()); userBuild.setUserName(user.getUserName()); userBuild.setPhoneNum(user.getPhoneNum()); userBuild.setCreateTime(user.getCreateTime()); userBuild.setOpenId(user.getOpenId()); userBuild.setIntroduct(user.getIntroduct()); userBuild.setSex(user.isSex()); userBuild.setUserImg(user.getUserImg()); userBuild .toByteArray(); //得到byte[] //以user为例解码 UserOuterClass.User.Builder userBuild = UserOuterClass.User.newBuilder(); User user= user.build(); user=User.parseFrom(data.getValue().getBytes()); |
三、protobuf在实际操作中存在的问题
protobuf主要用于与前端通信编解码,那么在后台收到二进制如何存入到数据库中呢,或者说从数据库中取得的数据怎么映射到protobean呢。
由于protoc生成的java文件与我们平时写的java文件有区别,但是实际上都是有getset方法,不怕麻烦的童鞋可以直接通过两个类的值getset方法直接转换,效率可观,但是操作起来确实有些麻烦。这里我们提供一个更加便捷的工具类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | /** * 该方法将javabean对象转换成protobuf对应的bean * * @param javaBean * @param protoBuilder */ @SuppressWarnings ({ "unchecked" , "rawtypes" }) public static Object javaBeanToProtoBean(Object javaBean, Object protoBuilder) { try { Method mm = protoBuilder.getClass().getMethod( "getDescriptorForType" ); Descriptors.Descriptor descriptor = (Descriptor) mm.invoke(protoBuilder); Field[] fields = javaBean.getClass().getDeclaredFields(); for (Field item : fields) { try { String fName = item.getName(); item.setAccessible( true ); Object jObject = item.get(javaBean); if ( null == jObject){ break ; } FieldDescriptor fd = descriptor.findFieldByName(fName); if ( null != fd){ if (fd.isRepeated()){ boolean isDefined = false ; Method[] mmm = protoBuilder.getClass().getMethods(); for (Method mItem : mmm){ try { String mName = mItem.getName(); String mName1 = "add" + StringUtil.firstToUpper(fName); if (mName1.equals(mName) && mItem.getParameterTypes().length == 1 ){ Class[] ccList = mItem.getParameterTypes(); Class cc = ccList[ 0 ]; Method me = cc.getMethod( "newBuilder" ); Object oBuilder = me.invoke( null ); //获取自定义对象builder List<Object> dList = (List<Object>) jObject; //数据为List集合 List<Object> pBeanList = new ArrayList<Object>(); for (Object oItem : dList){ Object pBean = javaBeanToProtoBean(oItem,oBuilder); pBeanList.add(pBean); } Method mee = protoBuilder.getClass().getMethod( "addAll" +StringUtil.firstToUpper(fName),Iterable. class ); mee.invoke(protoBuilder, pBeanList); isDefined = true ; } } catch (Exception e){ } } if (!isDefined){ try { Method me = protoBuilder.getClass().getMethod( "addAll" +StringUtil.firstToUpper(fName),Iterable. class ); me.invoke(protoBuilder, jObject); } catch (Exception e){ logger .info( "this repeated field is a user-defined field" ); e.printStackTrace(); } } } else { boolean isDefined1 = false ; try { // 自定义对象继续需要通过builder来解析处理,回调、 这一块很占计算时间。有待优化 Method bM = protoBuilder.getClass().getMethod( "getFieldBuilder" , FieldDescriptor. class ); Object subBuilder = bM.invoke(protoBuilder, fd); Object pBean = javaBeanToProtoBean(jObject,subBuilder); Method me = protoBuilder.getClass().getMethod( "setField" , FieldDescriptor. class , Object. class ); me.invoke(protoBuilder, fd, pBean); isDefined1 = true ; } catch (Exception e){ // logger .info("this required field is not a user-defined field"); } if (!isDefined1){ Method me = protoBuilder.getClass().getMethod( "setField" , FieldDescriptor. class , Object. class ); me.invoke(protoBuilder, fd, jObject); } } } } catch (Exception e){ logger .error( "javaBeanToProtoBean method item reflect error, item name:" +item.getName()); } } Method buildM = protoBuilder.getClass().getMethod( "build" ); Object rObject = buildM.invoke(protoBuilder); /* Method byteM = rObject.getClass().getMethod("toByteArray"); Object byteObject = byteM.invoke(rObject); byte[] pbByte = (byte[]) byteObject; String pbStr = new String(Base64.getEncoder().encode(pbByte), "UTF-8");*/ return rObject; } catch (Exception e) { e.printStackTrace(); logger.error( "convert javabean to protobuf bean error,e:" , e); return null ; } } |
以上方法可以通用的讲前端发送过来的protobean转成我们需要的普通javabean,但是在性能上比getset慢上许多,普通项目用起来是没问题,也能达到每秒几万次,但是对性能有要求的童鞋可以关注我注释的那一行代码。
1 2 3 4 5 6 7 8 9 10 11 | try { // 自定义对象继续需要通过builder来解析处理,回调、 这一块很占计算时间。有待优化 Method bM = protoBuilder.getClass().getMethod( "getFieldBuilder" , FieldDescriptor. class ); Object subBuilder = bM.invoke(protoBuilder, fd); Object pBean = javaBeanToProtoBean(jObject,subBuilder); Method me = protoBuilder.getClass().getMethod( "setField" , FieldDescriptor. class , Object. class ); me.invoke(protoBuilder, fd, pBean); isDefined1 = true ; } catch (Exception e){ // logger .info("this required field is not a user-defined field"); } |
由于转换中有这里要对包含其他bean做处理,所以在普通操作时经常进了catch代码块,所以浪费了很长时间(众所周知,catch是很浪费时间的),但是去掉这块代码转包含关系的bean就有问题,这块难题暂时博主也没解决,留给你们去,能解决的可以在下方留言。如果解决不了但是还是想简单方便的,可以关注我的下一篇博文,protostuff。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?