Linux 序列化、反序列化、实现网络版计算器

目录

一、序列化与反序列化

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环境下,序列化可以通过以下几种常见方法来实现:

  1. 文本格式:如JSON、XML、YAML等。这些格式易于阅读和编辑,适用于跨语言交互和人机接口。但它们通常比二进制格式占用更多空间,序列化和反序列化速度较慢。

  2. 二进制格式:如Protocol Buffers(Protobuf)、Apache Thrift、MessagePack、FlatBuffers等。这些格式紧凑高效,适合高性能、低延迟的网络通信,但不如文本格式直观易读。

  3. 语言特定序列化库:如Java的java.io.Serializable接口、Python的pickle模块、C++的boost::serialization库等。这些库针对特定语言设计,提供了便捷的序列化和反序列化功能。

  4. 自定义协议:开发人员可以自行设计一套二进制或文本协议,规定数据字段的排列顺序、长度、类型标识等,然后编写相应的序列化和反序列化函数来处理数据。

序列化过程:

  • 对象遍历:对要序列化的对象进行深度遍历,访问其所有属性和嵌套结构。
  • 类型转换:将对象属性值(如字符串、整数、浮点数、布尔值、枚举、日期等)转换为字节表示。
  • 编码:按照选定的序列化格式或协议,将转换后的字节数据组织起来,可能包括添加字段标识符、长度前缀、校验和等额外信息。
  • 输出:将最终形成的字节序列写入网络套接字(socket)或文件,以便传输或存储。

2、反序列化(Deserialization)

定义与目的: 反序列化是序列化的逆过程,即将从网络接收的字节序列或从存储介质读取的二进制数据还原为程序内部可直接使用的数据结构。它的目的是确保接收到的数据能够正确地重新构建为原始对象,保持数据的完整性和一致性。

实现方式: 反序列化同样依赖于所选的序列化格式或协议:

  1. 解析字节流:从网络套接字或文件中读取字节序列。
  2. 类型检测与解析:根据协议规范,识别各个字段的类型标识、长度等信息,从字节流中提取对应的数据。
  3. 类型转换:将解析出的字节数据转换回原对象属性应有的数据类型(如字符串转回字符串,整数转回整数等)。
  4. 对象重建:根据数据字段的顺序和嵌套关系,将转换后的数据填充到目标数据结构(如对象、数组、结构体)中。

反序列化过程中的安全考量:

  • 数据验证:检查接收到的数据是否符合协议规范,如字段数量、类型、长度范围等,防止因恶意或损坏的数据导致程序崩溃或安全漏洞。
  • 输入净化:对反序列化过程中产生的字符串或其他可变类型进行安全处理,避免注入攻击。
  • 版本兼容:处理不同版本间的数据格式差异,确保旧版本程序能正确解析新版本数据,或者新版本程序能向下兼容旧版本数据。

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));// 程序遇到严重错误,退出并返回错误码 2exit(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;       // 设置地址族为 IPv4local.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));// 程序遇到严重错误,退出并返回错误码 3exit(3);}}// 将指定套接字设置为监听状态,开始接受客户端连接请求void Listen(int sock){if (listen(sock, gbacklog) < 0){// 记录 FATAL 级别的日志消息,并附带错误号和错误描述logMessage(FATAL, "监听错误,%d:%s", errno, strerror(errno));// 程序遇到严重错误,退出并返回错误码 4exit(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));// 返回错误码 -1return -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;       // 设置地址族为 IPv4server.sin_port = htons(server_port); // 将端口号转换为网络字节序并存入结构体server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // 将 IP 地址字符串转换为二进制并存入结构体// 尝试建立连接,如果成功(返回值为 0),返回 true;否则返回 falseif (connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)return true;elsereturn 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",表示监听所有本地接口)。具体步骤如下:

  1. 初始化 sockaddr_in 结构体 local,用于存储 IP 地址和端口信息。
  2. 设置 local.sin_family 为 AF_INET(IPv4 地址族)。
  3. 将给定的 port 转换为网络字节序(大端序)并存入 local.sin_port
  4. 使用 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 地址和端口号。

