🔥 在网络编程中,协议是一个关键概念。协议本质上是一种“约定”,规定了两方在通信时如何格式化和处理数据。本文将深入探讨如何通过协议进行结构化数据的传输,并且通过一个具体的网络版计算器( TCP服务器-客户端)示例,展示序列化与反序列化的实现。
我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序,都是在应用层
🔥 协议,简单来说,就是通信双方都遵守的规则。比如:我们每天上学,为了避免不迟到,就会定一个闹钟来提醒自己在一个特定的时间起床,这就是一种约定——协议。
🧀 在网络通信中,数据通常以字节流的形式发送和接收。
而这里的结构体如message就是传说中的业务协议。 因为它规定了我们聊天时网络通信的数据。
结论:协议就是双方约定好的结构化的数据
🔥 为了简化结构化数据的传输,我们通常将多个独立的信息合并为一个报文。这就是序列化的过程:将数据打包成一个字符串或字节流,再通过网络发送。接收方收到数据后,需要通过反序列化,将收到的数据解析回原来的结构化数据。
为了方便理解,我们下面举一个网络版计算器的例子
例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端.
约定方案一:
约定方案二:
无论我们采用方案一, 还是方案二,还是其他的方案, 只要保证一端发送时构造的数据,在另一端能够正确的进行解析就是可以的。这种约定就是 应用层协议
但是,为了深刻理解协议,下面将会自定义实现一下协议的过程。
如果要实现它,我们需要用到我们之前所学的知识
上面要实现序列化和反序列化,有两种方案
这里我们为了增加可读性,建议将结构化数据转化为 json(jsoncpp) 的字符串,这篇文章主要是关于第二种方案
在主机 A 中,我们创建一个 tcp sockfd 的时候,在 OS 内部会给 Tcp 套接字创建两个缓冲区(发送、接收缓冲区),同样主机 B 也是如此
主机 B 接收数据,主机 A Tcp 内的发送缓冲区内的数据 经过 网络 拷贝到对方的接受缓冲区,而如果主机 B 的接受缓冲区内没数据,此时 read 就会阻塞,就相当于应用层进程就卡住,而一旦有数据,就拷贝到 buffer,那么也就是说 recv 也是拷贝,一切皆拷贝
为什么 TCP 支持全双工
从 内核原码 的角度,我们可以看到 OS 内部存在大量报文,对其管理 (先描述,再组织)
🔥 假设主机 A 的发送缓冲区有 20 个字节,对方接收缓存区假设只剩10个字节的空间了,由于 tcp你不是要进行未来要进行各种控制嘛,如果你要发送的时候对方只剩10个了,tcp 将来以一定的方式就知道了对方只剩10 个字节,所以 tcp 也就只能发送10个字节过去,因为 tcp 要进行流量控制等的,这些东西所以我们对应的tcp它就把这这个报文的一部分发给对方了
此时 read 读取就有问题了,只读取了原始报文的一部分
还记得我们之前说的
不完整原因:这里读到 inbuffer 不一定是对方完整发过来的,Tcp 不进行报文完整性处理的
因此我们需要对之前的两个方案进行改变
注意:实际在处理的时候还要添加完整的报头
由于我们这里用到是 jsoncpp ,因此 Makefile 文件里面也要带上 -ljsoncpp
.PHONY:all
all:server_tcp client_tcp
server_tcp:TcpServer.cc
g++ -o $@ $^ -std=c++17 -lpthread -ljsoncpp
client_tcp:TcpClient.cc
g++ -o $@ $^ -std=c++17 -ljsoncpp
.PHONY:clean
clean:
rm -f client_tcp server_tcp
根据我们之前在 Socket 网络编程Tcp 文章里面远程命令版本的 EchoServer 来编写
TcpServer.hpp
#include <iostream>
...
using namespace LogModule;
using namespace ThreadPoolModule;
static const uint16_t gport = 8080;
using handler_t = std::function<std::string (std::string&)>;
class TcpServer
{
using task_t = std::function<void()>;
struct ThreadData
{
int sockfd;
TcpServer *self;
};
public:
TcpServer(handler_t handler, int port = gport)
: _handler(handler),
_port(port),
_isrunning(false)
{
}
void InitServer()
{
// 1. 创建 Tcp Socket
_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0); // TCP SOCKET
if(_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "socket";
Die(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket create success, socked is : " << _listensockfd;
struct sockaddr_in local;
memset(&local, 0, sizeof(local));;
local.sin_family = AF_INET;
local.sin_port = htons(gport);
local.sin_addr.s_addr = INADDR_ANY;
// 2. bind
int n = ::bind(_listensockfd, CONV(&local), sizeof(local));
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
Die(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success, sockfd is : " << _listensockfd;
// 3.cs, tcp 是面向连接的,因此需要 tcp 随时随地等待被连接
// tcp 需要将 socket 设置为监听状态
n = ::listen(_listensockfd, BACKLOG);
if(n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
Die(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "listen success, sockfd is : " << _listensockfd;
}
void HandlerRequest(int sockfd) // TCP 同UDP 一样,也全双工通信
{
LOG(LogLevel::INFO) << "HandlerRequest, sockfd is : " << sockfd;
char inbuffer[4096];
std::string package;
// 长任务,在线程池中一般是处理短任务的, 因此对应的我们的线程池应该调大点
while(true)
{
memset(inbuffer, 0, sizeof(inbuffer)); // 清空缓冲区
// 约定:用户发过来的是一个完整命令 string
ssize_t n = ::recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
if(n > 0)
{
LOG(LogLevel::INFO) << inbuffer;
inbuffer[n] = 0; // 确保字符串以 null 结尾
package += inbuffer; // 报文风格:len\r\n{json}\r\n
std::string cmd_result = _handler(package);
if(cmd_result.empty()) continue;
::send(sockfd, cmd_result.c_str(), cmd_result.size(), 0);
}
else if(n == 0)
{
// read 如果读取返回值为 0,表示 client 退出
LOG(LogLevel::INFO) << "client quit: " << sockfd;
break;
}
else
{
// 处理 read 错误
if (errno == EINTR) {
continue; // 信号中断,重试
}
LOG(LogLevel::ERROR) << "read error: " << strerror(errno) ;
break;
}
}
// 关闭套接字
::close(sockfd); // fd 泄露问题
}
......
private:
int _listensockfd; // 监听 socket
uint16_t _port;
bool _isrunning;
// 处理上层任务的入口
handler_t _handler;
};
Protocol.hpp
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <jsoncpp/json/json.h>
bool Encode(std::string message) // 添加报头
{
}
bool Decode(std::string &package)
{
}
// _x _oper _y
class Request
{
public:
Request(int x, int y, char oper)
: _x(x),
_y(y),
_oper(oper)
{
}
// 序列化
bool Serizlize(std::string &out_string)
{
}
// 反序列化
bool Deserialize(std::string &in_string)
{
}
private:
int _x;
int _y;
char _oper;
};
class Response
{
public:
Response(int result, int code)
: _result(result),
_code(code)
{
}
// 序列化
bool Serizlize()
{}
// 反序列化
bool Deserialize()
{}
private:
int _result; // 结果
int _code; // 出错码,0,1,2,3,4
};
// _x _oper _y
class Request
{
public:
Request() : _x(0), _y(0), _oper(0)
{}
Request(int x, int y, char oper)
: _x(x),
_y(y),
_oper(oper)
{}
// 序列化
bool Serialize(std::string &out_string)
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::StreamWriterBuilder wb;
std::unique_ptr<Json::StreamWriter> w(wb.newStreamWriter());
std::stringstream ss;
w->write(root, &ss);
out_string = ss.str();
return true;
}
// 反序列化
bool Deserialize(std::string &in_string)
{
Json::Value root;
Json::Reader reader;
bool parsingSuccessful = reader.parse(in_string, root);
if (!parsingSuccessful)
{
std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;
return false;
}
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();
return true;
}
void Print()
{
std::cout << _x << std::endl;
std::cout << _oper << std::endl;
std::cout << _y << std::endl;
}
int X() const {return _x;}
int Y() const {return _y;}
char Oper() const {return _oper;}
private:
int _x;
int _y;
char _oper;
};
然后我们检测一下该代码是否编写成功
#include "Protocol.hpp"
int main()
{
Request req(10, 20, '+');
std::string s;
req.Serizlize(s);
std::cout << s << std::endl;
req.Deserialize(s);
req.Print();
return 0;
}
编译运行指令如下:
g++ Test.cc -ljsoncpp
运行结果如下:
同上面类似,就不做过多检测和解释了,直接看代码
class Response
{
public:
Response(): _result(0), _code(0)
{}
Response(int result, int code)
: _result(result),
_code(code)
{}
// 序列化
bool Serialize(std::string &out_string)
{
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::StreamWriterBuilder wb;
std::unique_ptr<Json::StreamWriter> w(wb.newStreamWriter());
std::stringstream ss;
w->write(root, &ss);
out_string = ss.str();
return true;
}
// 反序列化
bool Deserialize(std::string &in_string)
{
Json::Value root;
Json::Reader reader;
bool parsingSuccessful = reader.parse(in_string, root);
if(!parsingSuccessful)
{
std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;
return false;
}
_result = root["result"].asInt();
_code = root["code"].asInt();
return true;
}
int Result() const {return _result;}
int Code() const {return _code;}
void SetResult(int res) {_result = res;}
void SetCode(int c) {_code = c;}
private:
int _result; // 结果
int _code; // 出错码,0,1,2,3,4
};
const std::string Sep = "\r\n";
// {json} -> len\r\n{json}\r\n
bool Encode(std::string &message) // 添加报头
{
if(message.size() == 0) return false;
std::string package = std::to_string(message.size()) + Sep + message + Sep; // 完整报文
message = package;
return true;
}
// len\r\n{json}\r\n
// 123\r\n{json}\r\n
// 不完整的就不处理
// 123\r\n
// 123\r\n{json}
// 123\r\n{json}\r\n123\r\n{json}\r
bool Decode(std::string &package, std::string *content)
{
auto pos = package.find(Sep);
if(pos == std::string::npos) return false;
std::string content_length_str = package.substr(0, pos);
int content_length = std::stoi(content_length_str);
int full_length = content_length_str.size() + content_length + 2*Sep.size();
if(package.size() < full_length) return false;
*content = package.substr(pos + Sep.size(), content_length);
// package erase
package.erase(0, full_length);
return true;
}
#pragma once
#include <iostream>
#include "Protocol.hpp"
class Calculator
{
public:
Calculator()
{}
Response Execute(const Request &req)
{
// 我们拿到的都是结构化数据,拿到就相当于类对象
Response resp;
switch(req.Oper())
{
case '+':
resp.SetResult(req.X() + req.Y());
break;
case '-':
resp.SetResult(req.X() - req.Y());
break;
case '*':
resp.SetResult(req.X() * req.Y());
break;
case '/':
{
if(req.Y() == 0) {
resp.SetCode(1); // 1 就是除0
}
else{
resp.SetResult(req.X() / req.Y());
}
}
break;
case '%':
{
if(req.Y() == 0) {
resp.SetCode(2); // 2 就是mod 0
}
else{
resp.SetResult(req.X() % req.Y());
}
}
break;
default:
resp.SetCode(3); // 3 表示用户发来的计算类型 无法识别
break;
}
return resp;
}
~Calculator()
{}
};
TcpServer.cc
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Calculator.hpp"
#include <memory>
using cal_fun = std::function<Response(const Request &req)>;
// 这里 package 不一定有完整报文
// 如果不完整让底层继续读
// 完整 -> 提取 -> 反序列化 -> Request -> 计算模块进行处理
class Parse
{
public:
Parse(cal_fun c): _cal(c)
{}
std::string Entry(std::string &package)
{
// 1. 判断报文完整性
std::string message;
std::string respstr;
while(Decode(package, &message)) // 一次处理多个请求
{
LOG(LogLevel::DEBUG) << "Content: \n" << message;
if(message.empty()) break;
// 2. 反序列化:message 是一个曾经被序列化的 request
Request req;
if(!req.Deserialize(message)) break;
std::cout << "##############" << std::endl;
req.Print();
std::cout << "##############" << std::endl;
// 3. 计算
Response resp = _cal(req);
// 4. 序列化
std::string res;
resp.Serialize(res);
LOG(LogLevel::DEBUG) << "序列化: \n" << res;
// 5. 添加长度报头字段
Encode(res);
LOG(LogLevel::DEBUG) << "Encode: \n" << res;
// 6. 拼接应答
respstr += res;
}
LOG(LogLevel::DEBUG) << "resptr: \n" << respstr;
return respstr;
}
~Parse()
{}
private:
cal_fun _cal;
};
int main()
{
ENABLE_CONSOLE_LOG();
// 1. 计算模块
Calculator mycal;
// 2. 解析对象
Parse myparse([&mycal](const Request& req){
return mycal.Execute(req);
});
// 3. 通信模块
// 只负责进行 IO
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>([&myparse](std::string &package){
return myparse.Entry(package);
});
tsvr->InitServer();
tsvr->start();
return 0;
}
TcpClient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Protocol.hpp" // 形成约定
// ./client_tcp server_ip server_port
int main(int argc, char* argv[])
{
if(argc != 3)
{
std::cout << "Usage:./client_tcp server_ip server_port" << std::endl;
return 1;
}
// 获取 server ip 和 port
std::string server_ip = argv[1]; // "192.168.1.1" 点分十进制的 ip 地址
int server_port = std::atoi(argv[2]);
// 创建 socket
int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
std::cout << "Error: Failed to create socket" << std::endl;
return 2;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(server_port);
server_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());
// client 不需要显示的进行 bind,tcp 是面向连接的协议,需要先建立连接
int n = ::connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if(n < 0)
{
std::cout << "Error: Failed to connect to server" << std::endl;
return 3;
}
// 发送数据
std::string message;
while(true)
{
int x, y;
char oper;
std::cout << "input x: ";
std::cin >> x;
std::cout << "input y: ";
std::cin >> y;
std::cout << "input oper: ";
std::cin >> oper;
Request req(x, y, oper);
// 1. 序列化
req.Serialize(message);
// 2. Encode
Encode(message);
// 3. 发送
n = ::send(sockfd, message.c_str(), message.size(), 0);
if(n > 0)
{
char inbuffer[1024];
// 4. 获得应答
int m = ::recv(sockfd, inbuffer, sizeof(inbuffer), 0);
if(m > 0)
{
inbuffer[m] = 0;
std::string package = inbuffer;
std::string content;
// 4. 读到应答完整 -- 暂定 decode
Decode(package, &content);
// 5. 反序列化
Response resp;
resp.Deserialize(content);
// 6. 得到结构化数据
std::cout << resp.Result() << "[" << resp.Code() << "]" << std::endl;
}
else break;
}
else break;
}
::close(sockfd);
return 0;
}
结果如下:
Jsoncpp 是一个用于处理 JSON 数据的 C++库。它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++数据结构的功能。Jsoncpp 是一个开源库,广泛用于各种需要处理 JSON 数据的 C++ 项目中。
💡 主要功能
Json::Value
)。
null
。
Json::Value
)序列化为 JSON 字符串。
C++
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel
当使用 Jsoncpp 库进行JSON 的序列化和反序列化时,确实存在不同的做法和工具类可供选择。以下是对 Jsoncpp 中序列化和反序列化操作的详细介绍:
🔥 序列化指的是将数据结构或对象转换为一种格式,以便在网络上传输或存储到文件中。Jsoncpp 提供了多种方式进行序列化:
① 使用 Json::Value 的 tostyledstring 方法
示例:
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
std::string s = root.toStyledString();
std::cout << s << std::endl;
return 0;
}
② 使用 Json::StreamWriter
示例:
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe"; root["sex"] = "男";
Json::StreamWriterBuilder wbuilder; // StreamWriter 的工厂
std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());
std::stringstream ss;
writer->write(root, &ss);
std::cout << ss.str() << std::endl;
return 0;
}
③ 使用 Json::FastWriter
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
输出:
{ "name":"joe","sex" : "\u7537" }
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
// Json::FastWriter writer;
Json::StyledWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
// 输出
{
"name" : "joe",
"sex" : "\u7537"
}
🔥 反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。Jsoncpp 提供 了以下方法进行反序列化:
① 使用 Json::Reader (已弃用)
Json::CharReaderBuilder
替代)示例:
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main() {
// JSON 字符串
std::string json_string = "{\"name\":\"张三\", \"age\":30, \"city\":\"北京\"}";
// 解析 JSON 字符串
Json::Reader reader;
Json::Value root;
// 从字符串中读取 JSON 数据
bool parsingSuccessful = reader.parse(json_string,
root);
if (!parsingSuccessful) {
// 解析失败,输出错误信息
std::cout << "Failed to parse JSON: " <<
reader.getFormattedErrorMessages() << std::endl;
return 1;
}
// 访问 JSON 数据
std::string name = root["name"].asString();
int age = root["age"].asInt();
std::string city = root["city"].asString();
// 输出结果
std::cout << "Name: " << name << std::endl;
std::cout << "Age: " << age << std::endl;
std::cout << "City: " << city << std::endl;
return 0;
}
使用 Json::CharReader 的派生类(不推荐了,上面的足够了):
Json::Value 是 Jsoncpp 库中的一个重要类,用于表示和操作 JSON 数据结构。以下是一些常用的 Json::Value 操作列表:
ValueType(如 nullValue, intValue, stringValue 等)创建一个 Json::Value 对象。
函数名称 | 作用 |
---|---|
bool isNull() | 检查值是否为 null |
bool isBool() | 检查值是否为布尔类型 |
bool isInt() | 检查值是否为整数类型 |
bool isInt64() | 检查值是否为 64 位整数类型 |
bool isUInt() | 检查值是否为无符号整数类型 |
bool isUInt64() | 检查值是否为 64 位无符号整数类型 |
bool isIntegral() | 检查值是否为整数或可转换为整数的浮点数 |
bool isDouble() | 检查值是否为双精度浮点数 |
bool isNumeric() | 检查值是否为数字(整数或浮点数) |
bool isString() | 检查值是否为字符串 |
bool isArray() | 检查值是否为数组 |
bool isObject(): | 检查值是否为对象(即键值对的集合) |
名称 | 作用 |
---|---|
Json::Value& operator=(bool value) | 将布尔值赋给 Json::Value 对象 |
Json::Value& operator=(int value) | 将整数赋给 Json::Value 对象 |
Json::Value& operator=(unsigned int value) | 将无符号整数赋给 Json::Value 对象 |
Json::Value& operator=(Int64 value) | 将 64 位整数赋给 Json::Value 对象 |
Json::Value& operator=(UInt64 value) | 将 64 位无符号整数赋给 Json::Value 对象 |
Json::Value& operator=(double value) | 将双精度浮点数赋给 Json::Value 对象 |
Json::Value& operator=(const char* value) | 将 C 字符串赋给Json::Value 对象 |
Json::Value& operator=(const std::string& value) | 将 std::string 赋给 Json::Value 对 |
bool asBool() | 将值转换为布尔类型(如果可能) |
int asInt() | 将值转换为整数类型(如果可能) |
Int64 asInt64() | 将值转换为 64 位整数类型(如果可能) |
unsigned int asUInt() | 将值转换为无符号整数类型(如果可能) |
UInt64 asUInt64() | 将值转换为 64 位无符号整数类型(如果可能) |
double asDouble() | 将值转换为双精度浮点数类型(如果可能) |
std::string asString() | 将值转换为字符串类型(如果可能) |
上面代码在我的gitee 网络版计算器 里面有
【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【Linux】的内容,请持续关注我 !!