文章目录
- protobuf综述
- 传输协议与指令
- 创建协议
- 编译协议
- 介绍addressbook.pb.h文件
- 序列化与反序列化的接口
- 利用soctet实现客户端与服务端传输协议
- Linux(Ubuntu)安装protoc步骤
- 编写案例代码
- Cartoon.proto
- tcpsocket.h
- MyTcpsocket.h
- client.cpp
- server.cpp
- CMakeList.tx:
- 编译案例
protobuf综述
- Protocol Buffers,是Google公司开发的一种数据格式,类似于XML和json,是一种用于数据传输时将数据序列化和反序列化的一个跨平台(支持目前主流的各种语言)工具库,可用于数据存储、通信协议等方面。它不依赖于语言和平台并且可扩展性极强。
protobuf的优点:
-
更小、更快、更简单。
-
支持多种编程语言 。
-
解析速度快。
-
可扩展性强。
传输协议与指令
创建协议
protobuf的核心是一个.proto
文件,我们自定义一个.proto来创建我们的协议数据,然后使用protocol buffer 编译器工具编译生成两个"文件名.pb.cc"
和"文件名.pb.h"
的文件
例如我们创建一个addressbook.proto
文件:
- 该文件定义为“地址簿”应用程序,可以在文件中读写联系人的详细信息。地址簿中的每个人都有姓名、ID、电子邮件地址和联系电话。
syntax = "proto2";
package tutorial;
message Person {
required string name = 1;//对每个字段需要带上一个编号,编号是从1开始的:
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;//移动电话
HOME = 1;//家庭电话
WORK = 2;//工作电话
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;
message AddressBook{
repeated Person people = 1;
}
}
-
syntax为语法版本
- 有proto2、proto3两个版本,语法上是存在区别的,proto3并不是完全兼容proto2的。同C/C++语言类似,
-
FieldType fieldName = fieldNumber;
- .proto规定了一些数据格式,如proto2的数据类型有:double 、 float、 int32 、 uint32 、 string、bool等。
- fieldName是字段的名称,可以根据需求自定义。
- fieldNumber是字段的唯一标识号,用于在消息的二进制编码中标识字段。标识号的作用是确保在消息的编解码过程中能够准确地识别每个字段。在同一个Message中,每个字段的标识号必须是唯一的。
- 标识号的范围是1到2^29 - 1(Protobuf 3版本中是1到536,870,911)。
- [1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。
-
message为关键字
- 修饰的Person 会对应生成C++中的Person 的结构体。
- 支持嵌套,实际上生成了多个类,有几个message就生成几个类
-
required为前缀修饰【字段只能出现1次】
-
表明该字段是必填字段,被修饰的变量必须要赋值。还有其它两个修饰关键字:
-
optional:被修饰的变量可以不赋值,未赋值则采用默认值,例如bool类型为false,int32为0,string为“”,enum为0等【字段可以出现0次或1次】
-
repeated:作用是用来发送一个list的,当我们对某个变量发送的是一个list的时候,就需要给这个变量前面加上repeated【字段可以出现任意多次(包含0次)】
-
-
-
enum类型数据
- enum编号是从0开始的,因为每个枚举定义必须包含一个映射到零的常量作为第一个元素
- 每个枚举值应以分号结束,而不是逗号。
-
package为命名空间
- 指定生成后.pd.h类的命名空间
- 通过使用包,可以在一个大型的Protobuf项目中组织消息类型,避免不同消息类型之间的命名冲突。同时,包还可以在生成的代码中生成对应的命名空间,以便在编程语言中进行访问和引用。
-
import导入另外一个文件的pb
- proto可以导入在不同的文件中的定义。通过在文件顶端加入一个import语句
- 例如:
import "myproject/other_protos.proto";
- Import Message 的用处主要在于提供了方便的代码管理机制,类似 C 语言中的头文件。您可以将一些公用的 Message 定义在一个 package 中,然后在别的 .proto 文件中引入该 package,进而使用其中的消息定义。Google Protocol Buffer 可以很好地支持嵌套 Message 和引入 Message。
对于proto3版本还有多个关键字:
- singular 不是关键字,代表这个变量是一种类型,就和上面的例子一样,就是 类型+变量名的格式,在传输时,可以不给这种类型赋值,若没赋值则采用默认值,例如bool类型为false,int32为0,string为“”等
- repeated 作用是用来发送一个list的,当我们对某个变量发送的是一个list的时候,就需要给这个变量前面加上repeated
- map:这是一个成对的键值对字段
- optional:与 singular 相同,不过您可以检查该值是否明确设置
参考博文:C/C++编程:Protobuf 使用
编译协议
使用protocol buffer 编译器工具编译addressbook.proto文件的命令
:
protoc --cpp_out=. ./addressbook.proto
此时编译会生成addressbook.pb.cc、addressbook.pb.h两个文件。
介绍addressbook.pb.h文件
addressbook.pb.h里生成了一个协议数据结构体与操作该结构体的一些接口,包括组包与解包(序列化与反序列化)接口,对应的addressbook.pb.cc里就是这些接口对应的实现。
序列化:通俗讲就是将类的实例化对象转成二进制数据然后通过TCP和UDP等传输
反序列化:通俗讲就是将序列化后的二进制数据转换成类的实例化对象,也就是将数据取出来并还原成原先的类的实例化对象
生成的"addressbook.pb.h"中,有几个message就生成几个类
Person类中,编译器会为每个字段生成访问器,例如
每个message类还包含许多其他方法,可用于检查或操作整个类:
检查是否所有 required 字段已被初始化
bool IsInitialized() const;
返回 message 的人类可读的表达形式,对调试特别有用
string DebugString() const;
用给定的 message 的值覆盖 message 特别有用
void CopyFrom(const Person& from);
/将所有元素清除回 empty 状态
void Clear();
而用repeated修饰的对象,需要遍历,然后加入值时有个方法是add_对象名()
people.add_phones();
序列化与反序列化的接口
每个 protocol buffer 类都有提供通过使用 protocobuffer 的二进制格式来读写message 的方法:
序列化消息并将二进制字节存储在给定的字符串中
bool SerializeToString(string* output) const
将给定字符串解析为message
bool ParseFromString(const string& data);
将 message 写入给定的 C++ 的 ostream
bool SerializeToOstream(ostream* output) const;
将给定 C++ istream 解析为message
bool ParseFromIstream(istream* input);
参考博文:protobuf学习笔记
利用soctet实现客户端与服务端传输协议
运行环境:ubuntu18.04,C++11,cmake3.15.5,protobuf3.17.3
Linux(Ubuntu)安装protoc步骤
安装protobuf需要依赖一些工具,需要先安装依赖:
sudo apt-get install autoconf automake libtool curl make g++ unzip
进入自定义的目录下安装proto3,解压并编译安装
wget -O protobuf-3.17.3.tar.gz https://codeload.github.com/protocolbuffers/protobuf/tar.gz/refs/tags/v3.17.3
tar -zxf protobuf-3.17.3.tar.gz
cd protobuf-3.17.3/cmake/
mkdir build && cd build
cmake .. -Dprotobuf_BUILD_TESTS=OFF
make -j4 install
安装好后使用 protoc --version
命令看看是否出现版本号,出现则代表安装成功
如果通过weget不能下载,则在windows端下载
https://github.com/protocolbuffers/ProtoBuf/releases/download/v3.17.3/protoc-3.17.3-linux-x86_64.zip
编写案例代码
创建目录结构如下:
Cartoon.proto
syntax = "proto3";
import "google/protobuf/timestamp.proto";
message Cartoon{
int32 Id = 1;
string name = 2;
string company = 3;
google.protobuf.Timestamp time = 4;
}
message CartoonList{
repeated Cartoon cartoonList = 1; //CartoonList
}
message CartoonRequest{
int32 query = 1; //命令码 1->add 2->selectById 3->selectAll
CartoonList cartoon = 2; //add 时一个Cartoon list, 默认放在 CartoonInf 文件中
int32 selectById = 3; //select 时 Cartoon的Id
bool selectAll = 4; //查询所有的 Cartoon
}
message CartoonResponse{
string res = 1;
CartoonList cartoon = 2; //返回的 select Cartoon的信息
}
tcpsocket.h
//
// Created by Aaj on 2021/8/9.
//
#ifndef MYPROTOCOLBUF_TCPSOCKET_H
#define MYPROTOCOLBUF_TCPSOCKET_H
#include <iostream>
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<unistd.h>
using std::cout;
using std::endl;
using std::cin;
using std::string;
inline void CHECK(bool operation) {
if (operation == false) {
exit(-1);
}
}
class TcpSocket {
public:
TcpSocket() : _sock(-1) {}
void setNewSockFd(int newsockfd) {
_sock = newsockfd;
}
// 创建套接字
bool Socket() {
_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_sock < 0) {
perror("socket error");
return false;
}
return true;
}
// 为套接字绑定地址信息
bool Bind(string& ip, uint16_t& port) const {
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
size_t len = sizeof(struct sockaddr_in);
int binding = bind(_sock, (struct sockaddr*)&addr, len);
if (binding < 0) {
perror("bind error");
return false;
}
return true;
}
// 服务端监听
bool Listen(int backlog = 5) const {
int listening = listen(_sock, backlog);
if (listening < 0) {
perror("listen error");
return false;
}
return true;
}
// 客户端请求连接
bool Connect(string& ip, uint16_t& port) const {
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
size_t len = sizeof(struct sockaddr_in);
int connecting = connect(_sock, (struct sockaddr*)&addr, len);
if (connecting < 0) {
perror("connect error");
return false;
}
return true;
}
// 服务端接受客户端请求
bool Accept(TcpSocket& cli_sock, struct sockaddr_in* cli_addr = NULL) {
TcpSocket clisock;
size_t len = sizeof(struct TcpSocket);
int newsockfd = accept(_sock, (struct sockaddr*)&clisock, (socklen_t *)&len);
if (newsockfd < 0) {
perror("accept error");
return false;
}
if (cli_addr != NULL) {
memcpy(cli_addr, &clisock, len);
cli_sock.setNewSockFd(newsockfd);
}
return true;
}
// 发送数据
bool Send(string& buf) {
size_t size = send(_sock, buf.c_str(), buf.size(), 0);
if (size < 0) {
perror("send error");
return false;
}
return true;
}
// 接收数据
bool Recv(string& buf) {
char buf_tmp[4096] = {0};
size_t size = recv(_sock, buf_tmp, sizeof(buf_tmp), 0);
if (size < 0) {
perror("recv error");
return false;
} else if (size == 0) {
perror("peer shutdown");
return false;
}
buf.assign(buf_tmp, size);
return true;
}
void Close() {
close(_sock);
_sock = -1;
}
private:
int _sock;
};
#endif //MYPROTOCOLBUF_TCPSOCKET_H
MyTcpsocket.h
//
// Created by Aaj on 2021/8/6.
//
#ifndef MYPROTOCOLBUF_MYTCPSOCKET_H
#define MYPROTOCOLBUF_MYTCPSOCKET_H 1
#include<string.h>
#include<iostream>
#include<string>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<unistd.h>
using namespace std;
void CHECK(bool res){
if(res == false){
exit(-1);
}
}
class MyTcpSocket{
private:
int _sock;
public:
MyTcpSocket() : _sock(-1){}
void setNewSockFd(int newsockfd){
_sock = newsockfd;
}
//1.创建套接字
bool Socket(){
//函数原型 int socket(int domain, int type, int protocol);
//int domain(协议族) 参数为 AF_INET 表示 TCP/IP_IPv4
//int type(套接字类型) 参数为SOCK_STREAM 表示 TPC流
//int protocol 参数为0,因为 domain 和 type 已经确定
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sock < 0){
perror("socket error");
return false;
}
return true;
}
//2.为套接字绑定地址信息
bool Bind(string& ip,uint16_t& port) const{
struct sockaddr_in addr; //加上struct为c写法
bzero(&addr, sizeof(addr)); //清零
addr.sin_family = AF_INET; //TCP
addr.sin_port = htons(port); //htons是将整型变量从主机字节顺序转变成网络字节顺序, 就是整数在地址空间存储方式变为高位字节存放在内存的低地址处。
addr.sin_addr.s_addr = inet_addr(ip.c_str()); //将IP转为二进制形式
size_t len = sizeof(struct sockaddr_in);
int binding = bind(_sock,(struct sockaddr*)&addr, len);
if(binding < 0){
perror("bind error");
return false;
}
return true;
}
//3.服务端监听
bool Listen(int backlog = 5) const {
int listening = listen(_sock,backlog);
if(listening < 0){
perror("listen error");
return false;
}
return true;
}
//4.客户端请求连接
bool Connect(string& ip,uint16_t& port) const {
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
size_t len = sizeof(struct sockaddr_in);
int connecting = connect(_sock,(struct sockaddr*)& addr,len);
if(connecting < 0){
perror("connect error");
return false;
}
return true;
}
//5.服务端接受客户端请求
bool Accept(MyTcpSocket& cli_sock,struct sockaddr_in* cli_addr = NULL){
MyTcpSocket clisock;
size_t len = sizeof(struct MyTcpSocket);
int newsockfd = accept(_sock, (struct sockaddr*)&clisock, (socklen_t *)&len);
if(newsockfd < 0){
perror("accept error");
return false;
}
if (cli_addr != NULL) {
memcpy(cli_addr, &clisock, len);
cli_sock.setNewSockFd(newsockfd);
}
return true;
}
//6.发送数据
bool Send(string& buf){
size_t size = send(_sock, buf.c_str(),buf.size(), 0);
if(size < 0){
perror("send error");
return false;
}
return true;
}
//7.接收数据
bool Recv(string& buf){
char buf_tmp[4096] = {0};
size_t size = recv(_sock,buf_tmp, sizeof(buf_tmp),0);
if(size < 0){
perror("recv error");
return false;
}
else if(size == 0){
perror("peer shutdown");
return false;
}
buf.assign(buf_tmp, size);
return true;
}
void Close(){
close(_sock);
_sock = -1;
}
};
#endif //MYPROTOCOLBUF_MYTCPSOCKET_H
client.cpp
//
// Created by Aaj on 2021/8/6.
//
#include "src/Cartoon.pb.h"
#include "src/MyTcpSocket.h"
#include <google/protobuf/util/time_util.h>
#include<cstdio>
#include<iostream>
#include<fstream>
#include<ctime>
#include<list>
#include <netdb.h>
using namespace std;
using google::protobuf::util::TimeUtil;
void addCartoon(CartoonRequest &cartoonRequest){
while(true){
Cartoon *cartoon = cartoonRequest.mutable_cartoon()->add_cartoonlist();
cout<<"输入动画Id: ";
int Id;
cin>>Id;
//cout<<Id<<endl;
cin.ignore(256,'\n');//忽略掉一个回车
cartoon->set_id(Id);
cout<<"输入动画名称: ";
string name;
getline(cin,name);
//cout<<name<<endl;
cartoon->set_name(name);
cout<<"输入动画出品公司: ";
string company;
getline(cin,company);
//cout<<company<<endl;
cartoon->set_company(company);
*cartoon->mutable_time() = TimeUtil::SecondsToTimestamp(time(NULL));
string res;
while(true){
cout<<"输入 0 结束,1 继续输入"<<endl;
cin>>res;
if(res == "0" || res == "1")
break;
}
if(res == "0"){
// /*这里暂时写成直接写入CartoonInf文件方式*/
// fstream output("CartoonInf",ios::out | ios::binary | ios::app); //app代表追加方式写入
// CartoonList cartoonList = cartoonRequest.cartoon();
// if(!cartoonList.SerializePartialToOstream(&output)){
// cerr<<"无法写入CartoonInf\n";
// exit(-1);
// }
return ;
}
}
}
void selectById(CartoonRequest &cartoonRequest){
cout<<"请输入Id: ";
int Id;
cin>>Id;
cin.ignore(256,'\n');//忽略掉一个回车
cartoonRequest.set_selectbyid(Id);
}
void selectAll(CartoonRequest &cartoonRequest){
cartoonRequest.set_selectall(true);
}
int main(){
//如果没有CartoonInf则先创建一个
if(!fopen("CartoonInf","r")){
fopen("CartoonInf","w");
}
string ip = "127.0.0.1";//本机
uint16_t port = 9999;
MyTcpSocket client;
CHECK(client.Socket());
CHECK(client.Connect(ip,port));
while(1){
CartoonRequest cartoonRequest;
cout<<"输入请求码 1->add 2->selectById 3->selectAll,0->退出:";
int code;
cin>>code;
cin.ignore(256,'\n');//忽略掉一个回车
//cout<<code<<endl;
cartoonRequest.set_query(code);
//add
if(code == 1){
addCartoon(cartoonRequest);
//break;
}
//selectById
else if(code == 2){
selectById(cartoonRequest);
//break;
}
//selectAll
else if(code == 3){
selectAll(cartoonRequest);
//break;
}
else if(code == 0 ){
cout<<"退出成功!"<<endl;
break;
}
else{
cout<<"请求码非法,重新输入!"<<endl;
continue;
}
string data;
cartoonRequest.SerializeToString(&data);
CHECK(client.Send(data));
data.clear();
CHECK(client.Recv(data));
CartoonResponse cartoonResponse;
cartoonResponse.ParseFromString(data);
cout<<cartoonResponse.res()<<endl;
if(code == 2 || code == 3){
for(int i=0;i<cartoonResponse.mutable_cartoon()->cartoonlist_size();++i){
Cartoon cartoon = cartoonResponse.mutable_cartoon()->cartoonlist(i);
cout<<"动画Id:"<<cartoon.id()<<endl;
cout<<"动画名称:"<<cartoon.name()<<endl;
cout<<"动画出品公司:"<<cartoon.company()<<endl;
cout<<"***************************************"<<endl;
}
}
}
client.Close();
return 0;
}
server.cpp
//
// Created by Aaj on 2021/8/6.
//
#include"src/Cartoon.pb.h"
#include "src/MyTcpSocket.h"
#include<iostream>
#include <fstream>
#include <string>
using namespace std;
void addCartoon(CartoonRequest &cartoonRequest){
fstream output("CartoonInf",ios::out | ios::binary | ios::app); //app代表追加方式写入
CartoonList cartoonList = cartoonRequest.cartoon();
if(!cartoonList.SerializePartialToOstream(&output)){
cerr<<"无法写入CartoonInf"<<endl;
exit(-1);
}
return ;
}
void selectByCartoonId(int Id,CartoonResponse &cartoonResponse){
CartoonList cartoonList;
fstream input("CartoonInf",ios::in | ios::binary);
if(input && !cartoonList.ParseFromIstream(&input)){
cerr<<"无法读取CartoonInf"<<endl;
exit(-1);
}
// /*这里暂时就直接显示出来*/
// cout<<"*selectByCartoonId*"<<endl;
// for(int i=0;i<cartoonList.cartoonlist_size();++i){
// Cartoon cartoon = cartoonList.cartoonlist(i);
// cout<<"动画Id:"<<cartoon.id()<<endl;
// cout<<"动画名称:"<<cartoon.name()<<endl;
// cout<<"动画出品公司:"<<cartoon.company()<<endl;
// cout<<"***************************************"<<endl;
// }
for(int i=0;i<cartoonList.cartoonlist_size();++i){
if(cartoonList.cartoonlist(i).id() == Id){
Cartoon* cartoon = cartoonResponse.mutable_cartoon()->add_cartoonlist();
cartoon->set_id(cartoonList.cartoonlist(i).id());
cartoon->set_name(cartoonList.cartoonlist(i).name());
cartoon->set_company(cartoonList.cartoonlist(i).company());
break;
}
}
return ;
}
void selectAllCartoon(CartoonResponse &cartoonResponse){
CartoonList cartoonList;
fstream input("CartoonInf",ios::in | ios::binary);
if(input && !cartoonList.ParseFromIstream(&input)) {
cerr << "无法读取CartoonInf"<<endl;
exit(-1);
}
// /*这里暂时就直接显示出来*/
// cout<<"*selectAllCartoon*"<<endl;
// for(int i=0;i<cartoonList.cartoonlist_size();++i){
// Cartoon cartoon = cartoonList.cartoonlist(i);
// cout<<"动画Id:"<<cartoon.id()<<endl;
// cout<<"动画名称:"<<cartoon.name()<<endl;
// cout<<"动画出品公司:"<<cartoon.company()<<endl;
// cout<<"***************************************"<<endl;
// }
for(int i=0;i<cartoonList.cartoonlist_size();++i){
Cartoon* cartoon = cartoonResponse.mutable_cartoon()->add_cartoonlist();
cartoon->set_id(cartoonList.cartoonlist(i).id());
cartoon->set_name(cartoonList.cartoonlist(i).name());
cartoon->set_company(cartoonList.cartoonlist(i).company());
}
return ;
}
int main(){
//如果没有CartoonInf则先创建一个
if(!fopen("CartoonInf","r")){
fopen("CartoonInf","w");
}
string ip = to_string(INADDR_ANY);
uint16_t port = 9999;
MyTcpSocket service;
CHECK(service.Socket());
CHECK(service.Bind(ip,port));
CHECK(service.Listen());
MyTcpSocket clisock;
struct sockaddr_in cliaddr;
CHECK(service.Accept(clisock, &cliaddr));
//selectAllCartoon(); //测试用
while(1){
string data;
CartoonRequest cartoonRequest;
CHECK(clisock.Recv(data));
cartoonRequest.ParseFromString(data);
CartoonResponse cartoonResponse;
if(cartoonRequest.query() == 1){
cout<<"addCartoon"<<endl;
addCartoon(cartoonRequest);
cartoonResponse.set_res("addCartoon success!");
}
else if(cartoonRequest.query() == 2){
cout<<"selectByCartoonId"<<endl;
selectByCartoonId(cartoonRequest.selectbyid(),cartoonResponse);
if(cartoonResponse.cartoon().cartoonlist_size()==0){
cartoonResponse.set_res("Cartoons don't exist");
}
else{
cartoonResponse.set_res("selectByCartoonId success!");
}
}
else if(cartoonRequest.query() == 3){
cout<<"selectAllCartoon"<<endl;
selectAllCartoon(cartoonResponse);
if(cartoonResponse.cartoon().cartoonlist_size()==0){
cartoonResponse.set_res("Cartoons don't exist");
}
else{
cartoonResponse.set_res("selectAllCartoon success!");
}
}
else{
cartoonResponse.set_res("query error!");
}
cartoonResponse.SerializeToString(&data);
CHECK(clisock.Send(data));
}
service.Close();
return 0;
}
CMakeList.tx:
cmake_minimum_required(VERSION 3.0)
project(myProtocolBuf)
set(CMAKE_CXX_STANDARD 11)
find_package(Protobuf REQUIRED)
include_directories(${Protobuf_INCLUDE_DIRS})
link_libraries(${Protobuf_LIBRARY})
FILE(GLOB SRC ./src/*.cc ./src/*.h)
add_executable(client client.cpp ${SRC})
add_executable(server server.cpp ${SRC})
编译案例
进入src目录,编译生成相应的”.pb.cc“和“.pb.h”文件
protoc --cpp_out=. ./Cartoon.proto
进入build目录,执行命令:
cmake ..
make
运行服务端与客户端并查看结果;
参考博文:使用socket和TCP网络协议实现的客户端和服务端示例,采用谷歌的protobuf数据传输协议(类似json),部署在Linux上
拓展博文:
嵌入式大杂烩 | Protobuf:一种更小、更快、更高效的协议
干货 | protobuf-c之嵌入式平台使用
干货 | 项目乏力?nanopb助你一臂之力