大小端字节序
网络字节序为大端序
,计算机内部存储有可能是大端,有可能是小端。
- 网络传输时,需要将本地数据转换为大端序,实现字节序的转换。
判断是大端还是小端:
// 判断当前系统的字节序是大端序还是小端序
bool is_big_endian() {
int num = 1;
if (*(char*)&num == 1) {
// 当前系统为小端序
return false;
} else {
// 当前系统为大端序
return true;
}
}
-
在 boost::asio 库中,可以使用
boost::asio::detail::socket_ops::host_to_network_long()
和boost::asio::detail::socket_ops::host_to_network_short()
函数将主机字节序转换为网络字节序。 -
network_to_host_short从网络字节序转成本地字节序。
-
在服务器的发送数据时会构造消息节点,构造消息节点时,将发送长度由本地字节序转化为网络字节序.
MsgNode(char * msg, short max_len):_total_len(max_len + HEAD_LENGTH),_cur_len(0){
_data = new char[_total_len+1]();
//转为网络字节序
int max_len_host = boost::asio::detail::socket_ops::host_to_network_short(max_len);
memcpy(_data, &max_len_host, HEAD_LENGTH);
memcpy(_data+ HEAD_LENGTH, msg, max_len);
_data[_total_len] = '\0';
}
消息队列控制
- 发送的数据放到消息队列中,异步发送数据时,要用队列来保证发送的时序性,有的时候发送的频率过高会导致队列增大,所以要对队列的大小做限制,当队列大于指定数量的长度时,就丢弃要发送的数据包,以保证消息的快速收发。
#define MAX_SENDQUE 1024
// 实现发送接口
void CSession::Send(char* msg, int max_length) {
bool pending = false; // pending为true表示上一次数据没有发完。
int send_que_size = _send_que.size();
if (send_que_size > MAX_SENDQUE) {
cout << "session: " << _uuid << " send que fulled, size is " << MAX_SENDQUE << endl;
return;
}
// 队列里有数据,就不发送了,让队列里面的回调函数发送就行了。
_send_que.push(make_shared<MsgNode>(msg, max_length));
if (send_que_size>0) {
return;
}
boost::asio::async_write(_socket, boost::asio::buffer(msg, max_length),
std::bind(&CSession::HandleWrite,this,std::placeholders::_1,shared_from_this()));
}
protobuf
- protobuf是谷歌设计一种轻便高效的序列化数据结构的协议,独立于语言和平台。是一种二进制的格式,比使用 xml 、json进行数据交换快许多。
http信息交互中有的信息类是抽象数据构成的,而tcp是面向字节流的,需要将类结构序列化为字符串来进行传输。
- 下载protobuf的源码之后,用cmake构建C++代码,生成适用于各种构建系统的构建文件(如makefile,visual studio解决方案等)
- 使用 protobuf 能够多(数据结构支持复杂结构、语言支持程度覆盖主流编程语言)、快(编解码效率)、好(数据保存方式为二进制,保存更节省空间、读取转换时间)、省(数据保存大小)。
windows下编译protobuf
- 下载Cmake桌面程序,点击source code选择protobuf源码路径下的cmake目录。
- build binaries目录选择自己创建的visualstudio目录。
- 点击config,再点击generate就可以生成了。
- 就可以看到visualstudio目录内生成了.sln文件。
- 点击.sln文件打开项目
- 单独选择libprotobuf编译。
- 编译时可以选择debug和release,建议每一个都编译一遍,这样就有debug和release两个版本的库了。
- debug版本的库,编译之后,会在visualstudio目录下的Debug文件夹里。
- 生成了protobuf相关的lib库和dll库,一个是静态类型的,一个是动态类型的。
- 在该文件夹内创建一个bin文件夹(用来存储刚才protobuf生成的库)和include文件夹(用来存储protobuf的头文件)。有了库,需要给用户头文件,让用户去使用。
- 将libprotobufd.lib和libprotocd.lib, 以及protoc.exe拷贝到bin目录下,将protobuf文件夹下src文件夹里的google文件夹及其内容拷贝到protoc的include文件夹。
- 添加环境变量,将protoc命令配置到环境变量,在系统环境变量里添加一个环境变量PROTOBUF_HOME,设置它的值为D:\cppsoft\protoc\bin。这个环境变量的值要根据自己创建的bin目录去设置。
- 可以直接使用protoc.exe了。
- 在visual studio中使用protobuf,需要进行一下配置,和boost的配置是一样的
- 配置选择Debug,平台选择X64,选择VC++目录
- 在包含目录中添加 D:\cppsoft\protoc\include
- 在库目录中添加 D:\cppsoft\protoc\bin
- 在链接器里的输入选项中添加protobuf用到的lib库
libprotobufd.lib
libprotocd.lib
Linux下编译protobuf
# clone code
https://github.com/protocolbuffers/protobuf/releases
# 解压缩
tar zxvf protobuf-cpp-3.21.12.tar.gz
# 进入到解压目录
cd protobuf-3.21.12/
# 构建并安装
./configure # 检查安装环境, 生成 makefile
make # 编译
sudo make install # 安装
# 测试
protoc --version
# 动态库位于/usr/local/lib目录,接下来需要需要将这目录添加到/etc/ld.so.conf这个配置文件
sudo vim /etc/ld.so.conf
# 打开文件后,把目录添加到第二行
/usr/local/lib
# 重新load配置文件
sudo ldconfig
# 测试
protoc --version
libprotoc 3.21.12
使用protobuf
- protobuf可以序列化单一数据类型和复合数据类型。
- 创建一个新的文件, 文件名随意指定, 文件后缀为 .proto
- 根据protobuf的语法, 编辑.proto文件
- 使用 protoc 命令将 .proto 文件转化为相应的 C++ 文件
- 源文件: xxx.pb.cc –> xxx对应的名字和 .proto文件名相同。
- 头文件: xxx.pb.h –> xxx对应的名字和 .proto文件名相同。
- 需要将生成的c++文件添加到项目中, 通过文件中提供的类 API 实现数据的序列化/反序列化
生成pb文件
8. 使用protobuf的序列化功能,需要生成pb文件,pb文件包含了我们要序列化的类信息。我们先创建一个msg.proto,该文件用来定义我们要发送的类信息。
syntax = "proto3"; # 指定Protobuf的版本号
message Book
{
string name = 1; # 数字标识符,用于标识该字段在二进制流中的位置。
int32 pages = 2;
float price = 3;
}
用protoc.exe 基于msg.proto生成我们要用的C++类
protoc --cpp_out=. ./msg.proto
目录生成了msg.pb.h和msg.pb.cc两个文件,这两个文件就是我们要用到的头文件和cpp文件。
将这两个文件添加到项目里,然后在主函数中包含msg.pb.h,做如下测试:将book对象先序列化为字符串,再将字符串反序列化为book2对象。代码中调用的函数都是自动生成的
#include <iostream>
#include "msg.pb.h"
int main()
{
Book book;
book.set_name("CPP programing");
book.set_pages(100);
book.set_price(200);
std::string bookstr;
book.SerializeToString(&bookstr); //将book对象先序列化为字符串
std::cout << "serialize str is " << bookstr << std::endl;
Book book2;
book2.ParseFromString(bookstr); // 字符串反序列化为book2对象。
std::cout << "book2 name is " << book2.name() << " price is "
<< book2.price() << " pages is " << book2.pages() << std::endl;
getchar();
}
网络消息服务中使用protobuf
- 先为服务器定义一个用来通信的proto
syntax = "proto3";
message MsgData
{
int32 id = 1;
string data = 2;
}
- 用proto生成对应的pb.h和pb.cc文件,将proto,pb.cc,pb.h三个文件复制服务器项目里并且配置。
- 修改服务器接收数据和发送数据的逻辑。
- 当服务器收到数据后,完成切包处理后,将信息反序列化为具体要使用的结构,打印相关的信息,然后再发送给客户端。
MsgData msgdata;
std::string receive_data;
msgdata.ParseFromString(std::string(_recv_msg_node->_data, _recv_msg_node->_total_len));
std::cout << "recevie msg id is " << msgdata.id() << " msg data is " << msgdata.data() << endl;
std::string return_str = "server has received msg, msg data is " + msgdata.data();
MsgData msgreturn;
msgreturn.set_id(msgdata.id());
msgreturn.set_data(return_str);
msgreturn.SerializeToString(&return_str);
Send(return_str);
- 客户端在发送的时候也利用protobuf进行消息的序列化,然后发给服务器。
JSONCPP
JSONCPP源码链接:https://github.com/open-source-parsers/jsoncpp
- JSOCPP源码下载以后,首先复制一份include文件夹下的json文件夹,头文件留着后续备用。
- 使用Cmake生成项目。在IDE中编译jsoncpp_lib,可以在项目的lib/Debug文件夹下找到jsoncpp.lib,在bin/Debug/文件夹下找到jsoncpp.dll。将头文件和动态链接库文件,放入项目中即可使用。
jsoncpp库中的类被定义到了一个Json命名空间中,使用时最好先声明这个命名空间。
配置Jsoncpp:
- 项目属性中,VC++包含目录设置为 D:\JSONCPP\include
- 库目录选择为 VC++库目录设置为 D:\JSONCPP\lib
使用jsoncpp库解析json格式的数据,三个类:
- Value 类:将json支持的数据类型进行了包装,最终得到一个Value类型。
- FastWriter类:将Value对象中的数据序列化为字符串。
- Reader类:反序列化,将json字符串解析成Value类型。
#include <iostream>
#include <json/json.h>
#include <json/value.h>
#include <json/reader.h>
int main()
{
Json::Value root;
root["id"] = 1001;
root["data"] = "hello world";
std::string request = root.toStyledString(); //序列化为字符串
std::cout << "request is " << request << std::endl;
Json::Value root2;
Json::Reader reader;
reader.parse(request, root2); // 反序列化为value类型。
std::cout << "msg id is " << root2["id"] << " msg is " << root2["data"] << std::endl;
}
在网络通信中的JSONCPP
- 客户端发送数据时,将发送的数据进行序列化
Json::Value root;
root["id"] = 1001;
root["data"] = "hello world";
std::string request = root.toStyledString(); // 序列化为字符串
size_t request_length = request.length();
char send_data[MAX_LENGTH] = { 0 };
//转为网络字节序
int request_host_length = boost::asio::detail::socket_ops::host_to_network_short(request_length);
memcpy(send_data, &request_host_length, 2); //先拷贝2字节的数据长度信息
memcpy(send_data + 2, request.c_str(), request_length); // 再拷贝数据本身
boost::asio::write(sock, boost::asio::buffer(send_data, request_length + 2)); // 封装到buffer中进行发送
- 服务器进行反序列化,注意要转换成本地字节序
Json::Reader reader;
Json::Value root;
reader.parse(std::string(_recv_msg_node->_data, _recv_msg_node->_total_len), root);
std::cout << "recevie msg id is " << root["id"].asInt() << " msg data is "
<< root["data"].asString() << endl;
参考列表:
https://subingwen.cn/cpp/protobuf/?highlight=proto