这一系列文章主要是对protocol buffer这种编码格式的使用方式、特点、使用技巧进行说明,并在原生protobuf的基础上进行扩展和优化,使得它能更好地为我们服务。

在上一篇文章中,我们完整了解了protobuf的编码原理,那么在这篇文章中,我将会展示在使用过程中遇到的问题,以及解决方案。并在此基础上根据我们实际的使用场景进行改进。

本文主要涉及以下2个部分

1.protobuf的使用背景及所遇到的问题

2.自己完成一个protobuf的编码、解码类库,兼容官方的编码过程

protobuf的使用背景

我在日常工作中是进行APP服务端开发的,服务端与客户端的数据交互格式使用的是最常用的json。

众所周知,在移动互联网的使用场景下,单次请求耗时对于用户来说是一个非常敏感的数据指标,而影响单次请求耗时的因素有很多,其中最重要的自然是服务端的数据处理能力与网络信号的状态。服务端的处理数据处理能力是完全在我们自己的掌控之中,可以有很多方法提高响应速度。然而用户的网络信号状态是我们无法控制的,也许是3G信号,也许是4G信号,也许正在经过一个隧道,也许正在地下商场等等。如果我们能降低每一次网络请求的数据量,那么也算是在我们所能掌控的范围内去优化请求响应时长的问题了。

在我接触到protobuf之后,了解到其编码后的字节数量会比json小许多,就开始思考有没有可能在移动互联网场景下使用protobuf代替json格式。网上搜索了一下之后发现并没有相关内容,于是就着手以自己工作中的APP为基础进行protobuf的实际应用探索。(当然grpc也是一种选项,不过改造成本比较大,我这里只考虑对编码方式进行改进)

使用阶段一:直接使用原生类库

在第一阶段中,自然是考虑直接使用google提供的各版本类库。在服务端和android端使用的是java版本的类库,而ios端使用的是swift类库。

在系列的第一篇文章中,已经展示java类库的使用流程。在此过程中我们会发现,我们定义好.proto文件后,需要使用google提供的编译器来生成相应的.java模型文件。而即使是一个简单的模型都会生成一个庞大的.java文件,原因在之前编码原理的文章中都有提及,即protobuf为了减少编码后的字节数,抛弃了很多数据相关的信息(因此protobuf是一个不可以自解释的编码方式),因此为了实现信息的正确编码和解码,信息的发送方和接收方都必须拥有同一个定义好的.java文件,该java文件需要包含完整的编码解码逻辑

对于服务端来说,模型文件的大小并不是一个大的问题,然而对于android客户端来说,这却是非常致命的。在移动互联网场景下,单次请求的时长对于用户来说很敏感,而客户端的大小对于用户来说也是一个不可忽略的问题。特别在很多线下业务推广场景下,需要客户当场下载APP,此时客户端的下载速度将会极大地影响推广的成功率(想象一下,如果一个app有200MB,在非wifi情况下,很多用户应该都会犹豫的吧。即使在wifi情况下,1分钟下载完毕和2分钟下载完毕对于用户的体验上也是天壤之别)。

在我的实际使用中,仅仅一个略复杂的.java模型文件会达到800kb!!而整个APP包含的模型文件何止百个,如果完全使用原生类库,android客户端的大小将成为一个灾难。

而对于ios客户端来说,情况相对好一些,不过类库本身的大小也达到了10MB,基于同样的原因,这也并不是一个可以接受的方案。

因此需要解决的第一个问题就是原生类库大小的问题。

原生类库大小解决方案

首先,我们需要分析protobuf官方.java文件巨大的原因。

正如之前提到的,因为protobuf是一个不可自解释的数据格式,特别是不同的数据内容编码后的结果可以是完全相同的(参见上一篇文章最后的例子),所以需要在编译器生成的.java文件中包含定制的编码、解码逻辑,以将相同的编码结果对应到不同的java类型上。

我们摘取一段protobuf生成的.java文件中的分支代码,其中的tag正是表示序号和类型的字节,所以在编码与解码的时候就是根据这个字节的值进入不同的case分支,进行数据的读取和写入。所以对于protobuf的官方类库而言,表示序号和类型的字节是灵魂,因为这个字节一旦发生了变化,编码的结果将完全不同。

