详解协议缓存区的概念与实践

网站建设4年前发布
31 0 0

译者 | 陈峻,审校 | 孙淑娟,让我们试想一种场景:团队中几个说不同语言的人见面了。为了相互理解,他们需要使用一种每个人都能听得懂的语言进行交流。为此,他们都应该在自己的母语以及该通用语言之间,执行信息的转换。同理,如果我们使用协议缓存区(Protocol Buffers)消息语言,则能够让整个团队都使用自己特定的编程语言去创建消息,接着被翻译成通用语言的形式。正如​​Wikipedia解释的那样​​:“Google正广泛地使用协议缓冲区,来存储和交换各种结构化的信息。该方法作为自定义的远程过程调用(Remote procedure call,RPC)系统的基础,可用于几乎所有与Google交互的机器间通信。”,20230306014016529100f626100e79b7c78031b4a1159b082dea510,由于我经常需要通过参与各个行业的开发项目,与定制软件打交道,而且专注于在嵌入式系统中使用Modern C++、以及Qt去构建应用程序,因此下面我将与您分享自己在内存受限的嵌入式系统上,使用协议缓冲区的经验。,正如Google所言,“协议缓冲区能够让您以.proto文件的形式,定义需要的数据结构,以便您使用特定生成的源代码,从不同的数据流,使用不同的语言,去轻松地读写自己的结构化数据。”我们可以通常下图了解其基本原理:,2023030601401797e96e7445792644021876464d1634ff47e69a737,根据协议缓存区语言指南,我们首先来讨论一个非常简单的例子。假设您准备定义一个Person消息格式,由于每个人都有一个名字、年龄和电子邮件等属性,因此被用于定义消息类型的.proto文件的内容可设定为如下形式:,其中第一行便指定了使用文件的proto3语法。接着,Person的消息定义指定了三个字段(即,名称/值对)。每一个都可以被用于您需要包含在此类消息的数据块中。而且每个字段包含了名称、类型和字段数的信息。,拥有了.proto文件,您便可以为特定的语言生成源代码语言。例如,C++会使用一种特殊的被称为协议编译器(protocol compiler,又名 protoc)的编译器。请参见下图:,2023030601401863409eb2664c222de14940b42103dda2a4d5d9995,在此,我们把能够生成包含原生语言(language-native)结构,以操控消息的接口称为API。API可以为您提供所有需要set和retrieve数据的类和方法,以及对字节流的serialization to和parsing from方法。,在C++中,各种生成的文件都包含了Person类和所有必要的方法,以处理底层的数据。例如:,此外,Person类通过继承来自google::protobuf::Message的方法,对数据流进行序列化或反序列化(解析)。例如:,如果您正在编写一个完整的静态分配系统,那么可能需要用到的是C而不是C++。下面,我们来讨论如何使用静态分配的缓冲区,而不是动态分配的内存,去编写一个定制的分配器。,默认情况下,Protobuf-C会通过malloc(),来调用动态的分配内存。Protobuf-C会向您提供一种定制分配器(custom allocator)的能力,以替代malloc()和free()函数。下图展示了malloc()和serial_alloc()在处理上的不同,我将对serial_alloc()进行后续讨论。 ,20230306014018b764cb23792f5adbcb80434f725f80adb22553786,在本例中,我将实现自定义的malloc()和free()函数,并将其使用到自定义分配--serial_allocator中。它会通过Protobuf-C库将数据转换成一个连续的、静态分配的内存块。下面两张图分别展示了malloc()和serial_alloc()的具体差异:,20230306014018e81d973950cbf6bf7d9277f22cc84f9215dca9542,在heap上的malloc()分配,20230306014019f41d048614b62d432f1245188fdcf9a66850f2896,静态缓冲区上的serial_alloc()分配,由于malloc()在堆(heap)上分配内存的“随机性”,会导致内存碎片有待整理。而我们定制的serial_alloc()会在序列中静态分配内存,因此无需内存碎片整理。,下面,我将在Ubuntu 22.04 LTS上,通过如下命令,安装protoc-c编译器、以及协议缓存区的C Runtime:,并通过如下命令检查其是否运行:,如果一切正常,屏幕上会返回已安装的版本号:,您可以通过链接-- https://github.com/protobuf-c/protobuf-c,查看它在GitHub库中的完整代码。,下面,我们来查看在message.proto文件中创建的简单Protobuf消息。,然后通过运行如下命令,以生成message.pb-c.h和message.pb-c.c两个文件:,请通过如下命令,使用protobuf-c库与生成的代码,去编译C语言程序:,程序代码会使用Protobuf-C依次进行序列化、编码、包装成为静态缓冲区--pack_buffer,然后经过反序列化、解码、拆包到另一个静态缓冲区—out。下面展示了其完整的代码:,在unpack_to_message_wrapper_from_buffer()中,我们创建了ProtobufCAllocator对象,并将serial_alloc()和serial_free()函数(作为malloc()和free()的替代品)放入其中。然后,我们通过调用message__unpack和传递serial_allocator去解包消息(请参见如下代码):,下面,我们来比较默认的Protobuf-C行为(基于malloc())和自定义分配器的行为。其中使用动态内存分配的Protobuf-C行为是:,使用定制分配器(无动态内存配置)的是:,unpacked_message_wrapper的结构只是一个简单的Proto消息包装器。它的容量足以缓冲解包后的数据存储,以便next_free_index跟踪缓冲区里的已使用空间。请参见如下代码:,其中,虽然Message对象不会改变它的大小,但是Message往往是一个广泛的.proto。例如,重复性的字段通常会涉及到多个malloc()的调用。因此,您可能需要比Message本身更多的空间。为此,我们可以将buffer和Message联合起来,并让MAX_UNPACKED_MESSAGE_LENGTH足够大。而且,unpacked_message_wrapper的结构,就是要将预定义的内存缓冲区与跟踪缓冲区的分配放在一处。,serial_alloc()的签名遵循着ProtobufCAllocator的各项要求,例如:,其中,serial_alloc()可以分配被请求的size到allocator_data处,然后增加next_free_index到下个词的开始边界处(这是一个优化过的连续数据块,紧贴着下一个词的边界)。而size则来自Protobuf-C内部解析或解码的数据。请参考如下代码:,当serial_alloc()被第一次调用时,程序会将next_free_index设置为已分配的大小,并将指针返回至缓冲区的开始处。下图展示了其内部逻辑:,20230306014111997ebff58670687a87932124c4c70b913c3f2d460,在第二次调用时,它会重新计算next_free_index的值,并返回地址给下一块数据。下图展示了其对应的逻辑:,2023030601402012caec7497320135d95772c73e6ce2b0063b8a562,下图展示了第三次调用的逻辑:,2023030601402167896f083d25a23c37e808b869718185db477c448,而serial_free()函数会将使用缓冲区空间设置为零。请参见如下代码:,当serial_free()被调用时,它会通过设置next_free_index为零,以“释放”所有内存,好让缓冲区可以被重用。请参见下图:,2023030601402106a051365f650acdf026370960ce9a3c3b625c135,我们可以使用知名的运行时诊断工具—Valgrind,通过如下命令来运行上述程序:,在下面生成的报告中,您会发现并无任何分配的出现:,如果您手头的项目是一个内存受限的系统,那么您需要事先在serial_alloc中确定MAX_UNPACKED_MESSAGE_LENGTH的大小。请参见如下代码:,在上例中,我们得到了:,而当.proto message变得更加复杂时,我们可以为其添加一个新字段。请参见如下代码:,然后,我们再为message添加各种新的属性:,下面便是我们能够看到的输出:,由结果可知,我们至少需要一个95字节的缓冲区。而且在真实项目中,您往往需要比95字节更多的空间。 ,综上所述,若要编写和使用一个定制的分配器,您需要:,对应地,在上面的例子中,我们实现了:,陈峻 (Julian Chen),51CTO社区编辑,具有十多年的IT项目实施经验,善于对内外部资源与风险实施管控,专注传播网络与信息安全知识与经验。,原文标题:What Are Protocol Buffers?,作者:Mateusz Patyk

© 版权声明

相关文章