甘肃省建设厅查询网站,承德在线招聘,六安开发区网站,遵义做什么网站好目录
一、序列化与反序列化
1、序列化#xff08;Serialization#xff09;
2、反序列化#xff08;Deserialization#xff09;
3、Linux环境中的应用实例
二、实现网络版计算器
Sock.hpp
TcpServer.hpp
Jsoncpp库
Protocol.hpp
类 Request
类 Response
辅助函…目录
一、序列化与反序列化
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 BuffersProtobuf、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));// 程序遇到严重错误退出并返回错误码 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表示监听所有本地接口。具体步骤如下
初始化 sockaddr_in 结构体 local用于存储 IP 地址和端口信息。设置 local.sin_family 为 AF_INETIPv4 地址族。将给定的 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;elsereturn 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.hnamespace ns_tcpserver
{// 定义一个类型别名用于表示接受一个整型参数int的可调用对象如函数指针、lambda表达式等using func_t std::functionvoid(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_castThreadData *(args);// 调用 TcpServer 实例的 Excute 方法处理客户端连接td-server_-Excute(td-sock_);// 关闭与客户端的连接close(td-sock_);// 释放 ThreadData 对象占用的内存delete td;// 返回 nullptr表示线程完成return nullptr;}public:// TcpServer 构造函数接收监听端口和可选的 IP 地址默认为 0.0.0.0TcpServer(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::vectorfunc_t func_;};
}
命名空间 ns_tcpserver
namespace ns_tcpserver
{// ...
}
这个命名空间用于组织相关类和函数避免与其他代码中的同名实体冲突。
类型别名 func_t
using func_t std::functionvoid(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::vectorfunc_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_castThreadData *(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::vectorfunc_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.hint 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.hnamespace 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 5678bool 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 100bool 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, 3int 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\n123789\r\nif (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\nabcstd::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\nstd::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// 自定义反序列化逻辑...
#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 宏定义的不同选择两种反序列化方式 自主实现从输入字符串中解析出 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\n123789\r\nif (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\nabcstd::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\nstd::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. 忽略信号SIGPIPESIGCHLDsignal(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_tcpserver和ns_protocol命名空间中的类与函数。
#include TcpServer.hpp
#include Protocol.hpp
#include Daemon.hpp
#include memory
#include signal.husing 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_ptrTcpServer 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.hppusing 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 $ $^ -stdc11 -ljsoncpp
CalServer:CalServer.ccg -o $ $^ -stdc11 -ljsoncpp -lpthread.PHONY:clean
clean:rm -f client CalServer