...
int tag = input.readTag();
switch (tag) {
case 0:
done = true;
break;
case 8: {
age_ = input.readInt32();
break;
}
case 16: {
hairCount_ = input.readInt64();
break;
}
case 24: {
isMale_ = input.readBool();
break;
}
case 34: {
java.lang.String s = input.readStringRequireUtf8();
name_ = s;
break;
}
...
}
...

并且为了实现跨平台、跨语言地使用,protobuf所依赖的模型定义是.proto文件,而.java文件仅仅是根据.proto定义所生成的,并非是模型的原始定义。为了摆脱.proto的束缚,我们还必须将模型的定义直接放到.java文件中。

例如我们原先定义.proto文件如下

syntax = "proto3";

option java_package = "cn.tera.protobuf.model";
option java_outer_classname = "BasicUsage"; message Person {
string name = 1;
int32 id = 2;
string email = 3;
}

现在直接将其定义到.java文件中,且抛弃了outer_classname

package cn.tera.protobuf.model;

public class Person {
String name;
int id;
String email;
}

接着就需要考虑这样一个问题,之前一直在强调,.proto中定义的字段的序号和类型是protobuf的灵魂,然而此时我们同时抛弃了.proto的定义和编译器生成的定制化.java文件,那又该如何去确定字段的序号和类型呢?

答案是依赖定义的java模型本身。

java语言自身其实就是一个强类型的语言,它在编码和解码的过程中,完全可以知晓每一个字段的数据类型,而不需要根据.proto文件生成各种定制的逻辑。

而序号问题我们可以通过一些约定,例如字段名的小写字母顺序进行排序。

既然解决了protobuf的核心依赖问题,那么接着就可以着手编写编码和解码的类库了

先看编码部分的功能,我们将其定义为BasicEncoder。

public class BasicEncoder {
}

在使用的时候为了简化和直观,我们定义入口方法的形式如下

public class BasicEncoder {
public static <T> byte[] serialize(T obj, Class<T> clazz) {
...
}
}

因为很多时候涉及到子对象的写入,因此需要做递归的调用,那么我们就再包一层writeObject方法

public class BasicEncoder {
public static <T> byte[] serialize(T obj, Class<T> clazz) {
//主逻辑函数,为了方便递归调用
List<Byte> bytes = writeObject(0, obj, clazz);
//将List转换成Array
byte[] result = new byte[bytes.size()];
for (int i = 0; i < bytes.size(); i++) {
result[i] = bytes.get(i);
}
return result;
}
}

接着我们就来看writeObject方法

/**
* 主逻辑方法
*
* @param o 序号,当第一次被调用时会传入0
* @param obj 模型实例
* @param clazz 模型类
* @param <T> 泛型
* @return
*/
public static <T> List<Byte> writeObject(int o, T obj, Class<T> clazz) {
//结果字节,因为在编码结束前是不确定总大小的,因此用List来作为返回参数
List<Byte> bytes = new ArrayList<>();
try {
List<Field> fields = Helper.getAllFields(clazz);
Map<Integer, Field> fieldList = Helper.sortFields(fields);
List<Integer> fieldNums = fieldList.keySet().stream().collect(Collectors.toList());
fieldNums.sort(Comparator.comparing(f -> f));
for (int order : fieldNums) {
Field f = fieldList.get(order);
f.setAccessible(true);
Object value = f.get(obj);
if (value != null) {
if (value instanceof String) {
bytes.addAll(writeString(order, (String) value));
} else if (value instanceof Boolean) {
bytes.addAll(writeBoolean(order, (Boolean) value));
} else if (value instanceof Integer) {
bytes.addAll(writeInt32(order, (Integer) value));
} else if (value instanceof Double) {
bytes.addAll(writeFixed64(order, (Double) value));
} else if (value instanceof Float) {
bytes.addAll(writeFixed32(order, (Float) value));
} else if (value instanceof Long) {
bytes.addAll(writeInt64(order, (Long) value));
} else if (value instanceof List) {
bytes.addAll(writeList(order, (List) value));
} else {
Class c = f.getType();
bytes.addAll(writeObject(order, f.get(obj), c));
}
}
order++;
}
//序号+类型字节
List<Byte> headBytes = new ArrayList<>();
if (o != 0) {
headBytes.addAll(writeTag(o, 2));
}
if (headBytes.size() > 0) {
headBytes.addAll(writeUInt32NoTag(bytes.size()));
bytes.addAll(0, headBytes);
}
} catch (Exception e) {
System.out.println(e);
}
return bytes;
}

