自定义协议

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.rhkb.cn/news/479353.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring Boot 3.4.0 发行:革新与突破的里程碑

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;…

android 11添加切换分屏功能

引言 自Android 7开始官方就支持分屏显示,但没有切换分屏的功能,即交换上下屏幕。直到Android 13开始才支持切换分屏,操作方式是:分屏模式下双击中间分割线就会交换上下屏位置。本文的目的就是在Android 11上实现切换分屏的功能。 下图是Android13切换分屏演示 切换分屏…

PyTorch基础05_模型的保存和加载

目录 一、模型定义组件——重构线性回归 二、模型的加载和保存 2、序列化保存对象和加载 3、保存模型参数 一、模型定义组件——重构线性回归 回顾之前的手动构建线性回归案例&#xff1a; 1.构建数据集&#xff1b;2.加载数据集(数据集转换为迭代器)&#xff1b;3.参数初…

JavaScript核心语法(3)

前两篇文章大概把JavaScript的基础语法讲了一下&#xff0c;这篇文章主要讲讲ES6的核心语法。ES6的核心语法说实话其实有点多&#xff0c;我重点挑一些经常在项目中用到的来讲&#xff0c;其他一些我没怎么见过的就不讲了。 目录 1.变量和常量 变量&#xff08;let 和 var&a…

爬虫开发(5)如何写一个CSDN热门榜爬虫小程序

笔者 綦枫Maple 的其他作品&#xff0c;欢迎点击查阅哦~&#xff1a; &#x1f4da;Jmeter性能测试大全&#xff1a;Jmeter性能测试大全系列教程&#xff01;持续更新中&#xff01; &#x1f4da;UI自动化测试系列&#xff1a; SeleniumJava自动化测试系列教程❤ &#x1f4da…

NIO三大组件

现在互联网环境下&#xff0c;分布式系统大相径庭&#xff0c;而分布式系统的根基在于网络编程&#xff0c;而netty恰恰是java领域的网络编程的王者&#xff0c;如果要致力于并发高性能的服务器程序、高性能的客户端程序&#xff0c;必须掌握netty网络编程。 NIO基础 NIO是从ja…

34 基于单片机的指纹打卡系统

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 基于STC89C52RC&#xff0c;采用两个按键替代指纹&#xff0c;一个按键按下&#xff0c;LCD12864显示比对成功&#xff0c;则 采用ULN2003驱动步进电机转动&#xff0c;表示开门&#xff0c;另一个…

李宏毅机器学习课程知识点摘要(14-18集)

线性回归&#xff0c;逻辑回归&#xff08;线性回归sigmoid&#xff09;&#xff0c;神经网络 linear regression &#xff0c; logistic regression &#xff0c; neutral network 里面的偏导的相量有几百万维&#xff0c;这就是neutral network的不同&#xff0c;他是…

文件上传upload-labs-docker通关

&#xff08;图片加载不出&#xff0c;说明被和谐了&#xff09; 项目一&#xff1a; sqlsec/ggctf-upload - Docker Image | Docker Hub 学习过程中,可以对照源码进行白盒分析. 补充&#xff1a;环境搭建在Linux虚拟机上的同时&#xff0c;以另一台Windows虚拟机进行测试最…

【Android】静态广播接收不到问题分析思路

参考资料&#xff1a; Android 静态广播注册流程(广播2)-CSDN博客 Android广播发送流程(广播3)_android 发送广播-CSDN博客 https://zhuanlan.zhihu.com/p/347227068 在Android中&#xff0c;静态广播如果静态广播不能接收&#xff0c;我们可以从整个流程中去分析&#xff…

2024 APMCM亚太数学建模C题 - 宠物行业及相关产业的发展分析和策略(详细解题思路)

