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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容