首先我们自然要取出该类的所有字段,包括其父类的字段

List<Field> fields = Helper.getAllFields(clazz);

接着对字段做一个排序,将其按照小写字母的顺序进行排序,并将序号和对应的字段做一个map

Map<Integer, Field> fieldList = Helper.sortFields(fields);

对序号进行一个排序

List<Integer> fieldNums = fieldList.keySet().stream().collect(Collectors.toList());
fieldNums.sort(Comparator.comparing(f -> f));

根据序号的顺序,遍历所有的字段,然后根据字段的类型写入数据。注意最后一个else,就是一个对于子对象的递归调用

for (int order : fieldNums) {
Field f = fieldList.get(order);
f.setAccessible(true);
Object value = f.get(obj);
if (value != null) {
if (value instanceof String) {
bytes.addAll(writeString(order, (String) value));
} else if (value instanceof Boolean) {
bytes.addAll(writeBoolean(order, (Boolean) value));
} else if (value instanceof Integer) {
bytes.addAll(writeInt32(order, (Integer) value));
} else if (value instanceof Double) {
bytes.addAll(writeFixed64(order, (Double) value));
} else if (value instanceof Float) {
bytes.addAll(writeFixed32(order, (Float) value));
} else if (value instanceof Long) {
bytes.addAll(writeInt64(order, (Long) value));
} else if (value instanceof List) {
bytes.addAll(writeList(order, (List) value));
} else {
Class c = f.getClass();
bytes.addAll(writeObject(order, f.get(obj), c));
}
}
}

上面这一段if else解决了protobuf的类型依赖性

接着需要判断这次数据写入是否是一个子对象。因为如果是子对象的话,它除了自身的数据,还需要根据数据长度写入自身的序号、类型和数据长度。

//序号+类型字节
List<Byte> headBytes = new ArrayList<>();
//如果是第一次调用writeObject方法,o就是0,说明是主对象的写入,那就不需要序号和类型了
if (o != 0) {
headBytes.addAll(writeTag(o, 2));
}
if (headBytes.size() > 0) {
headBytes.addAll(writeUInt32NoTag(bytes.size()));
bytes.addAll(0, headBytes);
}

writeUInt32NoTag方法是从google官方类库中提取出来的

整个数据的写入过程其实并不复杂,接着我们来细看每一个方法内部逻辑是怎样的

getAllFields方法,获取所有字段

这里涉及到一个Ignore的注解,用来忽略不需要被编码的字段

/**
* 获取所有有效字段
*
* @param clazz
* @return
*/
public static List<Field> getAllFields(Class clazz) {
List<Field> fields = new ArrayList<>();
//需要循环查找父类的字段
while (clazz != null && !clazz.equals(Object.class)) {
//这里需要所有的字段,包括private的
fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
clazz = clazz.getSuperclass();
}
//过滤ignore字段
fields.removeIf(f -> {
Ignore ignore = f.getAnnotation(Ignore.class);
return ignore != null;
});
return fields;
}

sortFields方法,根据字段名的小写值进行排序

这里涉及到一个Version注解,需要解决一个原生APP的版本兼容问题。因为某个版本的APP的客户端在发布之后是无法对代码进行更新的(当然现在有一些热更新技术,不过一般也不会涉及到模型的变更这种基础的东西)。

例如我们发布了1.0版本的客户端,某个服务端接口返回3个字段

