本文已参与「新人创作礼」活动,一起开启掘金创作之路。
零、最快捷的方式pip安装
(1)如下pip安装protobuf并制定版本
sudo yum install pip #安装pip
pip install protobuf==2.5.0 #pip安装并制定protobuf的版本
sudo pip install --upgrade protobuf #更新protobuf版本
pip install gevent==1.4.0 #pip安装gevent并制定版本
pip install -i https://pypi.douban.com/simple/ locustio==0.11.0 #pip安装locust并制定版本
一、protobuf的下载安装
1、protobuf的下载:这里(next可找到更早版本)。此处下载的是protobuf-all-3.5.1.tar.gz。
2、安装准备:安装protobuf前确保以下软件都已经被安装。方法也很简单yum -y install XXXX即可。
#下面这些可能都是需要是事先安装的(如果后续安装出错往这里想想)
autoconf
automake
libtool
make
g++
unzip
注1:可能会遇到一个蛋疼的问题。那就是执行make提示你要“yum install make”安装;指令安装语句又提示你“already install”,特别的蛋疼(见下图)。
解决办法:remove后重新安装即可。
#查看rpm是否安装
rpm -qa | grep "make"
#通过yum卸载
yum remove make
#然后重新安装
yum install make -y
注2:值得注意的是我们要将g++设为默认使用C++11,可以在终端执行:
alias g++='g++ -std=c++11'
但是切记:这个只是临时生效的,永久有效的方法是将上述语句写入/root/.bashrc 文件中。
vim /root/.bashrc
添加:alias g++='g++ -std=c++11'
3、安装protobuf:
执行如下命令,安装时间有点长,耐心等待(可以写成自动化脚本,就无需一步一步执行):
./configure
make #时间有点长
make check #时间有点长
sudo make install
sudo ldconfig # refresh shared library cache.
这时一般来说
libprotobuf库在/usr/local/lib路径下;protoc一般在/usr/local/bin路径下 。如下:
4、配置环境变量
主要是完善PATH和PKG_CONFIG_PATH等环境变量
$ sudo vim /etc/profile
添加
export PATH=$PATH:/usr/local/bin/
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig/ #这个参数是必须的
#我添加了这些,感觉有些没有也可以。
export PATH=$PATH:/usr/local/bin
export PATH=$PATH:/usr/local/include
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
保存生效
source /etc/profile
注:(1)保存生效的语句需要在每个终端都执行一遍才可以生效;
(2)通过echo $PATH / echo $PKG_CONFIG_PATH的方法验证配置在本终端有没有生效。
(3)上述配置中PKG_CONFIG_PATH是必须的(当然其他可能也是必须的),如果没有在编译的时候会报如下错误:
这里的protobuf.pc存了protobuf相关的头文件和库相关的信息,这些信息都是编译的时候要用到的。这里是因为我们使用了pkg-config来方便引用了的,关于pkg-config参见 这里。
5、配置动态路径
sudo vim /etc/ld.so.conf
追加
/usr/local/lib
然后以root权限更新动态库路径
ldconfig
6、验证有没有安装成功
执行:
protoc --version
能够显示相应版本信息就说明安装成功。
7、继续安装python版protobuf的话继续指令如下指令即可:
cd ./python
python setup.py build
python setup.py test
python setup.py install
之后python脚本导入编译后的pb文件后就不会出现“ImportError: No module named google.protobuf.internal”报错了。
二、使用举例
1、person.proto文件:
syntax = "proto3";#指定使用proto3
package tencent; #命名空间
message Person
{
string name = 1;
uint32 age = 2;
uint64 phnum = 3;
}
注意:proto3和proto2的区别还是蛮大的,此处没有了proto2中常见的optional、required之类的修饰。
2、编译.proto文件。 执行如下指令对.proto文件按照c++、python、go形式编译,此时会生成相对应的pb.cc/pb.h、*py2.py、.pb.go文件。
#推荐这种顺序
protoc person.proto --cpp_out=./ #末尾的'./'表示生成的cc、h文件存放在当前目录(可调)
protoc person.proto --python_out=./ #使用python进行编译
protoc person.proto --go_out=./ #使用go进行编译。注意原生protoc不包含go版本的插件,需额外装protoc-gen-go(其实就是把可执行文件protoc-gen-go放到默认搜索路径即可如/bin)
#当然调换一下参数顺序也是一样的
protoc --cpp_out=./ person.proto
protoc --python_out=./ person.proto
protoc --go_out=./ person.proto
#另外也是可以一次性编很多文件的,如下:
protoc com.tencent.*.proto --python_out=.
protoc com.tencent.epc.qidian.cc*.proto --python_out=.
protoc *.protoc --go_out=.
注意事项1:“=”号后面不能有空格,一个都不能有。否则出错
效果如下:
注意事项2:原生的protoc并不包含Go版本的插件,我们需要额外安装下protoc-gen-go(其实就是把可执行文件protoc-gen-go放到可搜索路径例如/bin下即可)。这个就是protobuf编译插件系列的go版本。
3、数据读写之write.cc写数据到本地硬盘
#include<iostream>
#include<fstream>
#include "person.pb.h"
using namespace std;
using namespace tencent;
int main()
{
Person p1;
p1.set_name("shuozhuo");
p1.set_age(22);
p1.set_phnum(17367078333);
fstream output("./log",ios::out | ios::trunc | ios::binary);
if (!p1.SerializeToOstream(&output)){
cerr << "Failed to write msg."<<endl;
}else{
cout << "serialize success!" << endl;
}
return 0;
}
执行如下语句进行编译,生成可执行文件write,并执行之:
g++ person.pb.h person.pb.cc write.cc -o write `pkg-config --cflags --libs protobuf` -lpthread
注意:(1)上述指令中的“`”并不是单引号,而是键盘上按键“1”左边的那个按键对应的符号。
(2)关于pkg_config还是值得继续学习的。
(3)编译 时添加 -lprotobuf库到编译选项中。
4、编写read.cc从本地读取数据
#include<iostream>
#include<fstream>
#include "person.pb.h"
using namespace std;
using namespace tencent;
void PrintInfo(const Person &p1){
cout << "The information is:" << endl;
cout << " name:" << p1.name() <<endl;
cout << " age:" << p1.age() <<endl;
cout << " phnum:" << p1.phnum() <<endl;
}
int main()
{
cout<<"read Test"<<endl;
Person p1;
fstream input("./log",ios::in | ios::binary);
if(!p1.ParseFromIstream(&input)){
cerr << "Failed to parse address book." << endl;
return -1;
}else{
cout << "Parse success" << endl;
}
PrintInfo(p1);
return 0;
}
编译语句如下,完成后生成可执行文件read:
g++ person.pb.h person.pb.cc read.cc -o read `pkg-config --cflags --libs protobuf` -lpthread
执行read,效果如下:
5、如果编译的时候报了类似“#error This file was generated by an older version of protoc which is”的错误
说明:你的proto版本应该是有升级过,这里提示现在编译链接的pb.h、pb.cc文件是由老版本生成的不太行。
解决:生成新的pb.h、pb.cc文件即可 “protoc *.proto --cpp_out=./”。
6、尽量不要使用required。经过测试如果required字段每天序列化不会出问题,但是反序列化会失败。
这种问题查起来恶心,所以还是能避免就避免比较好。
序列化: SerializeAsString SerializeToString
反序列化:ParseFromString ParseFromArray
7、另外值得注意的是ParseFromString返回值为true表示正确解析
三、protobuf学习
更多学习,参见 protobuf使用手册
四、protobuf使用进阶
这里面要讲的东西很简单。那就是我们要善于利用.proto文件编译得到的pb.h、pb.cc。具体来说就是看pb.h里面都生成了哪些方法然后使用之。
1、测试pb文件名为zju.ee.master.proto,内容如下:
package zju.ee.master;
message Student{
required string name = 1; //姓名
required int32 age = 2; //年龄
optional string addr = 3;
}
message Class{
required string name = 1; //班级名称
repeated Student member = 2; //班级成员
}
2、执行编译语句得到pb.h、pb.cc两个输出文件。其中ph.h文件放在附录处。几个用到的点会截图给到。
protoc zju.ee.master.proto --cpp_out=./
大致说一句。如果proto结构体的变量是基础变量,比如int、string等等,那么get直接根据变量名,set的时候直接调用set_xxx即可。
如果变量是自定义类型,那么C++的生成代码中,就没有set_xxx函数名,取而代之的是三个函数名:set_allocated_xxx()、release_xxx()、mutable_xxx()。
3、功能验证——optional、required单字段功能
如下图分别是required字段name和optional字段addr生成的操作方法。可见两者完全一样。所以随便拿一个字段过来验证就好了。
(1)has_xx:判断某个字段是否存储,就不介绍了。
(2)clear_xx():作用就是清空这个字段,在示例代码中会有体现。
(3)set_xx():提供了多种重载,一个字段是可以多次执行set_xx()的,效果就是覆盖,很直观。
(4)mutable_xx():生成一个关于原调用对象指向xx字段的指针,通过修改这个指针的指向就可以实现原对象的修改;一般用来操作嵌套结构。
(5)release_xx():
(6)set_allocated_xx():使用这个方法还是注意点好,参见 Protobuf c++使用小坑(set_allocated函数)_84970000的专栏-CSDN博客_protobuf set_allocated
问题一:如何使用如何修改pb repeated对象中的某个元素(序号为index)。
答:使用如下的 mutable_elems(int index) 组合CopyFrom即可实现。
//首先是构建要替换成的elem
::tencent::im::msg::Elem elem;
::tencent::im::msg::CrmElem* pCrmElem = elem.mutable_crm_elem();
pCrmElem->set_crm_buf(str_ccnotify);
//然后调用此语句替换
updated_rich_text.mutable_elems(index)->CopyFrom(elem);
问题二:内嵌结构如何引用
如下图所示结构如何定义个MsgDetail结构的变量?—— 很简单用"_"间隔就行。
message QidianRobotMessageCopyHippoMsg
{
message MsgDetail
{
optional uint64 uint64_time = 1; //消息时间
optional uint32 uint32_dir = 2; //消息方向 1. b2c 2. c2b
optional bytes bytes_content = 3; //普通文本消息内容
optional im.msg.MsgBody msg_immsg = 4; //富媒体消息,复用MsgBody结构
}
optional uint64 uint64_kfuin = 1; //主号
optional uint32 uint32_channel_type = 2;
optional bytes bytes_robot_id = 3; //机器人id
optional uint64 uint64_visitid = 4; //WebIm visitid
optional MsgDetail msg_detail = 5; //消息详情
}
这样描述即可“QidianRobotMessageCopyHippoMsg_MsgDetail”,如下:
txplus::QidianRobotMessageCopyHippoMsg oRobotMsg = oQidianMsg.robot_msg();
txplus::QidianRobotMessageCopyHippoMsg_MsgDetail msg_detail = oRobotMsg.msg_detail();
注意事项:
关于copyfrom经过验证确实不能跨pb的拷贝,即使拷贝的结构在两个pb文件中一毛一样。其会引发c++程序的coredump。也就是说必须for循环逐个字段的拷贝了。