自定义协议
1. 问题引入
问题:TCP是面向字节流的(TCP不关心发送的数据是消息、文件还是其他任何类型的数据。它简单地将所有数据视为一个字节序列,即字节流。这意味着TCP不会对发送的数据进行任何特定的边界划分,它只是确保数据的顺序和完整性。),这怎么能保证,读上来的数据是一个 完整 的报文呢?
解答:通过协议(Protocol)来约定。协议定义了数据交换的规则和标准,使得不同设备之间能够相互通信和理解。
例如:
- 固定长度报文: 如果每个报文的长度都是固定的,那么接收方可以简单地读取固定数量的字节来构成一个完整的报文。
- 长度前缀: 在报文的开始处添加一个字段,指定了报文的长度。接收方首先读取长度字段,然后根据指定的长度读取后续的字节来构成完整的报文。
- 特殊分隔符: 使用一个特殊的字节序列或字符串作为报文的分隔符。接收方在流中搜索这个分隔符来确定报文的边界。例如,HTTP协议使用"\r\n\r\n"作为请求头和请求体的分隔符。
其它知识:传输层TCP是全双工的,意味着,TCP的收发是可以同时进行的。亦即接收的时候可以发送,发送的时候也可以接收,两者互不冲突,可同时进行。实际上客户端和服务端维护者两个缓冲区
2. 协议定制
2.1 序列化和反序列化的概念
序列化(Serialization)
序列化是指将对象的状态信息转换为可以存储或传输的格式(如JSON、XML、二进制等格式)的过程。序列化后的数据可以写入到文件中,或者通过网络发送到其他计算机。序列化的主要目的包括:
- 数据持久化:将内存中的数据结构保存到文件中,以便在程序下次运行时能够恢复。
- 网络传输:在网络上传输数据时,需要将复杂的数据结构转换为一种可以在网络上传输的格式。
- 跨语言和平台:不同的编程语言和平台之间交换数据时,需要一种中间格式来实现互操作性。
反序列化(Deserialization)
反序列化是序列化的逆过程,它是指将序列化后的数据(如文件中的数据或网络上接收到的数据)转换回原始的数据结构或对象状态的过程。反序列化使得程序能够从持久化的数据中恢复对象,或者接收并处理来自其他程序的数据。
序列化和反序列化的用途
- 数据库存储:将对象序列化后存储到数据库中,需要时再反序列化以恢复对象。
- 网络通信:在分布式系统或网络应用中,对象需要在网络上传输,因此需要序列化和反序列化。
- 缓存:将对象序列化后存储在缓存中,可以减少内存的使用。
- 消息队列:在使用消息队列(如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;
}
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;
}
3\n6 0
是Response
序列化加上报头结果
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);}}
}
现在的运行结果如下图
2.4 使用json
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。它基于JavaScript的一个子集,但是独立于语言,可以被多种编程语言读取。JSON采用文本格式,易于阅读和编写,同时也易于机器解析和生成。
JSON的结构包括:
- 键值对:JSON中的每个元素都由键值对组成,键和值之间用冒号(:)分隔,键名必须用双引号括起来。
- 数据类型:JSON支持的数据类型包括字符串(String)、数字(Number)、对象(Object)、数组(Array)、布尔值(Boolean)和空值(null)。
- 数组:数组在JSON中用方括号([])表示,数组中的元素可以是任何类型的值。
- 对象:对象在JSON中用花括号({})表示,对象中的元素是键值对。
一个简单的JSON示例如下:
{"name": "John","age": 30,"is_student": false,"courses": ["Math", "Science", "History"],"address": {"street": "123 Main St","city": "Anytown","state": "CA"}
}
在这个例子中,name
、age
、is_student
是键值对,courses
是一个数组,address
是一个对象。JSON格式的数据可以被JavaScript直接解析,也可以被其他支持JSON的编程语言解析和生成。
2.4.1 安装json
sudo yum install -y jsoncpp-devel
安装完成后,json库用到的头文件
json库的位置
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;
}
除了使用Json::FastWriter
,也可以使用Json::StyledWriter
,可读性会好一点
// 将上面代码的第16行改为:
Json::StyledWriter w;
继续写上面的代码,下面进行反序列化
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;
}
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;
}
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
运行结果如下
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;
}
可以看到,守护进程已经被正确初始化了
相关文章:

自定义协议
1. 问题引入 问题:TCP是面向字节流的(TCP不关心发送的数据是消息、文件还是其他任何类型的数据。它简单地将所有数据视为一个字节序列,即字节流。这意味着TCP不会对发送的数据进行任何特定的边界划分,它只是确保数据的顺序和完整…...

在 Taro 中实现系统主题适配:亮/暗模式
目录 背景实现方案方案一:CSS 变量 prefers-color-scheme 媒体查询什么是 prefers-color-scheme?代码示例 方案二:通过 JavaScript 监听系统主题切换 背景 用Taro开发的微信小程序,需求是页面的UI主题想要跟随手机系统的主题适配…...

autogen框架中使用chatglm4模型实现react
本文将介绍如何使用使用chatglm4实现react,利用环境变量、Tavily API和ReAct代理模式来回答用户提出的问题。 环境变量 首先,我们需要加载环境变量。这可以通过使用dotenv库来实现。 from dotenv import load_dotenv_ load_dotenv()注意.env文件处于…...

读《Effective Java》笔记 - 条目9
条目9:与try-finally 相比,首选 try -with -resource 什么是 try-finally? try-finally 是 Java 中传统的资源管理方式,通常用于确保资源(如文件流、数据库连接等)被正确关闭。 BufferedReader reader n…...

【软件入门】Git快速入门
Git快速入门 文章目录 Git快速入门0.前言1.安装和配置2.新建版本库2.1.本地创建2.2.云端下载 3.版本管理3.1.添加和提交文件3.2.回退版本3.2.1.soft模式3.2.2.mixed模式3.2.3.hard模式3.2.4.使用场景 3.3.查看版本差异3.4.忽略文件 4.云端配置4.1.Github4.1.1.SSH配置4.1.2.关联…...

nextjs window is not defined
问题产生的原因 在 Next.js 中,“window is not defined” 错误通常出现在服务器端渲染(Server - Side Rendering,SSR)的代码中。这是因为window对象是浏览器环境中的全局对象,在服务器端没有window这个概念。例如&am…...

C语言实现冒泡排序:从基础到优化全解析
一、什么是冒泡排序? 冒泡排序(Bubble Sort)是一种经典的排序算法,其工作原理非常直观:通过多次比较和交换相邻元素,将较大的元素“冒泡”到数组的末尾。经过多轮迭代,整个数组会变得有序。 二…...

windows11下git与 openssl要注意的问题
看了一下自己贴文的历史,有一条重要的忘了写了。 当时帮有位同事配置gitlab,众说周知gitlab是不太好操作。 但我还是自认自己git还是相当熟的。 解决了一系列问题,如配置代理,sshkey,私有库,等等࿰…...

lua除法bug
故事背景,新来了一个数值,要改公式。神奇的一幕出现了,公式算出一个非常大的数。排查是lua有一个除法bug,1除以大数得到一个非常大的数。 function div(a, b)return tonumber(string.format("%.2f", a/b)) end print(1/73003) pri…...

Ubuntu下Docker容器java服务往mysql插入中文数据乱码
一、问题描述 1、java服务部署在ubuntu下的docker容器内,但是会出现部分插入中文数据显示乱码,如图所示: 二、解决方案 1、查看mysql是否支持utf8,登录进入Mysql 输入命令: mysql -u root -pshow variables like c…...

C语言根据字符串变量获取/设置结构体成员值
一、背景 在项目中需要根据从数据库中获取的字段与对应的键值付给对应结构体成员上,而c语言中没有类似的反射机制,所以需要实现类似功能。例,从表中查到a 10,在结构体t中,需要将 t.a 10。 二、实现 感谢ChatGPT&…...

Selenium 自动化测试demo
场景描述: 模拟用户登录页面操作,包括输入用户名、密码、验证码。验证码为算数运算,如下: 使用到的工具和依赖: 1. Selenium:pip install selenium 2. 需要安装浏览器驱动:这里使用的是Edge 3…...

LeetCode 111.二叉树的最小深度
题目: 给定一个二叉树,找出其最小深度。 最小深度是从根节点到最近叶子节点的最短路径上的节点数量。 说明:叶子节点是指没有子节点的节点。 思路:自底向上(归)/自顶向下(递) DF…...

大工C语言作业答案
前言 这里是大连理工大学新版C语言课程MOOC作业的答案。 后期我会把全部的作业答案开源出来,希望对大家有帮助。 第九周第一题 #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int B(int i) {int sum 1;while (i > 0){sum i * sum;i--;}return su…...

【Unity踩坑】Unity中父对象是非均匀缩放时出现倾斜或剪切现象
The game object is deformed when the parent object is in non-uniform scaling. 先来看一下现象 有两个Cube, Cube1(Scale2,1,1),Cube2(Scale1,1,1) 将Cube2拖拽为Cube2的子对象。并且将position设置为(-0.6,1,0&a…...

QT 跨平台实现 SSDP通信 支持多网卡
一.多网卡场景 在做SSDP通信的时候,客户端发出M-search命令后, 主机没有捕捉到SSDP的消息,你可以查看下,是不是局域网下,既打开了wifi,又连接了本地网络,mac os下很容易出现这种场景。此时,我们发送消息时,需要遍历所有网卡并发送M-search命令。 二.QT相关接口介绍 1…...

如何寻找适合的HTTP代理IP资源?
一、怎么找代理IP资源? 在选择代理IP资源的时候,很多小伙伴往往将可用率作为首要的参考指标。事实上,市面上的住宅IP或拨号VPS代理IP资源,其可用率普遍在95%以上,因此IP可用率并不是唯一的评判标准 其实更应该关注的…...

数据结构(ArrayList顺序表)
一、引言 1.什么是顺序表 定义: 顺序表是一种基于阵列实现的线性表结构,用连续的存储空间保存表中的数据元素,并按顺序排列。 底层依赖阵列,支持随机访问。元素之间没有额外的连接信息,如指针或链表节点。通过动态扩容…...

直接抄作业!Air780E模组LuatOS开发:位运算(bit)示例
在嵌入式开发中,位运算是一种高效且常用的操作技巧。本文将介绍如何使用Air780E模组和LuatOS进行位运算,并通过示例代码帮助读者快速上手。 一、位运算概述 位运算是一种在计算机系统中对二进制数位进行操作的运算。由于计算机内部数据的存储和处理都是…...

RK3588-LinuxSDK安装
安装依赖软件 执行如下命令,安装 LinuxSDK 开发包依赖软件。 备注:安装过程中,请保证 Ubuntu 可正常访问互联网,若提示"*** is already the newest version ***"表示该软件已安装,请忽略。 Host# sudo apt-get install -y git ssh make gcc libssl-dev \ liblz…...

MATLAB 中有关figure图表绘制函数设计(论文中常用)
在撰写论文时,使用 MATLAB 导出的图像常常因大小和格式不统一,导致投稿时编辑部频繁退稿,要求修改和调整。这不仅浪费时间,也增加了工作量。为了减少这些麻烦,可以在 MATLAB 中导出图像时提前设置好图表的大小、格式和…...

Unity UGUI原理剖析
UI最重要的两部分 UI是如何渲染出来的点击事件如何触发何时发生UI重绘 1:UI如何渲染出来的 UI渲染一定是有顶点的,没有顶点就没法确定贴图的采样,UGUI的顶点在一张Mesh上创建,经过渲染管线UI就渲染到屏幕上了,UI的渲染…...

Spring框架使用xml方式配置ThreadPoolTaskExecutor线程池,并且自定义线程工厂
一、自定义线程工厂 自定义线程工厂需要实现java.util.concurrent.ThreadFactory接口,重写newThread方法。 示例代码: package com.xiaobai.thread;import org.apache.log4j.Logger;import java.util.concurrent.ThreadFactory; import java.util.conc…...

架构-微服务-服务网关
文章目录 前言一、网关介绍1. 什么是API网关2. 核心功能特性3. 解决方案 二、Gateway简介三、Gateway快速入门1. 基础版2. 增强版3. 简写版 四、Gateway核心架构1. 基本概念2. 执行流程 五、Gateway断言1. 内置路由断言工厂2. 自定义路由断言工厂 六、过滤器1. 基本概念2. 局部…...

基于springboot的HttpClient、OKhttp、RestTemplate对比
HttpClient详细 Httpclient基础!!!!实战训练!!!!-CSDN博客 OKhttp使用 OKhttp导包 <!-- ok的Http连接池 --><dependency><groupId>com.squareup.okhttp3</g…...

(计算机组成原理)期末复习
第一章 计算机的基本组成:硬件软件(程序)计算机系统 软件有系统软件(系统管理工具),应用软件 计算机硬件:包括主机和外设,主机包括CPU和内存,***CPU由运算器和控制器所组…...

从0到1部署Tomcat和添加servlet(IDEA2024最新版详细教程)
本文不仅细化了每一个步骤,实现了从0到1部署Tomcat和添加servlet。还针对IDEA2024版和以前的版本在部署上的区别,做了详细介绍,尤其是add framework support部分。与此同时,针对控制台中文乱码问题,本文也给出了详细解…...

【Java从入门到放弃 之 Java程序基础】
Java程序基础 Java程序基础基本数据类型和变量数据类型变量赋值基本运算算术运算比较运算逻辑运算 Java程序基础 基本数据类型和变量 数据类型 对Java语言而言,有如下基本数据类型。 整数类型:有4种整型byte/short/int/long,它们占用的字…...

2024年11月26日Github流行趋势
项目名称:v2rayN 项目维护者:2dust yfdyh000 CGQAQ ShiinaRinne Lemonawa 项目介绍:一个支持Xray核心及其他功能的Windows和Linux图形用户界面客户端。 项目star数:70,383 项目fork数:11,602 项目名称:fre…...

相亲交友小程序项目介绍
一、项目背景 在当今快节奏的社会生活中,人们忙于工作和事业,社交圈子相对狭窄,寻找合适的恋爱对象变得愈发困难。相亲交友作为一种传统而有效的社交方式,在现代社会依然有着巨大的需求。我们的相亲交友项目旨在为广大单身人士提…...