当发布2.0版本客户端时,该接口需要新增一个返回字段,而1.0版本的客户端是无法更新到该新增字段的,如果不加以兼容,那么老版本的客户端很有可能就会无法解析接口的返回数据。所以定义了Version注解,进行排序时会优先将同一批Version的字段放到一起

public static Map<Integer, Field> sortFields(List<Field> fields) {
Map<Integer, Field> result = new HashMap<>();
List<Field> sortedFields = new ArrayList<>();
//根据Version注解对字段进行分组
Map<Integer, List<Field>> groups = Helper.groupBy(fields, f -> {
Version sort = f.getAnnotation(Version.class);
if (sort == null) {
return -1;
} else {
return sort.value();
}
});
//对分组后的Version进行排序,从小到大
List<Integer> sorts = groups.keySet().stream().collect(Collectors.toList());
sorts.sort(Comparator.comparing(f -> f));
//同一个分组的字段将会被放在一起,其内部还是按照小写的字段名进行排序
for (int s : sorts) {
groups.get(s).sort(Comparator.comparing(f -> f.getName().toLowerCase()));
sortedFields.addAll(groups.get(s));
}
//最后将所有的字段按照顺序放入map
int fieldNum = 1;
for (Field field : sortedFields) {
result.put(fieldNum++, field);
}
return result;
}

上面这2个方法解决了protobuf中的序号依赖性

接着我们来看下每一个java类型的数据究竟是如何被写入的

writeString方法,写入String类型的数据

public static List<Byte> writeString(int order, String value) {
List<Byte> bytes = new ArrayList<>();
if (value == null || value.isEmpty()) {
return bytes;
}
bytes.addAll(writeTag(order, 2));
bytes.addAll(writeStringNoTag(value));
return bytes;
}

这里涉及到2个方法

writeTag方法,就是写入序号和类型,order是传入的,而2则是protobuf定义的String类型的Type

writeStringNoTag方法,就是写入String的值,这个方法是从protobuf的官方类库中提取出来的

writeBoolean方法,写入Boolean类型的数据

public static List<Byte> writeBoolean(int order, Boolean value) {
List<Byte> bytes = new ArrayList<Byte>();
if (value == null || !value) {
return bytes;
}
bytes.addAll(writeTag(order, 0));
bytes.add((byte) 1);
return bytes;
}

这里会多做一个判断,如果value值是false,那么就不用写入数据了

因为Boolean在protobuf中的类型为Varint,所以writeTag写入的类型就是0

writeInt32和writeInt64方法,写入int和long类型的数据

public static List<Byte> writeInt32(int order, int value) {
List<Byte> result = new ArrayList<>();
if (value == 0) {
return result;
}
result.addAll(writeTag(order, 0));
result.addAll(writeInt32NoTag(value));
return result;
} public static List<Byte> writeInt64(int order, long value) {
List<Byte> result = new ArrayList<>();
if (value == 0L) {
return result;
}
result.addAll(writeTag(order, 0));
result.addAll(writeUInt64NoTag((value)));
return result;
}

因为int32和int64在protobuf中的类型为Varint,所以writeTag写入的类型就是0

这里的writeInt32NoTag和writeUInt64NoTag方法是从google的官方类库中提取出来的

writeFixed32和writeFixed64方法,写入float和double类型的数据

public static List<Byte> writeFixed64(int order, Double value) {
List<Byte> bytes = new ArrayList<Byte>();
if (value == null || value == 0) {
return bytes;
}
bytes.addAll(writeTag(order, 1));
bytes.addAll(writeFixed64NoTag(Double.doubleToRawLongBits(value)));
return bytes;
} public static List<Byte> writeFixed32(int order, Float value) {
List<Byte> bytes = new ArrayList<Byte>();
if (value == null || value == 0) {
return bytes;
}
bytes.addAll(writeTag(order, 5));
bytes.addAll(writeFixed32NoTag(Float.floatToRawIntBits(value)));
return bytes;
}

这里特别注意,调用了java的2个native方法,将float和double类型转换为IEEE754标准的二进制的形式

