自定义协议

ops/2024/11/27 21:36:46/

1. 问题引入

问题:TCP是面向字节流的(TCP不关心发送的数据是消息、文件还是其他任何类型的数据。它简单地将所有数据视为一个字节序列,即字节流。这意味着TCP不会对发送的数据进行任何特定的边界划分,它只是确保数据的顺序和完整性。),这怎么能保证,读上来的数据是一个 完整 的报文呢?
解答:通过协议(Protocol)来约定。协议定义了数据交换的规则和标准,使得不同设备之间能够相互通信和理解。
例如:

  1. 固定长度报文: 如果每个报文的长度都是固定的,那么接收方可以简单地读取固定数量的字节来构成一个完整的报文。
  2. 长度前缀: 在报文的开始处添加一个字段,指定了报文的长度。接收方首先读取长度字段,然后根据指定的长度读取后续的字节来构成完整的报文。
  3. 特殊分隔符: 使用一个特殊的字节序列或字符串作为报文的分隔符。接收方在流中搜索这个分隔符来确定报文的边界。例如,HTTP协议使用"\r\n\r\n"作为请求头和请求体的分隔符。

其它知识:传输层TCP是全双工的,意味着,TCP的收发是可以同时进行的。亦即接收的时候可以发送,发送的时候也可以接收,两者互不冲突,可同时进行。实际上客户端和服务端维护者两个缓冲区

image-20241113202509891

2. 协议定制

2.1 序列化和反序列化的概念

序列化(Serialization)

序列化是指将对象的状态信息转换为可以存储或传输的格式(如JSON、XML、二进制等格式)的过程。序列化后的数据可以写入到文件中,或者通过网络发送到其他计算机。序列化的主要目的包括:

  1. 数据持久化:将内存中的数据结构保存到文件中,以便在程序下次运行时能够恢复。
  2. 网络传输:在网络上传输数据时,需要将复杂的数据结构转换为一种可以在网络上传输的格式。
  3. 跨语言和平台:不同的编程语言和平台之间交换数据时,需要一种中间格式来实现互操作性。

反序列化(Deserialization)

反序列化是序列化的逆过程,它是指将序列化后的数据(如文件中的数据或网络上接收到的数据)转换回原始的数据结构或对象状态的过程。反序列化使得程序能够从持久化的数据中恢复对象,或者接收并处理来自其他程序的数据。

序列化和反序列化的用途

  1. 数据库存储:将对象序列化后存储到数据库中,需要时再反序列化以恢复对象。
  2. 网络通信:在分布式系统或网络应用中,对象需要在网络上传输,因此需要序列化和反序列化。
  3. 缓存:将对象序列化后存储在缓存中,可以减少内存的使用。
  4. 消息队列:在使用消息队列(如RabbitMQ、Kafka)时,消息内容通常需要序列化。

2.2 网络版计算器 (服务端)

我们需要实现一个服务器版的加法器,在客户端把要计算的两个加数发过去, 然后由服务器进行计算,最后再把结果返回给客户端。

自己定制序列化规则:比如要计算a+b的结果,将其改为"len\n""a + b\n",第一个len相当于报头,第二个字符串相当于报文的有效载荷

10+20 变为 "7\n""10 + 20\n"

2.2.1 自己定义协议

自定义协议 Protocol.hpp

