文章目录
- protobuf综述
- 传输协议与指令
- 创建协议
- 编译协议
- 利用soctet实现客户端与服务端传输协议
- Linux(Ubuntu)安装protoc步骤
- 编写案例代码
protobuf综述
- Protocol Buffers,是Google公司开发的一种数据格式,类似于XML和json,是一种用于数据传输时将数据序列化和反序列化的一个跨平台(支持目前主流的各种语言)工具库,可用于数据存储、通信协议等方面。它不依赖于语言和平台并且可扩展性极强。
protobuf的优点:
-
更小、更快、更简单。
-
支持多种编程语言 。
-
解析速度快。
-
可扩展性强。
传输协议与指令
创建协议
protobuf的核心是一个.proto
文件,我们自定义一个.proto来创建我们的协议数据,然后使用protocol buffer 编译器工具编译生成两个"文件名.pb.cc"
和"文件名.pb.h"
的文件
例如我们创建一个addressbook.proto文件:
该文件定义为“地址簿”应用程序,可以在文件中读写联系人的详细信息。地址簿中的每个人都有姓名、ID、电子邮件地址和联系电话。
syntax = "proto3";
package myDemo;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++语言类似,.proto也规定了一些数据格式,如proto2的数据类型有:
double 、 float、 int32 、 uint32 、 string、bool
等。
- 有proto2、proto3两个版本,语法上是存在区别的,proto3并不是完全兼容proto2的。同C/C++语言类似,.proto也规定了一些数据格式,如proto2的数据类型有:
-
message为关键字
- 修饰的Person 会对应生成C++中的Person 的结构体。
- 支持嵌套,实际上生成了多个类,有几个message就生成几个类
-
required为前缀修饰【字段只能出现1次】
-
表明该字段是必填字段,被修饰的变量必须要赋值。还有其它两个修饰关键字:
-
optional:被修饰的变量可以不赋值,未赋值则采用默认值,例如bool类型为false,int32为0,string为“”等【字段可以出现0次或1次】
-
repeated:作用是用来发送一个list的,当我们对某个变量发送的是一个list的时候,就需要给这个变量前面加上repeated【字段可以出现任意多次(包含0次)】
-
-
-
enum类型数据
- enum编号是从0开始的
-
package xxx
- 指定生成后.pd.h类的命名空间
对于proto3版本还有两个关键字:
- singular 不是关键字,代表这个变量是一种类型,就和上面的例子一样,就是 类型+变量名的格式,在传输时,可以不给这种类型赋值,若没赋值则采用默认值,例如bool类型为false,int32为0,string为“”等
- repeated 作用是用来发送一个list的,当我们对某个变量发送的是一个list的时候,就需要给这个变量前面加上repeated
编译协议
使用protocol buffer 编译器工具编译addressbook.proto文件的命令
:
protoc --cpp_out=. ./addressbook.proto
此时编译会生成addressbook.pb.cc、addressbook.pb.h两个文件。
我们可以看到student.pb.h里生成了一个协议数据结构体与操作该结构体的一些接口,包括组包与解包(序列化与反序列化)接口,对应的student.pb.cc里就是这些接口对应的实现。
序列化:通俗讲就是将类的实例化对象转成二进制数据然后通过TCP和UDP等传输
反序列化:通俗讲就是将序列化后的二进制数据转换成类的实例化对象,也就是将数据取出来并还原成原先的类的实例化对象
生成的"person.pb.h"中,有几个message就生成几个类
普通对象(没有用关键词修饰)可以调用set,get函数去操作
而用repeated修饰的对象,需要遍历,然后加入值时有个方法是add_对象名()
person.add_phones();
序列化与反序列化函数,例如:
person.SerializeToString
参考博文: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
命令看看是否出现版本号,出现则代表安装成功
编写案例代码
创建目录结构如下:
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->selectAllCartoonList cartoon = 2; //add 时一个Cartoon list, 默认放在 CartoonInf 文件中int32 selectById = 3; //select 时 Cartoon的Idbool 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; //TCPaddr.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);//addif(code == 1){addCartoon(cartoonRequest);//break;}//selectByIdelse if(code == 2){selectById(cartoonRequest);//break;}//selectAllelse 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助你一臂之力
版本冲突解决