文章目录
1. Log.hpp-日志记录器
Log.hpp
// 1. 头文件和宏定义
#pragma once // 防止头文件重复包含
// 系统头文件
#include <iostream> // 标准输入输出
#include <time.h> // 时间相关函数
#include <stdarg.h> // 可变参数函数
#include <sys/types.h> // 基本系统数据类型
#include <sys/stat.h> // 文件状态
#include <fcntl.h> // 文件控制
#include <unistd.h> // POSIX系统调用
#include <stdlib.h> // 标准库函数
// 缓冲区大小
#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"
// 2. 日志类定义
class Log {
private:
int printMethod; // 日志输出方式
std::string path; // 日志文件路径
public:
// 2.1 构造函数:设置默认输出方式
Log() {
printMethod = Screen; // 默认输出到屏幕
path = "./log/"; // 默认日志目录
}
// 2.2 设置日志输出方式
void Enable(int method) {
printMethod = method;
}
// 2.3 日志级别转字符串
std::string levelToString(int level) {
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";
}
}
// 2.4 日志输出函数
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;
}
}
// 2.5 输出到单个文件
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);
}
// 2.6 根据日志级别输出到不同文件
void printClassFile(int level, const std::string &logtxt) {
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // 例如: "log.txt.Debug"
printOneFile(filename, logtxt);
}
// 2.7 重载函数调用运算符
void operator()(int level, const char *format, ...) {
// 1. 获取当前时间
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
// 2. 格式化时间和日志级别信息
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);
// 3. 处理可变参数
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 4. 组合完整的日志信息
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// 5. 输出日志
printLog(level, logtxt);
}
};
// 3. 创建全局日志对象
Log lg;
2. Daemon.hpp-守护进程工具
Daemon.hpp
将进程转换为守护进程的工具类
#pragma once // 防止头文件重复包含
#include <iostream> // 标准输入输出
#include <cstdlib> // exit()函数
#include <unistd.h> // fork(), setsid(), chdir()等系统调用
#include <signal.h> // 信号处理
#include <string> // 字符串类
#include <sys/types.h> // 基本系统数据类型
#include <sys/stat.h> // 文件状态
#include <fcntl.h> // 文件控制选项
// 定义空设备文件路径
const std::string nullfile = "/dev/null";
// 守护进程化函数,参数cwd为工作目录
void Daemon(const std::string &cwd = "")
{
// 1. 忽略一些可能的干扰信号
signal(SIGCLD, SIG_IGN); // 忽略子进程状态改变信号
signal(SIGPIPE, SIG_IGN); // 忽略管道破裂信号
signal(SIGSTOP, SIG_IGN); // 忽略停止进程信号
// 2. 创建守护进程
if (fork() > 0) // 父进程退出
exit(0);
setsid(); // 创建新会话,使进程成为会话组长
// 3. 改变工作目录
if (!cwd.empty()) // 如果指定了工作目录
chdir(cwd.c_str()); // 切换到指定目录
// 4. 重定向标准输入输出到/dev/null
int fd = open(nullfile.c_str(), O_RDWR); // 以读写方式打开/dev/null
if(fd > 0)
{
dup2(fd, 0); // 重定向标准输入
dup2(fd, 1); // 重定向标准输出
dup2(fd, 2); // 重定向标准错误
close(fd); // 关闭文件描述符
}
}
3. Protocol.hpp-通信协议解析器
Protocol.hpp
定义客户端服务器间通信协议,处理消息的序列化和反序列化
#pragma once
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h> // JSON序列化支持
// #define MySelf 1 // 自定义协议开关
// 定义分隔符
const std::string blank_space_sep = " "; // 空格分隔符
const std::string protocol_sep = "\n"; // 协议分隔符
// 协议编码函数:将内容封装成格式化的报文
std::string Encode(std::string &content)
{
std::string package = std::to_string(content.size()); // 内容长度
package += protocol_sep; // 添加分隔符
package += content; // 添加内容
package += protocol_sep; // 添加分隔符
return package;
}
// 协议解码函数:从报文中提取内容
// 格式:"len"\n"x op y"\nXXXXXX
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
// 自定义协议格式:"x op y"
std::string s = std::to_string(x);
s += blank_space_sep;
s += op;
s += blank_space_sep;
s += std::to_string(y);
*out = s;
#else
// JSON格式
Json::Value root;
root["x"] = x;
root["y"] = y;
root["op"] = op;
Json::StyledWriter w;
*out = w.write(root);
#endif
return true;
}
// 反序列化:将字符串解析为请求对象
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);
std::size_t right = in.rfind(blank_space_sep);
if (right == std::string::npos) return false;
std::string part_y = in.substr(right + 1);
if (left + 2 != right) return false;
op = in[left + 1];
x = std::stoi(part_x);
y = std::stoi(part_y);
#else
// 解析JSON格式
Json::Value root;
Json::Reader r;
r.parse(in, root);
x = root["x"].asInt();
y = root["y"].asInt();
op = root["op"].asInt();
#endif
return true;
}
void DebugPrint()
{
std::cout << "新请求构建完成: " << x << op << y << "=?" << std::endl;
}
public:
int x; // 第一个操作数
int y; // 第二个操作数
char op; // 运算符
};
// 响应类:处理计算响应
class Response
{
// [响应类的实现与Request类似,只是处理result和code两个字段]
// result: 计算结果
// code: 状态码,0表示成功,非0表示各种错误
};
4. ServerCal.hpp-计算器服务处理器
ServerCal.hpp
实现服务器端的核心计算逻辑
#pragma once
#include <iostream>
#include "Protocol.hpp"
// 定义错误码枚举
enum
{
Div_Zero = 1, // 除零错误
Mod_Zero, // 取模零错误
Other_Oper // 未知运算符错误
};
// 服务器端计算器类
class ServerCal
{
public:
ServerCal() {}
// 核心计算功能辅助函数
Response CalculatorHelper(const Request &req)
{
Response resp(0, 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) // 处理除零错误
resp.code = Div_Zero;
else
resp.result = req.x / req.y;
}
break;
case '%': // 取模运算
{
if (req.y == 0) // 处理取模零错误
resp.code = Mod_Zero;
else
resp.result = req.x % req.y;
}
break;
default: // 未知运算符
resp.code = Other_Oper;
break;
}
return resp;
}
// 主计算函数:处理完整的请求-响应流程
// 输入格式示例:"len"\n"10 + 20"\n
std::string Calculator(std::string &package)
{
// 1. 解码请求包
std::string content;
bool r = Decode(package, &content); // 解析出实际内容
if (!r)
return "";
// 2. 反序列化请求内容
Request req;
r = req.Deserialize(content); // 将内容转换为请求对象
if (!r)
return "";
// 3. 执行计算
content = ""; // 清空content准备存储响应
Response resp = CalculatorHelper(req); // 调用计算辅助函数
// 4. 构建响应包
resp.Serialize(&content); // 序列化响应对象
content = Encode(content); // 编码响应内容
return content; // 返回完整的响应包
}
~ServerCal() {}
};
5. Socket.hpp-Socket通信封装类
Socket.hpp
封装底层Socket网络通信功能
#pragma once
#include <iostream>
#include <string>
#include <unistd.h> // Unix标准函数
#include <cstring> // memset等字符串操作
#include <sys/types.h> // 基本系统数据类型
#include <sys/stat.h> // 文件状态
#include <sys/socket.h> // Socket接口
#include <arpa/inet.h> // IP地址转换函数
#include <netinet/in.h> // IPv4地址结构
#include "Log.hpp" // 日志功能
// 错误码枚举
enum
{
SocketErr = 2, // Socket创建错误
BindErr, // 绑定错误
ListenErr, // 监听错误
};
// 监听队列长度
const int backlog = 10;
// Socket封装类
class Sock
{
public:
Sock() {}
~Sock() {}
public:
// 创建Socket
void Socket()
{
// 创建TCP Socket
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
// 创建失败,记录错误并退出
lg(Fatal, "socker error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
}
// 绑定端口
void Bind(uint16_t port)
{
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; // 绑定所有网卡
// 绑定地址和端口
if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
exit(BindErr);
}
}
// 开始监听
void Listen()
{
if (listen(sockfd_, backlog) < 0)
{
lg(Fatal, "listen error, %s: %d", strerror(errno), errno);
exit(ListenErr);
}
}
// 接受新连接
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 接受客户端连接
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
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;
}
// 连接服务器
bool Connect(const std::string &ip, const uint16_t &port)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
// 连接服务器
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;
}
// 关闭Socket
void Close()
{
close(sockfd_);
}
// 获取文件描述符
int Fd()
{
return sockfd_;
}
private:
int sockfd_; // Socket文件描述符
};
6. TcpServer.hpp-TCP服务器框架
TcpServer.hpp
实现TCP服务器的主框架
#pragma once
#include <functional>
#include <string>
#include <signal.h>
#include "Log.hpp"
#include "Socket.hpp"
// 定义回调函数类型:接收字符串参数,返回字符串
using func_t = std::function<std::string(std::string &package)>;
// TCP服务器类
class TcpServer
{
public:
// 构造函数:初始化端口和回调函数
TcpServer(uint16_t port, func_t callback) : port_(port), callback_(callback)
{
}
// 初始化服务器
bool InitServer()
{
listensock_.Socket(); // 创建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)
{
// 接受新的客户端连接
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(); // 子进程关闭监听socket
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); // 父进程关闭客户端socket
}
}
~TcpServer()
{
}
private:
uint16_t port_; // 服务器端口
Sock listensock_; // 监听socket
func_t callback_; // 处理请求的回调函数
};
7. ClientCal.cc-计算器客户端
ClientCal.cc
实现客户端程序,发送计算请求
#include <iostream>
#include <string>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"
// 打印使用方法
static void Usage(const std::string &proc)
{
std::cout << "\nUsage: " << proc << " serverip serverport\n"
<< std::endl;
}
// 客户端主程序:./clientcal ip port
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]);
// 创建并连接Socket
Sock sockfd;
sockfd.Socket();
bool r = sockfd.Connect(serverip, serverport);
if(!r) return 1;
// 初始化随机数种子(使用时间和进程ID)
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 resp;
r = resp.Deserialize(content);
assert(r); // 确保反序列化成功
// 打印响应结果
resp.DebugPrint();
}
std::cout << "=================================================" << std::endl;
sleep(1); // 延时1秒
cnt++;
}
// 关闭连接
sockfd.Close();
return 0;
}
8. ServerCal.cc-计算器服务器
ServerCal.cc
实现服务器程序,处理客户端请求
#include "TcpServer.hpp"
#include "ServerCal.hpp"
#include <unistd.h>
// #include "Daemon.hpp"
// 打印使用方法
static void Usage(const std::string &proc)
{
std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}
// 服务器主程序:./servercal 8080
int main(int argc, char *argv[])
{
// 检查命令行参数
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
// 获取端口号
uint16_t port = std::stoi(argv[1]);
// 创建计算器服务对象
ServerCal cal;
// 创建TCP服务器对象
// 使用std::bind绑定Calculator方法作为回调函数
TcpServer *tsvp = new TcpServer(port,
std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));
// 初始化服务器
tsvp->InitServer();
// 将进程变成守护进程
// Daemon(); // 自定义守护进程函数
daemon(0, 0); // 系统提供的守护进程函数
// 第一个参数0:切换工作目录到根目录
// 第二个参数0:关闭标准输入输出和错误流
// 启动服务器
tsvp->Start();
/* 以下是测试代码,已注释
// 测试响应序列化和反序列化
Response resp(1000, 0);
std::string content;
resp.Serialize(&content);
std::cout << content << std::endl;
std::string package = Encode(content);
std::cout << package;
content = "";
bool r = Decode(package, &content);
std::cout << content << std::endl;
Response temp;
temp.Deserialize(content);
std::cout << temp.result << std::endl;
std::cout << temp.code << std::endl;
// 测试请求序列化和反序列化
Request req(12364566, 43454356, '+');
std::string s;
req.Serialize(&s);
s = Encode(s);
std::cout << s;
std::string content;
bool r = Decode(s, &content);
std::cout << content << std::endl;
Request temp;
temp.Deserialize(content);
std::cout << temp.x << std::endl;
std::cout << temp.op << std::endl;
std::cout << temp.y << std::endl;
*/
return 0;
}
9. 代码时序
1. 服务器启动时序
ServerCal.cc (主程序)
↓
1. 解析命令行参数(端口号)
↓
2. 创建ServerCal对象
↓
3. 创建TcpServer对象
|→ 绑定Calculator回调函数
↓
4. 初始化服务器(InitServer)
|→ 创建Socket
|→ 绑定端口
|→ 开始监听
↓
5. 守护进程化
|→ 后台运行
|→ 重定向标准IO
↓
6. 启动服务器(Start)
|→ 注册信号处理
|→ 进入主循环
2. 客户端连接时序
TcpServer::Start (主循环)
↓
1. Accept等待连接
↓
2. 收到新连接
|→ 获取客户端信息(IP/端口)
|→ 记录连接日志
↓
3. Fork子进程
|→ 子进程:处理客户端请求
|→ 父进程:继续Accept新连接
3. 请求处理时序
子进程处理流程
↓
1. 读取客户端数据
|→ 追加到输入缓冲区
↓
2. 解析协议(Protocol::Decode)
|→ 提取消息长度
|→ 检查完整性
↓
3. 调用回调函数(Calculator)
|→ 反序列化请求
|→ 执行计算
|→ 序列化响应
↓
4. 发送响应
|→ 编码响应包
|→ 写入socket
4. 完整的请求-响应时序
客户端 服务器 子进程
| | |
|------ 连接请求 ------>| |
| |--- fork() ------------->|
| | |
|------ 计算请求 ----------------------→ |
| | |
| | 1. 解析请求 |
| | 2. 执行计算 |
| | 3. 构造响应 |
| | |
|<----- 计算结果 ---------------------- |
| | |
|------ 关闭连接 ------>| |
| | |
5. 数据处理时序
Request数据流
↓
1. 序列化(Serialize)
|→ JSON格式或自定义格式
↓
2. 协议封装(Encode)
|→ 添加长度和分隔符
↓
3. 网络传输
|→ write/read
↓
4. 协议解析(Decode)
|→ 提取有效载荷
↓
5. 反序列化(Deserialize)
|→ 还原对象数据
6. 日志记录时序
Log系统
↓
1. 生成日志内容
|→ 时间戳
|→ 日志级别
|→ 具体信息
↓
2. 根据配置输出
|→ 屏幕显示
|→ 单一文件
|→ 分级文件
7. 资源释放时序
程序退出流程
↓
1. 子进程退出
|→ 关闭客户端socket
|→ exit(0)
↓
2. 父进程清理
|→ SIGCHLD信号处理
|→ 僵尸进程回收
这种时序设计的优点:
- 多进程并发处理请求
- 父子进程职责明确
- 协议设计清晰
- 资源管理完善
- 错误处理周到
主要的时序特点是采用了经典的多进程并发服务器模型,每个客户端连接由独立的子进程处理,保证了请求处理的隔离性和可靠性。