#pragma once
#include <iostream>
#include <string>
using namespace std;static const string BLANK_STRING = " ";
static const string PROTOCOL_SEP = "\n";static string encode(const string& text)
{// 添加报头string ret = to_string(text.size());ret += PROTOCOL_SEP;ret += text;ret += PROTOCOL_SEP;return ret;
}static bool decode(string& text, string* out)
{// 移除报头   "len\n""a op b\n"???size_t pos = text.find(PROTOCOL_SEP);if(pos == string::npos)    return false;string headStr = text.substr(0, pos);size_t textLen = stoi(headStr);size_t totalLen = textLen + 2 + headStr.size();// text可能除了"len\na op b\n"外增加了其它字符,比如"len\na op b\nlen\n..."。这里拿取了一个完整的if(text.size() < totalLen)  return false;*out = text.substr(pos+1, textLen);// 移除这一个完整的报文,防止text越来越大text.erase(0, totalLen);return true;
}struct Request
{int _a;int _b;char _op;Request(int a, int b, char op) : _a(a), _b(b), _op(op) {}Request() {}bool serialize(string* out){   // 构建有效载荷,将成员属性变为 "_a op _b"string tmp = to_string(_a);tmp += BLANK_STRING;tmp += _op;tmp += BLANK_STRING;tmp += to_string(_b);*out = tmp;return true;}bool deserialize(const string& in){// 反序列化 将"_a op _b"拆分size_t left = in.find(BLANK_STRING);if(left == string::npos)    {cerr << "if(left == string::npos) err" << endl;return false;}_a = stoi(in.substr(0, left));size_t right = in.rfind(BLANK_STRING);if(right == string::npos)   {cerr << "if(right == string::npos) err" << endl;return false;}_b = stoi(in.substr(right+1));if(left + 2 != right)   {cerr << "if(left + 2 != right) err" << endl;return false;}_op = in[left+1];return true;}void printInfo(){printf("%d %c %d = ?\n", _a, _op, _b);}
};struct Response
{int _res;int _exitCode;      // 0 表示可信Response(int res, int exitCode) : _res(res), _exitCode(exitCode) {}Response() {}bool serialize(string* out){   // 构建有效载荷,将成员属性变为 "_res op _exitCode"string tmp = to_string(_res);tmp += BLANK_STRING;tmp += to_string(_exitCode);*out = tmp;return true;}bool deserialize(const string& in){// 反序列化 将"_res op _exitCode"拆分size_t left = in.find(BLANK_STRING);if(left == string::npos)    return false;_res = stoi(in.substr(0, left));_exitCode = stoi(in.substr(left+1));}void printInfo(){printf("res: %d, exitCode: %d\n", _res, _exitCode);}
};

测试代码如下 SvrCal.cc

#include <iostream>
#include "TcpSvr.hpp"
#include "Protocol.hpp"using namespace std;void test1()
{// 测试Request, 序列化 + 添加报头Request r(122223, 456, '*');string s;r.serialize(&s);s = encode(s);cout << s;// 去掉报头string out;decode(s, &out);cout << out << endl;// 反序列化Request tmp;tmp.deserialize(out);printf("%d %c %d\n", tmp._a, tmp._op, tmp._b);printf("===================================\n");
}void test2()
{// 测试Reponse, 序列化 + 添加报头Response r(9999, 0);string s;r.serialize(&s);s = encode(s);cout << s;// 去掉报头string out;decode(s, &out);cout << out << endl;// 反序列化Response tmp;tmp.deserialize(out);printf("%d %d\n", tmp._res, tmp._exitCode);printf("===================================\n");
}int main()
{test1();test2();return 0;
}

image-20241115211807675

2.2.2 网络部分

Socket.hpp,封装提供网络的系统调用接口

#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/types.h>         
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <string>
#include <functional>
#include "log.hpp"
#define BACKLOG 10
Log log;using namespace std;
class Sock
{
public:Sock();~Sock();void Socket();void Bind(uint16_t port);void Listen();int Accept(string* ip, uint16_t* port);bool Connect(const string& ip, const uint16_t& port);int GetFd();void Close();
private:int _socketFd;
};Sock::Sock() : _socketFd(-1)
{}Sock::~Sock()
{}inline void Sock::Socket()
{_socketFd = socket(AF_INET, SOCK_STREAM, 0);if(_socketFd < 0) {log(FATAL, "Sock::Socket() error! why: %s\n", strerror(errno));exit(-1);}
}inline void Sock::Bind(uint16_t port)
{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(_socketFd, (sockaddr*)&local, sizeof local) < 0) {log(FATAL, "Sock::Bind() error! why: %s\n", strerror(errno));exit(-1);}
}inline void Sock::Listen()
{if(listen(_socketFd, BACKLOG) < 0) {log(FATAL, "Sock::Listen() error!\n");exit(-1);}
}inline int Sock::Accept(string* peerIp, uint16_t* peerPort)
{sockaddr_in peer;memset(&peer, 0, sizeof peer);socklen_t len = sizeof len;int socketFd = accept(_socketFd, (sockaddr*)&peer, &len);if(socketFd < 0) {log(WARNING, "Sock::Accept() error!\n");return -1;}// 将客户端的ip输出出去char buf[64];if(inet_ntop(AF_INET, &peer.sin_addr, buf, sizeof buf) == nullptr) {log(WARNING, "Sock::Accept() error!\n");return -1;}*peerIp = buf;// 将客户端的端口号输出出去*peerPort = ntohs(peer.sin_port);return socketFd;
}inline bool Sock::Connect(const string& ip, const uint16_t& port)
{sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(port);inet_pton(AF_INET, ip.c_str(), &server.sin_addr.s_addr);int ret = connect(_socketFd, (sockaddr*)&server, sizeof server);if(ret < 0) {cerr << "Sock::Connect error!" << endl;return false;}return true;
}inline int Sock::GetFd()
{return _socketFd;
}inline void Sock::Close()
{close(_socketFd);
}