具体步骤如下:

  1. 初始化 sockaddr_in 结构体 src 用于存储客户端信息。
  2. 调用 accept() 系统调用,接受一个连接请求并返回新的套接字描述符。如果出错,记录 ERROR 级别日志并返回 -1。
  3. 如果指针 port 非空,将接收到的客户端端口号从网络字节序转换为主机字节序(小端序)并赋值给 *port
  4. 如果指针 ip 非空,使用 inet_ntoa() 函数将接收到的客户端二进制 IP 地址转换为点分十进制字符串形式并赋值给 *ip
  5. 最后返回新建立的连接套接字 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;elsereturn false;
}

此函数使用给定的 sock 套接字连接到指定的 server_ip 和 server_port。具体步骤如下:

  1. 初始化 sockaddr_in 结构体 server 存储服务器信息。
  2. 设置 server.sin_family 为 AF_INET
  3. 将给定的 server_port 转换为网络字节序并存入 server.sin_port
  4. 使用 inet_addr() 函数将字符串形式的 server_ip 转换为二进制 IP 地址并存入 server.sin_addr.s_addr
  5. 调用 connect() 系统调用尝试建立连接。如果连接成功(返回值为 0),函数返回 true;否则返回 false

析构函数 ~Sock()

~Sock() {}

这是一个空的析构函数,不执行任何操作。由于 Sock 类本身并不直接管理任何资源,因此不需要在析构函数中释放任何资源。如果有需要,可以在类中添加成员变量(如套接字描述符)并在此处关闭或释放相关资源。

TcpServer.hpp

 这个代码定义了一个名为 ns_tcpserver 的命名空间,其中包含两个类:ThreadData 和 TcpServerTcpServer 类封装了一个简单的多线程 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类存在的必要性:

  1. 封装线程所需数据: ThreadData类封装了两个关键成员变量:

    • int sock_: 存储了与客户端建立连接后的套接字描述符。这个描述符是线程需要处理的实际网络连接,用于读写数据。
    • TcpServer *server_: 指向TcpServer实例的指针,使得线程能够访问服务器对象的方法和成员,如Excute()函数。

    这种封装方式简化了线程启动时的数据传递,只需传递一个ThreadData对象的指针即可,避免了直接传递多个独立参数给线程创建函数。

  2. 线程安全性和生命周期管理: 由于ThreadData类的对象由TcpServer创建并在ThreadRoutine中使用,其生命周期与线程的执行周期紧密关联。通过new操作符动态创建ThreadData对象,并将其作为参数传递给pthread_create(),确保了在线程执行期间该对象始终有效。当线程执行结束时,ThreadData对象在ThreadRoutine末尾通过delete释放。虽然实际代码中注释掉了delete语句,但在实际应用中应确保正确释放资源以防止内存泄漏。

  3. 结构清晰、易于维护: 使用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 对象的指针作为参数。函数执行以下操作:

  1. 使用 pthread_detach() 使线程在结束后自动回收资源,无需显式调用 pthread_join()
  2. 将输入的 void * 参数转换回 ThreadData * 类型,并访问其成员。
  3. 调用 TcpServer 实例的 Excute() 方法,传入客户端套接字描述符,执行用户提供的服务函数。
  4. 关闭已处理完的客户端套接字。
  5. (注释掉的)删除 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() 方法启动服务器主循环,不断接受新的客户端连接并为每个连接创建一个新线程。具体步骤如下:

  1. 无限循环等待客户端连接。
  2. 使用 sock_.Accept() 接收一个连接请求,获取客户端套接字描述符、IP 地址和端口号。若接收到错误,跳过本次循环继续等待。
  3. 记录一条 NORMAL 级别的日志消息,显示成功创建的新连接套接字描述符。
  4. 创建一个 ThreadData 对象,存储客户端套接字描述符和指向 TcpServer 实例的指针。
  5. 调用 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 aint 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;elseop_ = 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 MYSELFstd::string s;s = std::to_string(code_);s += SPACE;s += std::to_string(result_);return s;
#elseJson::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 MYSELFstd::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;
#elseJson::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 宏定义的不同,可以选择两种序列化方式:

  1. 自主实现:按照格式 "x_ op_ y_"(例如 "1234 + 5678")生成字符串。

  2. 使用JsonCpp库:将 x_y_ 和 op_ 作为键值对放入 JSON 对象,然后使用 Json::FastWriter 将 JSON 对象写为字符串。

bool Deserialized(const std::string &str)

