计算机网络 : 应用层自定义协议与序列化
计算机网络 : 应用层自定义协议与序列化
目录
- 计算机网络 : 应用层自定义协议与序列化
- 引言
- 1. 应用层协议
- 1.1 再谈协议
- 1.2 网络版计算器
- 1.3 序列化与反序列化
- 2. 重新理解全双工
- 3. `socket`和协议的封装
- 4. 关于流失数据的处理
- 5. `Jsoncpp`
- 5.1 特性
- 5.2 安装
- 5.3 序列化
- 5.4 反序列化
- 5.5 总结
- 5.6 `Json::Value`(了解)
- 6. 进程间关系与守护进程
- 6.1 进程组
- 6.1.1 进程组概念
- 6.1.2 组长进程
- 6.2 会话
- 6.2.1 会话概念
- 6.2.2 如何创建会话
- 6.2.3 会话ID(SID)
- 6.3 控制终端
- 6.4 作业控制
- 6.4.1 作业和作业控制的概念
- 6.4.2 作业号
- 6.4.3 作业状态
- 6.4.4 作业的挂起与切回
- 6.4.5 查看后台执行或挂起的作业
- 6.4.6 作业控制相关的信号
- 6.5 守护进程
- 7. 完整版网络计算器服务的代码实现
- 7.1 `Common.hpp`
- 7.2 `Daemon.hpp`
- 7.3 `InetAddr.hpp`
- 7.4 `Log.hpp`
- 7.5 `Makefile`
- 7.6 `Mutex.hpp`
- 7.7 `NetCal.hpp`
- 7.8 `Protocol.hpp`
- 7.9`Socket.hpp`
- 7.10 `TcpClient.cc`
- 7.11 `TcpServer.hpp`
- 7.12 `main.cc`
引言
在计算机网络编程中,应用层协议的设计与实现是构建网络应用的核心。本文深入探讨了如何自定义应用层协议,并通过序列化与反序列化技术实现结构化数据的网络传输。内容涵盖以下关键点:
- 应用层协议的基本概念与设计方法
- 序列化与反序列化的原理与实现(以JSON为例)
- TCP全双工通信的本质与Socket封装
- 流式数据的边界处理(解决粘包/半包问题)
- 守护进程的实现与进程关系管理
- 完整网络计算器服务的代码实现
1. 应用层协议
程序员编写的一个个解决我们生活问题的网络程序,都是在应用层实现的。
1.1 再谈协议
协议是一种“约定”。socket api
的接口,在读写数据时,都是按“字符串”的方式来发送接受的。如果我们要传输一些“结构化的数据”怎么办呢?其实,协议就是双方约定好的结构化的数据。
1.2 网络版计算器
- 例如,我们需要实现一个服务器版的加法器,客户端将需要计算的两个加数发送到服务器,由服务器完成计算后返回结果给客户端。
- 约定方案一:客户端发送一个形如"1+1"的字符串,其中包含两个整型操作数和一个运算符(仅限"+"),且数字与运算符之间无空格。
- 约定方案二:定义结构体表示交互信息,发送时将结构体按规则转换为字符串(序列化),接收时再按相同规则将字符串转回结构体(反序列化)。
1.3 序列化与反序列化
- 无论我们采用方案一、方案二还是其他方案,只要保证一端发送时构造的数据能在另一端正确解析,就是可行的。这种约定就是应用层协议。
- 但为了深入理解协议,我们将自定义实现协议的过程:我们采用方案二,并体现协议定制的细节;同时引入序列化和反序列化(下面代码中直接使用现成的
jsoncpp
库),并对socket
的字节流进行读取处理。
2. 重新理解全双工
- 在任何一台主机上,
TCP
连接既有发送缓冲区,又有接收缓冲区,因此在内核中可以在发送消息的同时接收消息,即全双工通信。 - 这就是为什么一个
TCP sockfd
既可以读又可以写的原因。 - 主机间通信的本质:把发送方的发送缓冲区内部的部署,拷贝到对端的接受缓冲区。
- 计算机世界:通信即拷贝
- 实际数据何时发送、发送多少以及出错如何处理都由
TCP
协议控制,因此TCP
被称为传输控制协议。
3. socket
和协议的封装
-
socket.hpp
#pragma once // 防止头文件重复包含// 包含必要的系统头文件 #include <iostream> #include <string> #include <cstring> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> // IPv4地址结构体定义 #include <arpa/inet.h> // IP地址转换函数 #include <unistd.h> // close()函数// 将地址结构体指针转换为通用sockaddr*类型的宏 #define Convert(addrptr) ((struct sockaddr *)addrptr)// 定义网络相关的命名空间 namespace Net_Work {// 默认socket文件描述符值const static int defaultsockfd = -1;// 监听队列最大长度const int backlog = 5;// 错误码枚举enum{SocketError = 1, // socket创建错误BindError, // bind绑定错误ListenError, // listen监听错误};// Socket抽象基类,使用模板方法设计模式class Socket{public:virtual ~Socket() {} // 虚析构函数// 纯虚函数,子类必须实现virtual void CreateSocketOrDie() = 0; // 创建socketvirtual void BindSocketOrDie(uint16_t port) = 0; // 绑定端口virtual void ListenSocketOrDie(int backlog) = 0; // 开始监听virtual Socket* AcceptConnection(std::string* peerip, // 接受连接uint16_t* peerport) = 0;virtual bool ConnectServer(std::string& serverip, // 连接服务器uint16_t serverport) = 0;virtual int GetSockFd() = 0; // 获取socket fdvirtual void SetSockFd(int sockfd) = 0; // 设置socket fdvirtual void CloseSocket() = 0; // 关闭socketvirtual bool Recv(std::string* buffer, int size) = 0; // 接收数据virtual void Send(std::string& send_str) = 0; // 发送数据public:// 模板方法:构建监听socket的标准流程void BuildListenSocketMethod(uint16_t port, int backlog){CreateSocketOrDie(); // 1. 创建socketBindSocketOrDie(port); // 2. 绑定端口ListenSocketOrDie(backlog); // 3. 开始监听}// 模板方法:构建客户端连接socket的标准流程bool BuildConnectSocketMethod(std::string& serverip, uint16_t serverport){CreateSocketOrDie(); // 1. 创建socketreturn ConnectServer(serverip, serverport); // 2. 连接服务器}// 模板方法:构建普通socket(已存在socket fd)void BuildNormalSocketMethod(int sockfd){SetSockFd(sockfd); // 直接设置socket fd}};// TCP Socket实现类class TcpSocket : public Socket{public:// 构造函数,可指定初始socket fdTcpSocket(int sockfd = defaultsockfd) : _sockfd(sockfd) {}// 析构函数~TcpSocket() {}// 创建TCP socketvoid CreateSocketOrDie() override{// 创建IPv4的TCP socket_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0)exit(SocketError); // 创建失败则退出程序}// 绑定socket到指定端口void BindSocketOrDie(uint16_t port) override{struct sockaddr_in local;memset(&local, 0, sizeof(local)); // 清空结构体// 设置地址族、IP地址和端口local.sin_family = AF_INET; // IPv4local.sin_addr.s_addr = INADDR_ANY; // 任意本地IPlocal.sin_port = htons(port); // 端口号(网络字节序)// 绑定socketint n = ::bind(_sockfd, Convert(&local), sizeof(local));if (n < 0)exit(BindError); // 绑定失败则退出程序}// 开始监听连接请求void ListenSocketOrDie(int backlog) override{int n = ::listen(_sockfd, backlog); // 设置监听队列长度if (n < 0)exit(ListenError); // 监听失败则退出程序}// 接受客户端连接Socket* AcceptConnection(std::string* peerip, uint16_t* peerport) override{struct sockaddr_in peer;socklen_t len = sizeof(peer);// 接受连接,返回新的socket fdint newsockfd = ::accept(_sockfd, Convert(&peer), &len);if (newsockfd < 0)return nullptr; // 接受失败返回空指针// 获取客户端IP和端口*peerport = ntohs(peer.sin_port); // 端口号(主机字节序)*peerip = inet_ntoa(peer.sin_addr); // IP地址字符串// 创建新的TcpSocket对象并返回Socket* s = new TcpSocket(newsockfd);return s;}// 连接到服务器bool ConnectServer(std::string& serverip, uint16_t serverport) override{struct sockaddr_in server;memset(&server, 0, sizeof(server)); // 清空结构体// 设置服务器地址信息server.sin_family = AF_INET; // IPv4server.sin_addr.s_addr = inet_addr(serverip.c_str()); // 服务器IPserver.sin_port = htons(serverport); // 服务器端口// 发起连接int n = ::connect(_sockfd, Convert(&server), sizeof(server));return n == 0; // 返回连接是否成功}// 获取socket文件描述符int GetSockFd() override{return _sockfd;}// 设置socket文件描述符void SetSockFd(int sockfd) override{_sockfd = sockfd;}// 关闭socketvoid CloseSocket() override{if (_sockfd > defaultsockfd)::close(_sockfd); // 关闭socket文件描述符}// 接收数据bool Recv(std::string* buffer, int size) override{char inbuffer[size]; // 接收缓冲区// 接收数据ssize_t n = recv(_sockfd, inbuffer, size-1, 0);if(n > 0) // 接收成功{inbuffer[n] = 0; // 添加字符串结束符*buffer += inbuffer; // 将数据拼接到输出字符串return true;}else if(n == 0) // 连接关闭return false;else // 接收错误return false;}// 发送数据void Send(std::string& send_str) override{// 简单发送数据(不考虑非阻塞情况)send(_sockfd, send_str.c_str(), send_str.size(), 0);}private:int _sockfd; // socket文件描述符}; } // namespace Net_Work
-
定制协议
-
定制基本的结构化字段,这个就是协议。
-
class Request { private:// _data_x _oper _data_y// 报文的自描述字段// "len\r\nx op y\r\n" : \r\n 不属于报文的一部分,约定// 很多工作都是在做字符串处理!int _data_x; // 第一个参数 int _data_y; // 第二个参数char _oper; // + - * / % }; class Response { private:// "len\r\n_result _code\r\n"int _result; // 运算结果int _code; // 运算状态 };
-
protocal.hpp
#pragma once // 防止头文件被重复包含#include <iostream> #include <memory> // 用于智能指针 #include <jsoncpp/json/json.h> // JSON库,用于序列化和反序列化namespace Protocol {// 协议解决的问题:// 1. 结构化数据的序列化和反序列化// 2. 解决用户区分报文边界问题(数据包粘包问题)// 协议格式说明:// "protocol_code\r\nlen\r\nx op y\r\n" : \r\n 不属于报文的一部分,是分隔符const std::string ProtSep = " "; // 协议字段分隔符(空格)const std::string LineBreakSep = "\r\n"; // 行分隔符(回车换行)/*** @brief 编码函数,将消息封装成协议格式* @param message 要编码的消息内容* @return 返回编码后的完整协议包* * 协议格式:消息长度\r\n消息内容\r\n*/std::string Encode(const std::string &message) {std::string len = std::to_string(message.size()); // 计算消息长度std::string package = len + LineBreakSep + message + LineBreakSep; // 拼接协议包return package;}/*** @brief 解码函数,从协议包中提取消息* @param package 待解码的协议包(可能包含不完整数据)* @param message 输出参数,存放解码后的消息* @return 解码成功返回true,失败返回false* * 该函数同时会检查报文完整性,并处理粘包问题*/bool Decode(std::string &package, std::string *message) {// 查找第一个行分隔符位置auto pos = package.find(LineBreakSep);if (pos == std::string::npos) // 如果没有找到分隔符,说明报文不完整return false;// 提取消息长度部分std::string lens = package.substr(0, pos);int messagelen = std::stoi(lens); // 将字符串长度转换为整数// 计算完整报文的总长度:长度字段长度 + 分隔符长度 + 消息长度 + 结尾分隔符长度int total = lens.size() + messagelen + 2 * LineBreakSep.size();if (package.size() < total) // 如果缓冲区数据不足一个完整报文return false;// 提取消息内容*message = package.substr(pos + LineBreakSep.size(), messagelen);// 从缓冲区中移除已处理的报文package.erase(0, total);return true;}/*** @brief 请求类,封装客户端请求数据*/class Request {public:Request() : _data_x(0), _data_y(0), _oper(0) {} // 默认构造函数Request(int x, int y, char op) : _data_x(x), _data_y(y), _oper(op) {} // 带参构造函数// 调试输出请求内容void Debug() {std::cout << "_data_x: " << _data_x << std::endl;std::cout << "_data_y: " << _data_y << std::endl;std::cout << "_oper: " << _oper << std::endl;}// 自增操作,用于测试void Inc() {_data_x++;_data_y++;}/*** @brief 序列化方法,将结构体转换为JSON字符串* @param out 输出参数,存放序列化后的字符串* @return 总是返回true*/bool Serialize(std::string *out) {Json::Value root; // 创建JSON根节点root["datax"] = _data_x; // 添加数据字段root["datay"] = _data_y;root["oper"] = _oper;Json::FastWriter writer; // 使用快速写入器*out = writer.write(root); // 写入字符串return true;}/*** @brief 反序列化方法,从JSON字符串解析结构体* @param in 输入的JSON字符串* @return 解析成功返回true,失败返回false*/bool Deserialize(std::string &in) {Json::Value root;Json::Reader reader;bool res = reader.parse(in, root); // 解析JSON字符串if(res) { // 如果解析成功,提取字段值_data_x = root["datax"].asInt();_data_y = root["datay"].asInt();_oper = root["oper"].asInt();}return res;}// Getter方法int GetX() { return _data_x; }int GetY() { return _data_y; }char GetOper() { return _oper; }private:int _data_x; // 第一个操作数int _data_y; // 第二个操作数char _oper; // 操作符(+ - * / %)};/*** @brief 响应类,封装服务器响应数据*/class Response {public:Response() : _result(0), _code(0) {} // 默认构造函数Response(int result, int code) : _result(result), _code(code) {} // 带参构造函数/*** @brief 序列化方法,将结构体转换为JSON字符串* @param out 输出参数,存放序列化后的字符串* @return 总是返回true*/bool Serialize(std::string *out) {Json::Value root;root["result"] = _result; // 添加结果字段root["code"] = _code; // 添加状态码字段Json::FastWriter writer;*out = writer.write(root);return true;}/*** @brief 反序列化方法,从JSON字符串解析结构体* @param in 输入的JSON字符串* @return 解析成功返回true,失败返回false*/bool Deserialize(std::string &in) {Json::Value root;Json::Reader reader;bool res = reader.parse(in, root);if(res) {_result = root["result"].asInt();_code = root["code"].asInt();}return res;}// Setter和Getter方法void SetResult(int res) { _result = res; }void SetCode(int code) { _code = code; }int GetResult() { return _result; }int GetCode() { return _code; }private:int _result; // 运算结果int _code; // 状态码};/*** @brief 工厂类,用于创建请求和响应对象* * 实现了简单的工厂模式,封装对象创建逻辑*/class Factory {public:// 创建默认请求对象std::shared_ptr<Request> BuildRequest() {std::shared_ptr<Request> req = std::make_shared<Request>();return req;}// 创建带参数的请求对象std::shared_ptr<Request> BuildRequest(int x, int y, char op) {std::shared_ptr<Request> req = std::make_shared<Request>(x, y, op);return req;}// 创建默认响应对象std::shared_ptr<Response> BuildResponse() {std::shared_ptr<Response> resp = std::make_shared<Response>();return resp;}// 创建带参数的响应对象std::shared_ptr<Response> BuildResponse(int result, int code) {std::shared_ptr<Response> req = std::make_shared<Response>(result, code);return req;}}; } // namespace Protocol
-
4. 关于流失数据的处理
-
如何保证完整读取请求缓冲区的所有内容? 关键在于正确处理TCP流式数据的特点:
- 通过协议设计(如固定长度头部+内容、分隔符或自描述格式如JSON)来界定请求边界;
- 循环读取直到满足协议约定的结束条件(如收齐指定长度或遇到分隔符);
- 在读取过程中校验数据完整性(如校验和或哈希)。
- 注意事项:必须处理半包(未读完完整请求)和粘包(多个请求粘连)问题,可通过状态机记录当前解析进度,确保即使分段读取也能正确重组请求。TCP缓冲区本身不保证消息完整性,需应用层协议和代码逻辑共同保障。
-
// 定义协议分隔符常量 const std::string ProtSep = " "; // 空格分隔符,用于协议中字段间的分隔 const std::string LineBreakSep = "\n"; // 换行分隔符,用于协议中不同部分的分隔/** 编码函数:将原始消息封装为协议格式* 协议格式:"长度\n消息内容\n"(注意:\n是分隔符,不属于消息内容)* 例如:"11\nhello world\n"*/ std::string Encode(const std::string &message) {// 1. 计算消息长度并转换为字符串std::string len = std::to_string(message.size());// 2. 按照协议格式拼接报文:长度 + 换行 + 消息内容 + 换行std::string package = len + LineBreakSep + message + LineBreakSep;return package; }/** 解码函数:从可能不完整的数据包中提取完整消息* 处理各种可能的边界情况:* - 不完整长度部分:"l", "le", "len"* - 不完整分隔符:"len", "len\n"* - 不完整消息内容:"len\nx", "len\nx op", "len\nx op y"* - 不完整结束符:"len\nx op y", "len\nx op y\n"* - 多个报文混合:"len\nx op y\n""len", "len\nx op y\n""len\nx op y\n"*/ bool Decode(std::string &package, std::string *message) {// 1. 查找第一个换行分隔符,确定长度部分结束位置auto pos = package.find(LineBreakSep);// 如果没有找到换行符,说明长度部分都不完整,无法解析if (pos == std::string::npos) {return false;}// 2. 提取长度字符串并转换为整数std::string lens = package.substr(0, pos); // 获取长度部分int messagelen = std::stoi(lens); // 转换为整数// 3. 计算完整报文应有的总长度:// 长度部分 + 换行符 + 消息内容 + 换行符int total = lens.size() + LineBreakSep.size() + messagelen + LineBreakSep.size();// 4. 检查当前数据包是否包含完整报文if (package.size() < total) {return false; // 数据不完整,等待更多数据}// 5. 提取消息内容:// 从第一个换行符后开始,取messagelen长度的内容*message = package.substr(pos + LineBreakSep.size(), messagelen);// 6. 从数据包中移除已处理的部分package.erase(0, total);// 7. 返回成功解析return true; }
5. Jsoncpp
Jsoncpp
是一个用于处理 JSON 数据的 C++ 库,它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。Jsoncpp
是开源的,广泛用于各种需要处理 JSON 数据的 C++ 项目中。
5.1 特性
- 简单易用:
Jsoncpp
提供了直观的 API,使得处理 JSON 数据变得简单; - 高性能**:
Jsoncpp
的性能经过优化,能够高效地处理大量 JSON 数据;** - 全面支持**:支持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null;**
- 错误处理:在解析 JSON 数据时,
Jsoncpp
提供了详细的错误信息和位置,方便开发者调试。
5.2 安装
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel
5.3 序列化
当使用Jsoncpp
库进行 JSON 的序列化和反序列化时,存在不同的做法和工具类可供选择,以下是对Jsoncpp
中序列化和反序列化操作的详细介绍。
序列化指的是将数据结构或对象转换为一种格式,以便在网络上传输或存储到文件中。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" : "男" }
-
5.4 反序列化
反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。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
方法就足够了。
- 在某些情况下,你可能需要更精细地控制解析过程,可以直接使用
5.5 总结
toStyledString
、StreamWriter
和FastWriter
提供了不同的序列化选项,你可以根据具体需求选择使用;Json::Reader
和parseFromStream
函数是Jsoncpp
中主要的反序列化工具,它们提供了强大的错误处理机制;- 在进行序列化和反序列化时,请确保处理所有可能的错误情况,并验证输入和输出的有效性。
5.6 Json::Value
(了解)
Json::Value
是 Jsoncpp 库中的一个重要类,用于表示和操作 JSON 数据结构。以下是一些常用的 Json::Value
操作列表:
-
构造函数:
Json::Value()
默认构造函数,创建一个空的Json::Value
对象;Json::Value(ValueType type, bool allocated = false)
根据给定的ValueType
(如nullValue
、intValue
、stringValue
等)创建一个Json::Value
对象。
-
访问元素:
Json::Value& operator[](const char* key)
通过键(字符串)访问对象中的元素,如果键不存在则创建一个新的元素;Json::Value& operator[](const std::string& key)
同上,但使用std::string
类型的键;Json::Value& operator[](ArrayIndex index)
通过索引访问数组中的元素,如果索引超出范围则创建一个新的元素;Json::Value& at(const char* key)
通过键访问对象中的元素,如果键不存在则抛出异常;Json::Value& at(const std::string& key)
同上,但使用std::string
类型的键。
-
类型检查:
bool isNull()
:检查值是否为 null;bool isBool()
:检查值是否为布尔类型;bool isInt()
:检查值是否为整数类型;bool isInt64()
:检查值是否为 64 位整数类型;bool isUInt()
:检查值是否为无符号整数类型;bool isUInt64()
:检查值是否为 64 位无符号整数类型;bool isIntegral()
:检查值是否为整数或可转换为整数的浮点数;bool isDouble()
:检查值是否为双精度浮点数;bool isNumeric()
:检查值是否为数字(整数或浮点数);bool isString()
:检查值是否为字符串;bool isArray()
:检查值是否为数组;bool isObject()
:检查值是否为对象(即键值对的集合)。
-
赋值和类型转换:
Json::Value& operator=(bool value)
:将布尔值赋给Json::Value
对象;Json::Value& operator=(int value)
:将整数赋给Json::Value
对象;Json::Value& operator=(unsigned int value)
:将无符号整数赋给Json::Value
对象;Json::Value& operator=(Int64 value)
:将 64 位整数赋给Json::Value
对象;Json::Value& operator=(UInt64 value)
:将 64 位无符号整数赋给Json::Value
对象;Json::Value& operator=(double value)
:将双精度浮点数赋给Json::Value
对象;Json::Value& operator=(const char* value)
:将 C 字符串赋给Json::Value
对象;Json::Value& operator=(const std::string& value)
:将 std::string 赋给Json::Value
对象。bool asBool()
:将值转换为布尔类型(如果可能);int asInt()
:将值转换为整数类型(如果可能);Int64 asInt64()
:将值转换为 64 位整数类型(如果可能);unsigned int asUInt()
:将值转换为无符号整数类型(如果可能);UInt64 asUInt64()
:将值转换为 64 位无符号整数类型(如果可能);double asDouble()
:将值转换为双精度浮点数类型(如果可能);std::string asString()
:将值转换为字符串类型(如果可能)。
-
数组和对象操作
size_t size()
:返回数组或对象中的元素数量;bool empty()
:检查数组或对象是否为空;void resize(ArrayIndex newSize)
:调整数组的大小;void clear()
:删除数组或对象中的所有元素;void append(const Json::Value& value)
:在数组末尾添加一个新元素;Json::Value& operator[](const char* key, const Json::Value& defaultValue = Json::nullValue)
:在对象中插入或访问一个元素,如果键不存在则使用默认值;Json::Value& operator[](const std::string& key, const Json::Value& defaultValue = Json::nullValue)
:同上,但使用 std::string 类型的键。
6. 进程间关系与守护进程
6.1 进程组
6.1.1 进程组概念
之前我们提到了进程的概念,其实每一个进程除了有一个进程ID(PID)之外还属于一个进程组。进程组是一个或者多个进程的集合,一个进程组可以包含多个进程。每一个进程组也有一个唯一的进程组ID(PGID),并且这个PGID类似于进程ID,同样是一个正整数,可以存放在pid_t
数据类型中。
$ ps -eo pid,pgid,ppid,comm | grep test
#结果如下
PID PGID PPID COMMAND
2830 2830 2259 test
# -e 选项表示 every 的意思, 表示输出每一个进程信息
# -o 选项以逗号操作符(,)作为定界符, 可以指定要输出的列
6.1.2 组长进程
**每一个进程组都有一个组长进程,组长进程的ID等于其进程组ID。**我们可以通过ps
命令看到组长进程的现象:
[node@localhost code]$ ps -o pid,pgid,ppid,comm | cat
# 输出结果
PID PGID PPID COMMAND
2806 2806 2805 bash
2880 2880 2806 ps
2881 2880 2806 cat
- 从结果上看,
ps
进程的PID和PGID相同,说明ps
进程是该进程组的组长进程,该进程组包括ps
和cat
两个进程。 - 进程组组长的作用是创建一个进程组或者创建该组中的进程。
- 进程组的生命周期从进程组创建开始到其中最后一个进程离开为止;需要注意的是,只要进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否已经终止无关。
6.2 会话
6.2.1 会话概念
会话其实和进程组息息相关,**会话可以看成是一个或多个进程组的集合,一个会话可以包含多个进程组。每一个会话也有一个会话ID(SID)。**用户登录就是建立会话的过程,关闭终端就是销毁会话的过程。
通常我们都是使用管道将几个进程编成一个进程组,如上图的进程组2和进程组3可能是由下列命令形成的。
代码示例:
[node@localhost code]$ proc2 | proc3 &
[node@localhost code]$ proc4 | proc5 | proc6 &
# &表示将进程组放在后台执行
# 用管道和 sleep 组成一个进程组放在后台运行
[node@localhost code]$ sleep 100 | sleep 200 | sleep 300 &# 查看 ps 命令打出来的列描述信息
[node@localhost code]$ ps axj | head -n1# 过滤 sleep 相关的进程信息
[node@localhost code]$ ps axj | grep sleep | grep -v grep
# a 选项表示不仅列当前⽤户的进程,也列出所有其他⽤户的进程
# x 选项表示不仅列有控制终端的进程,也列出所有⽆控制终端的进程
# j 选项表示列出与作业控制相关的信息, 作业控制后续会讲
# grep 的-v 选项表示反向过滤, 即不过滤带有 grep 字段相关的进程
# 结果如下
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
2806 4223 4223 2780 pts/2 4229 S 1000 0:00 sleep 100
2806 4224 4223 2780 pts/2 4229 S 1000 0:00 sleep 200
2806 4225 4223 2780 pts/2 4229 S 1000 0:00 sleep 300
从上述结果来看3个进程对应的PGID相同,即属于同一个进程组。
6.2.2 如何创建会话
可以调用 setsid
函数来创建一个会话,前提是调用进程不能是一个进程组的组长。
#include <unistd.h>
/*
*功能:创建会话
*返回值:创建成功返回 SID, 失败返回-1
*/
pid_t setsid(void);
该接口调用之后会发生:
- 调用进程会变成新会话的会话首进程,此时新会话中只有唯一的一个进程;
- 调用进程会变成进程组组长,新进程组 ID 就是当前调用进程 ID;
- 该进程没有控制终端,如果在调用
setsid
之前该进程存在控制终端,则调用之后会切断联系。
需要注意的是,如果调用进程原来是进程组组长,则会报错。为了避免这种情况,通常的解决方法是先调用 fork
创建子进程,父进程终止,子进程继续执行,因为子进程会继承父进程的进程组 ID,而进程 ID 则是新分配的,就不会出现错误的情况。
6.2.3 会话ID(SID)
会话 ID 可以理解为会话首进程的进程 ID,因为会话首进程是具有唯一进程 ID 的单个进程。注意:会话 ID 在有些地方也被称为会话首进程的进程组 ID,因为会话首进程总是一个进程组的组长进程,所以两者是等价的。
6.3 控制终端
-
控制终端是指**用户通过终端登录系统后得到的 Shell 进程所关联的终端。控制终端信息保存在 PCB 中,
fork
会复制 PCB 信息,因此由 Shell 启动的其他进程也会继承相同的控制终端。**默认情况下,进程的标准输入、标准输出和标准错误都指向控制终端。 -
关于会话、进程组和控制终端的关系:
- 一个会话可以有一个控制终端,通常由会话首进程打开终端后建立连接,该终端成为会话的控制终端;
- 建立连接的会话首进程被称为控制进程;
- 一个会话中的进程组可分为一个前台进程组和一个或多个后台进程组;
- 如果会话有控制终端,则前台进程组接收终端输入和信号(如
Ctrl+C
会发送中断信号给前台进程组),其他进程组为后台进程组; - 如果终端检测到断开(如网络断开),挂断信号会发送给控制进程(会话首进程)。
-
特性如图所示:
6.4 作业控制
6.4.1 作业和作业控制的概念
- 作业是用户为完成某项任务而启动的进程集合,可以包含单个或多个进程(如管道连接的进程)。
- 作业控制是Shell 通过作业控制管理前台和后台作业(或进程组),一个前台作业可由多个进程组成,后台作业同理。
- 例如,命令
cmd1 | cmd2
是一个由两个进程组成的前台作业(进程组),Shell 可同时运行一个前台作业和多个后台作业。
6.4.2 作业号
放在后台执行的程序或命令称为后台命令,可以在命令的后面加上 &
符号让 Shell 识别这是一个后台命令。后台命令无需等待执行完成即可立即接收新命令,执行完成后会返回作业号(Job ID)和进程号(PID)。
例如,以下命令在后台启动了一个作业,该作业由两个进程组成,均在后台运行:
[node@localhost code]$ cat /etc/filesystems | grep ext &
[1] 2202
ext4
ext3
ext2
# 按下回车
[1]+ 完成 cat /etc/filesystems | grep --
color=auto ext
- 第一行显示作业号(如
[1]
)和进程 ID(如2202
)。 - 第 2-4 行为程序运行结果(如过滤
/etc/filesystems
中ext
相关的内容)。 - 第 6 行标识作业号、默认作业标记、作业状态及执行的命令。
默认作业规则:
- 每个用户同一时间仅有一个默认作业(标记为
+
)和一个候选默认作业(标记为-
)。 - 当默认作业退出时,候选作业(
-
)会自动升级为默认作业。 - 无符号标记的作业为普通后台作业。
6.4.3 作业状态
6.4.4 作业的挂起与切回
-
作业挂起
我们在执行某个作业时,可以通过
Ctrl+Z
键将该作业挂起,该作业就会到后台执行,然后 Shell 会显示相关的作业号、状态以及所执行的命令信息。例如我们运行一个死循环的程序,通过
Ctrl+Z
将该作业挂起,观察一下对应的作业状态。#include <stdio.h> int main() {while (1){printf("hello\n");}return 0; }
# 运行可执行程序 [node@localhost code]$ ./test #键入 Ctrl + Z 观察现象
# 结果依次对应作业号 默认作业 作业状态 运行程序信息 [1]+ 已停止 ./test7
可以发现通过
Ctrl+Z
将该作业挂起,其作业状态已经变成了停止状态。 -
作业切回
如果想将挂起的作业切回,可以通过
fg
命令,fg
后面可以跟作业号或作业的命令名称。如果参数缺省则会默认将作业号为1
的作业切到前台来执行,若当前系统只有一个作业在后台进行,则可以直接使用fg
命令不带参数直接切回。具体的参数参考如下:例如我们把刚刚挂起的
./test
作业切回到前台:[node@localhost code]$ fg %%
运行结果为开始无限循环打印
hello
,可以发现该作业已经切换到前台了。注意:当通过fg
命令切回作业时,若没有指定作业参数,此时会将默认作业切到前台执行,即带有+
的作业号的作业。
6.4.5 查看后台执行或挂起的作业
我们可以直接通过输入 jobs
命令查看本用户当前后台执行或挂起的作业,参数 -l
则显示作业的详细信息,参数 -p
则只显示作业的 PID。
例如,我们先在后台及前台运行两个作业,并将前台作业挂起,然后用 jobs
命令查看作业相关的信息。
# 在后台运行一个作业 sleep
[node@localhost code]$ sleep 300 &
# 运行刚才的死循环可执行程序
[node@localhost code]$ ./test
# 键入 Ctrl + Z 挂起作业
# 使用 jobs 命令查看后台及挂起的作业
[node@localhost code]$ jobs -l
# 结果依次对应作业号 默认作业 作业状态 运行程序信息
[1]- 2265 运行中 sleep 300 &
[2]+ 2267 停止 ./test7
6.4.6 作业控制相关的信号
- 上面我们提到了键入
Ctrl + Z
可以将前台作业挂起,实际上是将SIGTSTP
信号发送至前台进程组作业中的所有进程,后台进程组中的作业不受影响。 - 在 Unix 系统中,存在 3 个特殊字符可以使得终端驱动程序产生信号,并将信号发送至前台进程组作业,它们分别是:
Ctrl + C
(中断字符,会产生 SIGINT 信号)、Ctrl + \
(退出字符,会产生 SIGQUIT 信号)和Ctrl + Z
(挂起字符,会产生SIGTSTP
信号)。 - 终端的 I/O(即标准输入和标准输出)和终端产生的信号总是从前台进程组作业连接实际终端。我们可以通过具体操作来观察作业控制的功能。
6.5 守护进程
-
daemon
daemon
是一个用于将普通程序转变为守护进程的系统调用(严格来说是glibc
提供的库函数,底层通过fork
+ 系统调用实现)。它的作用是使程序脱离终端在后台运行,通常用于服务器或长期运行的服务。- 函数原型(
<unistd.h>
)
int daemon(int nochdir, int noclose);
-
参数说明
nochdir
:0
: 将进程的工作目录改为根目录 (/
)非0
: 保持当前工作目录不变
noclose
:0
: 将标准输入、输出、错误重定向到/dev/null
非0
: 保持文件描述符不变
- 重定向到
/dev/null
:- 写入数据(如
printf
输出):数据会被直接丢弃,不占用任何存储或内存。 - 读取数据(如
scanf
输入):立即返回 EOF(文件结束符),表现为读取失败。
- 写入数据(如
-
返回值
- 成功时返回
0
- 失败返回
-1
并设置errno
- 成功时返回
-
底层行为
- 调用
fork()
创建子进程,父进程退出(使子进程成为孤儿进程,由init
接管) - 调用
setsid()
创建新会话,脱离终端控制 - 根据参数决定是否切换工作目录或重定向标准 I/O
- 调用
- 函数原型(
-
Daemon.hpp
#pragma once // 防止头文件被重复包含#include <iostream> // 标准输入输出库 #include <cstdlib> // 标准库函数(如exit) #include <signal.h> // 信号处理相关 #include <unistd.h> // POSIX操作系统API(如fork, setsid等) #include <fcntl.h> // 文件控制选项 #include <sys/types.h> // 系统数据类型定义 #include <sys/stat.h> // 文件状态信息// 定义常量路径 const char *root = "/"; // 根目录路径 const char *dev_null = "/dev/null"; // Linux的空设备文件路径/*** @brief 将当前进程转变为守护进程* @param ischdir 是否将工作目录切换到根目录* @param isclose 是否直接关闭标准输入输出错误流* * 守护进程是在后台运行的进程,不受终端控制。* 该函数实现了创建守护进程的标准步骤。*/ void Daemon(bool ischdir, bool isclose) {// 1. 忽略可能引起程序异常退出的信号signal(SIGCHLD, SIG_IGN); // 忽略子进程状态改变信号(防止僵尸进程)signal(SIGPIPE, SIG_IGN); // 忽略管道破裂信号(防止写入已关闭的管道导致程序退出)// 2. 创建子进程并终止父进程(让自己不要成为进程组组长)// fork()返回值:// >0: 父进程中返回子进程PID// =0: 子进程中返回0// <0: 出错if (fork() > 0) {exit(0); // 父进程退出,子进程继续执行}// 3. 设置新的会话(脱离终端控制)// setsid()创建一个新的会话,并使自己成为会话组长和进程组长// 返回值:成功时返回新会话ID,失败返回-1setsid();// 4. 可选:更改当前工作目录到根目录(防止占用可卸载的文件系统)// 每个进程都有自己的当前工作目录(CWD)if (ischdir) {chdir(root); // 将工作目录切换到根目录}// 5. 处理标准输入输出错误流if (isclose) {// 直接关闭标准文件描述符close(0); // 关闭标准输入(stdin)close(1); // 关闭标准输出(stdout)close(2); // 关闭标准错误(stderr)} else {// 更安全的做法:将标准输入输出重定向到/dev/null// 以读写方式打开/dev/null设备文件int fd = open(dev_null, O_RDWR);if (fd > 0) {// 将标准输入(0)、输出(1)、错误(2)重定向到/dev/nulldup2(fd, 0); // 复制文件描述符到stdindup2(fd, 1); // 复制文件描述符到stdoutdup2(fd, 2); // 复制文件描述符到stderr// 关闭原始的文件描述符(因为dup2已经复制了)close(fd);}// 如果打开失败,保持原有文件描述符不变} }
-
如何将服务守护进程化
/*** @file server.cpp* @brief TCP服务器主程序入口*/#include <iostream> #include <memory> #include <string>// 程序使用说明提示信息 // 用法: ./server port int main(int argc, char *argv[]) {// 检查命令行参数数量是否正确// argc == 2 表示程序名 + 端口号两个参数if (argc != 2){// 打印正确的使用方式// argv[0] 是程序名std::cout << "Usage : " << argv[0] << " port" << std::endl;return 0; // 参数错误,退出程序}// 将字符串形式的端口号转换为16位无符号整数// stoi() 可能抛出异常,但这里没有捕获处理uint16_t localport = std::stoi(argv[1]);// 设置守护进程// 第一个false表示不改变工作目录到根目录// 第二个false表示不关闭标准输入输出和错误Daemon(false, false);//// 创建TCP服务器实例// 使用unique_ptr智能指针管理TcpServer对象// 参数1: localport - 服务器监听的端口号// 参数2: HandlerRequest - 请求处理函数指针std::unique_ptr<TcpServer> svr(new TcpServer(localport, HandlerRequest));// 启动服务器事件循环// Loop() 方法会一直运行,直到服务器关闭svr->Loop();// 程序正常退出return 0; }
7. 完整版网络计算器服务的代码实现
7.1 Common.hpp
// 防止头文件被重复包含的预处理指令
#pragma once// 包含必要的系统头文件
#include <iostream> // 标准输入输出流
#include <functional> // 函数对象相关
#include <unistd.h> // POSIX系统调用接口
#include <string> // 字符串类
#include <cstring> // C风格字符串操作
#include <sys/socket.h> // 套接字相关定义
#include <sys/types.h> // 系统数据类型定义
#include <arpa/inet.h> // 网络地址转换
#include <netinet/in.h> // 互联网地址族定义// 定义程序退出状态码枚举
enum ExitCode
{OK = 0, // 正常退出USAGE_ERR, // 使用方式错误SOCKET_ERR, // 套接字创建失败BIND_ERR, // 绑定失败LISTEN_ERR, // 监听失败CONNECT_ERR, // 连接失败FORK_ERR, // 进程创建失败OPEN_ERR // 文件打开失败
};// 禁止拷贝的基类
class NoCopy
{
public:// 默认构造函数NoCopy() {}// 默认析构函数~NoCopy() {}// 删除拷贝构造函数(禁止拷贝构造)NoCopy(const NoCopy &) = delete;// 删除拷贝赋值运算符(禁止拷贝赋值)const NoCopy &operator=(const NoCopy &) = delete;
};// 宏定义:将任意地址结构体转换为通用套接字地址结构体指针
#define CONV(addr) ((struct sockaddr*)&addr)
7.2 Daemon.hpp
#pragma once // 防止头文件被重复包含#include <iostream> // 标准输入输出流
#include <string> // 字符串操作
#include <cstdio> // C标准IO
#include <sys/types.h> // 系统类型定义
#include <unistd.h> // POSIX系统调用
#include <signal.h> // 信号处理
#include <sys/stat.h> // 文件状态
#include <fcntl.h> // 文件控制
#include "Log.hpp" // 日志模块
#include "Common.hpp" // 通用定义using namespace LogModule; // 使用日志模块的命名空间const std::string dev = "/dev/null"; // 定义空设备文件路径/*** @brief 将当前进程转换为守护进程* * @param nochdir 如果为0,将工作目录更改为根目录* @param noclose 如果为0,将标准输入/输出/错误重定向到/dev/null* * 守护进程特点:* 1. 在后台运行* 2. 脱离终端控制* 3. 不受用户登录/注销影响*/
void Daemon(int nochdir, int noclose)
{// 1. 忽略信号处理// SIGPIPE: 防止写入已关闭的管道导致进程终止;当进程向一个已经关闭的管道或套接字写入数据时,内核会发送这个信号。默认行为是终止进程。对于守护进程来说,通常希望自行处理这种错误而不是直接退出。// SIGCHLD: 防止子进程退出时产生僵尸进程signal(SIGPIPE, SIG_IGN); // 忽略管道破裂信号signal(SIGCHLD, SIG_IGN); // 忽略子进程退出信号// 2. 创建子进程并终止父进程// 这一步使子进程成为孤儿进程,由init进程接管if (fork() > 0) // 父进程返回子进程PID(>0)exit(0); // 父进程直接退出// 3. 创建新会话并成为会话组长// 这一步使进程完全脱离终端控制setsid(); // 创建一个新的会话并设置进程组ID// 可选步骤:更改工作目录到根目录// 防止守护进程的工作目录被卸载导致问题if(nochdir == 0) chdir("/"); // 将工作目录更改为根目录// 4. 处理标准输入/输出/错误// 守护进程通常不需要这些文件描述符if (noclose == 0){// 打开空设备文件int fd = ::open(dev.c_str(), O_RDWR); // 以读写方式打开/dev/nullif (fd < 0) // 打开失败处理{LOG(LogLevel::FATAL) << "open " << dev << " errno"; // 记录致命错误exit(OPEN_ERR); // 退出并返回打开错误码}else{// 将标准输入/输出/错误重定向到/dev/nulldup2(fd, 0); // 标准输入dup2(fd, 1); // 标准输出dup2(fd, 2); // 标准错误close(fd); // 关闭原始文件描述符}}
}
7.3 InetAddr.hpp
#pragma once
#include "Common.hpp"/*** @class InetAddr* @brief 网络地址转换类,用于处理IPv4地址在主机和网络字节序之间的转换* * 封装了sockaddr_in结构体,提供IP地址和端口的设置、获取和转换功能*///在 sockaddr_in 结构体中,sin_ 前缀是 "socket internet" 的缩写,用于表示这是与网络套接字(Internet Socket)相关的成员变量。
//在 struct in_addr 中,s_addr 的 s_ 前缀是 "socket" 的缩写,用于表示这是与套接字(socket)相关的成员变量。
class InetAddr
{
public:/*** @brief 默认构造函数*/InetAddr() {}/*** @brief 从sockaddr_in结构体构造InetAddr对象* @param addr 已初始化的sockaddr_in结构体*/InetAddr(struct sockaddr_in &addr){SetAddr(addr);}/*** @brief 通过IP字符串和端口号构造InetAddr对象* @param ip 点分十进制格式的IP地址字符串(如"192.168.1.1")* @param port 主机字节序的端口号*/InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port){// 初始化_addr结构体memset(&_addr, 0, sizeof(_addr));// 设置地址族为IPv4_addr.sin_family = AF_INET;// 将点分十进制IP转换为网络字节序的二进制形式inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);// 将端口号从主机字节序转换为网络字节序_addr.sin_port = htons(_port);}/*** @brief 仅通过端口号构造InetAddr对象(自动绑定到所有网络接口)* @param port 主机字节序的端口号*/InetAddr(uint16_t port) : _port(port), _ip(){// 初始化_addr结构体memset(&_addr, 0, sizeof(_addr));// 设置地址族为IPv4_addr.sin_family = AF_INET;// 绑定到所有可用的网络接口(INADDR_ANY)_addr.sin_addr.s_addr = INADDR_ANY;// 将端口号从主机字节序转换为网络字节序_addr.sin_port = htons(_port);}/*** @brief 设置网络地址* @param addr sockaddr_in结构体引用*/void SetAddr(struct sockaddr_in &addr){_addr = addr;// 将网络字节序的端口号转换为主机字节序_port = ntohs(_addr.sin_port);// 将网络字节序的IP地址转换为点分十进制字符串char ipbuffer[64];inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));_ip = ipbuffer;}/*** @brief 获取端口号(主机字节序)* @return uint16_t 端口号*/uint16_t Port() { return _port; }/*** @brief 获取IP地址字符串* @return std::string 点分十进制格式的IP地址*/std::string Ip() { return _ip; }/*** @brief 获取网络地址结构体引用* @return const struct sockaddr_in& */const struct sockaddr_in &NetAddr() { return _addr; }/*** @brief 获取指向网络地址结构体的指针(用于系统调用)* @return const struct sockaddr* *///einterpret_cast 是 C++ 中最强大但也最危险的类型转换操作符,它提供了低级别的重新解释位模式的转换能力。//在 NetAddrPtr() 函数中,它被用来将 sockaddr_in* 转换为 sockaddr*。const struct sockaddr *NetAddrPtr(){return reinterpret_cast<const struct sockaddr*>(&_addr);}/*** @brief 获取网络地址结构体的大小* @return socklen_t 结构体大小*/socklen_t NetAddrLen(){return sizeof(_addr);}/*** @brief 重载==运算符,比较两个InetAddr对象是否相等* @param addr 要比较的InetAddr对象* @return bool 是否相等*/bool operator==(const InetAddr &addr){return addr._ip == _ip && addr._port == _port;}/*** @brief 获取地址的字符串表示形式(IP:PORT)* @return std::string 格式如"192.168.1.1:8080"*/std::string StringAddr(){return _ip + ":" + std::to_string(_port);}/*** @brief 析构函数*/~InetAddr() {}private:struct sockaddr_in _addr; ///< 网络地址结构体std::string _ip; ///< 点分十进制格式的IP地址字符串uint16_t _port; ///< 主机字节序的端口号
};
7.4 Log.hpp
#ifndef __LOG_HPP__ // 防止头文件重复包含
#define __LOG_HPP__// 包含必要的标准库头文件
#include <iostream> // 标准输入输出
#include <cstdio> // C风格输入输出
#include <string> // 字符串处理
#include <filesystem> // 文件系统操作(C++17)
#include <sstream> // 字符串流
#include <fstream> // 文件流
#include <memory> // 智能指针
#include <ctime> // 时间处理
#include <unistd.h> // POSIX操作系统API(获取进程ID)
#include "Mutex.hpp" // 自定义互斥锁头文件namespace LogModule // 日志模块命名空间
{using namespace MutexModule; // 使用互斥锁模块的命名空间const std::string gsep = "\r\n"; // 全局日志分隔符(回车换行)// 策略模式基类:定义日志刷新策略接口class LogStrategy{public:virtual ~LogStrategy() = default; // 虚析构函数virtual void SyncLog(const std::string &message) = 0; // 纯虚函数,同步日志};// 控制台日志策略:将日志输出到控制台class ConsoleLogStrategy : public LogStrategy{public:ConsoleLogStrategy() {} // 默认构造函数void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex); // 加锁保证线程安全std::cout << message << gsep; // 输出日志到标准输出}~ConsoleLogStrategy() {} // 析构函数private:Mutex _mutex; // 互斥锁,保证线程安全};// 文件日志策略:将日志写入文件const std::string defaultpath = "/var/log/"; // 默认日志路径const std::string defaultfile = "my.log"; // 默认日志文件名class FileLogStrategy : public LogStrategy{public:// 构造函数,可指定路径和文件名FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile): _path(path), _file(file){LockGuard lockguard(_mutex); // 加锁保证线程安全// 检查日志目录是否存在,不存在则创建if (std::filesystem::exists(_path)) return;try {std::filesystem::create_directories(_path); // 递归创建目录} catch (const std::filesystem::filesystem_error &e) {std::cerr << e.what() << '\n'; // 捕获并打印异常}}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex); // 加锁保证线程安全// 构建完整文件路径std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;// 以追加模式打开文件std::ofstream out(filename, std::ios::app);if (!out.is_open()) return; // 文件打开失败则返回out << message << gsep; // 写入日志out.close(); // 关闭文件}~FileLogStrategy() {} // 析构函数private:std::string _path; // 日志文件路径std::string _file; // 日志文件名Mutex _mutex; // 互斥锁,保证线程安全};// 日志等级枚举enum class LogLevel{DEBUG, // 调试信息INFO, // 普通信息WARNING, // 警告信息ERROR, // 错误信息FATAL // 致命错误};// 将日志等级转换为字符串std::string Level2Str(LogLevel level){switch (level){case LogLevel::DEBUG: return "DEBUG";case LogLevel::INFO: return "INFO";case LogLevel::WARNING: return "WARNING";case LogLevel::ERROR: return "ERROR";case LogLevel::FATAL: return "FATAL";default: return "UNKNOWN";}}// 获取当前时间戳std::string GetTimeStamp(){time_t curr = time(nullptr); // 获取当前时间struct tm curr_tm; // 时间结构体localtime_r(&curr, &curr_tm); // 转换为本地时间(线程安全版本)char timebuffer[128]; // 时间格式化缓冲区snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d", // 格式化字符串: YYYY-MM-DD HH:MM:SScurr_tm.tm_year + 1900, // 年份(从1900开始)curr_tm.tm_mon + 1, // 月份(0-11)curr_tm.tm_mday, // 日curr_tm.tm_hour, // 时curr_tm.tm_min, // 分curr_tm.tm_sec // 秒);return timebuffer;}// 日志器类:负责生成和刷新日志class Logger{public:Logger(){EnableConsoleLogStrategy(); // 默认使用控制台日志策略}// 启用文件日志策略void EnableFileLogStrategy(){_fflush_strategy = std::make_unique<FileLogStrategy>();}// 启用控制台日志策略void EnableConsoleLogStrategy(){_fflush_strategy = std::make_unique<ConsoleLogStrategy>();}// 日志消息类:表示一条具体的日志class LogMessage{public:// 构造函数,初始化日志基本信息LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger): _curr_time(GetTimeStamp()), // 获取当前时间_level(level), // 日志等级_pid(getpid()), // 进程ID_src_name(src_name), // 源文件名_line_number(line_number), // 行号_logger(logger) // 所属日志器{// 构建日志头部信息std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << Level2Str(_level) << "] "<< "[" << _pid << "] "<< "[" << _src_name << "] "<< "[" << _line_number << "] "<< "- ";_loginfo = ss.str(); // 保存日志头部}// 重载<<运算符,支持链式输出日志内容template <typename T>LogMessage &operator<<(const T &info){std::stringstream ss;ss << info; // 将各种类型转换为字符串_loginfo += ss.str(); // 追加到日志信息中return *this; // 返回自身以支持链式调用}// 析构函数:在对象销毁时刷新日志~LogMessage(){if (_logger._fflush_strategy){_logger._fflush_strategy->SyncLog(_loginfo); // 调用策略刷新日志}}private:std::string _curr_time; // 日志时间LogLevel _level; // 日志等级pid_t _pid; // 进程IDstd::string _src_name; // 源文件名int _line_number; // 行号std::string _loginfo; // 完整的日志信息Logger &_logger; // 所属日志器引用};// 重载()运算符,创建日志消息对象LogMessage operator()(LogLevel level, std::string name, int line){return LogMessage(level, name, line, *this); // 返回临时对象}~Logger() {} // 析构函数private:std::unique_ptr<LogStrategy> _fflush_strategy; // 日志刷新策略(智能指针)};Logger logger; // 全局日志对象// 定义宏简化日志调用#define LOG(level) logger(level, __FILE__, __LINE__) // 自动获取文件名和行号#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy() // 启用控制台日志#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy() // 启用文件日志
}#endif // __LOG_HPP__
7.5 Makefile
# .PHONY 声明伪目标,表示这些目标不是实际的文件名
# 伪目标总是会被执行,即使有同名文件存在
.PHONY: all# all 是默认目标,当直接运行 make 时会执行这个目标
# 它依赖于 ServerNetCald 和 client_netcal 两个目标
all: ServerNetCald client_netcal # 定义 ServerNetCald 目标的构建规则
# 它依赖于 main.cc 文件
ServerNetCald: main.cc# 使用 g++ 编译 main.cc 生成 ServerNetCald 可执行文件# $@ 表示目标文件名(ServerNetCald)# $^ 表示所有依赖文件(main.cc)# -std=c++17 指定使用 C++17 标准# -ljsoncpp 链接 jsoncpp 库# -static 指定静态链接# -lpthread 链接 pthread 线程库g++ -o $@ $^ -std=c++17 -ljsoncpp -static -lpthread# 定义 client_netcal 目标的构建规则
# 它依赖于 TcpClient.cc 文件
client_netcal: TcpClient.cc# 使用 g++ 编译 TcpClient.cc 生成 client_netcal 可执行文件# 参数含义同上g++ -o $@ $^ -std=c++17 -ljsoncpp -static -lpthread# output 伪目标,用于打包输出文件
.PHONY: output
output:# 创建 output 目录@mkdir output# 递归创建 output/bin 目录(-p 参数确保父目录不存在时也能创建)@mkdir -p output/bin# 创建 output/conf 目录@mkdir -p output/conf# 创建 output/log 目录@mkdir -p output/log# 将 ServerNetCald 可执行文件复制到 output/bin@cp ServerNetCald output/bin# 将 client_netcal 可执行文件复制到 output/bin@cp client_netcal output/bin# 将 test.conf 配置文件复制到 output/conf@cp test.conf output/conf# 将 install.sh 安装脚本复制到 output/@cp install.sh output/# 将 uninstall.sh 卸载脚本复制到 output/@cp uninstall.sh output/# 将整个 output 目录打包压缩为 output.tgz 文件@tar czf output.tgz output# clean 伪目标,用于清理生成的文件
.PHONY: clean
clean:# 删除 ServerNetCald 和 client_netcal 可执行文件# 删除 output 目录和 output.tgz 压缩包rm -rf ServerNetCald client_netcal output output.tgz
7.6 Mutex.hpp
#pragma once // 防止头文件被重复包含#include <iostream> // 标准输入输出库(虽然本例未使用,但通常用于调试)
#include <pthread.h> // POSIX线程库,提供互斥锁等相关操作// 定义一个命名空间 MutexModule,封装与互斥锁相关的功能
namespace MutexModule
{// Mutex 类封装 POSIX 互斥锁的基本操作class Mutex{public:// 构造函数:初始化互斥锁Mutex(){// pthread_mutex_init 初始化互斥锁// 参数1:指向互斥锁的指针// 参数2:互斥锁属性(nullptr 表示使用默认属性)pthread_mutex_init(&_mutex, nullptr);}// 加锁操作void Lock(){// pthread_mutex_lock 尝试获取互斥锁// 如果锁已被其他线程持有,则当前线程会阻塞int n = pthread_mutex_lock(&_mutex);(void)n; // 显式忽略返回值(实际工程中应检查错误)}// 解锁操作void Unlock(){// pthread_mutex_unlock 释放互斥锁int n = pthread_mutex_unlock(&_mutex);(void)n; // 显式忽略返回值}// 析构函数:销毁互斥锁~Mutex(){// pthread_mutex_destroy 销毁互斥锁// 注意:必须在互斥锁未被锁定的状态下销毁pthread_mutex_destroy(&_mutex);}// 获取底层 pthread_mutex_t 对象的指针// 用于需要直接操作原生锁的场景(如条件变量)pthread_mutex_t *Get(){return &_mutex;}private:pthread_mutex_t _mutex; // 底层的 POSIX 互斥锁对象};// LockGuard 类:RAII(资源获取即初始化)风格的锁守卫// 构造时自动加锁,析构时自动解锁,避免忘记解锁class LockGuard{public:// 构造函数:接收一个 Mutex 引用并立即加锁LockGuard(Mutex &mutex) : _mutex(mutex){_mutex.Lock(); // 在对象构造时自动加锁}// 析构函数:自动解锁~LockGuard(){_mutex.Unlock(); // 在对象销毁时自动解锁}private:Mutex &_mutex; // 引用形式的 Mutex 对象(不负责生命周期管理)// 禁止拷贝构造和赋值操作LockGuard(const LockGuard&) = delete;LockGuard& operator=(const LockGuard&) = delete;};
}
7.7 NetCal.hpp
#pragma once // 防止头文件被重复包含#include "Protocol.hpp" // 包含自定义协议头文件,定义了Request和Response类
#include <iostream> // 标准输入输出库(虽然此处未直接使用,但可能用于调试)// 计算器类,封装了基本的算术运算逻辑
class Cal
{
public:/*** @brief 执行计算请求* @param req 包含计算请求的参数(操作数和运算符)* @return 返回计算结果和状态码的Response对象*/Response Execute(Request &req) {// 初始化响应对象,默认code=0表示成功,result=0Response resp(0, 0); // 根据运算符执行相应计算switch (req.Oper()) {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:除零错误}else {resp.SetResult(req.X() / req.Y());}}break;case '%': // 取模运算{if (req.Y() == 0) {resp.SetCode(2); // 错误码2:模零错误}else {resp.SetResult(req.X() % req.Y());}}break;default: // 非法运算符resp.SetCode(3); // 错误码3:非法操作break;}return resp; // 返回计算结果}
};
7.8 Protocol.hpp
#pragma once
#include "Socket.hpp" // 自定义Socket模块
#include <iostream>
#include <string>
#include <memory>
#include <jsoncpp/json/json.h> // JSON序列化/反序列化库
#include <functional>// 实现一个基于网络的自定义计算器服务using namespace SocketModule; // 使用Socket模块的命名空间// 协议设计说明:
// client -> server 通信格式:
// 格式: content_len\r\njsonstring\r\n
// 示例: 50\r\n{"x": 10, "y" : 20, "oper" : '+'}\r\n
// 其中:
// - 50 表示后续JSON字符串的长度
// - \r\n 是分隔符
// - {"x": 10, "y" : 20, "oper" : '+'} 是实际的JSON数据// 请求类:封装客户端发送给服务器的计算请求
class Request
{
public:Request() {} // 默认构造函数// 带参构造函数Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper) {}// 序列化方法:将请求对象转换为JSON字符串std::string Serialize(){Json::Value root; // 创建JSON根节点root["x"] = _x; // 添加x字段root["y"] = _y; // 添加y字段root["oper"] = _oper; // 添加操作符字段Json::FastWriter writer; // 使用快速写入器std::string s = writer.write(root); // 生成JSON字符串return s;}// 反序列化方法:从JSON字符串解析为请求对象bool Deserialize(std::string &in){Json::Value root; // 创建JSON根节点Json::Reader reader; // 创建JSON解析器bool ok = reader.parse(in, root); // 尝试解析JSONif (ok) // 如果解析成功{_x = root["x"].asInt(); // 提取x值_y = root["y"].asInt(); // 提取y值_oper = root["oper"].asInt(); // 提取操作符(作为ASCII码值)}return ok; // 返回解析结果}~Request() {} // 析构函数// 以下为访问私有成员的公共方法int X() { return _x; }int Y() { return _y; }char Oper() { return _oper; }private:int _x; // 第一个操作数int _y; // 第二个操作数char _oper; // 操作符: +, -, *, /, %
};// 响应类:封装服务器返回给客户端的计算结果
class Response
{
public:Response() {} // 默认构造函数// 带参构造函数Response(int result, int code) : _result(result), _code(code) {}// 序列化方法:将响应对象转换为JSON字符串std::string Serialize(){Json::Value root; // 创建JSON根节点root["result"] = _result; // 添加结果字段root["code"] = _code; // 添加状态码字段Json::FastWriter writer; // 使用快速写入器return writer.write(root); // 生成JSON字符串}// 反序列化方法:从JSON字符串解析为响应对象bool Deserialize(std::string &in){Json::Value root; // 创建JSON根节点Json::Reader reader; // 创建JSON解析器bool ok = reader.parse(in, root); // 尝试解析JSONif (ok) // 如果解析成功{_result = root["result"].asInt(); // 提取结果值_code = root["code"].asInt(); // 提取状态码}return ok; // 返回解析结果}~Response() {} // 析构函数// 设置结果值void SetResult(int res) { _result = res; }// 设置状态码void SetCode(int code) { _code = code; }// 显示计算结果void ShowResult(){std::cout << "计算结果是: " << _result << "[" << _code << "]" << std::endl;}private:int _result; // 计算结果int _code; // 状态码: 0=成功, 1-4=不同的错误情况
};const std::string sep = "\r\n"; // 协议分隔符// 定义函数类型别名,用于处理请求的业务逻辑(接受Request&参数,返回Response)
using func_t = std::function<Response(Request &req)>;// 协议类:处理网络通信的编码/解码和请求/响应处理
class Protocol
{
public:Protocol() {} // 默认构造函数// 带参构造函数,接收业务处理函数Protocol(func_t func) : _func(func) {}// 编码方法:为JSON字符串添加长度报头std::string Encode(const std::string &jsonstr){std::string len = std::to_string(jsonstr.size()); // 计算长度return len + sep + jsonstr + sep; // 组装成完整协议格式}// 解码方法:从缓冲区提取完整报文bool Decode(std::string &buffer, std::string *package){// 查找第一个分隔符位置ssize_t pos = buffer.find(sep);if (pos == std::string::npos)return false; // 没有找到完整报头// 提取长度字符串std::string package_len_str = buffer.substr(0, pos);int package_len_int = std::stoi(package_len_str); // 转换为整数// 计算完整报文的总长度int target_len = package_len_str.size() + package_len_int + 2 * sep.size();// 检查缓冲区是否有足够数据if (buffer.size() < target_len)return false; // 数据不完整// 提取完整JSON报文*package = buffer.substr(pos + sep.size(), package_len_int);// 从缓冲区移除已处理的数据buffer.erase(0, target_len);return true;}// 服务器端:获取并处理客户端请求void GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client){std::string buffer_queue; // 接收缓冲区while (true){int n = sock->Recv(&buffer_queue); // 接收数据if (n > 0) // 成功接收数据{std::cout << "-----------request_buffer--------------" << std::endl;std::cout << buffer_queue << std::endl;std::cout << "------------------------------------" << std::endl;std::string json_package;// 循环处理缓冲区中的所有完整请求while (Decode(buffer_queue, &json_package)){// 记录请求日志LOG(LogLevel::DEBUG) << client.StringAddr() << " 请求: " << json_package;// 反序列化请求Request req;bool ok = req.Deserialize(json_package);if (!ok)continue; // 反序列化失败则跳过// 调用业务处理函数获取响应Response resp = _func(req);// 序列化响应std::string json_str = resp.Serialize();// 添加协议报头std::string send_str = Encode(json_str);// 发送响应给客户端sock->Send(send_str);}}else if (n == 0) // 客户端断开连接{LOG(LogLevel::INFO) << "client:" << client.StringAddr() << "Quit!";break;}else // 接收错误{LOG(LogLevel::WARNING) << "client:" << client.StringAddr() << ", recv error";break;}}}// 客户端:获取服务器响应bool GetResponse(std::shared_ptr<Socket> &client, std::string &resp_buff, Response *resp){while (true){int n = client->Recv(&resp_buff); // 接收数据if (n > 0) // 成功接收数据{std::string json_package;// 循环处理缓冲区中的所有完整响应while (Decode(resp_buff, &json_package)){// 反序列化响应resp->Deserialize(json_package);}return true; // 成功获取响应}else if (n == 0) // 服务器断开连接{std::cout << "server quit " << std::endl;return false;}else // 接收错误{std::cout << "recv error" << std::endl;return false;}}}// 构建请求字符串(客户端使用)std::string BuildRequestString(int x, int y, char oper){// 1. 构建请求对象Request req(x, y, oper);// 2. 序列化为JSONstd::string json_req = req.Serialize();// 3. 添加协议报头return Encode(json_req);}~Protocol() {} // 析构函数private:func_t _func; // 业务处理函数
};
7.9Socket.hpp
#pragma once // 防止头文件被重复包含// 包含必要的系统头文件
#include <iostream>
#include <string>
#include <unistd.h> // POSIX 操作系统 API
#include <sys/socket.h> // 套接字相关函数
#include <sys/types.h> // 系统数据类型
#include <netinet/in.h> // 互联网地址族
#include <arpa/inet.h> // 互联网操作函数
#include <cstdlib> // 标准库函数
#include "Common.hpp" // 自定义公共头文件
#include "Log.hpp" // 日志模块
#include "InetAddr.hpp" // 网络地址处理模块namespace SocketModule
{using namespace LogModule; // 使用日志模块的命名空间const static int gbacklog = 16; // 默认的监听队列长度// Socket 抽象基类,采用模板方法模式设计// 定义了套接字编程的基本接口,大部分为纯虚函数class Socket {public:virtual ~Socket() {} // 虚析构函数// 以下是必须由子类实现的纯虚函数virtual void SocketOrDie() = 0; // 创建套接字,失败则退出程序virtual void BindOrDie(uint16_t port) = 0; // 绑定端口,失败则退出virtual void ListenOrDie(int backlog) = 0; // 开始监听,失败则退出virtual std::shared_ptr<Socket> Accept(InetAddr* client) = 0; // 接受连接virtual void Close() = 0; // 关闭套接字virtual int Recv(std::string* out) = 0; // 接收数据virtual int Send(const std::string& message) = 0; // 发送数据virtual int Connect(const std::string& server_ip, uint16_t port) = 0; // 连接服务器public:// 构建TCP服务端套接字的模板方法// 按顺序执行: 创建套接字 -> 绑定端口 -> 开始监听void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog) {SocketOrDie();BindOrDie(port);ListenOrDie(backlog);}// 构建TCP客户端套接字的模板方法// 只需创建套接字void BuildTcpClientSocketMethod() {SocketOrDie();}// 注释掉的UDP套接字构建方法/*void BuildUdpSocketMethod() {SocketOrDie();BindOrDie();}*/};const static int defaultfd = -1; // 默认的文件描述符无效值// TCP套接字实现类,继承自Socket基类class TcpSocket : public Socket {public:// 默认构造函数,初始化套接字描述符为无效值TcpSocket() : _sockfd(defaultfd) {}// 带参构造函数,直接使用已有的文件描述符TcpSocket(int fd) : _sockfd(fd) {}~TcpSocket() {} // 析构函数// 创建TCP套接字,失败则记录日志并退出程序void SocketOrDie() override {// 调用系统socket函数创建流式套接字(AF_INET: IPv4, SOCK_STREAM: TCP)_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0) {LOG(LogLevel::FATAL) << "socket error"; // 记录致命错误exit(SOCKET_ERR); // 退出程序并返回套接字错误码}LOG(LogLevel::INFO) << "socket success"; // 记录成功日志}// 绑定套接字到指定端口,失败则记录日志并退出程序void BindOrDie(uint16_t port) override {InetAddr localaddr(port); // 创建本地地址对象// 调用系统bind函数绑定套接字int n = ::bind(_sockfd, localaddr.NetAddrPtr(), localaddr.NetAddrLen());if (n < 0) {LOG(LogLevel::FATAL) << "bind error";exit(BIND_ERR); // 退出程序并返回绑定错误码}LOG(LogLevel::INFO) << "bind success";}// 开始监听连接请求,失败则记录日志并退出程序void ListenOrDie(int backlog) override {// 调用系统listen函数开始监听int n = ::listen(_sockfd, backlog);if (n < 0) {LOG(LogLevel::FATAL) << "listen error";exit(LISTEN_ERR); // 退出程序并返回监听错误码}LOG(LogLevel::INFO) << "listen success";}// 接受客户端连接请求std::shared_ptr<Socket> Accept(InetAddr* client) override {struct sockaddr_in peer; // 存储客户端地址信息socklen_t len = sizeof(peer);// 调用系统accept函数接受连接int fd = ::accept(_sockfd, CONV(peer), &len);if (fd < 0) {LOG(LogLevel::WARNING) << "accept warning ..."; // 记录警告日志return nullptr; // 返回空指针,TODO: 可能需要更完善的错误处理}client->SetAddr(peer); // 设置客户端地址信息return std::make_shared<TcpSocket>(fd); // 返回新创建的TcpSocket对象}// 接收数据int Recv(std::string* out) override {char buffer[1024]; // 接收缓冲区// 调用系统recv函数接收数据ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);if (n > 0) {buffer[n] = 0; // 确保字符串以null结尾*out += buffer; // 将接收到的数据追加到输出字符串中}return n; // 返回实际接收的字节数}// 发送数据int Send(const std::string& message) override {// 调用系统send函数发送数据return send(_sockfd, message.c_str(), message.size(), 0);}// 关闭套接字void Close() override {if (_sockfd >= 0)::close(_sockfd); // 调用系统close函数关闭套接字}// 连接到服务器int Connect(const std::string& server_ip, uint16_t port) override {InetAddr server(server_ip, port); // 创建服务器地址对象// 调用系统connect函数连接服务器return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen());}private:int _sockfd; // 套接字文件描述符// 命名说明:// _sockfd - 通用套接字描述符// listensockfd - 监听套接字描述符// sockfd - 连接套接字描述符};// 注释掉的UDP套接字实现类/*class UdpSocket : public Socket {};*/
}
7.10 TcpClient.cc
/*** @file tcpclient.cpp* @brief TCP客户端实现,用于与服务器通信并执行计算请求*/#include "Socket.hpp" // 套接字相关头文件
#include "Common.hpp" // 通用定义和常量
#include "Protocol.hpp" // 协议处理相关头文件
#include <iostream> // 标准输入输出
#include <string> // 字符串处理
#include <memory> // 智能指针using namespace SocketModule; // 使用SocketModule命名空间/*** @brief 打印程序使用方法* @param proc 程序名称*/
void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " server_ip server_port" << std::endl;
}/*** @brief 从标准输入获取计算数据* @param x 指向第一个操作数的指针* @param y 指向第二个操作数的指针* @param oper 指向操作符的指针*/
void GetDataFromStdin(int *x, int *y, char *oper)
{std::cout << "Please Enter x: ";std::cin >> *x;std::cout << "Please Enter y: ";std::cin >> *y;std::cout << "Please Enter oper: ";std::cin >> *oper;
}/*** @brief 主函数* @param argc 参数个数* @param argv 参数数组* @return 程序执行状态* * 程序启动方式: ./tcpclient server_ip server_port*/
int main(int argc, char *argv[])
{// 1. 参数校验if (argc != 3){Usage(argv[0]);exit(USAGE_ERR); // 参数错误退出}// 2. 解析服务器地址和端口std::string server_ip = argv[1]; // 服务器IP地址uint16_t server_port = std::stoi(argv[2]); // 服务器端口号// 3. 创建并初始化TCP客户端套接字std::shared_ptr<Socket> client = std::make_shared<TcpSocket>();client->BuildTcpClientSocketMethod(); // 构建TCP客户端套接字// 4. 连接服务器if (client->Connect(server_ip, server_port) != 0){// 连接失败处理std::cerr << "connect error" << std::endl;exit(CONNECT_ERR); // 连接错误退出}// 5. 创建协议处理器std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>();std::string resp_buffer; // 响应缓冲区// 6. 主循环:与服务器交互while (true){// 6.1 从用户输入获取计算数据int x, y;char oper;GetDataFromStdin(&x, &y, &oper);// 6.2 构建请求字符串std::string req_str = protocol->BuildRequestString(x, y, oper);// 调试输出(注释状态)// std::cout << "-----------encode req string-------------" << std::endl;// std::cout << req_str << std::endl;// std::cout << "------------------------------------------" << std::endl;// 6.3 发送请求到服务器client->Send(req_str);// 6.4 获取服务器响应Response resp;bool res = protocol->GetResponse(client, resp_buffer, &resp);if(res == false)break; // 获取响应失败则退出循环// 6.5 显示计算结果resp.ShowResult();}// 7. 关闭客户端连接client->Close();return 0;
}
7.11 TcpServer.hpp
#include "Socket.hpp" // 自定义Socket类头文件
#include <iostream> // 标准输入输出流
#include <memory> // 智能指针相关
#include <sys/wait.h> // 进程等待相关函数
#include <functional> // 函数对象相关using namespace SocketModule; // Socket模块命名空间
using namespace LogModule; // 日志模块命名空间// 定义IO服务类型:接收一个共享指针的Socket对象和客户端地址
using ioservice_t = std::function<void(std::shared_ptr<Socket>& sock, InetAddr& client)>;// TcpServer类:主要解决连接和IO通信问题
// 设计理念:TcpServer不关心具体传输的信息内容,只负责网络通信
// 示例用途:可作为网络版计算器等长连接服务的基类
class TcpServer
{
public:// 构造函数:初始化端口号、创建监听socket、设置运行状态和IO服务// 参数:// port - 服务器监听端口号// service - IO服务处理函数TcpServer(uint16_t port, ioservice_t service) : _port(port), // 初始化端口_listensockptr(std::make_unique<TcpSocket>()), // 创建TcpSocket智能指针_isrunning(false), // 初始状态为未运行_service(service) // 设置IO服务处理函数{_listensockptr->BuildTcpSocketMethod(_port); // 构建TCP监听socket}// 启动服务器void Start(){_isrunning = true; // 设置运行标志// 主服务循环while (_isrunning){InetAddr client; // 存储客户端地址信息// 接受客户端连接// 返回:1. 与客户端通信的socket 2. 客户端网络地址auto sock = _listensockptr->Accept(&client); // 如果连接失败,继续等待下一个连接if (sock == nullptr){continue;}// 记录成功连接日志LOG(LogLevel::DEBUG) << "accept success ..." << client.StringAddr();// 创建子进程处理客户端请求pid_t id = fork();// fork失败处理if (id < 0){LOG(LogLevel::FATAL) << "fork error ...";exit(FORK_ERR); // 退出并返回错误码}// 子进程代码块else if (id == 0){// 子进程关闭监听socket(不需要监听新连接)_listensockptr->Close();// 二次fork创建孙子进程(避免僵尸进程)if (fork() > 0)exit(OK); // 子进程直接退出// 孙子进程(现在是孤儿进程,由init进程接管)执行实际任务_service(sock, client); // 调用IO服务处理函数sock->Close(); // 关闭通信socketexit(OK); // 正常退出}// 父进程代码块else{// 父进程关闭与客户端的通信socket(只需要管理连接)sock->Close();// 等待子进程结束(避免僵尸进程)pid_t rid = ::waitpid(id, nullptr, 0);(void)rid; // 忽略返回值(避免编译器警告)}}_isrunning = false; // 退出循环后更新运行状态}// 析构函数~TcpServer() {}private:uint16_t _port; // 服务器监听端口std::unique_ptr<Socket> _listensockptr; // 监听socket智能指针bool _isrunning; // 服务器运行状态标志ioservice_t _service; // IO服务处理函数对象
};
7.12 main.cc
/** TCP服务器主程序 - 网络计算器服务端* 功能:实现一个守护进程化的TCP服务器,接收客户端请求并返回计算结果*/// 包含必要的头文件
#include "NetCal.hpp" // 网络计算器业务逻辑
#include "Protocol.hpp" // 网络协议处理
#include "TcpServer.hpp" // TCP服务器实现
#include "Daemon.hpp" // 守护进程化工具
#include <memory> // 智能指针// 使用说明函数
void Usage(std::string proc)
{// 打印程序使用说明std::cerr << "Usage: " << proc << " port" << std::endl;
}/** 主函数* @param argc 参数个数* @param argv 参数数组* @return 程序执行状态码* * 程序启动方式示例:./tcpserver 8080*/
int main(int argc, char *argv[])
{// 1. 参数检查if (argc != 2){Usage(argv[0]); // 打印使用说明exit(USAGE_ERR); // 参数错误退出}// 2. 守护进程化std::cout << "服务器已经启动,已经是一个守护进程了" << std::endl;Daemon(0, 0); // 调用守护进程函数(不改变工作目录,不关闭标准IO)// daemon(1, 1); // 替代方案:改变工作目录,关闭标准IO// 3. 日志策略设置// Enable_Console_Log_Strategy(); // 控制台日志策略(已注释)Enable_File_Log_Strategy(); // 启用文件日志策略// 4. 三层架构初始化// 4.1 顶层 - 计算层(业务逻辑)std::unique_ptr<Cal> cal = std::make_unique<Cal>();// 4.2 中间层 - 协议层// 使用lambda表达式将计算层与协议层连接std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&cal](Request &req)->Response {return cal->Execute(req); // 协议层收到请求后调用计算层处理});// 4.3 底层 - 服务器层// 初始化TCP服务器,绑定端口,设置协议处理回调std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]), // 将字符串端口转换为整数[&protocol](std::shared_ptr<Socket> &sock, InetAddr &client) {protocol->GetRequest(sock, client); // 收到连接后调用协议层处理});// 5. 启动服务器tsvr->Start(); // 启动服务器主循环// sleep(5); // 测试用延时(已注释)return 0; // 正常退出(实际上守护进程不会到达这里)
}
相关文章:

计算机网络 : 应用层自定义协议与序列化
计算机网络 : 应用层自定义协议与序列化 目录 计算机网络 : 应用层自定义协议与序列化引言1. 应用层协议1.1 再谈协议1.2 网络版计算器1.3 序列化与反序列化 2. 重新理解全双工3. socket和协议的封装4. 关于流失数据的处理5. Jsoncpp5.1 特性5.2 安装5.3…...

Python Day42 学习(日志Day9复习)
补充:关于“箱线图”的阅读 以下图为例 浙大疏锦行 箱线图的基本组成 箱体(Box):中间的矩形,表示数据的中间50%(从下四分位数Q1到上四分位数Q3)。中位线(Median)&#…...

CMake在VS中使用远程调试
选中CMakeLists.txt, 右键-添加调试配置-选中"C\C远程windows调试" 之后将 aunch.vs.json文件改为如下所示: CMake在VS中使用远程调试时,Launch.vs.json中远程调试设置 ,远程电脑开启VS专用的RemoteDebugger {"version": "0.2.1","defaul…...

《图解技术体系》How Redis Architecture Evolves?
Redis架构的演进经历了多个关键阶段,从最初的内存数据库发展为支持分布式、多模型和持久化的高性能系统。以下为具体演进路径: 单线程模型与基础数据结构 Redis最初采用单线程架构,利用高效的I/O多路复用(如epoll)处…...
从零搭建到 App Store 上架:跨平台开发者使用 Appuploader与其他工具的实战经验
对于很多独立开发者或小型团队来说,开发一个 iOS 应用并不难,真正的挑战在于最后一步:将应用成功上架到 App Store。尤其是当你主要在 Windows 或 Linux 系统上开发,缺乏苹果设备和 macOS 环境时,上架流程往往变得繁琐…...
Spring Cloud 2025 正式发布啦
文章目录 一、版本兼容性二、Spring Cloud Gateway 重大更新1、新增功能1.1 Function & Stream 处理器集成1.2 Bucket4j 限流器支持 2、重要弃用2.1. WebClientRouting 基础设施2.2. 模块和启动器重命名 3、破坏性变更3.1 X-Forwarded-* 头部默认禁用3.2 配置受信任代理:3.…...

一文速通Python并行计算:12 Python多进程编程-进程池Pool
一文速通 Python 并行计算:12 Python 多进程编程-进程池 Pool 摘要: 在Python多进程编程中,Pool类用于创建进程池,可并行执行多个任务。通过map、apply等方法,将函数和参数分发到子进程,提高CPU利用率&…...
相机Camera日志分析之二十五:高通相机Camx 基于预览1帧的process_capture_request四级日志分析详解
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:相机Camera日志分析之二十四:高通相机Camx 基于预览1帧的process_capture_request三级日志分析详解 ok 这一篇我们开始讲: 相机Camera日志分析之二十五:高通相机Camx 基于预览1帧的process_capture_…...
React从基础入门到高级实战:React 实战项目 - 项目一:在线待办事项应用
React 实战项目:在线待办事项应用 欢迎来到本 React 开发教程专栏的第 26 篇!在之前的 25 篇文章中,我们从 React 的基础概念逐步深入到高级技巧,涵盖了组件、状态、路由和性能优化等核心知识。这一次,我们将通过一个…...
云部署实战:基于AWS EC2/Aliyun ECS与GitHub Actions的CI/CD全流程指南
在当今快速迭代的软件开发环境中,云部署与持续集成/持续交付(CI/CD)已成为现代开发团队的标配。本文将详细介绍如何利用AWS EC2或阿里云ECS结合GitHub Actions构建高效的CI/CD流水线,从零开始实现自动化部署的全过程。 最近挖到一个宝藏级人工智能学习网…...
golang 如何定义一种能够与自身类型值进行比较的Interface
定义一种具有比较能力的类型是一种常见需求,比如对一组相同类型的值进行排序,就需要进行两两比较,那么在Go语言中有没有办法定义一种具有比较能力的Interface,实现该接口的类型都具备比较能力呢,最常见最容易的办法是定…...

Web前端之原生表格动态复杂合并行、Vue
MENU 效果公共数据纯原生StyleJavaScript vue原生table 效果 原生的JavaScript原生table null 公共数据 const list [{id: "a1",title: "第一列",list: [{id: "a11",parentId: "a1",title: "第二列",list: [{ id: "…...

『uniapp』把接口的内容下载为txt本地保存 / 读取本地保存的txt文件内容(详细图文注释)
目录 预览效果思路分析downloadTxt 方法readTxt 方法 完整代码总结 欢迎关注 『uniapp』 专栏,持续更新中 欢迎关注 『uniapp』 专栏,持续更新中 预览效果 思路分析 downloadTxt 方法 该方法主要完成两个任务: 下载 txt 文件:通…...
C/C++ 面试复习笔记(2)
C语言如何实现快速排序算法? 答案:快排是一种分治算法,选择一个基准元素,将数据划分成两部分,然后递归排序 补充: void quick_sort(int arr[], int start, int end) {//判断是否需要排序if (start > …...
宝马集团推进数字化转型:强化生产物流与财务流程,全面引入SAP现代架构
2025年6月,宝马集团宣布在生产物流与财务流程领域取得重大数字化成果。这些进展标志着集团全球范围内采用基于云的新型SAP架构进入关键阶段,旨在提升运营效率、透明度和AI能力,为未来工业发展奠定技术基础。 一、生产物流全球数字化部署 宝…...

【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 时间事件处理部分)
揭秘高效存储模型与数据结构底层实现 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 时间事件:serverCron函数更新服务器时间缓存更新LRU时钟-lruclock更新服务器每秒执行命令次…...

【DAY40】训练和测试的规范写法
内容来自浙大疏锦行python打卡训练营 浙大疏锦行 知识点: 彩色和灰度图片测试和训练的规范写法:封装在函数中展平操作:除第一个维度batchsize外全部展平dropout操作:训练阶段随机丢弃神经元,测试阶段eval模式关闭drop…...
C语言 标准I/O函数全面指南
C标准I/O函数全面指南 本指南详细介绍了C语言中用于文件操作的标准输入/输出函数,包括单字符I/O、字符串I/O、格式化I/O、块I/O以及文件光标操作。每个部分包含函数定义、使用说明和实用示例,适合学习、复习以及博客发布。内容采用清晰的Markdown格式&a…...

el-select 实现分页加载,切换也数滚回到顶部,自定义高度
el-select 实现分页加载,切换也数滚回到顶部,自定义高度 1.html <el-form-item label"俱乐部:" prop"club_id" label-width"120px"><el-select :disabled"Boolean(match_id)" style"w…...

Langchaine4j 流式输出 (6)
Langchaine4j 流式输出 大模型的流式输出是指大模型在生成文本或其他类型的数据时,不是等到整个生成过程完成后再一次性 返回所有内容,而是生成一部分就立即发送一部分给用户或下游系统,以逐步、逐块的方式返回结果。 这样,用户…...
Jenkins:自动化流水线的基石,开启 DevOps 新时代
从持续集成到持续交付的全流程自动化工具 一、什么是 Jenkins? Jenkins 是一款开源的 自动化服务器,专注于持续集成(CI)和持续交付(CD)。它通过插件化的架构支持几乎所有的开发、运维和测试工具ÿ…...

学习经验分享【40】目标检测热力图制作
目标检测热力图在学术论文(尤其是计算机视觉、深度学习领域)中是重要的可视化分析工具和论证辅助手段,可以给论文加分不少。主要作用一是增强论文的可解释性与说服力:论文中常需解释模型 “如何” 或 “为何” 检测到目标…...

C#里与嵌入式系统W5500网络通讯(3)
有与W5500通讯时,需要使用下面的寄存器: PHYCFGR (W5500 PHY Configuration Register) [R/W] [0x002E] [0b10111XXX] PHYCFGR configures PHY operation mode and resets PHY. In addition, PHYCFGR indicates the status of PHY such as duplex, Speed, Link. 这张表格详细…...

用OpenNI2获取奥比中光Astra Pro输出的深度图(win,linux arm64 x64平台)
搞了一个奥比中光Astra Pro,想在windows平台,和linux rk3588 (香橙派,ubuntu2404,debian)上获取深度信息,之前的驱动下载已经不好用了,参考如下 Astra 3D相机选型建议 - 知乎https://zhuanlan.zhihu.com/p/594485674 …...

Unity VR/MR开发-VR设备与适用场景分析
视频讲解链接:【XR马斯维】VR/MR设备与适用场景分析?【UnityVR/MR开发教程--入门】_游戏热门视频...

Linux: network: switch:arp cache更新规则 [chatGPT]
文章目录 介绍概念普通包带有不同的mac,是否更新arp cache?普通包带有相同的mac,是否刷新 aging timeswitch是否会主动学习介绍 关于arp cache在switch侧的行为。有很多问题需要理解。 概念 HP L3 - IP Services Configuration Guide 文档里有写:dynamic arp entry的解说…...

Java网络编程API 1
Java中的网络编程API一共有两套:一套是UDP协议使用的API;另一套是TCP协议使用的API。这篇文章我们先来介绍UDP版本的API,并尝试来写一个回显服务器(接收到的请求是什么,返回的响应就是什么)。 UDP数据报套…...
Android协程学习
目录 Android上的Kotlin协程介绍基本概念与简单使用示例协程的高级用法 结构化并发线程调度器(Dispatchers)自定义调度器并发:同步 vs 异步 异步并发(async 并行执行)同步顺序执行协程取消与超时 取消机制超时控制异步数据流 Flow协程间通信 使用 Channel使用 StateFlow /…...
Angular报错:cann‘t bind to ngClass since it is‘t a known property of div
遇到的错误: Cant bind to ngClass since it isnt a known property of div这个错误是 Angular 中 最常见的模板编译错误之一,通常出现在你试图使用 ngClass 指令,但 Angular 没有识别它的情况下。 ✅ 错误的根本原因 Angular 不知道 ngCla…...
uniapp+vue3实现CK通信协议(基于jjc-tcpTools)
1. TCP 服务封装 (tcpService.js) export class TcpService {constructor() {this.connections uni.requireNativePlugin(jjc-tcpTools)this.clients new Map() // 存储客户端连接this.servers new Map() // 存储服务端实例}// 创建 TCP 服务端 (字符串模式)createStringSe…...