2.2.3 处理数据

SvrCal.hpp,处理来自服务器的数据,doCalculate(string &text)中的text需要满足自定义协议要求的字符串

#pragma once
#include "Protocol.hpp"enum {Div_Zero = 1,Mod_Zero,Other_Err,
};class SvrCal
{
public:SvrCal() {}~SvrCal() {}// 计算text中的数据,出错返回空字符串string doCalculate(string& text){// 将从网络上来的数据,去掉报头string out;bool r = decode(text, &out);if(r == false)  {// cerr << "decode() error" << endl;return "";}// printf("Now out: %s\n", out.c_str());// 反序列化Request req;r = req.deserialize(out);if(r == false)  {cerr << "deserialize() error" << endl;return "";}// 计算结果Response resp = calHelper(req);// 序列化out = "";resp.serialize(&out);// 将计算结果加上报头out = encode(out);return out;}
private:Response calHelper(Request& req){   // 将Request转换为ResponseResponse resp;switch (req._op){case '+':resp._res = req._a + req._b;break;case '-':resp._res = req._a - req._b;break;case '*':   resp._res = req._a * req._b;break;case '/':{if(req._b == 0)  resp._exitCode = Div_Zero;else resp._res = req._a / req._b;}break;case '%':{if(req._b == 0)  resp._exitCode = Mod_Zero;else resp._res = req._a % req._b;}break;default:resp._exitCode = Other_Err;break;}return resp;}
};

2.2.4 服务器代码

TcpSvr.hpp_callback后面会绑定到SvrCal::doCalculate(string& text)

#pragma once
#include "Socket.hpp"
#include <signal.h>extern Log log;/*
目前使用的是SvrCal.hpp中的
string doCalculate(const string& text)
*/
using fun_t = function<string(string&)>;class TcpSvr
{
public:TcpSvr();TcpSvr(uint16_t port, fun_t callback) : _port(port), _callBack(callback) {}bool initSvr();void startSvr();
private:uint16_t _port;Sock _listSock;fun_t _callBack;
};inline bool TcpSvr::initSvr()
{_listSock.Socket();_listSock.Bind(_port);_listSock.Listen();log(INFO, "TcpSvr Init over.\n");
}inline void TcpSvr::startSvr()
{signal(SIGCHLD, SIG_IGN);for(;;) {string peerIp; uint16_t peerPort;int socketFd = _listSock.Accept(&peerIp, &peerPort);if(socketFd < 0) {sleep(1);continue;}log(INFO, "TcpSvr is Accepting, client IP: %s, clientPort %d\n", peerIp.c_str(), peerPort);pid_t id = fork();if(id < 0) {log(WARNING, "TcpSvr::startSvr() fork error!\n");sleep(1);continue;} else if(id == 0) {// 关闭_listenFd的目的是为了防止子进程修改父进程文件描述符下的内容_listSock.Close();string inStr="";for(;;) {char buf[128];ssize_t n = read(socketFd, buf, sizeof buf);if (n < 0) {log(WARNING, "TcpSvr::startSvr() read error!\n");// sleep(1);break;} else if(n == 0) {log(WARNING, "TcpSvr::startSvr() read over!\n");// sleep(1);break;} else {buf[n] = 0;inStr += buf;log(DEBUG, "Read from clinet: \n%s", inStr.c_str());fflush(stdout);// 回调函数,计算结果string r = _callBack(inStr);if(r == "") {// 报文不正确,重新读取// log(WARNING, "TcpSvr::startSvr() doCalculate error!\n");continue;// sleep(1);}write(socketFd, r.c_str(), r.size());}}   exit(0);} else {// 父进程关闭socketFd的意义是为了防止文件描述符越用越少,关闭了之后能保证每一个新的进程用的都是fd都是4close(socketFd);}}
}

SvrCal.cc,编译的是该文件

#include <iostream>
#include <functional>
#include "TcpSvr.hpp"
#include "Protocol.hpp"
#include "SvrCal.hpp"using namespace std;// test1() 和 test()和2.2.1中测试代码部分一样
void test1()
{}void test2()
{}int main(int argc, char* argv[])
{if(argc != 2) {cout << "Usage error!\n";return -1;}uint16_t port = stoi(argv[1]);SvrCal cal;TcpSvr *svr = new TcpSvr(port, bind(&SvrCal::doCalculate, &cal, placeholders::_1));// printf("after new TcpSvr()---\n");svr->initSvr();// printf("after new initSvr()---\n");svr->startSvr();return 0;
}

image-20241121164117198

3\n6 0Response序列化加上报头结果

2.3 网络计算器 (客户端)

2.3.1 随机生成数字

客户端也要遵循协议

CliCal.cc

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include "Socket.hpp"
#include "Protocol.hpp"using namespace std;// ./myClient ip port
int main(int argc, char* argv[])
{if(argc != 3) {cerr << "Usage error" << endl;return -1;}string serverIp = argv[1];uint16_t serverPort = stoi(argv[2]);Sock sock;sock.Socket();bool ret = sock.Connect(serverIp, serverPort);if(ret == false) {return -1;}srand(time(0));const string ops = "+-*/&?=";       // 有一些不正确的符号,目的是为了测试出错的情况string inStr = "";for (int i = 1; i <= 10; ++i) {printf("==============第%d次================\n", i);int x = rand() % 100;usleep(100);int y = rand() % 100;usleep(100);char op = ops[rand() % ops.size()];string text;Request req(x, y, op);req.printInfo();req.serialize(&text);text = encode(text);printf("将要发送给服务器的请求:\n%s", text.c_str());// 向服务器发送数据ssize_t n = write(sock.GetFd(), text.c_str(), text.size());// n = write(sock.GetFd(), text.c_str(), text.size());// n = write(sock.GetFd(), text.c_str(), text.size());// n = write(sock.GetFd(), text.c_str(), text.size());if(n < 0) {cerr << "Client write error!\n" << endl;break;}// 从服务器读取数据char buf[128];memset(buf, 0, sizeof buf);n = read(sock.GetFd(), buf, sizeof(buf));if(n > 0) {buf[n] = 0;inStr += buf;string text;bool r = decode(inStr, &text);if(r == false) {cerr << "Client decode error!\n" << endl;break;}Response resp;resp.deserialize(text);printf("从服务器得到结果:\n");resp.printInfo();}printf("======================================\n");sleep(1);}sock.Close();return 0;
}

由于可能有多个客户端向服务器发送请求,所以TcpSvr.hpp中的startSvr()改为如下的格式

inline void TcpSvr::startSvr()
{signal(SIGCHLD, SIG_IGN);for(;;) {string peerIp; uint16_t peerPort;int socketFd = _listSock.Accept(&peerIp, &peerPort);if(socketFd < 0) {sleep(1);continue;}log(INFO, "TcpSvr is Accepting, client IP: %s, clientPort %d\n", peerIp.c_str(), peerPort);pid_t id = fork();if(id < 0) {log(WARNING, "TcpSvr::startSvr() fork error!\n");sleep(1);continue;} else if(id == 0) {// 关闭_listenFd的目的是为了防止子进程修改父进程文件描述符下的内容_listSock.Close();string inStr="";for(;;) {char buf[128];ssize_t n = read(socketFd, buf, sizeof buf);if (n < 0) {log(WARNING, "TcpSvr::startSvr() read error!\n");// sleep(1);break;} else if(n == 0) {log(WARNING, "TcpSvr::startSvr() read over!\n");// sleep(1);break;} else {buf[n] = 0;inStr += buf;log(DEBUG, "Read from clinet: \n%s", inStr.c_str());fflush(stdout);while (true) {// 客户端可能发送多次数据,所以这里一次全部处理干净// 回调函数,计算结果string r = _callBack(inStr);if(r == "") {break;}write(socketFd, r.c_str(), r.size());}}}   exit(0);} else {// 父进程关闭socketFd的意义是为了防止文件描述符越用越少,关闭了之后能保证每一个新的进程用的都是fd都是4close(socketFd);}}
}

现在的运行结果如下图

image-20241122160552239

2.4 使用json

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。它基于JavaScript的一个子集,但是独立于语言,可以被多种编程语言读取。JSON采用文本格式,易于阅读和编写,同时也易于机器解析和生成。

JSON的结构包括:

  1. 键值对:JSON中的每个元素都由键值对组成,键和值之间用冒号(:)分隔,键名必须用双引号括起来。
  2. 数据类型:JSON支持的数据类型包括字符串(String)、数字(Number)、对象(Object)、数组(Array)、布尔值(Boolean)和空值(null)。
  3. 数组:数组在JSON中用方括号([])表示,数组中的元素可以是任何类型的值。
  4. 对象:对象在JSON中用花括号({})表示,对象中的元素是键值对。

一个简单的JSON示例如下:

{"name": "John","age": 30,"is_student": false,"courses": ["Math", "Science", "History"],"address": {"street": "123 Main St","city": "Anytown","state": "CA"}
}

在这个例子中,nameageis_student 是键值对,courses 是一个数组,address 是一个对象。JSON格式的数据可以被JavaScript直接解析,也可以被其他支持JSON的编程语言解析和生成。

2.4.1 安装json

sudo yum install -y jsoncpp-devel

安装完成后,json库用到的头文件

image-20241122162624498

json库的位置
image-20241122162811030

2.4.2 简单使用

序列化,写一个main.cc用于测试

#include <iostream>
#include <jsoncpp/json/json.h>
#include <string>
using namespace std;int main()
{// 创建一个 Json::Value 类型的对象 root,它将作为 JSON 对象的根。Json::Value root;// 构建键值对root["x"] = 1;root["y"] = 2;root["op"] = "+";root["desc"] = "add operation";// 创建一个 Json::FastWriter 对象 w,用于将 JSON 对象转换为字符串。Json::FastWriter w;// 使用 w.write(root) 将 root JSON 对象转换为字符串,并存储在变量 res 中。string res = w.write(root);cout << res << endl;return 0;
}

image-20241122171520203

除了使用Json::FastWriter,也可以使用Json::StyledWriter,可读性会好一点

// 将上面代码的第16行改为:
Json::StyledWriter w;

image-20241122172323194

继续写上面的代码,下面进行反序列化

int main()
{// 创建一个 Json::Value 类型的对象 root,它将作为 JSON 对象的根。Json::Value root;root["x"] = 1;root["y"] = 2;root["op"] = "+";root["desc"] = "add operation";// 创建一个 Json::FastWriter 对象 w,用于将 JSON 对象转换为字符串。// Json::FastWriter w;Json::StyledWriter w;// 使用 w.write(root) 将 root JSON 对象转换为字符串,并存储在变量 res 中。string res = w.write(root);cout << res << endl;// 下面是反序列化Json::Value v;      // 用来存储解析后的 JSON 数据。Json::Reader r;     // 用来解析 JSON 字符串。r.parse(res, v);    // 将 JSON 字符串 res 解析到 v 对象中。int x = v["x"].asInt();int y = v["y"].asInt();string op = v["op"].asString();string desc = v["desc"].asString();cout << x << endl;cout << y << endl;cout << op << endl;cout << desc << endl;return 0;
}

image-20241122173337616

Json里面也可以再套一个Json

#include <iostream>
#include <jsoncpp/json/json.h>
#include <string>
using namespace std;int main()
{// 序列化Json::Value inner;inner["hello"] = "你好";inner["world"] = "世界";Json::Value root;root["test"] = inner;Json::StyledWriter w;string res = w.write(root);cout << res;// 反序列化Json:: Value v;Json:: Reader r;r.parse(res, v);cout << v["test"]["hello"].asString() << endl;cout << v["test"]["world"].asString() << endl;return 0;
}

image-20241122174438348

2.4.3 修改协议部分

给2.2.1中的Protocol.hpp添加条件编译,使用jsoncpp这个库

#pragma once
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
using namespace std;static const string BLANK_STRING = " ";
static const string PROTOCOL_SEP = "\n";static string encode(const string& text)
{// 添加报头string ret = to_string(text.size());ret += PROTOCOL_SEP;ret += text;ret += PROTOCOL_SEP;return ret;
}static bool decode(string& text, string* out)
{// 移除报头   "len\n""a op b\n"???size_t pos = text.find(PROTOCOL_SEP);if(pos == string::npos)    return false;string headStr = text.substr(0, pos);size_t textLen = stoi(headStr);size_t totalLen = textLen + 2 + headStr.size();// text可能除了"len\na op b\n"外增加了其它字符,比如"len\na op b\nlen\n..."。这里拿取了一个完整的if(text.size() < totalLen)  return false;*out = text.substr(pos+1, textLen);// 移除这一个完整的报文,防止text越来越大text.erase(0, totalLen);return true;
}struct Request
{int _a;int _b;char _op;Request(int a, int b, char op) : _a(a), _b(b), _op(op) {}Request() {}bool serialize(string* out){   
#ifdef MYSELF// 构建有效载荷,将成员属性变为 "_a op _b"string tmp = to_string(_a);tmp += BLANK_STRING;tmp += _op;tmp += BLANK_STRING;tmp += to_string(_b);*out = tmp;return true;
#else Json::Value tmp;tmp["x"] = _a;tmp["op"] = _op;tmp["y"] = _b;Json::FastWriter w;*out = w.write(tmp);return true;
#endif}bool deserialize(const string& in){
#ifdef MYSELF// 反序列化 将"_a op _b"拆分size_t left = in.find(BLANK_STRING);if(left == string::npos)    {cerr << "if(left == string::npos) err" << endl;return false;}_a = stoi(in.substr(0, left));size_t right = in.rfind(BLANK_STRING);if(right == string::npos)   {cerr << "if(right == string::npos) err" << endl;return false;}_b = stoi(in.substr(right+1));if(left + 2 != right)   {cerr << "if(left + 2 != right) err" << endl;return false;}_op = in[left+1];return true;
#else Json::Value v;Json::Reader r;r.parse(in, v);_a = v["x"].asInt();_op = v["op"].asInt();_b = v["y"].asInt();return true;
#endif}void printInfo(){printf("%d %c %d = ?\n", _a, _op, _b);}
};struct Response
{int _res;int _exitCode;      // 0 表示可信Response(int res, int exitCode) : _res(res), _exitCode(exitCode) {}Response() {}bool serialize(string* out){   
#ifdef MYSELF// 构建有效载荷,将成员属性变为 "_res op _exitCode"string tmp = to_string(_res);tmp += BLANK_STRING;tmp += to_string(_exitCode);*out = tmp;return true;
#else Json::Value tmp;tmp["res"] = _res;tmp["code"] = _exitCode;Json::FastWriter w;*out = w.write(tmp);return true;
#endif}bool deserialize(const string& in){
#ifdef MYSELF// 反序列化 将"_res op _exitCode"拆分size_t left = in.find(BLANK_STRING);if(left == string::npos)    return false;_res = stoi(in.substr(0, left));_exitCode = stoi(in.substr(left+1));
#elseJson::Value v;Json::Reader r;r.parse(in, v);_res = v["res"].asInt();_exitCode = v["code"].asInt();return true;
#endif}void printInfo(){printf("res: %d, exitCode: %d\n", _res, _exitCode);}
};

Makefile格式如下,默认将flag注释,这样就没有定义MYSELF

.PHONY : all
all : myClient myServerlib = -ljsoncpp
#flag = -DMYSELF=1		# 是否有该宏myClient : CliCal.ccg++ -o $@ $^ -std=c++11 $(lib) $(flag)
myServer : SvrCal.ccg++ -o $@ $^ -std=c++11 $(lib) $(flag).PHONY : clean
clean:rm -rf myClient myServer

运行结果如下

image-20241122201543505

2.4.4 守护进程话

可以使用链接中的3.3.2deamon.hpp
也可以调用下面的系统调用

#include <unistd.h>int daemon(int nochdir, int noclose);

参数说明:

  • nochdir:如果设置为非零值,daemon 函数不会改变当前工作目录到根目录(/)。默认情况下,daemon 函数会将当前工作目录改变到根目录。
  • noclose:如果设置为非零值,daemon 函数不会关闭所有文件描述符。默认情况下,daemon 函数会关闭所有文件描述符。

返回值:

  • 如果成功,返回 0。
  • 如果失败,返回 -1,并设置 errno 以指示错误。

修改SvrCal.c,加上daemon()

#include <iostream>
#include <functional>
#include <unistd.h>
#include "TcpSvr.hpp"
#include "Protocol.hpp"
#include "SvrCal.hpp"using namespace std;int main(int argc, char* argv[])
{if(argc != 2) {cout << "Usage error!\n";return -1;}uint16_t port = stoi(argv[1]);SvrCal cal;TcpSvr *svr = new TcpSvr(port, bind(&SvrCal::doCalculate, &cal, placeholders::_1));svr->initSvr();int r = daemon(0, 0);if(r < 0) {cout << "Daemon error!\n";return -2;}svr->startSvr();return 0;
}

可以看到,守护进程已经被正确初始化了

image-20241122203922223


http://www.ppmy.cn/ops/137180.html

相关文章

HarmonyOS开发者社区有奖征文二期活动开启!

HarmonyOS开发者社区有奖征文活动第二期如约而至&#xff01;在上一期的基础上&#xff0c;我们精心策划了更多样化的主题&#xff0c;旨在为开发者们提供一个更广阔的交流平台。无论您是想探讨HarmonyOS的技术细节&#xff0c;还是分享您的开发经验&#xff0c;或是记录您与Ha…

电脑自动关机时间如何定?Wise Auto Shutdown 设置关机教程

在日常使用电脑的过程中&#xff0c;有时我们需要让电脑在特定的时间自动关机&#xff0c;比如在下载大文件完成后、执行长时间的任务结束时&#xff0c;或者只是单纯想在某个预定时间让电脑自动关闭以节省能源。这时候&#xff0c;Wise Auto Shutdown 这款软件就能派上大用场了…

WordCloud去掉停用词(fit_words+generate)的2种用法

-------------词云图集合------------- WordCloud去掉停用词&#xff08;fit_wordsgenerate&#xff09;的2种用法 通过词频来绘制词云图&#xff08;jiebaWordCloud&#xff09; Python教程95&#xff1a;去掉停用词词频统计jieba.tokenize示例用法 将进酒—李白process_t…

DrissionPage爬虫工具教程

当然可以&#xff01;下面是一些更高级和复杂的 DrissionPage 使用示例&#xff0c;包括处理动态加载的内容、处理登录和会话、处理多页面操作等。 处理动态加载的内容 许多现代网站使用 JavaScript 动态加载内容。在这种情况下&#xff0c;我们需要等待特定的元素出现&#…

笔记mfc11

Subclass(子类化)是MFC中最常用的窗体技术之一。子类化完成两个工作&#xff1a;一是把窗体类对象attach到一个windows窗体实体中&#xff08;即把一个窗体的hwnd赋给该类&#xff09;。另外就是把该类对象的消息加入到消息路由中&#xff0c;使得该类可以捕获消息。 让edit能…

python tkinter 控件实现鼠标悬停提示,提示文本动态展示

展示效果 全部代码和使用示例 # _*_ coding:utf-8 _*_ import tkinter as tk import pyautoguiscreen_width, screen_height pyautogui.size()class WidgetTip:"""鼠标悬停提示"""def __init__(self, widget, text):self.widget widgetself.…

刘铁猛C#入门 026 重写与多态

类的继承 类成员的“横向扩展”(成员越来越多)类成员的“纵向扩展”(行为改变&#xff0c;版本增高)类成员的隐藏(不常用)重写与隐藏的发生条件&#xff1a;函数成员&#xff0c;可见&#xff0c;签名一致 函数成员:方法 、属性可见&#xff1a;父类修饰符是public protected …

决策树 DecisionTreeClassifier() 模型参数介绍

DecisionTreeClassifier() 是 scikit-learn 库中的决策树分类器&#xff0c;它有几个关键参数&#xff0c;用于控制模型的行为和性能。以下是其中一些重要的参数&#xff1a; criterion&#xff1a;用于划分节点的标准&#xff0c;可以是 ’gini’&#xff08;基尼指数&#xf…