因为float和double对应的protobuf中的类型为32-bit和64-bit,所以writeTag写入的类型分别是5和1

writeFixed64NoTag和writeFixed32NoTag方法是从google的官方类库中提取出来的

writeList方法,写入List类型的数据

public static List<Byte> writeList(int order, List value) {
List<Byte> bytes = new ArrayList<>();
if (value != null && value.size() > 0) {
Object v = value.get(0);
if (v instanceof String) {
bytes.addAll(writeStringList(order, value));
} else if (v instanceof Boolean) {
bytes.addAll(writeNoStringList(order, value, Boolean.class));
} else if (v instanceof Integer) {
bytes.addAll(writeNoStringList(order, value, Integer.class));
} else if (v instanceof Double) {
bytes.addAll(writeNoStringList(order, value, Double.class));
} else if (v instanceof Float) {
bytes.addAll(writeNoStringList(order, value, Float.class));
} else if (v instanceof Long) {
bytes.addAll(writeNoStringList(order, value, Long.class));
} else if (v instanceof List) {
bytes.addAll(writeList(order, (List) v));
} else {
bytes.addAll(writeObjectList(order, value));
}
}
return bytes;
}

对于List对象,自然是要根据其具体持有对象的类型进行区分

对于非String类型的对象,统一会调用writeNoStringList方法

writeNoStringList方法

public static <T> List<Byte> writeNoStringList(int order, List list, Class<T> clazz) {
List<Byte> bytes = new ArrayList<>();
bytes.addAll(writeTag(order, 2));
List<Byte> contentBytes = new ArrayList<>();
for (Object d : list) {
if (clazz.equals(Double.class)) {
contentBytes.addAll(writeFixed64NoTag(Double.doubleToRawLongBits((Double) d)));
} else if (clazz.equals(Float.class)) {
contentBytes.addAll(writeFixed32NoTag(Float.floatToRawIntBits((Float) d)));
} else if (clazz.equals(Integer.class)) {
contentBytes.addAll(writeInt32NoTag((Integer) d));
} else if (clazz.equals(Long.class)) {
contentBytes.addAll(writeUInt64NoTag((Long) d));
} else if (clazz.equals(Boolean.class)) {
contentBytes.add((byte) (((Boolean) d) ? 1 : 0));
}
}
bytes.addAll(writeUInt32NoTag(contentBytes.size()));
bytes.addAll(contentBytes);
return bytes;
}

这里就根据不同的数据类型,调用google提供的类库方法进行数据写入,和非list的写入方式一致

因为List类型对应的是protobuf中的repeated类型,所以写入tag的时候固定为2

这里的writeFixed64NoTag、writeFixed32NoTag、writeInt32NoTag、writeUInt64NoTag都是从google的类库中提取出来的底层方法。

而对于String类型的List,则调用writeStringList方法

writeStringList方法

public static List<Byte> writeStringList(int fieldNumber, List list) {
List<Byte> bytes = new ArrayList<>();
for (Object s : list) {
bytes.addAll(writeString(fieldNumber, (String) s));
}
return bytes;
}

循环List中的对象,通过writeString方法写入字符串信息

对于Object类型的List,则调用writeObjectList

writeObjectList方法

public static List<Byte> writeObjectList(int fieldNumber, List list) {
List<Byte> bytes = new ArrayList<>();
for (Object o : list) {
Class c = o.getClass();
bytes.addAll(writeObject(fieldNumber, o, c));
}
return bytes;
}

在这里就会循环List中的元素,递归调用writeObject方法

上述代码就是我们类库中的编码的主要逻辑。

其实解码的逻辑和编码是非常类似的,不过限于篇幅就不全部贴上来了,有兴趣的同学可以去git上查看,上面也包含了之前几篇文章的所有测试代码和.proto文件

https://github.com/TeraTian/optimized-protobuf

接着我们看一下这个类库的使用示例

/**
* 类库的基本使用方式
*/
@Test
public void basicEncoderTest() {
String source = "{\"score2\":13213.1231,\"age\":5,\"name\":\"Peter\",\"hairCount\":183728182371871131,\"isMale\":true,\"score\":13213.1231}";
test(source, Student.class, ProtobufStudent.Student.class);
}

