Protobuf原理分析

一、什么是Protobuf

Protobuf(Google Protocol Buffers)是Google提供一个具有高效的协议数据交换格式工具库,类似于常用的XML及JSON,但具有更小的传输体积、更高的编码、解码能力,特别适合于数据存储、网络数据传输等对存储体积、实时性要求高的领域。

二、Protobuf真的能压缩空间吗?

String.png

Protobuf String.png

经过对比我们发现使用了Protobuf的的数据比没有使用Protobuf编码的数据长度还多了两个字节,这到底是为什么?我们接着往下看一个Long类型的对比


Long.png

Protobuf Long.png

比如Long类型发现,没有Protobuf编码的Long类型的数据占了8个字节,而使用Protobuf的只占了数据只占了四个字节,这是为什么?接下来我们一起来分析下原因

三、Protobuf编码结构

image.png
  • Tag
    field_number: message 定义字段时指定的字段编号
    wire_type: ProtoBuf 编码类型,根据这个类型选择不同的 Value 编码方案。

  • Length 是可选的,不同类型的数据编码结构可能会变成 Tag - Value 的格式。没有Length如何确认Value的边界?答案就是 Varint 编码。

  • Value 数据内容根据数据类型不同使用不同方式的编码的数据

1、Varint这个类型编码方式会对针对sint 32和sint64做一个特殊处理,先用ZigTag编码处理再进行Varint编码。
2、Start group 和 End group 两种类型已被废弃。

四、Tag编解码

#Login.proto
message LoginRequest{
        string username = 1;
        string password = 2;
}

Tag编码

Tag= (field_number << 3 ) | wire_type
username 属性的tag编码
usernameTag = 1 << 3 | 2
usernameTag = 00001010

Tag解码

wire_type = Tag & 3
wire_type = 00001010 & 3 = 010
field_number = Tag >> 3
field_number = 00001010 >> 3 = 00001

Wire Type 的类型如下表所示

Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimi string, bytes, embedded messages, packed repeated fields
3 Start group Groups (deprecated)
4 End group Groups (deprecated)
5 32-bit fixed32, sfixed32, float

五、Varints 编解码

Varints 编码的规则主要特点:
在每个字节第一个 bit 设置了 msb(most significant bit ),标识是否需要继续读取下一个字节
存储数值对应的二进制补码
补码的低位排在前面

  • 编码

对388888的补码进行编码
00000000 00000000 00000000 00000000 00000000 00000101 11101111 00011000
1、去掉高位多余的0
101 11101111 00011000
2、从后依次向前取 7 位组并反转排序
0011000 1011110 0010111
3、加上 msb
1#0011000 1#1011110 0#0010111

  • 解码

10011000 11011110 00010111 …
每个字节的第一个 bit 为 msb 位,msb = 1 表示需要再读一个字节(还未结束),msb = 0 表示无需再读字节(读取到此为止)。
1、读取Value的值
10011000 11011110 00010111
2、去掉msb
0011000 1011110 0010111
3、将这三个 7-bit 组反转得到补码
0010111 1011110 0011000

好像一彻都辣么美好, but, 如果是负数怎么办?
-2
1111111111111111111111111111111111111111111111111111111111111110
答案接着往下看

六、ZigZag编解码

  • 编码

对-2的补码进行编码
1111111111111111111111111111111111111111111111111111111111111110
1、先将符号位放最右边
1111111111111111111111111111111111111111111111111111111111111101
2、取反(除符号位)
00000000000000000000000000000000000000000000000000000011
3、Varints编码
0#0000011

  • 解码

1、Varints解码(去掉高位的msg)
0000011
2、高位补0
00000000000000000000000000000000000000000000000000000011
5、低位换高位
10000000000000000000000000000000000000000000000000000001
6、取反(除符号位)
1111111111111111111111111111111111111111111111111111111111111110

七、Length-delimited

  • string , bytes :

    image.png

    Length-delimited类型编码主要应用类型string、bytes、embedded messages、repeated,Length-delimited编码结构是TLV,在这里我们终于见到了Length了,也是唯一一个TLV结构的编码方式。
    String Value部分解码参考:
    https://baike.baidu.com/item/ASCII/309296?fr=aladdin

  • embedded message


    image.png

嵌套message先解出第一层

  • Tag
    field = 00110 (4)
    wire_type = 010(2)
  • Length
    10 (2) 个字节
  • Value
    00001000 00000001
    再将Value按照上面的方式解析,只不过嵌套的message 里面只有一个wire_type = 0 编码的一个数值,所以Length被省略了。
  • repeated
    repeated 字符串和数值类型有一定区别,string和bytes 编码结构变为 Tag-Length-Value-Tag-Length-Value, 数值类型会进行打包处理,编码结构变为 Tag-Length-Value-Value-Value。
image.png

field_numbe = 00111(5)
wire_type= 010(2)
length = 011 (3字节)
11010111 00001000 00000010
通过msb判断可解析为 1#1010111 0#0001000 和 0#0000010
解析得到:
00010001010111(1111) 、0000010(2)

