目录
- 1. 网络版计算器
- 2. JSON 版本
- 2.1 了解 json 使用
- 2.2 JSON替换网络版计算器
- 2.3 转变守护进程
1. 网络版计算器
-
网络套接字:封装了 socket 网络通信。
// Socket.hpp #pragma once#include <iostream> #include <unistd.h> #include <cstring> #include <sys/types.h> #include <sys/socket.h> #include <sys/stat.h> #include <arpa/inet.h> #include <netinet/in.h>#include "Log.hpp"enum{SOCKET_ERR = 1,SOCKET_BIND_ERR,LISTEN_ERR,ACCEPT_ERR, };const int backlog = 10;class Sock { public:Sock() {}~Sock() {}void Socket(){_sockfd = socket(AF_INET, SOCK_STREAM, 0);if(_sockfd < 0){log(Fatal, "socket create failed! errno: %d, strerror: %s", errno, strerror(errno));exit(SOCKET_ERR);}} void Bind(uint16_t port){struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if(bind(_sockfd, (struct sockaddr*)&local, (socklen_t)sizeof(local)) < 0){log(Fatal, "socket bind failed! errno: %d, strerror: %s", errno, strerror(errno));exit(SOCKET_BIND_ERR);}} void Listen(){if(listen(_sockfd, backlog) < 0){log(Fatal, "socket listen failed! errno: %d, strerror: %s", errno, strerror(errno));exit(LISTEN_ERR);}} int Accept(std::string* clientip, uint16_t* clientport){struct sockaddr_in client;socklen_t len = sizeof(client);int newfd = accept(_sockfd, (struct sockaddr*)&client, &len);if(newfd < 0){log(Warning, "socket accept failed! errno: %d, strerror: %s", errno, strerror(errno));return -1;}char ipstr[32];inet_ntop(AF_INET, &client.sin_addr, ipstr, sizeof(ipstr));*clientip = ipstr;*clientport = ntohs(client.sin_port);return newfd;}bool Connect(const std::string ip, const uint16_t port){struct sockaddr_in peer;memset(&peer, 0, sizeof(peer));peer.sin_family = AF_INET;peer.sin_port = htons(port);inet_pton(AF_INET, ip.c_str(), &peer.sin_addr);int n = connect(_sockfd, (struct sockaddr*)&peer, sizeof(peer)); if(n < 0){std::cerr << "connect failed!" << std::endl;return false;}return true;}int Fd() { return _sockfd; }void Close() { close(_sockfd); } private:int _sockfd; };
-
定制协议:
class Request class Response { {int x; int result;int y; int code; char op; }; };
接着,在网络收发的时候对数据做序列化与反序列化的操作(将多个字段整合成字符串,以空格为分隔符)。但是在发送时可能一次连着发了好几次请求,例如
“10 + 20”“10 * 20”“10 / 20”
,接收方无法区分接收到的数据的完整性,也无法得知每个报文之间的界限,因此我们在每次发送的数据的结尾加上"\n"
分隔符,即"10 + 20"\n"10 * 20"\n"10 / 20"\n
。。再者,我们还可以再对每份数据前加上一个固定大小的报头,用于标记收发数据的长度(已经序列化的数据)。 因此经过协议约定后的数据可以是这样的
"9"\n"10 + 20"\n"9"\n"10 * 20"\n"9"\n"10 / 20"\n
。数据解析与封装的实现:
// Protocol.hpp #pragma once#include <iostream> #include <string> #include <cstdio>const std::string blank_space_sep = " "; const std::string protocol_sep = "\n";std::string Encode(const std::string& content) {std::string package = std::to_string(content.size());package += protocol_sep;package += content;package += protocol_sep;return package; }// "len"\n"x op y"\n --> "len"\n "x op y" \n bool Decode(std::string& package, std::string* content) {auto pos = package.find(protocol_sep);if(pos == std::string::npos) return false;std::string len_str = package.substr(0, pos); // 协议报头的长度(字符串形式)size_t len = std::stoi(len_str); // 有效载荷的长度(字符串形式)// len(package) = len_str + content_str + protocol_sep.size * 2size_t total_len = len_str.size() + len + protocol_sep.size() * 2;if(package.size() < total_len) return false; // 接收到的报文不完整*content = package.substr(pos + 1, len);// 移除上一个报文package.erase(0, total_len);return true; }class Request { public:Request(int data1, int data2, char oper) : x(data1), y(data2), op(oper){} Request() { }bool Serialization(std::string* out){ // 构建报文的有效载荷// struct --> string, "x op y" --> "len"\n"x op y"\nstd::string s = std::to_string(x) + blank_space_sep + op + blank_space_sep + std::to_string(y);*out = s;return true;}bool Deserialization(const std::string& in){auto left = in.find(blank_space_sep);if(left == std::string::npos) return false;std::string partX = in.substr(0, left);auto right = in.rfind(blank_space_sep);if(right == std::string::npos) return false;std::string partY = in.substr(right + 1);if(left + 2 != right) return false;op = in[left + 1];x = std::stoi(partX);y = std::stoi(partY);return true;} void DebugPrint(){printf("构建新请求:%d %c %d = ?\n", x, op, y);} public:int x;int y;char op; };class Response { public:Response(int ret, int c): result(ret), code(c){}Response() { }bool Serialization(std::string* out){// 构建报文的有效载荷: "reuslt code"std::string s = std::to_string(result) + blank_space_sep + std::to_string(code);*out = s;return true;}bool Deserialization(const std::string& in){auto pos = in.find(blank_space_sep);if(pos == std::string::npos) return false;std::string partLeft = in.substr(0, pos);std::string partRight = in.substr(pos + 1);result = std::stoi(partLeft);code = std::stoi(partRight);return true; } void DebugPrint(){printf("结果响应: ret: %d, code: %d\n", result, code);} public:int result;int code; // 0: success, !0: failed };
-
通信服务端:创建子进程进行网络数据的收发,通过回调函数将数据的解析与封装处理进行解耦,最终将计算后的数据经过封装响应给客户端。
// TcpServer.hpp #pragma once #include <iostream> #include <functional> #include <signal.h>#include "Socket.hpp" #include "Log.hpp"using func_t = std::function<std::string(std::string& package)>;class TcpServer { public:TcpServer(uint16_t port, func_t callback):_port(port), _callback(callback){}void Init(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();log(Info, "server init ... done");}void Start(){signal(SIGCHLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);while (true){std::string clientip;uint16_t clientport;int sockfd = _listensock.Accept(&clientip, &clientport);if(sockfd < 0) continue;log(Info, "accept a new link, sockfd: %d, client ip: %s, port: %d", sockfd, clientip.c_str(), clientport);if(fork() == 0){_listensock.Close();std::string inbuffer_stream;// 数据计算while (true){char buffer[128];ssize_t n = read(sockfd, buffer, sizeof(buffer));if(n > 0){buffer[n] = 0;inbuffer_stream += buffer;log(Debug, "debug: %s", inbuffer_stream.c_str());while (true) // 可能客户端一次发送多次请求{std::string info = _callback(inbuffer_stream);if(info.empty()) break;write(sockfd, info.c_str(), info.size());} }else break;}exit(0);}}}~TcpServer() {} private:uint16_t _port;Sock _listensock;func_t _callback; };
-
计算器服务器实现:主要负责将网络服务端接收到的数据做解析与封装。
// ServerCal.hpp #pragma once #include <iostream> #include "Protocol.hpp"enum{DIV_ZERO = 1,MOD_ZERO,OTHER_OPER };class ServerCal { public:ServerCal() {}Response CalculatorHandler(const Request& req){Response resp(0, 0);switch (req.op){case '+':resp.result = req.x + req.y;break;case '-':resp.result = req.x - req.y;break;case '*':resp.result = req.x * req.y;break;case '/':{if(req.y == 0) resp.code = DIV_ZERO;else resp.result = req.x / req.y;}break;case '%':{if(req.y == 0) resp.code = MOD_ZERO;else resp.result = req.x % req.y;}break;default:resp.code = OTHER_OPER;break;}return resp;}std::string Calculator(std::string& package){// 1. 服务器接收到数据包,先解析报文std::string content;bool r = Decode(package, &content); if(!r) return "";// 2. 再对数据做反序列化Request req;r = req.Deserialization(content);if(!r) return "";// 3. 计算content = "";Response resp = CalculatorHandler(req);// 4. 数据序列化resp.Serialization(&content);content = Encode(content);return content;}~ServerCal() {} };
-
计算器服务端:通过 bind 将回调函数与 ServerCal 类中的 Calculator 成员函数进行绑定、初始化和启动网络服务。
// ServerCal.cc #include "TcpServer.hpp" #include "ServerCal.hpp"int main(int argc, char* argv[]) {if(argc != 2){std::cout << "\nUsage: ./ServerCal <port>\n" << std::endl;exit(0);}uint16_t port = std::stoi(argv[1]);ServerCal cal;TcpServer* tcpserver = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));tcpserver->Init();tcpserver->Start();return 0; }
-
通信客户端:负责向网络服务器发送数据请求与接收服务器的响应。
// ClientCal.cc #include <iostream> #include <string> #include <ctime> #include <cassert> #include <unistd.h> #include "Socket.hpp" #include "Protocol.hpp"int main(int argc, char *argv[]) {if(argc != 3){std::cerr << "Usage: " << argv[0] << " <host> <port>" << std::endl;exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);Sock sockfd;sockfd.Socket();bool ret = sockfd.Connect(serverip, serverport);if(!ret) return 1;srand(time(nullptr) ^ getpid());int cnt = 1;const std::string opers = "+-*/%&|^=";std::string inbuffer_stream;while (cnt <= 20){std::cout << "================ 第" << cnt++ << "次测试 ====================\n";int x = rand() % 100 + 1;usleep(1234);int y = rand() % 100;usleep(4321);char op = opers[rand() % opers.size()];Request req(x, y, op);req.DebugPrint();std::string package;req.Serialization(&package);package = Encode(package);std::cout << "这是最新的请求:\n" << package;write(sockfd.Fd(), package.c_str(), package.size()); // 模拟发送多次请求write(sockfd.Fd(), package.c_str(), package.size()); write(sockfd.Fd(), package.c_str(), package.size()); write(sockfd.Fd(), package.c_str(), package.size()); char buffer[128];ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer)); // 读取响应(计算结果)if(n > 0){buffer[n] = 0;inbuffer_stream += buffer;do{std::string content;bool ret = Decode(inbuffer_stream, &content);assert(ret);Response resp;ret = resp.Deserialization(content); assert(ret);resp.DebugPrint();std::cout << "\n";} while (ret && !inbuffer_stream.empty()); }std::cout << "=================================================\n\n"; sleep(1);}sockfd.Close();return 0; }
很显然,上述这种方案并不轻松,我们需要从协议的定制到实现全部自己完成。因此市面上提供了现成的解决方案,比如json、Protobuf,都可以帮助我们完成数据的序列化与反序列化。
2. JSON 版本
2.1 了解 json 使用
如果在 cpp 中想使用 json 完成序列化与反序列化,那么需要先安装一下 jsoncpp 第三方库
[outlier@aliyun ~]$ sudo yum install json-cpp-devel -y # 安装第三方库[outlier@aliyun ~]$ ls /usr/include/jsoncpp # 查看系统内json头文件
json[outlier@aliyun ~]$ ls /usr/lib64/libjsoncpp.so # 查看系统内json动态库
/usr/lib64/libjsoncpp.so
#include <iostream>
#include <jsoncpp/json/json.h>// Json常见格式 {a:100, b:"123"} 内部是一组kv结构的数据
int main()
{Json::Value root; // Value:万能对象,可以存储任意类型的数据// 将数据设置为Json格式root["x"] = 100; root["y"] = 200;root["op"] = '+';root["dest"] = "this is a + operation";Json::FastWriter w1; // 字符串输出std::string ret1 = w1.write(root); // 序列化std::cout << "字符串输出:" << ret1 << std::endl;Json::StyledWriter w2; // 格式化输出(可读性强)std::string ret2 = w2.write(root); // 序列化std::cout << "格式化输出:\n" << ret2 << std::endl;Json::Value v;Json::Reader r; // 反序列化r.parse(ret2, v); // 第一个参数为反序列化的对象,第二个参数为反序列化后的结果对象int x = v["x"].asInt(); // 提取数据int y = v["y"].asInt();char op = v["op"].asInt(); // 字符本身也是一种整数std::string dest = v["dest"].asString();std::cout << "反序列化结果:\n";std::cout << x << "\n";std::cout << y << "\n";std::cout << op << "\n";std::cout << dest << "\n";return 0;
}
g++ test.cc -ljsoncpp # 编译
2.2 JSON替换网络版计算器
变更代码:
// Protocol.hpp#include <jsoncpp/json/json.h>// #define MySelf 1class Request
{bool Serialization(std::string* out){
#ifdef MySelf// 自定义协议(同上)
#elseJson::Value root;root["x"] = x;root["op"] = op;root["y"] = y;// Json::FastWriter w;Json::StyledWriter w;*out = w.write(root);return true;
#endif}bool Deserialization(const std::string& in){
#ifdef MySelf// 自定义协议(同上)
#elseJson::Value root;Json::Reader r;r.parse(in, root);x = root["x"].asInt();y = root["y"].asInt();op = root["op"].asInt();return true;
#endif}
};class Response
{
public:bool Serialization(std::string* out){
#ifdef MySelf// 自定义协议(同上)
#elseJson::Value root;root["result"] = result;root["code"] = code;// Json::FastWriter w;Json::StyledWriter w;*out = w.write(root);return true;
#endif}bool Deserialization(const std::string& in){
#ifdef MySelf// 自定义协议(同上)
#elseJson::Value root;Json::Reader r;r.parse(in, root);result = root["result"].asInt();code = root["code"].asInt();return true;
#endif}
};
# Makefile
.PHONY:all
all:serverCal clientCalFlag=-DMySelf=1 # 编译时在代码内定义MySelf宏
Lib=-ljsoncppserverCal:ServerCal.ccg++ -o $@ $^ -std=c++11 $(Lib) #$(Flag)
clientCal:ClientCal.ccg++ -o $@ $^ -std=c++11 $(Lib) #$(Flag).PHONY:clean
clean:rm -f serverCal clientCal
2.3 转变守护进程
NAMEdaemon - run in the backgroundSYNOPSIS#include <unistd.h>int daemon(int nochdir, int noclose);
DESCRIPTIONIf nochdir is zero, daemon() changes the calling process's current working directory to the rootdirectory ("/"); otherwise, the current working directory is left unchanged.If noclose is zero, daemon() redirects standard input, standard output and standard error to /dev/null; otherwise, no changes are made to these file descriptors.
// ServerCal.cc#include "TcpServer.hpp"
#include "ServerCal.hpp"int main(int argc, char* argv[])
{if(argc != 2){std::cout << "\nUsage: ./ServerCal <port>\n" << std::endl;exit(0);}uint16_t port = std::stoi(argv[1]);ServerCal cal;TcpServer* tcpserver = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));tcpserver->Init();tcpserver->Start();daemon(1, 0); // 不改路径、重定向标准输入输出/dev/nullreturn 0;
}
如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!
感谢各位观看!