在当下&#xff0c; 日益发展的时代&#xff0c;宠物的数量应该均为稳步上升&#xff0c;在美国出现了下降的趋势&#xff0c; 中国 2019-2020 年也下降&#xff0c;这部分变化可能与疫情相关。需要对该部分进行必要的解释说明。 问题 1: 基于附件 1 中的数据及您的团队收集的额…

Git简单介绍

一、 Git介绍与安装 1.1 Git简介 Git是一个开源的分布式版本控制系统&#xff0c;可以有效、高速地处理从很小到非常大的项目版本管理。 1.2集中式(SVN&#xff09; VS 分布式(git) 集中式版本控制系统&#xff0c;版本库是集中存放在中央服务器的&#xff0c;工作时要先从中央…

CSS之3D转换

三维坐标系 三维坐标系其实就是指立体空间&#xff0c;立体空间是由3个轴共同组成的。 x轴:水平向右注意:x右边是正值&#xff0c;左边是负值 y轴:垂直向下注意:y下面是正值&#xff0c;上面是负值 z轴:垂直屏幕注意:往外面是正值&#xff0c;往里面是负值 3D移动 translat…

kafka生产者和消费者命令的使用

kafka-console-producer.sh 生产数据 # 发送信息 指定topic即可 kafka-console-producer.sh \ --bootstrap-server bigdata01:9092 \ --topic topicA # 主题# 进程 29124 ConsoleProducer kafka-console-consumer.sh 消费数据 # 消费数据 kafka-console-consumer.sh \ --boo…

基于Springboot的心灵治愈交流平台系统的设计与实现

基于Springboot的心灵治愈交流平台系统 介绍 基于Springboot的心灵治愈交流平台系统&#xff0c;后端框架使用Springboot和mybatis&#xff0c;前端框架使用Vuehrml&#xff0c;数据库使用mysql&#xff0c;使用B/S架构实现前台用户系统和后台管理员系统&#xff0c;和不同级别…

【人工智能】Python常用库-Scikit-learn常用方法教程

Scikit-learn 是一个功能强大的机器学习库&#xff0c;支持数据预处理、分类、回归、聚类、降维等功能&#xff0c;广泛用于模型开发与评估。以下是 Scikit-learn 的常用方法及详细说明。 1. 安装与导入 安装 Scikit-learn&#xff1a; pip install scikit-learn导入基本模块…

Tcon技术和Tconless技术介绍

文章目录 TCON技术&#xff08;传统时序控制器&#xff09;定义&#xff1a;主要功能&#xff1a;优点&#xff1a;缺点&#xff1a; TCONless技术&#xff08;无独立时序控制器&#xff09;定义&#xff1a;工作原理&#xff1a;优点&#xff1a;缺点&#xff1a; TCON与TCONl…

计算机基础(下)

内存管理 内存管理主要做了什么&#xff1f; 操作系统的内存管理非常重要&#xff0c;主要负责下面这些事情&#xff1a; 内存的分配与回收&#xff1a;对进程所需的内存进行分配和释放&#xff0c;malloc 函数&#xff1a;申请内存&#xff0c;free 函数&#xff1a;释放内存…

【青牛科技】TS223 单触摸键检测IC

概 述 &#xff1a; TS223是 触 摸 键 检 测 IC&#xff0c; 提 供 1个 触 摸 键 。 触 摸 检 测 IC是 为 了用 可 变 面 积 的 键 取 代 传 统 的 按 钮 键 而 设 计 的 。低 功 耗 和 宽 工 作 电压是 触 摸 键 的 DC和 AC特 点 。TS223采 用 SSOP16、 SOT23-6的 封 装 形 式…

CUDA补充笔记

文章目录 一、不同核函数前缀二、指定kernel要执行的线程数量三、线程需要两个内置坐标变量来唯一标识线程四、不是blocksize越大越好&#xff0c;上限一般是1024个blocksize 一、不同核函数前缀 二、指定kernel要执行的线程数量 总共需要线程数是&#xff1a; 1 * N N个线程…