bool Request::Deserialized(const std::string &str)
{
#ifdef MYSELF// 自定义反序列化逻辑...
#elseJson::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 宏定义的不同,选择两种反序列化方式:

  1. 自主实现:从输入字符串中解析出 x_y_ 和 op_ 的值,要求字符串格式为 "x_ op_ y_"

  2. 使用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);  // stdindup2(1, devnull);  // stdoutdup2(2, devnull);  // stderr// 关闭原始的 /dev/null 文件描述符,保留重定向后的副本close(devnull);}
}

详细讲解:

void MyDaemon()
{// 1. 忽略信号,SIGPIPE,SIGCHLDsignal(SIGPIPE, SIG_IGN);signal(SIGCHLD, SIG_IGN);

这部分代码设置对特定信号的处理方式:

  • SIGPIPE:当尝试写入到已断开连接的管道时触发。守护进程通常忽略此信号,避免因意外断开的网络连接而终止。
  • SIGCHLD:当子进程终止或停止时发送给其父进程。守护进程通常忽略此信号,以避免僵尸进程积累,并让子进程自动清理。
    // 2. 不要让自己成为组长if (fork() > 0)exit(0);

使用fork()系统调用创建一个子进程。父进程(返回值大于0)立即退出,使得子进程成为一个孤儿进程,由init进程(PID为1)接管。这样做是为了确保守护进程不是进程组的组长,与原会话和控制终端彻底分离。

    // 3. 调用setsidsetsid();

调用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_tcpserverns_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; // 设置错误代码elseresp.result_ = req.x_ / req.y_;break;case '%': // 取模if (0 == req.y_) // 防止除数为零resp.code_ = 2; // 设置错误代码elseresp.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; // 程序正常结束
}
  1. 包含头文件

    • TcpServer.hpp:定义了TCP服务器类TcpServer,负责监听指定端口并处理客户端连接。
    • Protocol.hpp:包含自定义协议相关的类和函数,如RequestResponse及其序列化、反序列化方法。
    • Daemon.hpp:可能包含了使程序以守护进程方式运行的相关功能。
    • <memory>:用于智能指针std::unique_ptr的声明。
    • <signal.h>:包含处理信号的函数,如signal()
  2. 命名空间

    • 使用ns_tcpserverns_protocol命名空间中的功能。
  3. 辅助函数

    • Usage():打印程序的使用说明,提示用户如何正确传入端口号。
  4. 业务逻辑函数

    • calculatorHelper(const Request &req):根据请求对象req中的运算符和操作数执行相应的数学计算,并返回一个Response对象,其中包含计算结果和状态码。
  5. 主处理函数

    • calculator(int sock):处理与客户端的通信。主要步骤如下:
      • 接收数据:通过Recv()函数从给定的套接字sock中读取客户端发送的数据,并存入inbuffer字符串。
      • 解码报文:使用Decode()函数从inbuffer中提取出一个完整的请求报文(带有长度前缀和分隔符)。
      • 反序列化请求:将提取出的请求报文反序列化为Request对象req
      • 执行计算:调用calculatorHelper()函数,根据req执行计算并得到Response对象resp
      • 序列化响应:将resp对象序列化为字符串respString
      • 编码响应:使用Encode()函数为respString添加长度前缀和分隔符,形成完整的响应报文。
      • 发送响应:通过Send()函数将响应报文发送回客户端。
  6. 主函数

    • 命令行参数检查:检查命令行参数个数是否为2(程序名和端口号)。若不满足条件,则打印使用说明并退出。
    • 启动守护进程:调用MyDaemon()函数(未在代码中展示)使程序以守护进程方式运行。
    • 创建并配置TCP服务器
      • 创建一个TcpServer对象实例,传入命令行参数中的端口号。
      • 绑定服务处理函数calculator,使其在接收到客户端连接时被调用。
      • 调用Start()方法启动服务器监听。
    • 注释部分:代码中还包含一些被注释掉的测试代码,用于测试Request对象的序列化和反序列化功能。

