目录
一、序列化与反序列化
1、序列化(Serialization)
2、反序列化(Deserialization)
3、Linux环境中的应用实例
二、实现网络版计算器
Sock.hpp
TcpServer.hpp
Jsoncpp库
Protocol.hpp
类 Request
类 Response
辅助函数
Daemon.hpp
CalServer.cc
CalClient.cc
makefile
一、序列化与反序列化
在Linux网络编程中,序列化与反序列化是处理结构化数据在网络传输过程中编码与解码的核心机制。
1、序列化(Serialization)
定义与目的: 序列化是指将程序中的复杂数据结构(如对象、数组、结构体等)转换为一种便于在网络中传输或持久化存储的格式,通常是字节序列(二进制数据)。这一过程旨在解决不同系统间数据交换的兼容性问题,确保发送方和接收方能够以统一且准确的方式理解所传输的数据。
实现方式: 在Linux环境下,序列化可以通过以下几种常见方法来实现:
-
文本格式:如JSON、XML、YAML等。这些格式易于阅读和编辑,适用于跨语言交互和人机接口。但它们通常比二进制格式占用更多空间,序列化和反序列化速度较慢。
-
二进制格式:如Protocol Buffers(Protobuf)、Apache Thrift、MessagePack、FlatBuffers等。这些格式紧凑高效,适合高性能、低延迟的网络通信,但不如文本格式直观易读。
-
语言特定序列化库:如Java的
java.io.Serializable
接口、Python的pickle模块、C++的boost::serialization库等。这些库针对特定语言设计,提供了便捷的序列化和反序列化功能。 -
自定义协议:开发人员可以自行设计一套二进制或文本协议,规定数据字段的排列顺序、长度、类型标识等,然后编写相应的序列化和反序列化函数来处理数据。
序列化过程:
- 对象遍历:对要序列化的对象进行深度遍历,访问其所有属性和嵌套结构。
- 类型转换:将对象属性值(如字符串、整数、浮点数、布尔值、枚举、日期等)转换为字节表示。
- 编码:按照选定的序列化格式或协议,将转换后的字节数据组织起来,可能包括添加字段标识符、长度前缀、校验和等额外信息。
- 输出:将最终形成的字节序列写入网络套接字(socket)或文件,以便传输或存储。
2、反序列化(Deserialization)
定义与目的: 反序列化是序列化的逆过程,即将从网络接收的字节序列或从存储介质读取的二进制数据还原为程序内部可直接使用的数据结构。它的目的是确保接收到的数据能够正确地重新构建为原始对象,保持数据的完整性和一致性。
实现方式: 反序列化同样依赖于所选的序列化格式或协议:
- 解析字节流:从网络套接字或文件中读取字节序列。
- 类型检测与解析:根据协议规范,识别各个字段的类型标识、长度等信息,从字节流中提取对应的数据。
- 类型转换:将解析出的字节数据转换回原对象属性应有的数据类型(如字符串转回字符串,整数转回整数等)。
- 对象重建:根据数据字段的顺序和嵌套关系,将转换后的数据填充到目标数据结构(如对象、数组、结构体)中。
反序列化过程中的安全考量:
- 数据验证:检查接收到的数据是否符合协议规范,如字段数量、类型、长度范围等,防止因恶意或损坏的数据导致程序崩溃或安全漏洞。
- 输入净化:对反序列化过程中产生的字符串或其他可变类型进行安全处理,避免注入攻击。
- 版本兼容:处理不同版本间的数据格式差异,确保旧版本程序能正确解析新版本数据,或者新版本程序能向下兼容旧版本数据。
3、Linux环境中的应用实例
在Linux下使用C++进行网络编程时,可能会涉及以下步骤:
- 使用
socket()
、bind()
、listen()
、accept()
等系统调用创建并配置TCP服务器。 - 定义数据结构(如结构体)来描述要传输的对象。
- 选择或设计序列化协议,并编写序列化函数,将数据结构转换为字节序列。
- 在服务器端的
accept()
回调中,使用read()
或recv()
从套接字接收字节序列。 - 调用反序列化函数,将接收到的字节序列还原为数据结构。
- 对数据进行处理后,按需调用序列化函数将响应数据编码为字节序列。
- 使用
write()
或send()
将响应数据发送回客户端。 - 客户端执行类似操作,接收响应数据并进行反序列化。
综上所述,序列化和反序列化是Linux网络编程中不可或缺的部分,它们确保了不同系统、进程或网络节点之间能够准确无误地交换结构化数据。选择合适的序列化格式和库,并妥善处理安全性问题是实现高效、可靠网络通信的关键。
二、实现网络版计算器
Sock.hpp
这个 Sock
类封装了创建、配置、监听以及连接 TCP 套接字的基本操作。以下是对其各个成员函数的详细解释:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "Log.hpp"
// 定义一个名为 Sock 的类,用于封装 TCP 套接字的相关操作
class Sock
{
private:
// 定义一个静态常量,表示服务器监听套接字的连接请求队列最大长度
const static int gbacklog = 20;
public:
// 默认构造函数,不执行任何操作
Sock() {}
// 创建一个基于 IPv4 的 TCP 套接字,并返回套接字描述符
int Socket()
{
int listensock = socket(AF_INET, SOCK_STREAM, 0);
if (listensock < 0)
{
// 记录 FATAL 级别的日志消息,并附带错误号和错误描述
logMessage(FATAL, "创建套接字错误,%d:%s", errno, strerror(errno));
// 程序遇到严重错误,退出并返回错误码 2
exit(2);
}
// 记录 NORMAL 级别的日志消息,显示成功创建的套接字描述符
logMessage(NORMAL, "创建套接字成功,listensock: %d", listensock);
// 返回创建的套接字描述符
return listensock;
}
// 将指定套接字绑定到指定的端口和 IP 地址(默认为 0.0.0.0,监听所有本地接口)
void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
{
struct sockaddr_in local;
// 初始化 sockaddr_in 结构体,用于存储本地地址信息
memset(&local, 0, sizeof local);
local.sin_family = AF_INET; // 设置地址族为 IPv4
local.sin_port = htons(port); // 将端口号转换为网络字节序并存入结构体
inet_pton(AF_INET, ip.c_str(), &local.sin_addr); // 将 IP 地址字符串转换为二进制并存入结构体
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
// 记录 FATAL 级别的日志消息,并附带错误号和错误描述
logMessage(FATAL, "绑定错误,%d:%s", errno, strerror(errno));
// 程序遇到严重错误,退出并返回错误码 3
exit(3);
}
}
// 将指定套接字设置为监听状态,开始接受客户端连接请求
void Listen(int sock)
{
if (listen(sock, gbacklog) < 0)
{
// 记录 FATAL 级别的日志消息,并附带错误号和错误描述
logMessage(FATAL, "监听错误,%d:%s", errno, strerror(errno));
// 程序遇到严重错误,退出并返回错误码 4
exit(4);
}
// 记录 NORMAL 级别的日志消息,表示服务器初始化成功
logMessage(NORMAL, "初始化服务器成功");
}
// 从指定监听套接字接受一个客户端连接请求,返回新建立的连接套接字描述符,
// 并可选地填充客户端的 IP 地址和端口号
int Accept(int listensock, std::string *ip, uint16_t *port)
{
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
if (servicesock < 0)
{
// 记录 ERROR 级别的日志消息,并附带错误号和错误描述
logMessage(ERROR, "接受连接错误,%d:%s", errno, strerror(errno));
// 返回错误码 -1
return -1;
}
// 如果指针非空,将客户端端口号从网络字节序转换为主机字节序并赋值
if (port) *port = ntohs(src.sin_port);
// 如果指针非空,将客户端 IP 地址从二进制转换为点分十进制字符串并赋值
if (ip) *ip = inet_ntoa(src.sin_addr);
// 返回新建立的连接套接字描述符
return servicesock;
}
// 使用指定套接字连接到指定的服务器 IP 地址和端口号
bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
{
struct sockaddr_in server;
// 初始化 sockaddr_in 结构体,用于存储服务器地址信息
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET; // 设置地址族为 IPv4
server.sin_port = htons(server_port); // 将端口号转换为网络字节序并存入结构体
server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // 将 IP 地址字符串转换为二进制并存入结构体
// 尝试建立连接,如果成功(返回值为 0),返回 true;否则返回 false
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
return true;
else
return false;
}
// 析构函数,当前为空,不执行任何操作
~Sock() {}
};
构造函数 Sock()
Sock() {}
这是一个默认构造函数,不接受任何参数,也不做任何初始化工作。它的主要作用是创建一个空的 Sock
对象。
成员函数 int Socket()
int Socket()
{
int listensock = socket(AF_INET, SOCK_STREAM, 0);
if (listensock < 0)
{
logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "create socket success, listensock: %d", listensock);
return listensock;
}
此函数负责创建一个基于 IPv4 的 TCP 套接字。参数:
AF_INET
: 表示使用 IPv4 地址族。SOCK_STREAM
: 指定套接字类型为面向连接的流套接字(TCP)。
如果 socket()
系统调用失败(返回值小于 0),函数会记录一条 FATAL 级别的日志消息,包含错误号(errno
)和对应的错误描述(strerror(errno)
),然后调用 exit(2)
终止程序。否则,它记录一条 NORMAL 级别的日志消息,显示成功创建的套接字描述符(listensock
),并将其作为返回值。
成员函数 void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
{
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
exit(3);
}
}
此函数将给定的 sock
套接字绑定到指定的 port
和可选的 ip
(默认为 "0.0.0.0"
,表示监听所有本地接口)。具体步骤如下:
- 初始化
sockaddr_in
结构体local
,用于存储 IP 地址和端口信息。 - 设置
local.sin_family
为AF_INET
(IPv4 地址族)。 - 将给定的
port
转换为网络字节序(大端序)并存入local.sin_port
。 - 使用
inet_pton()
函数将字符串形式的ip
转换为二进制 IP 地址并存入local.sin_addr
。
如果 bind()
系统调用失败,函数同样记录一条 FATAL 级别的日志消息并以 exit(3)
终止程序。
成员函数 void Listen(int sock)
void Listen(int sock)
{
if (listen(sock, gbacklog) < 0)
{
logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "init server success");
}
此函数将给定的 sock
套接字设置为监听状态,允许它接受来自客户端的连接请求。参数 gbacklog
(常量值为 20)表示同时可排队的最大连接请求数量。如果 listen()
系统调用失败,函数记录 FATAL 级别日志并终止程序。成功后,记录一条 NORMAL 级别的日志消息,表示服务器初始化成功。
成员函数 int Accept(int listensock, std::string *ip, uint16_t *port)
int Accept(int listensock, std::string *ip, uint16_t *port)
{
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
if (servicesock < 0)
{
logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
return -1;
}
if (port) *port = ntohs(src.sin_port);
if (ip) *ip = inet_ntoa(src.sin_addr);
return servicesock;
}
此函数从给定的监听套接字 listensock
接受一个客户端连接请求,返回一个新的已连接套接字 servicesock
。同时,如果传入了非空指针 ip
和/或 port
,函数将填充客户端的 IP 地址和端口号。
具体步骤如下:
- 初始化
sockaddr_in
结构体src
用于存储客户端信息。 - 调用
accept()
系统调用,接受一个连接请求并返回新的套接字描述符。如果出错,记录 ERROR 级别日志并返回 -1。 - 如果指针
port
非空,将接收到的客户端端口号从网络字节序转换为主机字节序(小端序)并赋值给*port
。 - 如果指针
ip
非空,使用inet_ntoa()
函数将接收到的客户端二进制 IP 地址转换为点分十进制字符串形式并赋值给*ip
。 - 最后返回新建立的连接套接字
servicesock
。
成员函数 bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
return true;
else
return false;
}
此函数使用给定的 sock
套接字连接到指定的 server_ip
和 server_port
。具体步骤如下:
- 初始化
sockaddr_in
结构体server
存储服务器信息。 - 设置
server.sin_family
为AF_INET
。 - 将给定的
server_port
转换为网络字节序并存入server.sin_port
。 - 使用
inet_addr()
函数将字符串形式的server_ip
转换为二进制 IP 地址并存入server.sin_addr.s_addr
。 - 调用
connect()
系统调用尝试建立连接。如果连接成功(返回值为 0),函数返回true
;否则返回false
。
析构函数 ~Sock()
~Sock() {}
这是一个空的析构函数,不执行任何操作。由于 Sock
类本身并不直接管理任何资源,因此不需要在析构函数中释放任何资源。如果有需要,可以在类中添加成员变量(如套接字描述符)并在此处关闭或释放相关资源。
TcpServer.hpp
这个代码定义了一个名为 ns_tcpserver
的命名空间,其中包含两个类:ThreadData
和 TcpServer
。TcpServer
类封装了一个简单的多线程 TCP 服务器,它可以监听指定端口上的连接请求,并在新线程中为每个客户端连接执行用户提供的服务函数。
#pragma once
#include "Sock.hpp"
#include <vector>
#include <functional>
#include <pthread.h>
namespace ns_tcpserver
{
// 定义一个类型别名,用于表示接受一个整型参数(int)的可调用对象,如函数指针、lambda表达式等
using func_t = std::function<void(int)>;
// ThreadData 类用于封装线程所需的数据,包括与客户端连接的套接字描述符(sock_)和指向 TcpServer 实例的指针(server_)
class ThreadData
{
public:
// 构造函数,接收客户端连接的套接字描述符和指向 TcpServer 的指针
ThreadData(int sock, TcpServer *server) : sock_(sock), server_(server) {}
// 默认析构函数,不需要额外资源清理
~ThreadData() {}
// 成员变量:
// - sock_:存储与客户端连接的套接字描述符
// - server_:指向 TcpServer 实例的指针,用于调用 TcpServer 的方法
int sock_;
TcpServer *server_;
};
// TcpServer 类实现了 TCP 服务器的基本功能,包括监听端口、接收客户端连接、处理连接请求等
class TcpServer
{
private:
// ThreadRoutine 是一个静态成员函数,作为线程入口函数,处理每个客户端连接
static void *ThreadRoutine(void *args)
{
// 使线程在完成工作后自动解除关联,防止资源泄漏
pthread_detach(pthread_self());
// 解析参数,将 void* 类型的 args 强制转换为 ThreadData 类型指针
ThreadData *td = static_cast<ThreadData *>(args);
// 调用 TcpServer 实例的 Excute 方法,处理客户端连接
td->server_->Excute(td->sock_);
// 关闭与客户端的连接
close(td->sock_);
// 释放 ThreadData 对象占用的内存
delete td;
// 返回 nullptr,表示线程完成
return nullptr;
}
public:
// TcpServer 构造函数,接收监听端口和可选的 IP 地址(默认为 "0.0.0.0")
TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0")
{
// 创建监听套接字
listensock_ = sock_.Socket();
// 绑定监听套接字到指定的 IP 地址和端口
sock_.Bind(listensock_, port, ip);
// 开始监听连接请求
sock_.Listen(listensock_);
}
// BindService 方法用于注册一个回调函数(func_t 类型),当有新的客户端连接时,该函数将被调用
void BindService(func_t func)
{
// 将回调函数添加到 func_ 容器中
func_.push_back(func);
}
// Excute 方法负责调用所有已注册的回调函数,处理客户端连接
void Excute(int sock)
{
// 遍历注册的回调函数,并依次调用,传入客户端连接的套接字描述符
for (auto &f : func_)
{
f(sock);
}
}
// Start 方法启动 TCP 服务器,进入无限循环,持续接收客户端连接并创建新线程处理
void Start()
{
// 循环监听客户端连接请求
for (;;)
{
std::string clientip;
uint16_t clientport;
// 接受新的客户端连接,返回与客户端连接的套接字描述符
int sock = sock_.Accept(listensock_, &clientip, &clientport);
// 如果接收到无效的套接字描述符(如 -1),跳过此次循环
if (sock == -1)
continue;
// 记录日志,表示新连接建立成功
logMessage(NORMAL, "create new link success, sock: %d", sock);
// 创建一个新的 ThreadData 对象,封装客户端连接信息和指向 TcpServer 的指针
ThreadData *td = new ThreadData(sock, this);
// 创建新线程,将 ThreadRoutine 函数和新建的 ThreadData 对象作为参数传入
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, td);
}
}
// TcpServer 析构函数,关闭监听套接字,防止资源泄漏
~TcpServer()
{
if (listensock_ >= 0)
close(listensock_);
}
private:
// 成员变量:
// - listensock_:存储监听套接字的描述符
// - sock_:封装与网络通信相关的底层操作接口(Sock 类)
// - func_:存储已注册的回调函数,用于处理客户端连接
int listensock_;
Sock sock_;
std::vector<func_t> func_;
};
}
命名空间 ns_tcpserver
namespace ns_tcpserver
{
// ...
}
这个命名空间用于组织相关类和函数,避免与其他代码中的同名实体冲突。
类型别名 func_t
using func_t = std::function<void(int)>;
定义了一个类型别名 func_t
,表示一个接受一个整数参数(客户端套接字描述符)且无返回值的可调用对象。这个类型将用于存储用户提供的服务函数。
类 ThreadData
ThreadData
类用于封装线程相关数据,其目的是为了传递必要的上下文信息给线程执行例程ThreadRoutine
。
class ThreadData
{
public:
ThreadData(int sock, TcpServer *server):sock_(sock), server_(server)
{}
~ThreadData() {}
public:
int sock_;
TcpServer *server_;
};
ThreadData
类存在的必要性:
-
封装线程所需数据:
ThreadData
类封装了两个关键成员变量:int sock_
: 存储了与客户端建立连接后的套接字描述符。这个描述符是线程需要处理的实际网络连接,用于读写数据。TcpServer *server_
: 指向TcpServer
实例的指针,使得线程能够访问服务器对象的方法和成员,如Excute()
函数。
这种封装方式简化了线程启动时的数据传递,只需传递一个
ThreadData
对象的指针即可,避免了直接传递多个独立参数给线程创建函数。 -
线程安全性和生命周期管理: 由于
ThreadData
类的对象由TcpServer
创建并在ThreadRoutine
中使用,其生命周期与线程的执行周期紧密关联。通过new
操作符动态创建ThreadData
对象,并将其作为参数传递给pthread_create()
,确保了在线程执行期间该对象始终有效。当线程执行结束时,ThreadData
对象在ThreadRoutine
末尾通过delete
释放。虽然实际代码中注释掉了delete
语句,但在实际应用中应确保正确释放资源以防止内存泄漏。 -
结构清晰、易于维护: 使用
ThreadData
类将与线程执行相关的数据组织在一起,使得代码逻辑更清晰。
类 TcpServer
class TcpServer
{
// ...
private:
int listensock_;
Sock sock_;
std::vector<func_t> func_;
};
TcpServer
类实现了多线程 TCP 服务器的主要逻辑,包括创建监听套接字、绑定端口、监听连接、处理客户端请求以及线程管理。
listensock_
:存储服务器监听套接字的文件描述符,用于监听和接收客户端连接请求。sock_
:封装了与网络通信相关的底层操作,为TcpServer
类提供便捷的网络操作接口。func_
:存储了一系列可调用对象,用于在处理客户端连接时执行特定的操作。func_
用于存储一系列可调用对象(如函数指针、lambda表达式、std::bind产生的对象等),它们都接受一个整型参数(int
类型)。当新的客户端连接到来时,TcpServer
类的Excute()
方法遍历func_
容器,并依次调用其中的每个可调用对象,将接收到的套接字描述符作为参数传递。func_
成员变量的存在允许TcpServer
类灵活地注册和执行多个回调函数,这些函数将在处理客户端连接时执行特定的操作。
私有成员函数 static void *ThreadRoutine(void *args)
static void *ThreadRoutine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
td->server_->Excute(td->sock_);
close(td->sock_);
// delete td;
return nullptr;
}
ThreadRoutine
是一个静态成员函数,作为线程入口函数。它接收一个指向 ThreadData
对象的指针作为参数。函数执行以下操作:
- 使用
pthread_detach()
使线程在结束后自动回收资源,无需显式调用pthread_join()
。 - 将输入的
void *
参数转换回ThreadData *
类型,并访问其成员。 - 调用
TcpServer
实例的Excute()
方法,传入客户端套接字描述符,执行用户提供的服务函数。 - 关闭已处理完的客户端套接字。
- (注释掉的)删除
ThreadData
对象。实际上,由于ThreadData
对象由new
分配,这里应该删除它以避免内存泄漏。但在当前实现中,注释掉了这一行,可能导致内存泄漏。
构造函数 TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0")
TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0")
{
listensock_ = sock_.Socket();
sock_.Bind(listensock_, port, ip);
sock_.Listen(listensock_);
}
构造函数接受端口号和可选的 IP 地址(默认为 "0.0.0.0"
,监听所有本地接口)。它创建一个 Sock
对象并调用其 Socket()
、Bind()
和 Listen()
方法,设置服务器监听指定端口的连接请求。
公共成员函数 void BindService(func_t func)
void BindService(func_t func)
{
func_.push_back(func);
}
此方法用于注册用户提供的服务函数。每当新客户端连接时,这些函数将在新线程中按顺序执行。将服务函数以 func_t
类型存储在 func_
成员变量(std::vector<func_t>
)中。
公共成员函数 void Excute(int sock)
void Excute(int sock)
{
for(auto &f : func_)
{
f(sock);
}
}
Excute()
方法用于在一个客户端连接上执行所有已注册的服务函数。遍历 func_
中的所有函数,并对每个函数调用一次,传入客户端套接字描述符作为参数。
公共成员函数 void Start()
void Start()
{
for (;;)
{
std::string clientip;
uint16_t clientport;
int sock = sock_.Accept(listensock_, &clientip, &clientport);
if (sock == -1)
continue;
logMessage(NORMAL, "create new link success, sock: %d", sock);
pthread_t tid;
ThreadData *td = new ThreadData(sock, this);
pthread_create(&tid, nullptr, ThreadRoutine, td);
}
}
Start()
方法启动服务器主循环,不断接受新的客户端连接并为每个连接创建一个新线程。具体步骤如下:
- 无限循环等待客户端连接。
- 使用
sock_.Accept()
接收一个连接请求,获取客户端套接字描述符、IP 地址和端口号。若接收到错误,跳过本次循环继续等待。 - 记录一条 NORMAL 级别的日志消息,显示成功创建的新连接套接字描述符。
- 创建一个
ThreadData
对象,存储客户端套接字描述符和指向TcpServer
实例的指针。 - 调用
pthread_create()
创建新线程,传入ThreadRoutine
作为线程入口函数,以及新建的ThreadData
对象作为参数。
析构函数 ~TcpServer()
~TcpServer()
{
if (listensock_ >= 0)
close(listensock_);
}
析构函数确保在 TcpServer
对象销毁时关闭监听套接字,释放系统资源。
总结
ns_tcpserver
命名空间内定义了 ThreadData
和 TcpServer
类,实现了一个多线程 TCP 服务器。TcpServer
类负责监听指定端口、接受客户端连接、创建新线程执行用户提供的服务函数,并在析构时关闭监听套接字。ThreadData
类用于传递客户端套接字描述符和 TcpServer
实例指针给新线程。用户可以通过 BindService()
方法注册服务函数,并调用 Start()
方法启动服务器。注意,当前实现存在内存泄漏问题,应在 ThreadRoutine()
中删除 ThreadData
对象。
Jsoncpp库
Jsoncpp 是一个 C++ 库,用于处理 JSON (JavaScript Object Notation) 数据格式。它提供了简洁易用的 API 来实现 JSON 数据的序列化(将 C++ 对象或数据结构转换为 JSON 字符串)和反序列化(将 JSON 字符串解析为 C++ 对象或数据结构)。
1. 引入Jsoncpp库
首先,确保已经安装了Jsoncpp库,并在你的C++项目中正确包含了必要的头文件和链接了相应的库文件。通常,你需要包含 json/json.h
头文件,并在编译时链接 libjsoncpp
库。
2. JSON值对象(Json::Value)
Jsoncpp 中的核心数据结构是 Json::Value
类,它能够表示任何JSON类型(如对象、数组、字符串、数字、布尔值和null)。序列化和反序列化操作主要围绕这个类进行。
3. 序列化(将C++数据转换为JSON字符串)
使用Json::FastWriter或Json::StyledWriter
Jsoncpp 提供了两种不同的序列化工具类:Json::FastWriter
和 Json::StyledWriter
。它们都实现了将 Json::Value
对象转换为 JSON 格式的字符串的方法。
-
Json::FastWriter:生成紧凑的、无空格的 JSON 字符串,适合网络传输等对效率要求较高的场景。
-
Json::StyledWriter:生成带有缩进和换行的可读性更好的 JSON 字符串,适合日志输出或人眼阅读。
4、示例:
- 这段C++代码使用JsonCpp库来演示了创建、修改、嵌套和输出JSON数据的基本操作。
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
int a = 10;
int b = 20;
char c = '+';
Json::Value root;
root["aa"] = a;
root["bb"] = b;
root["op"] = c;
Json::Value sub;
sub["other"] = 200;
sub["other1"] = "hello";
root["sub"] = sub;
Json::StyledWriter writer;
// Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
}
首先,包含必要的头文件:
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
<jsoncpp/json/json.h>
引入JsonCpp库的头文件,提供JSON操作所需的类和函数。
定义一些基础数据类型变量作为JSON对象的值来源:
int main()
{
int a = 10;
int b = 20;
char c = '+';
int a
和int b
表示整数值。char c
表示字符值,这里用来模拟一个运算符。
使用JsonCpp创建一个JSON对象(Json::Value root
):
Json::Value root;
root["aa"] = a;
root["bb"] = b;
root["op"] = c;
- 初始化空的JSON对象
root
。 - 通过键值对的方式向
root
添加属性:root["aa"] = a;
将整数a
作为值,键为"aa"。root["bb"] = b;
将整数b
作为值,键为"bb"。root["op"] = c;
将字符c
作为值,键为"op"。注意这里将字符直接放入JSON对象中,实际应用中可能需要将其转换为字符串。
创建另一个JSON对象sub
,并添加属性:
Json::Value sub;
sub["other"] = 200;
sub["other1"] = "hello";
root["sub"] = sub;
-
初始化空的JSON对象
sub
。 -
同样通过键值对的方式向
sub
添加属性:sub["other"] = 200;
将整数200作为值,键为"other"。sub["other1"] = "hello";
将字符串"hello"作为值,键为"other1"。
-
将
sub
作为值,通过键"sub"添加到root
对象中,形成嵌套结构。
选择一个JSON写入器(Writer)来格式化输出JSON对象:
Json::StyledWriter writer;
// Json::FastWriter writer;
std::string s = writer.write(root);
-
这里使用
Json::StyledWriter
,它会产生带缩进和换行的美观格式。Json::StyledWriter
会生成带有缩进和换行的美观格式的JSON字符串。对于给定的root
对象,其输出如下:{ "aa": 10, "bb": 20, "op": "+", "sub": { "other": 200, "other1": "hello" } }
-
注释部分提到了另一种选择
Json::FastWriter
,它生成紧凑、无格式的JSON字符串,适用于对效率要求较高的场景。Json::FastWriter
旨在生成紧凑、无格式的JSON字符串,以提高序列化效率。对于相同的root
对象,其输出应类似于:{"aa":10,"bb":20,"op":"+","sub":{"other":200,"other1":"hello"}}
-
实例化选定的写入器
writer
。 -
调用
writer.write(root)
将root
对象转化为格式化的JSON字符串,并赋值给std::string s
。
std::cout << s << std::endl;
最后,使用std::cout
输出JSON字符串s
,并在末尾添加换行符std::endl
,以便在控制台清晰显示。
Protocol.hpp
这段代码定义了一个名为 ns_protocol
的命名空间,其中包含两个类 Request
和 Response
,分别表示客户端与服务器之间的请求和响应消息。同时,该命名空间还提供了一些辅助函数,如 Recv()
、Send()
、Decode()
和 Encode()
,用于处理通信过程中的数据收发和协议解析。
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <jsoncpp/json/json.h>
namespace ns_protocol
{
// #define MYSELF 0
// 定义空格字符及其长度(使用strlen而非sizeof,因为后者会返回整个字符串数组的大小)
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
// 定义消息分隔符及其长度(同样使用strlen)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP)
class Request
{
public:
// 1. 自定义序列化方法,格式为 "length\r\nx_ op_ y_\r\n"
// 2. 使用JsonCpp库进行序列化
std::string Serialize()
{
#ifdef MYSELF// 自定义序列化:拼接字符串并返回
std::string str;
str = std::to_string(x_);
str += SPACE;
str += op_;
str += SPACE;
str += std::to_string(y_);
return str;
#else// 使用JsonCpp序列化:创建Json对象,填充数据,然后使用FastWriter写入字符串并返回
Json::Value root;
root["x"] = x_;
root["y"] = y_;
root["op"] = op_;
Json::FastWriter writer;
return writer.write(root);
#endif
}
// 反序列化字符串 "x_ op_ y_",例如 "1234 + 5678"
bool Deserialized(const std::string &str)
{
#ifdef MYSELF// 自定义反序列化:解析字符串,提取x、y和op值
std::size_t left = str.find(SPACE);
if (left == std::string::npos)
return false;
std::size_t right = str.rfind(SPACE);
if (right == std::string::npos)
return false;
x_ = atoi(str.substr(0, left).c_str());
y_ = atoi(str.substr(right + SPACE_LEN).c_str());
if (left + SPACE_LEN > str.size())
return false;
else
op_ = str[left + SPACE_LEN];
return true;
#else// 使用JsonCpp反序列化:创建Json对象,使用Reader解析字符串,然后从Json对象中提取数据
Json::Value root;
Json::Reader reader;
reader.parse(str, root);
x_ = root["x"].asInt();
y_ = root["y"].asInt();
op_ = root["op"].asInt();
return true;
#endif
}
public:
Request() {} // 默认构造函数
Request(int x, int y, char op) : x_(x), y_(y), op_(op) {} // 构造函数
~Request() {} // 析构函数
public:
//格式: x_ op y_ 或 y_ op x_
int x_; // 未知整数
int y_; // 未知整数
char op_; // 运算符:'+' '-' '*' '/' '%'
};
// 定义响应类,用于封装响应相关的数据和序列化/反序列化方法
class Response
{
public:
// 序列化方法,生成 "code_ result_" 格式的字符串
std::string Serialize()
{
#ifdef MYSELF
std::string s;
s = std::to_string(code_);
s += SPACE;
s += std::to_string(result_);
return s;
#else
Json::Value root;
root["code"] = code_;
root["result"] = result_;
root["xx"] = x_;
root["yy"] = y_;
root["zz"] = op_;
Json::FastWriter writer;
return writer.write(root);
#endif
}
// 反序列化字符串 "code_ result_",例如 "111 100"
bool Deserialized(const std::string &s)
{
#ifdef MYSELF
std::size_t pos = s.find(SPACE);
if (pos == std::string::npos)
return false;
code_ = atoi(s.substr(0, pos).c_str());
result_ = atoi(s.substr(pos + SPACE_LEN).c_str());
return true;
#else
Json::Value root;
Json::Reader reader;
reader.parse(s, root);
code_ = root["code"].asInt();
result_ = root["result"].asInt();
x_ = root["xx"].asInt();
y_ = root["yy"].asInt();
op_ = root["zz"].asInt();
return true;
#endif
}
public:
Response() {} // 默认构造函数
Response(int result, int code, int x, int y, char op)
: result_(result), code_(code), x_(x), y_(y), op_(op) {} // 构造函数
~Response() {} // 析构函数
public:
// result_:计算结果
// code_:计算结果的状态码(例如:0, 1, 2, 3)
int result_;
int code_;
int x_;
int y_;
char op_;
};
// 接收完整的报文
bool Recv(int sock, std::string *out)
{
// 创建缓冲区接收数据
char buffer[1024];
// 使用recv接收数据
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 例如:接收到 "9\r\n123+789\r\n"
if (s > 0)
{
buffer[s] = 0;
*out += buffer; // 将接收到的数据添加到输出字符串中
}
else if (s == 0)
{
return false;
}
else
{
// std::cout << "recv error" << std::endl;
return false;
}
return true;
}
void Send(int sock, const std::string str)
{
int n = send(sock, str.c_str(), str.size(), 0);
if (n < 0)
std::cout << "send error" << std::endl;
}
// 解码缓冲区中的报文,格式为 "length\r\nmessage\r\n..."
// 示例:解码 "10\r\nabc"
std::string Decode(std::string &buffer)
{
std::size_t pos = buffer.find(SEP);
if(pos == std::string::npos) return ""; // 未找到分隔符,返回空字符串
int size = atoi(buffer.substr(0, pos).c_str()); // 提取长度字段
int surplus = buffer.size() - pos - 2*SEP_LEN; // 计算剩余缓冲区长度
if(surplus >= size) // 如果剩余缓冲区长度大于等于报文长度
{
// 至少存在一个完整的报文,可以进行提取
buffer.erase(0, pos + SEP_LEN); // 移除长度字段及分隔符
std::string s = buffer.substr(0, size); // 提取报文
buffer.erase(0, size + SEP_LEN); // 移除已处理的报文及分隔符
return s; // 返回解码后的报文
}
else
{
return ""; // 剩余缓冲区不足以构成一个完整报文,返回空字符串
}
}
// 对字符串进行编码,添加长度前缀和分隔符,格式为 "length\r\nmessage\r\n"
// 示例:编码 "XXXXXX" 为 "123\r\nXXXXXX\r\n"
std::string Encode(std::string &s)
{
std::string new_package = std::to_string(s.size());
new_package += SEP;
new_package += s;
new_package += SEP;
return new_package;
}
}
命名空间 ns_protocol
namespace ns_protocol
{
// ...
}
该命名空间用于组织相关类和函数,避免与其他代码中的同名实体冲突。
预处理器宏定义
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP) // 不能是sizeof!
定义了几个预处理器宏,用于简化代码中对特定字符串和其长度的引用:
SPACE
:表示单个空格字符。SPACE_LEN
:计算SPACE
字符串的长度,即 1。SEP
:表示换行符序列\r\n
。SEP_LEN
:计算SEP
字符串的长度,即 2。
类 Request
Request
类表示客户端发送给服务器的请求消息,包含两个整数值 x_
和 y_
,以及一个运算符 op_
。类中定义了以下几个成员函数:
std::string Serialize()
// 1. 自定义序列化方法,格式为 "length\r\nx_ op_ y_\r\n"
// 2. 使用JsonCpp库进行序列化
std::string Serialize()
{
#ifdef MYSELF// 自定义序列化:拼接字符串并返回
std::string str;
str = std::to_string(x_);
str += SPACE;
str += op_;
str += SPACE;
str += std::to_string(y_);
return str;
#else// 使用JsonCpp序列化:创建Json对象,填充数据,然后使用FastWriter写入字符串并返回
Json::Value root;
root["x"] = x_;
root["y"] = y_;
root["op"] = op_;
Json::FastWriter writer;
return writer.write(root);
#endif
}
该函数将 Request
对象序列化为字符串形式,以便通过网络传输。根据 MYSELF
宏定义的不同,可以选择两种序列化方式:
-
自主实现:按照格式
"x_ op_ y_"
(例如"1234 + 5678"
)生成字符串。 -
使用JsonCpp库:将
x_
、y_
和op_
作为键值对放入 JSON 对象,然后使用Json::FastWriter
将 JSON 对象写为字符串。
bool Deserialized(const std::string &str)
bool Request::Deserialized(const std::string &str)
{
#ifdef MYSELF
// 自定义反序列化逻辑...
#else
Json::Value root;
Json::Reader reader;
reader.parse(str, root);
x_ = root["x"].asInt();
y_ = root["y"].asInt();
op_ = root["op"].asInt();
return true;
#endif
}
该函数将接收到的字符串反序列化为 Request
对象。同样,根据 MYSELF
宏定义的不同,选择两种反序列化方式:
-
自主实现:从输入字符串中解析出
x_
、y_
和op_
的值,要求字符串格式为"x_ op_ y_"
。 -
使用JsonCpp库:使用
Json::Reader
解析输入字符串为 JSON 对象,调用reader.parse(str, root)
方法,将传入的JSON字符串str
解析到root
对象中。如果解析成功,此方法返回true
;否则,返回false
,表示解析失败。然后从 JSON 对象中提取x_
、y_
和op_
的值。
类 Response
Response
类表示服务器发送给客户端的响应消息,包含计算结果 result_
、状态码 code_
,以及可能的附加信息 x_
、y_
和 op_
。类中同样定义了 Serialize()
和 Deserialized()
函数,功能与 Request
类中的类似,但格式不同。
辅助函数
*bool Recv(int sock, std::string out)
作用:Recv
函数负责从指定的套接字 (sock
) 中接收数据,并将接收到的数据添加到传入的字符串指针 (out
) 所指向的对象中。
// 接收完整的报文
bool Recv(int sock, std::string *out)
{
// 创建缓冲区接收数据
char buffer[1024];
// 使用recv接收数据
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 例如:接收到 "9\r\n123+789\r\n"
if (s > 0)
{
buffer[s] = 0;
*out += buffer; // 将接收到的数据添加到输出字符串中
}
else if (s == 0)
{
return false;
}
else
{
// std::cout << "recv error" << std::endl;
return false;
}
return true;
}
实现细节:
- 创建一个固定大小(1024字节)的缓冲区
char buffer[1024]
用于存储接收到的数据。 - 调用
recv
函数接收数据:- 第一个参数是待接收数据的套接字。
- 第二个参数是接收数据的目标缓冲区。
- 第三个参数是缓冲区的最大可接收字节数(这里为1024-1,留出一个字节用于添加字符串结束符)。
- 第四个参数通常设置为0,表示不使用任何标志位。
- 若
recv
成功返回,其返回值s
表示实际接收到的字节数。将缓冲区最后一个位置置为\0
(字符串结束符),确保接收到的数据作为一个C字符串使用时正确。 - 将接收到的数据追加到
out
字符串中。 - 如果
recv
返回值s
为0,表示连接已关闭,函数返回false
;若s
小于0,表示发生错误,打印错误信息并返回false
。否则,返回true
表示接收数据成功。
void Send(int sock, const std::string str)
作用:Send
函数用于向指定的套接字 (sock
) 发送一个字符串 (str
)。
void Send(int sock, const std::string str)
{
int n = send(sock, str.c_str(), str.size(), 0);
if (n < 0)
std::cout << "send error" << std::endl;
}
实现细节:
- 调用
send
函数发送数据:- 第一个参数是待发送数据的目的套接字。
- 第二个参数是要发送的字符串的C字符串形式(通过
.c_str()
方法获取)。 - 第三个参数是待发送字符串的长度(通过
.size()
方法获取)。 - 第四个参数通常设置为0,表示不使用任何标志位。
- 如果
send
返回值小于0,表示发送失败,打印错误信息。
std::string Decode(std::string &buffer)
作用:Decode
函数用于从给定的输入缓冲区 buffer
中解码一个完整的报文。该程序中,报文格式为 "length\r\nmessage\r\n..."
。
// 解码缓冲区中的报文,格式为 "length\r\nmessage\r\n..."
// 示例:解码 "10\r\nabc"
std::string Decode(std::string &buffer)
{
std::size_t pos = buffer.find(SEP);
if(pos == std::string::npos) return ""; // 未找到分隔符,返回空字符串
int size = atoi(buffer.substr(0, pos).c_str()); // 提取长度字段
int surplus = buffer.size() - pos - 2*SEP_LEN; // 计算剩余缓冲区长度
if(surplus >= size) // 如果剩余缓冲区长度大于等于报文长度
{
// 至少存在一个完整的报文,可以进行提取
buffer.erase(0, pos + SEP_LEN); // 移除长度字段及分隔符
std::string s = buffer.substr(0, size); // 提取报文
buffer.erase(0, size + SEP_LEN); // 移除已处理的报文及分隔符
return s; // 返回解码后的报文
}
else
{
return ""; // 剩余缓冲区不足以构成一个完整报文,返回空字符串
}
}
实现细节:
- 首先查找报文中的分隔符
\r\n
,确定长度字段的位置。 - 若找不到分隔符,返回空字符串,表示无法解码一个完整的报文。
- 提取出长度字段,并将其转换为整数。
- 计算当前缓冲区中剩余的字节数与所需报文长度之间的差值(
surplus
),判断是否足以构成一个完整的报文。 - 若剩余字节数足够,按照报文格式提取出完整的报文,同时更新输入缓冲区
buffer
,移除已处理的部分。 - 返回解码得到的报文。
std::string Encode(std::string &s)
作用:Encode
函数将给定的字符串 s
编码为带有长度前缀和分隔符的完整报文。格式为 "length\r\nmessage\r\n"
。
// 对字符串进行编码,添加长度前缀和分隔符,格式为 "length\r\nmessage\r\n"
// 示例:编码 "XXXXXX" 为 "123\r\nXXXXXX\r\n"
std::string Encode(std::string &s)
{
std::string new_package = std::to_string(s.size());
new_package += SEP;
new_package += s;
new_package += SEP;
return new_package;
}
实现细节:
- 计算字符串
s
的长度,并将其转换为字符串形式。 - 向新字符串
new_package
添加长度字符串、分隔符\r\n
,然后添加原始字符串s
。 - 最后添加另一个分隔符
\r\n
,完成编码过程。 - 返回编码后的报文。
Daemon.hpp
这段C++代码定义了一个名为MyDaemon
的函数,用于将一个普通进程转变为一个守护进程。守护进程是一种在后台运行、脱离终端、不依赖于任何用户交互的特殊进程,通常用于执行系统服务、监控任务等。
#pragma once
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// 定义一个函数 MyDaemon,用于将普通进程转换为守护进程
void MyDaemon()
{
// 1. 忽略特定信号:SIGPIPE 和 SIGCHLD
// SIGPIPE:当进程尝试写入到已断开连接的管道时触发,守护进程通常忽略此信号,避免因意外断开的网络连接而终止。
// SIGCHLD:当子进程终止或停止时发送给其父进程,守护进程通常忽略此信号,以避免僵尸进程积累,并让子进程自动清理。
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
// 2. 使用 fork 创建子进程,并使父进程退出,使子进程成为孤儿进程,由 init 进程接管
// 这样做是为了确保守护进程不是进程组的组长,与原会话和控制终端彻底分离。
if (fork() > 0)
exit(0);
// 3. 调用 setsid 创建新的会话并成为该会话的组长,同时与原控制终端脱离关联
// 守护进程不再受任何终端的控制,真正成为后台进程。
setsid();
// 4. 将标准输入(stdin)、标准输出(stdout)和标准错误(stderr)重定向至 /dev/null
// /dev/null 是一个特殊的设备文件,所有写入的数据都将被丢弃,读取则永远返回空。
// 这样做是为了防止守护进程尝试向终端输出信息(可能导致错误或阻塞),以及避免不必要的输入操作。
int devnull = open("/dev/null", O_RDONLY | O_WRONLY);
if (devnull > 0)
{
// 使用 dup2 将标准输入、输出、错误的文件描述符替换为指向 /dev/null 的文件描述符
dup2(0, devnull); // stdin
dup2(1, devnull); // stdout
dup2(2, devnull); // stderr
// 关闭原始的 /dev/null 文件描述符,保留重定向后的副本
close(devnull);
}
}
详细讲解:
void MyDaemon()
{
// 1. 忽略信号,SIGPIPE,SIGCHLD
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
这部分代码设置对特定信号的处理方式:
SIGPIPE
:当尝试写入到已断开连接的管道时触发。守护进程通常忽略此信号,避免因意外断开的网络连接而终止。SIGCHLD
:当子进程终止或停止时发送给其父进程。守护进程通常忽略此信号,以避免僵尸进程积累,并让子进程自动清理。
// 2. 不要让自己成为组长
if (fork() > 0)
exit(0);
使用fork()
系统调用创建一个子进程。父进程(返回值大于0)立即退出,使得子进程成为一个孤儿进程,由init进程(PID为1)接管。这样做是为了确保守护进程不是进程组的组长,与原会话和控制终端彻底分离。
// 3. 调用setsid
setsid();
调用setsid()
创建一个新的会话并成为该会话的组长,同时与原控制终端脱离关联。这样,守护进程不再受任何终端的控制,真正成为后台进程。
// 4. 标准输入,标准输出,标准错误的重定向,守护进程不能直接向显示器打印消息
int devnull = open("/dev/null", O_RDONLY | O_WRONLY);
if(devnull > 0)
{
dup2(0, devnull);
dup2(1, devnull);
dup2(2, devnull);
close(devnull);
}
}
这部分代码将标准输入(stdin,文件描述符0)、标准输出(stdout,文件描述符1)和标准错误(stderr,文件描述符2)全部重定向至/dev/null
。/dev/null
是一个特殊的设备文件,所有写入的数据都将被丢弃,读取则永远返回空。这样做的目的是防止守护进程尝试向终端输出信息(由于已经与终端脱离关联,这种尝试可能导致错误或阻塞),以及避免不必要的输入操作。
综上所述,MyDaemon
函数通过忽略特定信号、脱离原进程组和会话、创建新会话并成为组长、以及重定向标准输入输出,成功将一个普通进程转化为守护进程,使其能够在后台独立、无干扰地运行。
CalServer.cc
这段代码定义了一个简单的TCP服务器程序,用于接收客户端发送的数学计算请求(加减乘除、取模),执行计算并返回结果。服务器端使用了自定义的ns_tcpserver
和ns_protocol
命名空间中的类与函数。
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Daemon.hpp"
#include <memory>
#include <signal.h>
using namespace ns_tcpserver;
using namespace ns_protocol;
// 输出程序使用说明的辅助函数
static void Usage(const std::string &process)
{
std::cout << "\nUsage: " << process << " port\n"
<< std::endl;
}
// 计算器助手函数,根据请求中的运算符和操作数执行相应计算,并返回响应
static Response calculatorHelper(const Request &req)
{
Response resp(0, 0, req.x_, req.y_, req.op_);
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 (0 == req.y_) // 防止除数为零
resp.code_ = 1; // 设置错误代码
else
resp.result_ = req.x_ / req.y_;
break;
case '%': // 取模
if (0 == req.y_) // 防止除数为零
resp.code_ = 2; // 设置错误代码
else
resp.result_ = req.x_ % req.y_;
break;
default: // 无效运算符
resp.code_ = 3;
break;
}
return resp;
}
// 处理客户端连接的函数,接收请求、执行计算并返回结果
void calculator(int sock)
{
std::string inbuffer;
while (true)
{
// 1. 从套接字读取数据,成功则表示接收到一个请求
bool res = Recv(sock, &inbuffer);
if (!res)
break;
// 2. 解码数据,确保得到一个完整的请求报文
std::string package = Decode(inbuffer);
if (package.empty())
continue;
// 3. 反序列化报文,将字节流转换为结构化的Request对象
Request req;
req.Deserialized(package);
// 4. 调用计算器助手函数执行计算,生成Response对象
Response resp = calculatorHelper(req);
// 5. 序列化计算结果,将其转换为字符串形式
std::string respString = resp.Serialize();
// 6. 添加长度信息,形成完整响应报文
respString = Encode(respString);
// 7. 将响应报文发送回客户端
Send(sock, respString);
}
}
// 注释:已注释掉的信号处理函数
// 主函数,接收命令行参数并启动服务器
int main(int argc, char *argv[])
{
if (argc != 2) // 检查参数数量
{
Usage(argv[0]); // 参数不正确时输出使用说明
exit(1); // 退出程序
}
// 启动守护进程模式
MyDaemon();
// 创建并初始化TCP服务器,监听指定端口
std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));
// 绑定计算器处理函数到服务器,用于处理客户端连接
server->BindService(calculator);
// 启动服务器,开始监听和处理客户端连接
server->Start();
// 注释:已注释掉的测试代码
return 0; // 程序正常结束
}
-
包含头文件:
TcpServer.hpp
:定义了TCP服务器类TcpServer
,负责监听指定端口并处理客户端连接。Protocol.hpp
:包含自定义协议相关的类和函数,如Request
、Response
及其序列化、反序列化方法。Daemon.hpp
:可能包含了使程序以守护进程方式运行的相关功能。<memory>
:用于智能指针std::unique_ptr
的声明。<signal.h>
:包含处理信号的函数,如signal()
。
-
命名空间:
- 使用
ns_tcpserver
和ns_protocol
命名空间中的功能。
- 使用
-
辅助函数:
Usage()
:打印程序的使用说明,提示用户如何正确传入端口号。
-
业务逻辑函数:
calculatorHelper(const Request &req)
:根据请求对象req
中的运算符和操作数执行相应的数学计算,并返回一个Response
对象,其中包含计算结果和状态码。
-
主处理函数:
calculator(int sock)
:处理与客户端的通信。主要步骤如下:- 接收数据:通过
Recv()
函数从给定的套接字sock
中读取客户端发送的数据,并存入inbuffer
字符串。 - 解码报文:使用
Decode()
函数从inbuffer
中提取出一个完整的请求报文(带有长度前缀和分隔符)。 - 反序列化请求:将提取出的请求报文反序列化为
Request
对象req
。 - 执行计算:调用
calculatorHelper()
函数,根据req
执行计算并得到Response
对象resp
。 - 序列化响应:将
resp
对象序列化为字符串respString
。 - 编码响应:使用
Encode()
函数为respString
添加长度前缀和分隔符,形成完整的响应报文。 - 发送响应:通过
Send()
函数将响应报文发送回客户端。
- 接收数据:通过
-
主函数:
- 命令行参数检查:检查命令行参数个数是否为2(程序名和端口号)。若不满足条件,则打印使用说明并退出。
- 启动守护进程:调用
MyDaemon()
函数(未在代码中展示)使程序以守护进程方式运行。 - 创建并配置TCP服务器:
- 创建一个
TcpServer
对象实例,传入命令行参数中的端口号。 - 绑定服务处理函数
calculator
,使其在接收到客户端连接时被调用。 - 调用
Start()
方法启动服务器监听。
- 创建一个
- 注释部分:代码中还包含一些被注释掉的测试代码,用于测试
Request
对象的序列化和反序列化功能。
整个程序的主要流程如下:
- 启动程序,检查命令行参数,确保正确传递了端口号。
- 使程序以守护进程方式运行。
- 创建TCP服务器,监听指定端口。
- 当有客户端连接时,服务器调用
calculator
函数处理连接:- 接收客户端发送的请求报文。
- 解码请求报文,提取完整的请求。
- 反序列化请求为
Request
对象。 - 执行计算,生成
Response
对象。 - 序列化并编码响应,形成完整的响应报文。
- 将响应报文发送回客户端。
- 服务器持续监听并处理后续客户端连接。
CalClient.cc
这段代码实现了一个简单的客户端程序,用于与上述服务器进行交互,执行数学计算。客户端接收用户输入的数学表达式(由操作数和运算符组成),将其序列化后发送至服务器。服务器返回计算结果或错误信息,客户端接收并解析响应,然后输出计算结果或错误消息。客户端程序通过命令行参数指定服务器的IP地址和端口号。程序通过一个循环持续接收用户输入并进行计算,直到用户选择退出。
#include <iostream>
#include "Sock.hpp"
#include "Protocol.hpp"
using namespace ns_protocol;
// 输出程序使用说明的辅助函数
static void Usage(const std::string &process)
{
std::cout << "\nUsage: " << process << " serverIp serverPort\n"
<< std::endl;
}
// 客户端主函数,接收命令行参数并与服务器建立连接进行交互
int main(int argc, char *argv[])
{
if (argc != 3) // 检查参数数量
{
Usage(argv[0]); // 参数不正确时输出使用说明
exit(1); // 退出程序
}
std::string server_ip = argv[1]; // 获取服务器IP地址
uint16_t server_port = atoi(argv[2]); // 获取服务器端口号
Sock sock; // 创建Socket对象
int sockfd = sock.Socket(); // 创建套接字
if (!sock.Connect(sockfd, server_ip, server_port)) // 连接到服务器
{
std::cerr << "Connect error" << std::endl;
exit(2); // 连接失败时退出程序
}
bool quit = false; // 标记是否退出循环
std::string buffer; // 用于临时存储接收的数据
while (!quit) // 循环接收用户输入并发送计算请求,直到用户选择退出
{
// 1. 获取用户输入的计算需求
Request req;
std::cout << "Please Enter # ";
std::cin >> req.x_ >> req.op_ >> req.y_; // 读取操作数和运算符
// 2. 序列化请求对象,将结构化数据转换为字符串
std::string s = req.Serialize();
// 3. 添加长度报头,形成完整请求报文
s = Encode(s);
// 4. 将请求报文发送给服务器
Send(sockfd, s);
// 5. 接收服务器响应
while (true)
{
bool res = Recv(sockfd, &buffer); // 从套接字接收数据
if (!res)
{
quit = true; // 收到错误信号时退出循环
break;
}
std::string package = Decode(buffer); // 解码接收到的数据,获取完整响应报文
if (package.empty()) // 若未接收到完整报文,则继续接收
continue;
Response resp; // 创建响应对象
resp.Deserialized(package); // 反序列化响应报文,填充响应对象
std::string err; // 存储可能的错误消息
switch (resp.code_) // 根据响应中的错误代码判断计算结果
{
case 1: // 除0错误
err = "除0错误";
break;
case 2: // 模0错误
err = "模0错误";
break;
case 3: // 非法操作
err = "非法操作";
break;
default: // 计算成功
std::cout << resp.x_ << resp.op_ << resp.y_ << " = " << resp.result_ << " [success]" << std::endl;
break;
}
if (!err.empty()) // 如果有错误消息,则输出错误信息
std::cerr << err << std::endl;
// sleep(1); // 原代码注释掉了此行,若需要暂停一段时间再接收下一个请求可取消注释
break; // 接收完一个完整响应后跳出内部循环
}
}
close(sockfd); // 关闭套接字
return 0; // 程序正常结束
}
makefile
.PHONY:all
all:client CalServer
client:CalClient.cc
g++ -o $@ $^ -std=c++11 -ljsoncpp
CalServer:CalServer.cc
g++ -o $@ $^ -std=c++11 -ljsoncpp -lpthread
.PHONY:clean
clean:
rm -f client CalServer