阅读导航
一、设计方案1. 日志模块 (Log.hpp)2. 协议模块 (Protocol.hpp)3. 服务器模块 (ServerCal.hpp、Socket.hpp 和 TcpServer.hpp )4. 客户端模块 (ClientCal.cpp) 二、日志模块、makefile文件✅Log.hpp✅makefile 三、协议模块(Protocol.hpp)✅Protocol.hpp 四、服务端模块✅ServerCal.hpp✅Socket.hpp✅TcpServer.hpp✅ServerCal.cpp 五、客户端模块✅ClientCal.cpp 六、设计方案总结
一、设计方案
1. 日志模块 (Log.hpp)
日志模块提供了一个简单的日志记录功能,允许将日志输出到控制台、单个文件或按日志级别分类的文件中。它定义了不同级别的日志(Info, Debug, Warning, Error, Fatal),并允许通过Enable
方法切换日志输出方式。
2. 协议模块 (Protocol.hpp)
协议模块定义了请求和响应的数据格式。Request
类封装了计算请求的数据,包括操作数和操作符。Response
类封装了计算结果和错误代码。这两个类都提供了序列化和反序列化的方法,以便将数据转换为网络传输的格式。
3. 服务器模块 (ServerCal.hpp、Socket.hpp 和 TcpServer.hpp )
服务器模块由ServerCal
、Socket.hpp
和TcpServer
类组成。
ServerCal
类负责处理计算请求,它定义了一个Calculator
方法来执行实际的算术运算,并返回结果和错误代码。Socket
类封装了基本的socket操作,如连接、读取和写入。TcpServer
类负责网络通信,它监听指定端口,接受客户端连接,并使用回调函数来处理接收到的数据。 4. 客户端模块 (ClientCal.cpp)
客户端模块提供了与服务器通信的能力,ClientCal
类使用Socket
类来向服务器发送计算请求,并接收响应。
客户端程序首先检查命令行参数是否正确,然后创建一个套接字并连接到服务器。程序将随机生成两个数字和一个操作符,创建一个请求对象,并将其序列化为字符串。然后,程序将这个字符串编码为网络字节流,并通过套接字发送给服务器。接收到服务器的响应后,程序将其解码并反序列化为响应对象,然后打印出请求和响应的详细信息。这个过程将重复10次,每次请求后程序会暂停1秒。最后,程序关闭套接字并退出。
二、日志模块、makefile文件
✅Log.hpp
// 预处理指令,确保头文件只被包含一次#pragma once// 引入必要的头文件#include <iostream> // 标准输入输出流#include <time.h> // 时间函数#include <stdarg.h> // 可变参数列表#include <sys/types.h> // 文件系统类型#include <sys/stat.h> // 文件状态#include <fcntl.h> // 文件控制#include <unistd.h> // UNIX标准函数#include <stdlib.h> // 标准库// 定义常量SIZE,用于缓冲区大小#define SIZE 1024// 定义日志级别,分别对应不同的日志重要性#define Info 0#define Debug 1#define Warning 2#define Error 3#define Fatal 4// 定义日志输出方式,分别对应控制台输出和文件输出#define Screen 1#define Onefile 2#define Classfile 3// 定义日志文件名#define LogFile "log.txt"// 日志类Log的声明class Log{public: // 构造函数,初始化日志输出方式为控制台输出,日志路径为当前目录下的log文件夹 Log() { printMethod = Screen; // 默认输出方式为控制台输出 path = "./log/"; // 默认日志路径 } // 设置日志输出方式的函数 void Enable(int method) { printMethod = method; // 根据传入的参数设置输出方式 } // 将日志级别转换为字符串的函数 std::string levelToString(int level) { // 使用switch语句根据日志级别返回对应的字符串 switch (level) { case Info: return "Info"; case Debug: return "Debug"; case Warning: return "Warning"; case Error: return "Error"; case Fatal: return "Fatal"; default: return "None"; } } // 打印日志的函数 void printLog(int level, const std::string &logtxt) { // 根据输出方式选择不同的打印方法 switch (printMethod) { case Screen: std::cout << logtxt << std::endl; // 控制台输出 break; case Onefile: printOneFile(LogFile, logtxt); // 单文件输出 break; case Classfile: printClassFile(level, logtxt); // 分类文件输出 break; default: break; } } // 单文件日志输出的实现 void printOneFile(const std::string &logname, const std::string &logtxt) { // 拼接完整的日志文件路径 std::string _logname = path + logname; // 打开文件,如果失败则直接返回 int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); if (fd < 0) return; // 将日志文本写入文件 write(fd, logtxt.c_str(), logtxt.size()); // 关闭文件描述符 close(fd); } // 分类文件日志输出的实现 void printClassFile(int level, const std::string &logtxt) { // 根据日志级别创建对应的文件名 std::string filename = LogFile; filename += "."; filename += levelToString(level); // 调用单文件输出函数 printOneFile(filename, logtxt); } // 析构函数,目前为空 ~Log() { } // 重载函数调用运算符,用于格式化输出日志 void operator()(int level, const char *format, ...) { // 获取当前时间 time_t t = time(nullptr); struct tm *ctime = localtime(&t); // 格式化时间字符串 char leftbuffer[SIZE]; snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(), ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec); // 使用可变参数列表进行格式化字符串 va_list s; va_start(s, format); char rightbuffer[SIZE]; vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); va_end(s); // 拼接完整的日志文本 char logtxt[SIZE * 2]; snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer); // 调用打印日志函数 printLog(level, logtxt); }private: // 日志输出方式 int printMethod; // 日志文件路径 std::string path;};// 定义一个Log类的全局对象lg,用于输出日志Log lg;
✅makefile
.PHONY:allall:servercal clientcalFlag=#-DMySelf=1Lib=-ljsoncppservercal:ServerCal.ccg++ -o $@ $^ -std=c++11 $(Lib) $(Flag)clientcal:ClientCal.ccg++ -o $@ $^ -std=c++11 -g $(Lib) $(Flag).PHONY:cleanclean:rm -f clientcal servercal
三、协议模块(Protocol.hpp)
✅Protocol.hpp
#pragma once // 确保头文件在整个程序中只被包含一次#include <iostream> // 包含标准输入输出流#include <string> // 包含字符串类#include <jsoncpp/json/json.h> // 包含JSONCPP库,用于JSON数据的处理// 宏定义,用于序列化和反序列化过程中的数据分隔const std::string blank_space_sep = " "; // 空格分隔符const std::string protocol_sep = "\n"; // 换行符作为协议的分隔符// 序列化函数,将内容字符串包装成网络传输的格式std::string Encode(std::string &content){ std::string package; // 创建一个字符串用于存储包装后的数据 package = std::to_string(content.size()); // 将内容的长度转换为字符串 package += protocol_sep; // 添加协议分隔符 package += content; // 添加内容本身 package += protocol_sep; // 再次添加协议分隔符,表示数据结束 return package; // 返回包装后的字符串}// 反序列化函数,将接收到的网络数据解析为内容字符串bool Decode(std::string &package, std::string *content){ std::size_t pos = package.find(protocol_sep); // 查找协议分隔符的位置 if(pos == std::string::npos) return false; // 如果找不到分隔符,解析失败 std::string len_str = package.substr(0, pos); // 提取长度字符串 std::size_t len = std::stoi(len_str); // 将长度字符串转换为数字 std::size_t total_len = len_str.size() + len + 2; // 计算总长度(长度字符串 + 内容 + 分隔符) if(package.size() < total_len) return false; // 如果实际数据长度小于预期,解析失败 *content = package.substr(pos+1, len); // 提取内容字符串 package.erase(0, total_len); // 从原始数据中移除已解析的部分 return true; // 解析成功}// 请求数据结构class Request{public: // 构造函数,初始化请求的参数 Request(int data1, int data2, char oper) : x(data1), y(data2), op(oper) {} // 默认构造函数 Request() {}public: // 序列化方法,将请求对象转换为字符串 bool Serialize(std::string *out) {#ifdef MySelf // 使用简单的字符串拼接方式 std::string s = std::to_string(x); // 将操作数x转换为字符串 s += blank_space_sep; // 添加空格分隔符 s += op; // 添加操作符 s += blank_space_sep; // 再次添加空格分隔符 s += std::to_string(y); // 将操作数y转换为字符串 *out = s; // 将拼接后的字符串赋值给输出参数 return true; // 返回成功#else // 使用JSON格式 Json::Value root; // 创建JSON值对象 root["x"] = x; // 添加操作数x root["y"] = y; // 添加操作数y root["op"] = op; // 添加操作符 Json::StyledWriter w; // 创建JSON格式化写入器 *out = w.write(root); // 将JSON对象转换为格式化的字符串 return true; // 返回成功#endif } // 反序列化方法,将字符串转换为请求对象 bool Deserialize(const std::string &in) {#ifdef MySelf // 使用简单的字符串拼接方式 std::size_t left = in.find(blank_space_sep); // 查找第一个空格分隔符 if (left == std::string::npos) return false; // 如果找不到分隔符,解析失败 std::string part_x = in.substr(0, left); // 提取操作数x的字符串 std::size_t right = in.rfind(blank_space_sep); // 查找最后一个空格分隔符 if (right == std::string::npos) return false; // 如果找不到分隔符,解析失败 std::string part_y = in.substr(right + 1); // 提取操作数y的字符串 if (left + 2 != right) return false; // 如果分隔符之间的长度不符合预期,解析失败 op = in[left + 1]; // 提取操作符 x = std::stoi(part_x); // 将操作数x的字符串转换为数字 y = std::stoi(part_y); // 将操作数y的字符串转换为数字 return true; // 返回成功#else // 使用JSON格式 Json::Value root; Json::Reader r; if (!r.parse(in, root)) return false; // 解析JSON字符串,如果失败则返回 x = root["x"].asInt(); // 提取操作数x y = root["y"].asInt(); // 提取操作数y op = root["op"].asInt(); // 提取操作符 return true; // 返回成功#endif } // 调试方法,打印请求对象的内容 void DebugPrint() { std::cout << "新请求构建完成: " << x << op << y << "=?" << std::endl; // 打印操作数和操作符 }public: // 请求的数据成员 int x; // 操作数x int y; // 操作数y char op; // 操作符,可以是 + - * / %};// 响应数据结构class Response{public: // 构造函数,初始化响应的参数 Response(int res, int c) : result(res), code(c) {} // 默认构造函数 Response() {}public: // 序列化方法,将响应对象转换为字符串 bool Serialize(std::string *out) {#ifdef MySelf // 使用简单的字符串拼接方式 std::string s = std::to_string(result); // 将结果转换为字符串 s += blank_space_sep; // 添加空格分隔符 s += std::to_string(code); // 将错误代码转换为字符串 *out = s; // 将拼接后的字符串赋值给输出参数 return true; // 返回成功#else // 使用JSON格式 Json::Value root; // 创建JSON值对象 root["result"] = result; // 添加结果 root["code"] = code; // 添加错误代码 Json::StyledWriter w; // 创建JSON格式化写入器 *out = w.write(root); // 将JSON对象转换为格式化的字符串 return true; // 返回成功#endif } // 反序列化方法,将字符串转换为响应对象 bool Deserialize(const std::string &in) {#ifdef MySelf // 使用简单的字符串拼接方式 std::size_t pos = in.find(blank_space_sep); // 查找空格分隔符 if (pos == std::string::npos) return false; // 如果找不到分隔符,解析失败 std::string part_left = in.substr(0, pos); // 提取结果字符串 std::string part_right = in.substr(pos + 1); // 提取错误代码字符串 result = std::stoi(part_left); // 将结果字符串转换为数字 code = std::stoi(part_right); // 将错误代码字符串转换为数字 return true; // 返回成功#else // 使用JSON格式 Json::Value root; Json::Reader r; if (!r.parse(in, root)) return false; // 解析JSON字符串,如果失败则返回 result = root["result"].asInt(); // 提取结果 code = root["code"].asInt(); // 提取错误代码 return true; // 返回成功#endif } // 调试方法,打印响应对象的内容 void DebugPrint() { std::cout << "结果响应完成, result: " << result << ", code: " << code << std::endl; // 打印结果和错误代码 }public: int result; // 计算结果 int code; // 错误代码,0表示成功,非0表示错误};
四、服务端模块
✅ServerCal.hpp
// 预处理指令,确保头文件只被包含一次#pragma once// 引入必要的头文件#include <iostream>// 引入自定义的协议头文件#include "Protocol.hpp"// 定义枚举类型,用于表示不同类型的操作和错误enum{ Div_Zero = 1, // 除数为零的错误代码 Mod_Zero, // 模数为零的错误代码 Other_Oper // 其他操作或者错误};// 声明ServerCal类,用于处理客户端的计算请求class ServerCal{public: // 构造函数 ServerCal() { } // 计算助手函数,根据请求计算结果并返回响应对象 Response CalculatorHelper(const Request &req) { Response resp(0, 0); // 创建一个响应对象,初始化结果和错误代码为0 // 根据请求中的操作符进行计算 switch (req.op) // 检查操作符 { case '+': // 加法 resp.result = req.x + req.y; break; case '-': // 减法 resp.result = req.x - req.y; break; case '*': // 乘法 resp.result = req.x * req.y; break; case '/': // 除法 { if (req.y == 0) // 如果除数为0,设置错误代码 resp.code = Div_Zero; else // 否则进行除法运算 resp.result = req.x / req.y; } break; case '%': { if (req.y == 0) // 如果模数为0,设置错误代码 resp.code = Mod_Zero; else // 否则进行模运算 resp.result = req.x % req.y; } break; default: // 如果操作符不是预定义的几种 resp.code = Other_Oper; // 设置错误代码为其他操作 break; } return resp; // 返回计算结果和错误代码 } // 计算函数,解析请求字符串,并返回计算结果 std::string Calculator(std::string &package) { std::string content; // 用于存储解码后的内容 // 解析请求包的长度和内容 bool r = Decode(package, &content); if (!r) return ""; // 如果解码失败,返回空字符串 // 从解码后的内容中反序列化请求对象 Request req; r = req.Deserialize(content); if (!r) return ""; // 如果反序列化失败,返回空字符串 // 清空content,准备存储响应内容 content = ""; // 调用助手函数进行计算 Response resp = CalculatorHelper(req); // 将计算结果和错误代码序列化到content resp.Serialize(&content); // 编码响应内容,添加长度前缀 content = Encode(content); return content; // 返回响应字符串 } // 析构函数 ~ServerCal() { }};
✅Socket.hpp
// 预处理指令,确保头文件只被包含一次#pragma once// 引入必要的头文件#include <iostream> // 标准输入输出流#include <string> // 字符串类#include <unistd.h> // UNIX标准函数,如sleep等#include <cstring> // C字符串处理函数#include <sys/types.h> // 系统类型定义#include <sys/stat.h> // 文件状态信息#include <sys/socket.h> // 套接字相关函数和结构定义#include <arpa/inet.h> // 网络地址转换#include <netinet/in.h> // 网络接口定义#include "Log.hpp" // 自定义的日志库// 定义枚举类型,用于表示不同的错误代码enum{ SocketErr = 2, // 套接字创建错误 BindErr, // 套接字绑定错误 ListenErr // 套接字监听错误};// 定义backlog常量,用于listen函数的参数const int backlog = 10;// Sock类声明,封装了套接字操作的一系列方法class Sock{public: // 构造函数 Sock() { } // 析构函数 ~Sock() { } // 创建套接字的方法 void Socket() { // 使用socket函数创建一个流式套接字,地址族为IPv4,类型为TCP sockfd_ = socket(AF_INET, SOCK_STREAM, 0); // 如果套接字创建失败,记录错误日志并退出程序 if (sockfd_ < 0) { lg(Fatal, "socket error, %s: %d", strerror(errno), errno); exit(SocketErr); } } // 绑定套接字到指定端口的方法 void Bind(uint16_t port) { // 创建一个sockaddr_in结构体,用于绑定操作 struct sockaddr_in local; memset(&local, 0, sizeof(local)); // 清零结构体 local.sin_family = AF_INET; // 设置地址族为IPv4 local.sin_port = htons(port); // 设置端口号,使用网络字节序 local.sin_addr.s_addr = INADDR_ANY; // 允许绑定到所有可用网络接口 // 使用bind函数将套接字绑定到指定端口,如果失败记录错误日志并退出程序 if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0) { lg(Fatal, "bind error, %s: %d", strerror(errno), errno); exit(BindErr); } } // 使套接字监听传入连接的方法 void Listen() { // 使用listen函数使套接字监听传入连接,backlog参数指定最大的连接请求队列长度 if (listen(sockfd_, backlog) < 0) { lg(Fatal, "listen error, %s: %d", strerror(errno), errno); exit(ListenErr); } } // 接受传入连接的方法 int Accept(std::string *clientip, uint16_t *clientport) { // 创建一个sockaddr_in结构体,用于存储客户端的地址信息 struct sockaddr_in peer; socklen_t len = sizeof(peer); // 使用accept函数接受一个传入连接,返回一个新的套接字描述符 int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len); // 如果接受连接失败,记录警告日志并返回-1 if(newfd < 0) { lg(Warning, "accept error, %s: %d", strerror(errno), errno); return -1; } // 将客户端的IP地址和端口号转换为可读格式 char ipstr[64]; inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr)); *clientip = ipstr; *clientport = ntohs(peer.sin_port); // 转换端口号为主机字节序 return newfd; // 返回新的套接字描述符 } // 连接到指定的IP地址和端口号的方法 bool Connect(const std::string &ip, const uint16_t &port) { // 创建一个sockaddr_in结构体,用于存储目标地址信息 struct sockaddr_in peer; memset(&peer, 0, sizeof(peer)); // 清零结构体 peer.sin_family = AF_INET; // 设置地址族为IPv4 peer.sin_port = htons(port); // 设置端口号,使用网络字节序 inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr)); // 将IP地址转换为网络字节序 // 使用connect函数尝试连接到目标地址,如果失败则记录错误并返回false int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer)); if(n == -1) { std::cerr << "connect to " << ip << ":" << port << " error" << std::endl; return false; } return true; // 连接成功返回true } // 关闭套接字的方法 void Close() { close(sockfd_); // 使用close函数关闭套接字 } // 获取套接字文件描述符的方法 int Fd() { return sockfd_; // 返回套接字文件描述符 }private: int sockfd_; // 套接字文件描述符};
✅TcpServer.hpp
// 预处理指令,确保头文件只被包含一次#pragma once// 引入必要的头文件#include <functional> // 用于支持 std::function#include <string> // 用于支持 std::string#include <signal.h> // 用于处理信号#include "Log.hpp" // 自定义的日志头文件#include "Socket.hpp" // 自定义的套接字头文件// 定义一个类型别名,用于简化函数参数using func_t = std::function<std::string(std::string &package)>;// 声明TcpServer类,用于创建和运行TCP服务器class TcpServer{public: // 构造函数,初始化服务器的端口号和回调函数 TcpServer(uint16_t port, func_t callback) : port_(port), callback_(callback) { } // 初始化服务器的函数,创建套接字,绑定端口,开始监听 bool InitServer() { // 创建监听套接字 listensock_.Socket(); // 绑定端口到套接字 listensock_.Bind(port_); // 开始监听连接请求 listensock_.Listen(); // 记录服务器初始化信息 lg(Info, "init server .... done"); return true; } // 启动服务器的主函数,处理客户端连接请求 void Start() { // 忽略子进程退出和管道信号 signal(SIGCHLD, SIG_IGN); signal(SIGPIPE, SIG_IGN); // 循环处理连接请求 while (true) { // 接受客户端的连接请求,获取客户端的IP地址和端口号 std::string clientip; uint16_t clientport; int sockfd = listensock_.Accept(&clientip, &clientport); // 如果接受连接失败,则继续下一次循环 if (sockfd < 0) continue; // 记录客户端连接信息 lg(Info, "accept a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport); // 处理客户端请求,创建子进程提供服务 if (fork() == 0) { // 关闭监听套接字 listensock_.Close(); std::string inbuffer_stream; // 循环读取客户端发送的数据 while (true) { // 读取数据到缓冲区 char buffer[1280]; ssize_t n = read(sockfd, buffer, sizeof(buffer)); // 如果读取到数据 if (n > 0) { // 将读取的数据添加到输入缓冲区 buffer[n] = 0; inbuffer_stream += buffer; // 记录调试信息 lg(Debug, "debug:\n%s", inbuffer_stream.c_str()); // 循环处理输入缓冲区中的数据 while (true) { // 调用回调函数处理数据,并获取响应信息 std::string info = callback_(inbuffer_stream); // 如果没有响应信息,则跳出循环 if (info.empty()) break; // 记录调试信息 lg(Debug, "debug, response:\n%s", info.c_str()); // 记录调试信息 lg(Debug, "debug:\n%s", inbuffer_stream.c_str()); // 将响应信息发送回客户端 write(sockfd, info.c_str(), info.size()); } } // 如果客户端关闭连接,则跳出循环 else if (n == 0) break; // 如果读取失败,则跳出循环 else break; } // 子进程服务结束后退出 exit(0); } // 关闭客户端套接字 close(sockfd); } } // 析构函数 ~TcpServer() { }private: // 服务器的端口号 uint16_t port_; // 监听套接字 Sock listensock_; // 回调函数,用于处理客户端请求 func_t callback_;};
✅ServerCal.cpp
// 引入必要的头文件#include "TcpServer.hpp" // 引入自定义的TCP服务器头文件#include "ServerCal.hpp" // 引入自定义的计算器逻辑头文件#include <unistd.h> // 引入UNIX标准函数库,用于系统调用如sleep等// #include "Daemon.hpp" // 引入自定义的守护进程头文件(当前被注释)// 定义Usage函数,用于输出程序的使用方法static void Usage(const std::string &proc){ std::cout << "\nUsage: " << proc << " port\n" << std::endl; // 输出程序的使用方法}// 主函数,程序的入口点int main(int argc, char *argv[]){ // 检查命令行参数数量是否正确 if(argc != 2) { Usage(argv[0]); // 如果参数不正确,输出使用方法并退出程序 exit(0); } // 将命令行参数转换为端口号 uint16_t port = std::stoi(argv[1]); // 创建计算器逻辑对象 ServerCal cal; // 创建TCP服务器对象,绑定端口号和计算器逻辑对象的Calculator方法 TcpServer *tsvp = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1)); // 初始化服务器 tsvp->InitServer(); // 调用守护进程相关函数,将程序转为守护进程运行(当前被注释) // Daemon(); daemon(0, 0); // 调用守护进程函数,参数设置为0表示不进行标准输出和错误输出的重定向 // 启动服务器,开始监听和处理客户端请求 tsvp->Start(); return 0; // 程序正常结束}
五、客户端模块
✅ClientCal.cpp
// 引入所需的头文件#include <iostream> // 用于输入输出流#include <string> // 用于字符串类#include <ctime> // 用于时间相关函数#include <cassert> // 用于断言检查#include <unistd.h> // 用于UNIX标准函数,如sleep#include "Socket.hpp" // 自定义的套接字操作库#include "Protocol.hpp" // 自定义的通信协议库// 定义Usage函数,用于输出程序的使用方法static void Usage(const std::string &proc){ std::cout << "\nUsage: " << proc << " serverip serverport\n" << std::endl;}// 主函数,程序的入口点int main(int argc, char *argv[]){ // 检查命令行参数数量是否正确 if (argc != 3) { Usage(argv[0]); exit(0); } // 从命令行参数获取服务器的IP地址和端口号 std::string serverip = argv[1]; uint16_t serverport = std::stoi(argv[2]); // 创建套接字对象 Sock sockfd; // 创建套接字 sockfd.Socket(); // 尝试连接到服务器 bool r = sockfd.Connect(serverip, serverport); if(!r) return 1; // 如果连接失败,则退出程序 // 初始化随机数生成器 srand(time(nullptr) ^ getpid()); // 定义测试次数 int cnt = 1; // 定义操作符字符串 const std::string opers = "+-*/%=-=&^"; // 定义输入缓冲区流 std::string inbuffer_stream; // 循环发送请求并接收响应,直到发送了10次 while(cnt <= 10) { // 输出测试次数信息 std::cout << "===============第" << cnt << "次测试....., " << "===============" << std::endl; // 生成随机数作为请求的参数 int x = rand() % 100 + 1; usleep(1234); // 微秒级的暂停 int y = rand() % 100; usleep(4321); // 微秒级的暂停 // 随机选择一个操作符 char oper = opers[rand() % opers.size()]; // 创建请求对象 Request req(x, y, oper); // 打印请求的详细信息(调试用) req.DebugPrint(); // 序列化请求对象到字符串 std::string package; req.Serialize(&package); // 将请求字符串编码为网络字节流 package = Encode(package); // 通过套接字发送请求 write(sockfd.Fd(), package.c_str(), package.size()); // 读取服务器的响应 char buffer[128]; ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer)); // 读取响应到缓冲区 if(n > 0) { // 确保读取的字符串以空字符结尾 buffer[n] = 0; // 将读取的内容添加到输入缓冲区流 inbuffer_stream += buffer; // 输出接收到的完整响应 std::cout << inbuffer_stream << std::endl; // 从输入缓冲区流中解码出响应内容 std::string content; bool r = Decode(inbuffer_stream, &content); // 解码响应内容 assert(r); // 断言解码成功 // 反序列化响应内容到Response对象 Response resp; r = resp.Deserialize(content); assert(r); // 断言反序列化成功 // 打印响应的详细信息(调试用) resp.DebugPrint(); } // 输出测试分隔线 std::cout << "=================================================" << std::endl; // 暂停一秒 sleep(1); // 增加测试次数 cnt++; } // 关闭套接字 sockfd.Close(); // 程序正常退出 return 0;}
六、设计方案总结
模块化:代码被分为不同的模块,每个模块负责一个特定的功能。这种设计使得代码易于理解和维护。
日志记录:通过Log
类,服务器和客户端可以记录操作信息和错误信息,有助于调试和监控。
自定义协议:通过Protocol
类,定义了一个简单的文本协议来传输请求和响应。协议的设计简单明了,易于理解和实现。
错误处理:服务器和客户端都实现了基本的错误处理逻辑,能够处理一些常见的错误情况。
多进程模型:TcpServer
类使用多进程模型来处理并发连接,每个客户端连接都在自己的进程中运行。
安全性:虽然代码中没有明确提到安全性措施,但在实际部署时,应该考虑使用加密通信、身份验证等安全措施来保护数据。
性能优化:在生产环境中,可能需要进一步优化服务器的性能,比如使用多线程或异步I/O代替多进程模型,或者使用专门的高性能网络库。
可扩展性:代码设计允许未来添加新的功能,比如支持更多的操作符或增加新的操作类型。
通过上述设计方案,我们可以得到一个基础的TCP服务器和客户端,它们可以根据自定义协议处理客户端请求,并支持基本的算术运算。这个系统可以作为“跨网络计算器”的基础,通过定义合适的回调函数和协议格式来实现计算器的功能。