image.png

Tag-Length-Value-Tag-Length-Value则比较简单,他们的TAG都是一样的,解析Tag,通过Length获取对应长度的字符Value即可

八、32-bit、64-bit

这个比较简单,32-bit、64-bit Value都是定长的分别是32bit(4个字节) 、64bit(8个字节),直接取得对应的字节数即可,当值通常大于 2的28次方和2的56次方,则比 uint32和uint64 更高效,因为Varints编码每个字节会占用一个bit的msb(most significant bit)。

九、如何落地?

image.png

在SpringMVC进入业务方法前,会根据@RequestBody注解选择适当的HttpMessageConverter实现类来将请求参数解析到string变量中,具体来说是使用了StringHttpMessageConverter类,它的canRead()方法返回true,然后它的read()方法会从请求中读出请求参数,绑定到业务方法的变量中。
当SpringMVC执行业务方法后,由于返回值标识了@ResponseBody,SpringMVC将使用StringHttpMessageConverter的write()方法,将业务方法的返回值写入响应报文,当然,此时canWrite()方法返回true。

public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<Message> {
    public static final Charset DEFAULT_CHARSET;
    public static final MediaType PROTOBUF;
    private static final Map<Class<?>, Method> methodCache;
    static {
        DEFAULT_CHARSET = StandardCharsets.UTF_8;
        PROTOBUF = new MediaType("application", "x-protobuf", DEFAULT_CHARSET);
        methodCache = new ConcurrentReferenceHashMap();
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return Message.class.isAssignableFrom(clazz);
    }


    @Override
    protected Message readInternal(Class<? extends Message> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
        MediaType contentType = httpInputMessage.getHeaders().getContentType();

        Message.Builder builder = this.getMessageBuilder(aClass);
        if (PROTOBUF.isCompatibleWith(contentType)) {
            builder.mergeFrom(httpInputMessage.getBody());
        } else {
            Log.error("Request must be set Content-Type = application/x-protobuf");
            return null;
        }
        return builder.build();
    }

    @Override
    protected void writeInternal(Message message, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
        MediaType contentType = httpOutputMessage.getHeaders().getContentType();
        if (PROTOBUF.isCompatibleWith(contentType)) {
            this.setProtoHeader(httpOutputMessage, message);
            //CodedOutputStream codedOutputStream = CodedOutputStream.newInstance(httpOutputMessage.getBody());
            //message.writeTo(codedOutputStream);
            //codedOutputStream.flush();
            httpOutputMessage.getBody().write(message.toByteArray());
            httpOutputMessage.getBody().flush();
        }
    }
    private void setProtoHeader(HttpOutputMessage response, Message message) {
        response.getHeaders().set("X-Protobuf-Schema", message.getDescriptorForType().getFile().getName());
        response.getHeaders().set("X-Protobuf-Message", message.getDescriptorForType().getFullName());
    }

    private Message.Builder getMessageBuilder(Class<? extends Message> clazz) {
        try {
            Method method = (Method)methodCache.get(clazz);
            if (method == null) {
                method = clazz.getMethod("newBuilder");
                methodCache.put(clazz, method);
            }

            return (Message.Builder)method.invoke(clazz);
        } catch (Exception var3) {
            throw new HttpMessageConversionException("Invalid Protobuf Message type: no invocable newBuilder() method on " + clazz, var3);
        }
    }
}

具体落地细节请参考Url: https://github.com/anthony3669000/web_protobuf

十、附录

Protobuf数据类型

proto Notes C++ Java C#
double double double double
float float float float
int32 使用可变长度编码。编码负数的效率低 - 如果你的字段可能有负值,请改用 sint32 int32 int int
int64 使用可变长度编码。编码负数的效率低 - 如果你的字段可能有负值,请改用 sint64 int64 long long
uint32 使用可变长度编码(无符号) uint32 int uint
uint64 使用可变长度编码(无符号 uint64 long ulong
sint32 使用可变长度编码。有符号的 int 值。这些比常规 int32 对负数能更有效地编码 int32 int int
sint64 使用可变长度编码。有符号的 long 值。这些比常规 int64 对负数能更有效地编码 uint64 long long
fixed32 总是四个字节。如果值通常大于 2的28次方,则比 uint32 更有效。 uint32 int uint
fixed64 总是八个字节。如果值通常大于 2的56次方,则比 uint64 更有效。 uint64 long ulong
sfixed32 总是四个字节 int32 int int
sfixed64 总是八个字节 int64 long long
bool bool boolean boolean
string 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本 string String string
bytes 可以包含任意字节序列 string ByteString string

十一、参考资料

https://github.com/anthony3669000/web_protobuf

https://github.com/protocolbuffers/protobuf/releases

https://developers.google.com/protocol-buffers/docs/overview

https://my.oschina.net/lichhao/blog/172562

https://www.jianshu.com/p/73c9ed3a4877

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352