test方法

/**
* test method
*
* @param source model json
* @param javaClass java class
* @param protobufClass protobuf class
*/
static <T, P extends Message> void test(String source, Class<T> javaClass, Class<P> protobufClass) {
try {
System.out.println("------------------- source json --------------------");
System.out.println(source);
System.out.println("count:" + source.getBytes().length);
System.out.println();
System.out.println("-------------------protobuf encode result-------------------");
Message.Builder builder = (Message.Builder) protobufClass.getMethod("newBuilder").invoke(null);
byte[] protoBytes = Helper.protobufSerialize(source, builder);
Helper.printBytes(protoBytes);
builder.mergeFrom(protoBytes); System.out.println();
System.out.println("------------------- tera encode result -------------------");
T javaModel = JSON.parseObject(source, javaClass);
byte[] teraBytes = BasicEncoder.serialize(javaModel, javaClass);
Helper.printBytes(teraBytes); System.out.println();
System.out.println("------------------- bytes compare result -------------------");
System.out.println(Helper.compareBytes(protoBytes, teraBytes)); System.out.println();
System.out.println("------------------- tera decode result -------------------");
T deserialJavaModel = new BasicDecoder().deserialize(teraBytes, javaClass);
System.out.println(JSON.toJSON(deserialJavaModel)); } catch (Exception e) {
System.out.println(e.getMessage());
}
}

输出结果

-------------------     source json     --------------------
{"score2":13213.1231,"age":5,"name":"Peter","hairCount":183728182371871131,"isMale":true,"score":13213.1231}
count:108 -------------------protobuf encode result-------------------
8 5 16 -101 -45 -125 -84 -17 -7 -82 -58 2 24 1 34 5 80 101 116 101 114 41 18 -91 -67 -63 -113 -50 -55 64 53 126 116 78 70
count:35 ------------------- tera encode result -------------------
8 5 16 -101 -45 -125 -84 -17 -7 -82 -58 2 24 1 34 5 80 101 116 101 114 41 18 -91 -67 -63 -113 -50 -55 64 53 126 116 78 70
count:35 ------------------- bytes compare result -------------------
true ------------------- tera decode result -------------------
{"score":13213.1231,"isMale":true,"score2":13213.123,"hairCount":183728182371871131,"name":"Peter","age":5}

可以看到对于这样一个数据结构,protobuf编码后为35个字节,而json则需要108个字节

接着比较了protobuf原生类库的编码结果和我自己完成类库的编码结果,是一致的。

当然,如果需要和原生protobuf兼容的话,需要将protobuf中字段的序号按照小写字母的顺序进行定义。不过开发该类库的目的并非是代替已经存在的protobuf原生类库,而是为了更方便地将数据格式从json切换到protobuf,所以原先考虑过定义Tag注解来强行指定字段的序号,不过觉得意义不大

例如之前定义的Student.proto,我们修改一下其中的字段顺序(暂时还不支持enum,所以去掉了Color)

syntax = "proto3";

option java_package = "cn.tera.protobuf.coder.models.protobuf";
option java_outer_classname = "CoderTestModel"; message Student{
int32 age = 1;
Parent father = 2;
repeated string friends = 3;
int64 hairCount = 4;
double height = 5;
repeated Hobby hobbies = 6;
bool isMale = 7;
Parent mother = 8;
string name = 9;
float weight = 10;
} message Parent {
int32 age = 1;
string name = 2;
} message Hobby {
int32 cost = 1;
string name = 2;
}

接着我们定义相应的java模型

package cn.tera.protobuf.coder.models.java;

import java.util.List;

public class CoderTestStudent {
public int age;
public Parent father;
public List<String> friends;
public long hairCount;
public double height;
public List<Hobby> hobbies;
public boolean isMale;
public Parent mother;
public String name;
public float weight; public class Parent {
public int age;
public String name;
} public class Hobby {
public int cost;
public String name;
}
}

json内容