整个程序的主要流程如下:

  1. 启动程序,检查命令行参数,确保正确传递了端口号。
  2. 使程序以守护进程方式运行。
  3. 创建TCP服务器,监听指定端口。
  4. 当有客户端连接时,服务器调用calculator函数处理连接:
    • 接收客户端发送的请求报文。
    • 解码请求报文,提取完整的请求。
    • 反序列化请求为Request对象。
    • 执行计算,生成Response对象。
    • 序列化并编码响应,形成完整的响应报文。
    • 将响应报文发送回客户端。
  5. 服务器持续监听并处理后续客户端连接。

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 CalServerclient:CalClient.ccg++ -o $@ $^ -std=c++11 -ljsoncpp
CalServer:CalServer.ccg++ -o $@ $^ -std=c++11 -ljsoncpp -lpthread.PHONY:clean
clean:rm -f client CalServer

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/826401.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

稳压二极管仿真实验

稳压二极管仿真实验 1、稳压管稳压实验 用Multisim搭建如下的仿真电路图&#xff0c;选用5.1V的稳压管&#xff0c;12V的直流电源&#xff0c;开启仿真后&#xff0c;12V电压将稳压管击穿&#xff0c;稳压管将两端的电压稳压到5.07V&#xff0c;该电压与限流电阻R1的阻值有关…

跟着Carl大佬学leetcode之977 有序数组的平方

来点强调&#xff0c;刷题是按照代码随想录的顺序进行的&#xff0c;链接如下https://www.programmercarl.com/本系列是记录一些刷题心得和学习过程&#xff0c;就看到题目自己先上手试试&#xff0c;然后看程序员Carl大佬的解释&#xff0c;自己再敲一遍修修补补&#xff0c;练…

msyql中SQL 错误 [1118] [42000]: Row size too large (> 8126)

场景&#xff1a; CREATE TABLE test-qd.eqtree (INSERT INTO test.eqtree (idocid VARCHAR(50) NULL,sfcode VARCHAR(50) NULL,sfname VARCHAR(50) NULL,sfengname VARCHAR(50) NULL,…… ) ENGINEInnoDB DEFAULT CHARSETutf8 COLLATEutf8_general_ci;或 alter table eqtre…

Token 在 LLM

大语言模型不能理解原始文本,所以需要把原始文本转换成大语言模型可以理解的数字表示形式,经过大模型处理以后,需要将该数字表示形式转换为人可以理解的文本。 原始文本到 token 原始文本转换为token序列的过程通常是在LLM的预处理阶段完成的。 在大型语言模型(LLM)中,tok…

力扣:LCR 022. 环形链表 II

力扣&#xff1a;LCR 022. 环形链表 II 给定一个链表&#xff0c;返回链表开始入环的第一个节点。 从链表的头节点开始沿着 next 指针进入环的第一个节点为环的入口节点。如果链表无环&#xff0c;则返回 null。 为了表示给定链表中的环&#xff0c;我们使用整数 pos 来表示链…

学习java时候的笔记(十九)

正则表达式 作用: 校验字符串是否满足规则在一段文本中查找满足要求的内容 字符类(只匹配一个字符) 说明[abc]只能是a,b或c[^abc]除了a,b,c之外的任何字符[a-zA-Z]a 到 z, A 到 Z(范围)[a-d[m-p]]a 到 d, 或 m 到 p[a - z && [def]]a 到 z和def的交集。为: d, e, f…

【树莓派学习】系统烧录及VNC连接、文件传输

【树莓派学习】系统烧录及VNC连接、文件传输 Raspberry Pi is a series of small single-board computers (SBCs) developed in the United Kingdom by the Raspberry Pi Foundation in association with Broadcom. Since 2013, Raspberry Pi devices have been developed and…

Ubuntu20.4版本安装ROS教程

一、配置源 安装成功的Ubuntu系统自带的工具下载速度慢&#xff0c;不太好用&#xff0c;所以我们可以使用国内稳定高速且免费的镜像网站。 清华源&#xff1a;https://pypi.tuna.tsinghua.edu.cn/simple/ 阿里云&#xff1a;https://mirrors.aliyun.com/pypi/simple 中科大&…

启动 UE4编辑器报 加载 Plugin 失败

启动 UE4编辑器报 加载 Plugin 失败&#xff0c;报如下错误&#xff1a; Plugin ‘SteamVR’ failer to load because module ‘SteamVR’ could not be found. Please ensure the plugin is properly installed, otherwise consider disabling the plugin for this project. …

TorchEEG文档_学习笔记1【代码详解】

