

文章目录
- 1. 应用层协议
- 2. 序列化与反序列化
- 3. 重新理解 read、write、recv、send 和 tcp 为什么支持全双工
- 4. 网络版本计算器实现
- 定制协议
- 计算器功能实现
- 服务器代码
- 客户端代码
- 运行结果
- Jsoncpp
- 序列化
- 反序列化
1. 应用层协议
- 应用层: 我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层。
- 协议: 协议是一种 “约定”。例如:
socket api
的接口, 在读写数据时, 都是按 “字符串” 的方式来发送接收的。其实,协议就是双方约定好的结构化的数据
2. 序列化与反序列化
定义结构体来表示我们需要交互的信息; 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体; 这个过程叫做 “序列化” 和 “反序列化”。
只要保证, 一端发送时构造的数据, 在另一端能够正确的进行解析, 就是 ok 的.。这种约定, 就是 应用层协议。
3. 重新理解 read、write、recv、send 和 tcp 为什么支持全双工
-
在任何一台主机上,
TCP
连接既有发送缓冲区,又有接受缓冲区,所以,在内核中,可以在发消息的同时,也可以收消息,即全双工; -
这就是为什么一个
tcp sockfd
读写都是它的原因; -
实际数据什么时候发,发多少,出错了怎么办,由
TCP
控制,所以TCP
叫做传输控制协议。
4. 网络版本计算器实现
我们可以自定义一个协议方便客户端与服务器之间进行IO交互,例如使用json
库来进行序列化与反序列化,所以客户端向服务器发送的信息可能是这样子的:len\r\n{json}\r\n
,json序列长度len——方便我们读取完整的内容,以及分隔符\r\n
和json序列。服务器向客户端发送的信息也该和上述一致,只不过json序列中包含的应该是result
和错误码code
,而客户端向服务器发送的json序列中包含的应该是操作数x
和y
以及操作方法operate
可以是加、减、乘、除、取模等。
定制协议
#pragma once
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
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 -> {json}
// 123\r\n
// 123\r\n{json
// 123\r\n{json}\r
// 123\r\n{json}\r\n123\r\n{json}\r\n123\r\n{js
// 协议,请求和应答序列化和反序列化还要加报头
// 使用JSON序列化与反序列化bool Decode(std::string &package, std::string *content)
{if (package.empty())return false;auto pos = package.find(Sep);if (pos == std::string::npos)return false;// 判断包是否完整int len = std::stoi(package.substr(0, pos));package = package.substr(pos + Sep.size());if (package.size() < len + Sep.size())return false;*content = package.substr(0, len);package = package.substr(len + Sep.size()); // 注意最后package要将已经解包的部分删除return true;
}class Request
{
public:Request() : _x(0), _y(0), _operate(0){}Request(int x, int y,char operate) : _x(x), _operate(operate), _y(y){}bool Serialize(std::string &out_string){Json::Value root;root["x"] = _x;root["y"] = _y;root["operate"] = _operate;out_string = root.toStyledString();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();_operate = root["operate"].asInt();Print();return true;}int X() const{return _x;}int Y() const{return _y;}char Operate() const{return _operate;}void Print(){std::cout<<"x: "<<_x<<std::endl;std::cout<<"y: "<<_y<<std::endl;std::cout<<"operate: "<<_operate<<std::endl;}
private:int _x;int _y;char _operate;
};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;out_string = root.toStyledString();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;}void SetResult(int result){_result = result;}void SetCode(int code){_code = code;}int Result(){return _result;}int Code(){return _code;}void Print(){std::cout<<"result: "<<_result<<std::endl;std::cout<<"code: "<<_code<<std::endl;}~Response(){}private:int _result;int _code;
};
应答序列和请求序列尽管使用的方法类似,但是因为序列中包含的内容不一样,所以还是需要使用两个类
Request
和Response
;Decode和Encode方法在不同请求和应答中都是一样的,所以直接定义在类外即可。
计算器功能实现
这里仅仅实现+、-、*、/、%这五种方法,通过Request
对象来执行并返回Response
对象:
#pragma once
#include <iostream>
#include "Protocol.hpp"class Calculator
{
public:Calculator(){}Response Execute(const Request &req){// 我们拿到的都是结构化的数据,拿到的不就是类对象吗!!!Response resp;switch (req.Operate()){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;}resp.Print();return resp;}~Calculator(){}
};
服务器代码
与之前实现的TCP通信代码类似,只是增加一个回调方法用来处理序列化与反序列化和计算:
#pragma once#include <iostream>
#include <string.h>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include <functional>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"
#include "ThreadPool.hpp"#define BACKLOG 8
using namespace InetAddrModule;
using namespace LogModule;
using namespace ThreadPoolModule;static const uint16_t defaultport = 8888;
using calculator_t = std::function<std::string(std::string&)>;
class TcpServer
{using task_t = std::function<void()>;struct ThreadData{int sockfd;TcpServer *self;};public:TcpServer(calculator_t calculator,uint16_t port = defaultport) :_calculator(calculator), _port(port), _listensockfd(-1), _isruning(false){}void InitServer(){// 1.创建Tcp套接字_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(LogLevel::ERROR) << "InitServer socket fail ...";Die(SOCKET_ERR);}// 填充信息struct sockaddr_in serveraddr;memset(&serveraddr, 0, sizeof(serveraddr));serveraddr.sin_family = AF_INET;serveraddr.sin_port = ::htons(_port); // aaa注意要转网络!!!!!!!!!!serveraddr.sin_addr.s_addr = INADDR_ANY; // 表示可以接收任意地址的信息// 2. bind;int n = ::bind(_listensockfd, CONV(&serveraddr), sizeof(serveraddr));if (n < 0){LOG(LogLevel::ERROR) << "InitServer bind fail ...";Die(BIND_ERR);}// 3.监听int m = ::listen(_listensockfd, BACKLOG);if (m < 0){LOG(LogLevel::ERROR) << "InitServer listen fail ...";Die(LISTEN_ERR);}LOG(LogLevel::INFO) << "ServerInit success...";}void handler(int sockfd){char buffer[4096];std::string package;while (true){ssize_t n = ::read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;LOG(LogLevel::INFO) << buffer;package += buffer;std::string result = _calculator(package);::write(sockfd, result.c_str(), result.size());}else if (n == 0) // client 退出{LOG(LogLevel::INFO) << "client quit: " << sockfd;break;}else{// 读取失败break;}}::close(sockfd); // fd泄漏问题!}void Start(){_isruning = true;while (_isruning){struct sockaddr_in peer;socklen_t peerlen = sizeof(peer); // 这个地方一定要注意,要不然,会有问题!LOG(LogLevel::DEBUG) << "accepting ...";// 我们要获取client的信息:数据(sockfd)+client socket信息(accept || recvfrom)int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);if (sockfd < 0){LOG(LogLevel::ERROR) << "StartServer accept fail ...";continue; // 继续接收}LOG(LogLevel::INFO) << "ServerStart success...";// version-3:线程池版本 比较适合处理短任务,或者是用户量少的情况ThreadPool<task_t>::GetInstance()->Enqueue([this, sockfd](){ this->handler(sockfd); });// 连接成功后就可以通信}_isruning = false;}~TcpServer(){}private:uint16_t _port;int _listensockfd;bool _isruning;calculator_t _calculator;
};
服务器执行代码:
#include "TcpServer.hpp"
#include "Log.hpp"
#include "Calculator.hpp"using namespace LogModule;
using cal_fun = std::function<Response(const Request &req)>;
class Parse
{
public:Parse(cal_fun c) : _func(c){}std::string Entry(std::string &package) // 解码——反序列化——计算——序列化——编码{std::string reqstr;std::string retstr;// 1.解码while (Decode(package, &reqstr)){LOG(LogLevel::DEBUG) << "Reqstr: \n"<< reqstr;if (reqstr.empty())break;// 2.反序列化Request req;req.Deserialize(reqstr);// 3. 计算Response res = _func(req);// 4.序列化std::string resstr;res.Serialize(resstr);LOG(LogLevel::DEBUG) << "resstr: \n"<< resstr;// 5.编码Encode(resstr);// 6.返回注意不是只返回一个,而是有多少返回多少retstr += resstr;}return retstr;}private:cal_fun _func;
};int main()
{ENABLE_FILE_LOG_STRATEGY();Calculator cal;Parse parse([&cal](const Request &req){ return cal.Execute(req); });std::unique_ptr<TcpServer> tcpserver = std::make_unique<TcpServer>([&parse](std::string& package){return parse.Entry(package);});tcpserver->InitServer();tcpserver->Start();return 0;
}
客户端代码
与服务器运行代码类似需要序列化与反序列化方法:
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.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;}std::string server_ip = argv[1]; // "192.168.1.1"int server_port = std::stoi(argv[2]);int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cout << "create socket failed" << 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是面向连接的, connect 底层会自动进行bindint n = ::connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));if (n < 0){std::cout << "connect failed" << std::endl;return 3;}// echo clientstd::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. EncodeEncode(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;//TODOstd::string content;// 4. 读到应答完整--暂定, decodeDecode(package, &content);// 5. 反序列化Response resp;resp.Deserialize(content);// 6. 得到结构化数据std::cout << resp.Result() << "[" << resp.Code() << "]" << std::endl;}elsebreak;}elsebreak;}::close(sockfd);return 0;
}
运行结果
Jsoncpp
Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。Jsoncpp 是开源的,广泛用于各种需要处理 JSON 数据的 C++ 项目中。 特性:
- 简单易用:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单。
- 高性能:Jsoncpp 的性能经过优化,能够高效地处理大量 JSON 数据。
- 全面支持:支持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null。
- 错误处理:在解析 JSON 数据时,Jsoncpp 提供了详细的错误信息和位置,方便开发者调试。
当使用 Jsoncpp 库进行 JSON 的序列化和反序列化时,确实存在不同的做法和工具类可供选择。以下是对 Jsoncpp 中序列化和反序列化操作的详细介绍:
安装
C++
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-deve
序列化
序列化指的是将数据结构或对象转换为一种格式,以便在网络上传输或存储到文件中。Jsoncpp 提供了多种方式进行序列化:
- 使用 Json::Value 的 toStyledString 方法:
- 优点:将 Json::Value 对象直接转换为格式化的 JSON 字符串。
- 示例:
#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;
}
$ ./test.exe
{
"name" : "joe",
"sex" : "男"
}
- 使用 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;
}
$ ./test.exe
{
"name" : "joe",
"sex" : "男"
}
- 使用 Json::FastWriter:
- 优点:比 StyledWriter 更快,因为它不添加额外的空格和换行符。
- 示例:
#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;
}
$ ./test.exe
{"name":"joe","sex":"男"}
#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;
Json::StyledWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
$ ./test.exe
{
"name" : "joe",
"sex" : "男"
}
反序列化
反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。Jsoncpp 提供了以下方法进行反序列化:
- 使用 Json::Reader:
- 优点:提供详细的错误信息和位置,方便调试。
- 示例:
#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;
}
$ ./test.exe
Name: 张三
Age: 30
City: 北京
- 使用 Json::CharReader 的派生类(不推荐了,上面的足够了):
- 在某些情况下,你可能需要更精细地控制解析过程,可以直接使用Json::CharReader 的派生类。
- 但通常情况下,使用 Json::parseFromStream 或 Json::Reader 的 parse方法就足够了。