{
"age": 13,
"father": {
"age": 45,
"name": "Tom"
},
"friends": ["mary", "peter", "john"],
"hairCount": 342728123942,
"height": 180.3,
"hobbies": [{
"cost": 130,
"name": "football"
}, {
"cost": 270,
"name": "basketball"
}],
"isMale": true,
"mother": {
"age": 45,
"name": "Alice"
},
"name": "Tera",
"weight": 52.34
}

测试代码

/**
* 一个相对复杂的模型测试
*/
@Test
public void complexModelTest() {
String source = "{\"age\":13,\"father\":{\"age\":45,\"name\":\"Tom\"},\"friends\":[\"mary\",\"peter\",\"john\"],\"hairCount\":342728123942,\"height\":180.3,\"hobbies\":[{\"cost\":130,\"name\":\"football\"},{\"cost\":270,\"name\":\"basketball\"}],\"isMale\":true,\"mother\":{\"age\":45,\"name\":\"Alice\"},\"name\":\"Tera\",\"weight\":52.34}";
test(source, CoderTestStudent.class, CoderTestModel.Student.class);
}

输出结果

-------------------     source json     --------------------
{"age":13,"father":{"age":45,"name":"Tom"},"friends":["mary","peter","john"],"hairCount":342728123942,"height":180.3,"hobbies":[{"cost":130,"name":"football"},{"cost":270,"name":"basketball"}],"isMale":true,"mother":{"age":45,"name":"Alice"},"name":"Tera","weight":52.34}
count:271 -------------------protobuf encode result-------------------
8 13 18 7 8 45 18 3 84 111 109 26 4 109 97 114 121 26 5 112 101 116 101 114 26 4 106 111 104 110 32 -90 -52 -64 -31 -4 9 41 -102 -103 -103 -103 -103 -119 102 64 50 13 8 -126 1 18 8 102 111 111 116 98 97 108 108 50 15 8 -114 2 18 10 98 97 115 107 101 116 98 97 108 108 56 1 66 9 8 45 18 5 65 108 105 99 101 74 4 84 101 114 97 85 41 92 81 66
count:102 ------------------- tera encode result -------------------
8 13 18 7 8 45 18 3 84 111 109 26 4 109 97 114 121 26 5 112 101 116 101 114 26 4 106 111 104 110 32 -90 -52 -64 -31 -4 9 41 -102 -103 -103 -103 -103 -119 102 64 50 13 8 -126 1 18 8 102 111 111 116 98 97 108 108 50 15 8 -114 2 18 10 98 97 115 107 101 116 98 97 108 108 56 1 66 9 8 45 18 5 65 108 105 99 101 74 4 84 101 114 97 85 41 92 81 66
count:102 ------------------- bytes compare result -------------------
true

java的类库编写和示例就到此为止。在下一篇文章中,将会展示swift的类库代码,并通过一个的http请求验证其可行性。

另外再根据原生APP的使用特性,在基本类库的基础上再次优化请求数据的大小,对于有些场景可以缩小到20%

本文总结

在移动互联网场景下使用protobuf可以减少单次请求的数据量。

使用google提供的原生类库会使得客户端的体积变大,因此无法直接应用

利用java强类型语言的特点,完成了自己编写的类库,使得编码、解码的流程完全摆脱对.proto文件的依赖,工作中怎么使用json,就可以怎么使用protobuf了