文章目录 一、用户文档1.安装Pytorch2.安装TorchEEG3.安装与图算法的插件 二、教程1.使用TorchEEG完成深度学习工作流程2datasets模块3.transforms模块4.models模块5.trainer模块6.使用Vanilla PyTorch训练模型 一、用户文档 1.安装Pytorch TorchEEG依赖于PyTorch,根据系统、…

MYSQL之增删改查(下)

前言&#xff1a; 以下是MySQL最基本的增删改查语句&#xff0c;很多IT工作者都必须要会的命令&#xff0c;也 是IT行业面试最常考的知识点&#xff0c;由于是入门级基础命令&#xff0c;所有所有操作都建立在单表 上&#xff0c;未涉及多表操作。 4.3 高级查询 4.3.1 聚合函…

Navicat 干货 | 了解 PostgreSQL 规则

PostgreSQL 是一个强大的开源关系型数据库管理系统&#xff0c;为增强数据管理和操作提供了丰富的功能。这些功能中包含了规则&#xff0c;这是一种用于控制数据库内部查询和命令处理方式的机制。本文将探讨 PostgreSQL 规则的工作原理&#xff0c;以及它们与触发器的区别&…

实验室三大常用仪器1---示波器的基本使用方法(笔记)

目录 示波器的作用 示波器的基础操作方法 示波器测量突变脉冲 示波器的作用 示波器能帮助我们干什么&#xff1f; 比如说某个电源用万用表测量是稳定的5V输出 但是用示波器一看确实波涛汹涌 这样的电源很可能回导致系统异常工作 又比如电脑和单片机进行串口通信时&#xf…

【C++干货基地】面向对象核心概念 const成员函数 | 初始化列表 | explicit关键字 | 取地址重载

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 引入 哈喽各位铁汁们好啊&#xff0c;我是博主鸽芷咕《C干货基地》是由我的襄阳家乡零食基地有感而发&#xff0c;不知道各位的…

sketchup创建3D打印机的模型

查了一下&#xff0c;这玩意有几个版本&#xff0c;其中一个sketchup free是免费的&#xff0c;到官网上看看 下载 SketchUp | 免费试用 3D 建模软件 | SketchUp 是个在线网页版&#xff0c;然后可以再这个网站上注册一个账号 弄个邮箱试试看 创建好进入后&#xff0c;里面就…

js 遍历数据结构,使不符合条件的全部删除

js 遍历数据结构&#xff0c;使不符合条件的全部删除 let newSourceJSON.parse(JSON.stringify(state.treeData))state.expandedKeys[]checkedKeys.map((item:any)>{loop(newSource,{jsonPath:item.split(&)[1]},state.expandedKeys)})function removeUnwantedNodes(tre…

SpringCloud系列(7)--Eureka服务端的安装与配置

前言&#xff1a;上一章节我们介绍了Eureka的基础&#xff0c;本章节则介绍Eureka服务端的安装与配置 Eureka架构原理图 1、创建Eureka Server端服务注册中心模块 (1)在父工程下新建模块 (2)选择模块的项目类型为Maven并选择模块要使用的JDK版本 (3)填写子模块的名称&#xf…

如何在Windows安装Ollama大语言模型工具并实现无公网IP异地远程使用

文章目录 前言1. 运行Ollama2. 安装Open WebUI2.1 在Windows系统安装Docker2.2 使用Docker部署Open WebUI 3. 安装内网穿透工具4. 创建固定公网地址 前言 本文主要介绍如何在Windows系统快速部署Ollama开源大语言模型运行工具&#xff0c;并安装Open WebUI结合cpolar内网穿透软…

详解数据在内存中的存储

系列文章目录 第一章 C语言基础知识 第二章 C语言控制语句 第三章 C语言函数详解 第四章 C语言数组详解 第五章 C语言操作符详解 第六章 C语言指针详解 第七章 C语言结构体详解 文章目录 1. 数据类型 1.1 基本数据类型 1.2 派生数据类型 2. 整形在内存中的存储 2.1 …

基于ssm汽车租赁系统业务管理子系统论文

系统简介 随着信息互联网购物的飞速发展&#xff0c;一般企业都去创建属于自己的管理系统。本文介绍了汽车租赁系统业务管理子系统的开发全过程。通过分析企业对于汽车租赁系统业务管理子系统的需求&#xff0c;创建了一个计算机管理汽车租赁系统业务管理子系统的方案。文章介…