google protocol buffer——protobuf的问题及改进一的更多相关文章

  1. google protocol buffer——protobuf的基本使用和模型分析

    这一系列文章主要是对protocol buffer这种编码格式的使用方式.特点.使用技巧进行说明,并在原生protobuf的基础上进行扩展和优化,使得它能更好地为我们服务. 1.什么是protobuf ...

  2. google protocol buffer——protobuf的使用特性及编码原理

    这一系列文章主要是对protocol buffer这种编码格式的使用方式.特点.使用技巧进行说明,并在原生protobuf的基础上进行扩展和优化,使得它能更好地为我们服务. 在上一篇文章中,我们展示了 ...

  3. google protocol buffer——protobuf的问题和改进2

    这一系列文章主要是对protocol buffer这种编码格式的使用方式.特点.使用技巧进行说明,并在原生protobuf的基础上进行扩展和优化,使得它能更好地为我们服务. 在上一篇文章中,我们举例了 ...

  4. google protocol buffer——protobuf的编码原理二

    这一系列文章主要是对protocol buffer这种编码格式的使用方式.特点.使用技巧进行说明,并在原生protobuf的基础上进行扩展和优化,使得它能更好地为我们服务. 在上一篇文章中,我们主要通 ...

  5. Google Protocol Buffer

    Google Protocol Buffer(protobuf)是一种高效且格式可扩展的编码结构化数据的方法.和JSON不同,protobuf支持混合二进制数据,它还有先进的和可扩展的模式支持.pro ...

  6. 前端后台以及游戏中使用Google Protocol Buffer详解

    前端后台以及游戏中使用Google Protocol Buffer详解 0.什么是protoBuf protoBuf是一种灵活高效的独立于语言平台的结构化数据表示方法,与XML相比,protoBuf更 ...

  7. Google Protocol Buffer 的使用和原理[转]

    本文转自: http://www.ibm.com/developerworks/cn/linux/l-cn-gpb/ Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构 ...

  8. Google Protocol Buffer 的使用

    简介 Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 ...

  9. Google Protocol Buffer的安装与.proto文件的定义

    什么是protocol Buffer呢? Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准. 我理解的就是:它是一种轻便高效的结构 ...

随机推荐

  1. PHP基础之查找

    前言 之前的文章介绍了PHP的运算符.流程控制.函数.排序等.有兴趣可以去看看. PHP入门之类型与运算符 PHP入门之流程控制 PHP入门之函数 PHP入门之数组 PHP基础之排序 下面简单介绍一下 ...

  2. Java 设置、删除、获取Word文档背景(基于Spire.Cloud.SDK for Java)

    本文介绍使用Spire.Cloud.SDK for Java 提供的BackgroundApi接口来操作Word文档背景的方法,可设置背景,包括设置颜色背景setBackgroundColor().图 ...

  3. Codechef July Challenge 2020 Division 1 记录

    目录 Missing a Point Chefina and Swaps Doctor Chef Chef and Dragon Dens LCM Constraints Weird Product ...

  4. Linux探测工具BCC(可观测性)

    BCC(可观测性) 目录 BCC(可观测性) 简介 动机 版本要求 安装 安装依赖 安装和编译LLVM 安装和编译BCC windows源码查看 BCC的基本使用 工具讲解 execsnoop ope ...

  5. java循环嵌套与跳转语句(break,continue)

    一 循环嵌套 嵌套循环是指在一个循环语句的循环体中再定义一个循环语句的语法结构.while.do…while. for循环语句都可以进行嵌套,并且它们之间也可以互相嵌套,如最常见的在for循环中嵌套f ...

  6. C#LeetCode刷题之#118-杨辉三角(Pascal‘s Triangle)

    问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/3688 访问. 给定一个非负整数 numRows,生成杨辉三角的前 ...

  7. super与this的区别,更进一步的区别!——Java学习

    文章目录 this与super的含义 前言 例证 this super 总结 this与super的含义 在Java中,this有两层含义: 指示隐式参数的引用(就是当前对象的引用) 调用该类的其他构 ...

  8. Remix+Geth 实现智能合约部署和调用详解

    Remix编写智能合约 编写代码 在线调试 实现部署 调用接口 Geth实现私有链部署合约和调用接口 部署合约 调用合约 获得合约实例 通过实例调用合约接口 Remix编写智能合约 编写代码 Remi ...

  9. 题解 poj 3304

    题目描述 线段和直线判交板子题 分析题目,如果存在这一条直线,那么过这条直线作垂线,一定有一条垂线穿过所有线段,否则不存在.题目转化为寻找一条直线与所有线段有交点. 直线线段判交方法: 1.先判断线段 ...

  10. SpringMVC接受表单数据

    @ 目录 pojo addProduct.jsp ProductController showProduct.jsp 测试结果 pojo 新建实体类Product package pojo; publ ...