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

news2024/11/26 22:22:03

目录

一、序列化与反序列化

1、序列化(Serialization)

2、反序列化(Deserialization)

3、Linux环境中的应用实例

二、实现网络版计算器

Sock.hpp

TcpServer.hpp

Jsoncpp库

Protocol.hpp

MyDaemon.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));
            // 程序遇到严重错误,退出并返回错误码 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",表示监听所有本地接口)。具体步骤如下:

  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;
    else
        return 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
{
    using func_t = std::function<void(int)>;

    class TcpServer;

    class ThreadData
    {
    public:
        ThreadData(int sock, TcpServer *server):sock_(sock), server_(server)
        {}
        ~ThreadData() {}
    public:
        int sock_;
        TcpServer *server_;
    };

    class TcpServer
    {
    private:
        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;
        }

    public:
        TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0")
        {
            listensock_ = sock_.Socket();
            sock_.Bind(listensock_, port, ip);
            sock_.Listen(listensock_);
        }
        void BindService(func_t func) 
        { 
            func_.push_back(func);
        }
        void Excute(int sock)
        {
            for(auto &f : func_)
            {
                f(sock);
            }
        }
        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);
            }
        }
        ~TcpServer()
        {
            if (listensock_ >= 0)
                close(listensock_);
        }

    private:
        int listensock_;
        Sock sock_;
        std::vector<func_t> func_;
        // std::unordered_map<std::string, func_t> func_;
    };
}

命名空间 ns_tcpserver

namespace ns_tcpserver
{
    // ...
}

这个命名空间用于组织相关类和函数,避免与其他代码中的同名实体冲突。

类型别名 func_t

using func_t = std::function<void(int)>;

定义了一个类型别名 func_t,表示一个接受一个整数参数(客户端套接字描述符)且无返回值的可调用对象。这个类型将用于存储用户提供的服务函数。

类 ThreadData

class ThreadData
{
public:
    ThreadData(int sock, TcpServer *server):sock_(sock), server_(server)
    {}
    ~ThreadData() {}

public:
    int sock_;
    TcpServer *server_;
};

ThreadData 类是一个简单数据结构,用于传递给新线程的参数。它包含两个成员变量:

  • sock_: 存储客户端套接字描述符。
  • server_: 指向 TcpServer 实例的指针,用于在新线程中调用 Excute() 方法。

类 TcpServer

class TcpServer
{
    // ...
};

TcpServer 类实现了多线程 TCP 服务器的主要逻辑,包括创建监听套接字、绑定端口、监听连接、处理客户端请求以及线程管理。

私有成员函数 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

#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP) // 不能是sizeof!

    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_; // TODO
            str += SPACE;
            str += std::to_string(y_);
            return str;
#else
            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
            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
            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_;
    };

    // 临时解决方案:
    // 调整方案2:期望接收完整的报文
    bool Recv(int sock, std::string *out)
    {
        // UDP面向数据报:
        // TCP面向字节流:
        // 使用recv无法确保接收到的是一个完整且有效的请求报文,如:
        // "1234 + 5678":只接收到部分报文,如 "1234 +" 或 "1234 + 5678 123+99"
        // "1234 ":不完整报文
        // 必须确保接收到的是完整的格式如:"1234 + 5678"
        // 因此需要对协议进行进一步定制以解决此问题
        char buffer[1024];
        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)
        {
            // std::cout << "client quit" << std::endl;
            return false;
        }
        else
        {
            // std::cout << "recv error" << std::endl;
            return false;
        }
        return true;
    }

    void Send(int sock, const std::string str)
    {
        // std::cout << "sent in" << std::endl;
        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

命名空间 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

class Request
{
    // ...
};

Request 类表示客户端发送给服务器的请求消息,包含两个整数值 x_ 和 y_,以及一个运算符 op_。类中定义了以下几个成员函数:

std::string Serialize()

std::string Serialize()
{
#ifdef MYSELF
    // 自主实现的序列化方式
#else
    // 使用JsonCpp库进行序列化
#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 Deserialized(const std::string &str)
{
#ifdef MYSELF
    // 自主实现的反序列化方式
#else
    // 使用JsonCpp库进行反序列化
#endif
}

该函数将接收到的字符串反序列化为 Request 对象。同样,根据 MYSELF 宏定义的不同,选择两种反序列化方式:

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

  2. 使用JsonCpp库:使用 Json::Reader 解析输入字符串为 JSON 对象,然后从 JSON 对象中提取 x_y_ 和 op_ 的值。

类 Response

class Response
{
    // ...
};

Response 类表示服务器发送给客户端的响应消息,包含计算结果 result_、状态码 code_,以及可能的附加信息 x_y_ 和 op_。类中同样定义了 Serialize() 和 Deserialized() 函数,功能与 Request 类中的类似,但格式不同。

辅助函数

bool Recv(int sock, std::string *out)
{
    // ...
}

void Send(int sock, const std::string str)
{
    // ...
}

std::string Decode(std::string &buffer)
{
    // ...
}

std::string Encode(std::string &s)
{
    // ...
}

这些函数用于处理客户端与服务器之间的数据收发和协议解析:

  • bool Recv(int sock, std::string *out):从给定套接字 sock 接收数据,将接收到的数据追加到 out 字符串中。该函数考虑了 UDP 和 TCP 的差异,但未完全实现确保接收完整请求报文的功能。在实际应用中,可能需要进一步完善以确保正确解析请求。

  • void Send(int sock, const std::string str):通过给定套接字 sock 发送字符串 str

  • std::string Decode(std::string &buffer):从给定缓冲区 buffer 中解码一个完整的报文。报文格式为 "length\r\nmessage\r\n...",其中 length 为整数,表示 message 的长度。函数返回解码后的报文,同时更新 buffer 以移除已处理的部分。

  • std::string Encode(std::string &s):将字符串 s 编码为带有长度前缀的报文格式,即 "length\r\nmessage\r\n",并返回编码后的报文。

总结:ns_protocol 命名空间内定义了 Request 和 Response 类,用于表示客户端与服务器之间的请求和响应消息,并提供了 Serialize() 和 Deserialized() 函数进行序列化和反序列化。此外,还提供了 Recv()Send()Decode() 和 Encode() 函数,用于处理通信过程中的数据收发和协议解析。在实际使用时,需根据需求选择合适的序列化/反序列化方式(自主实现或使用JsonCpp库),并可能需要完善 Recv() 函数以确保正确接收和解析请求报文。

MyDaemon.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_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; // 设置错误代码
        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; // 程序正常结束
}
  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 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

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

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

相关文章

3.1 iHRM人力资源 - 组织架构、树形结构、添加子部门

iHRM人力资源 - 组织架构 文章目录 iHRM人力资源 - 组织架构一、展示数据-树形组件1.1 组件说明1.2 树组件自定义结构获取作用域数据1.2.1 说明1.2.2 页面代码1.2.3 获取组织架构数据-api 1.3 效果图1.4 修改树形结构bug 二、添加子部门2.1 表单弹层2.1.1 下拉菜单点击事件2.1.…

中国科学院大学学位论文LaTeX模版

Word排版太麻烦了&#xff0c;公式也不好敲&#xff0c;推荐用LaTeX模版&#xff0c;全自动 官方模版下载位置&#xff1a;国科大sep系统 → \rightarrow → 培养指导 → \rightarrow → 论文 → \rightarrow → 论文格式检测 → \rightarrow → 撰写模板下载百度云&#…

Vitis HLS 学习笔记--readVec2Stream 函数-探究

目录 1. 高效内存存取的背景 2. readVec2Stream() 参数 3. 函数实现 4. 总结 1. 高效内存存取的背景 在深入研究《Vitis HLS 学习笔记--scal 函数探究》一篇文章之后&#xff0c;我们对于scal()函数如何将Y alpha * X这种简单的乘法运算复杂化有了深刻的理解。本文将转向…

前端console用法分享

console对于前端人员来讲肯定都不陌生&#xff0c;相信大部分开发者都会使用console来进行调试&#xff0c;但它能做的绝不仅限于调试。 最常见的控制台方法 作为开发者&#xff0c;最常用的 console 方法如下&#xff1a; 控制台打印结果&#xff1a; 今天我分享的是一些 co…

Vue3项目中快速引入ElementUI框架

ElementUI介绍 ElementUI是一个强大的PC端UI组件框架&#xff0c;它不依赖于vue&#xff0c;但是却是当前和vue配合做项目开发的一个比较好的ui框架&#xff0c;其包含了布局&#xff08;layout)&#xff0c;容器&#xff08;container&#xff09;等各类组件&#xff0c;基本上…

qutip,一个高级的 Python 量子力学研究库!

目录 前言 安装 特性 基本功能 量子态的创建和操作 量子态的测量 示例代码 动力学模拟 高级功能 退相干和噪声模拟 控制和优化 量子信息学工具 实际应用场景 量子态演化研究 量子计算机模拟 量子纠错协议 总结 前言 大家好&#xff0c;今天为大家分享一个高级的 Pytho…

【C++对于C语言的扩充】函数重载、引用以及内联函数

文章目录 &#x1f680;前言&#x1f680;函数重载注意&#xff1a;✈️为什么C可以实现函数重载&#xff0c;而C语言却不行呢&#xff1f; &#x1f680;引用✈️引用的特性✈️C中为什么要引入引用✈️引用与指针的区别 &#x1f680;内联函数✈️内联函数特性 &#x1f680;…

ClickHouse--17--聚合函数总结

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 环境1.创建clickhouse表2.插入数据 函数(1)count&#xff1a;计算行数(2)min&#xff1a;计算最小值(3)max&#xff1a;计算最大值(4)sum&#xff1a;计算总和&…

创新实训2024.04.11日志:self-instruct生成指令

1. 参考文献 代码&#xff1a;https://github.com/yizhongw/self-instruct论文&#xff1a;https://arxiv.org/abs/2212.10560 2. 前沿论文阅读 2.1. self-instruct技术的优势 作者在文章中提到&#xff1a; The recent NLP literature has witnessed a tremendous amount …

STM32H7的8个串口fifo收发(兼容232和485)

STM32H7的8个串口fifo收发&#xff08;兼容232和485&#xff09; 串口硬件串口时序串口高级特性同步和异步的区别单工、半双工、全双工的区别 STM32H78个串口fifo驱动定义数据结构uart_fifo.huart驱动包括中断配置等 应用示例RS485深入理解 仅供学习。 USART 的全称是 Universa…

【C++】开始使用stack 与 queue

送给大家一句话&#xff1a; 忍受现实给予我们的苦难和幸福&#xff0c;无聊和平庸。 – 余华 《活着》 开始使用queue 与 stack 1 前言2 stack与queue2.1 stack 栈2.2 queue 队列2.3 使用手册 3 开始使用Leetcode 155.最小栈牛客 JZ31 栈的弹出压入序列Leetcode 150.逆波兰表达…

Mac版2024 CleanMyMac X 4.15.2 核心功能详解 cleanmymac这个软件怎么样?cleanmymac到底好不好用?

近些年伴随着苹果生态的蓬勃发展&#xff0c;越来越多的用户开始尝试接触Mac电脑。然而很多人上手Mac后会发现&#xff0c;它的使用逻辑与Windows存在很多不同&#xff0c;而且随着使用时间的增加&#xff0c;一些奇奇怪怪的文件也会占据有限的磁盘空间&#xff0c;进而影响使用…

leetcode热题100.爬楼梯(从二进制到快速幂)

Problem: 70. 爬楼梯 文章目录 题目思路Code复杂度 题目 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 示例 1&#xff1a; 输入&#xff1a;n 2 输出&#xff1a;2 解释&#xff1a;有两种方…

Oracle+RAC静默安装系列(基于RHEL9/国产/麒麟/华为欧拉的生产案例)

由风哥发布的 OracleRAC静默安装系列&#xff08;基于RHEL9/国产/麒麟/华为欧拉的生产案例&#xff09;系列&#xff0c;适合运维人员/数据库/开发人员&#xff0c;可以用于业务生产环境。 为满足想快速安装布署Oracle数据库的学员&#xff0c;风哥特别设计的一套比较全面的全命…

DataX-Web,介绍-安装-部署-启动

使用文档&#xff1a;GitHub - WeiYe-Jing/datax-web: DataX集成可视化页面 目录 1、DataX-Web介绍 2、DataX-Web部署 3、DataX-Web启动命令 1、DataX-Web介绍 GitHub - WeiYe-Jing/datax-web&#xff1a;DataX集成可视化页面&#xff0c;选择数据源即可一键生成数据同步任务…

项目升级到jdk21后 SpringBoot相关组件的适配

了解到jdk21是一个LTS版本&#xff0c;可以稳定支持协程的功能。经过调研&#xff0c;将目前线上的jdk8升级到21&#xff0c;使用协程提升并发性能。 目前系统使用springBoot 2.0.3.RELEASE&#xff0c;并且引入了mybatis-spring-boot-starter、spring-boot-starter-data-redi…

【Entity Framework】你必须要了解EF中数据查询之数据加载

【Entity Framework】你必须要了解EF中数据查询之数据加载 文章目录 【Entity Framework】你必须要了解EF中数据查询之数据加载一、概述二、预先加载2.1 包含多个层级2.2 经过筛选的包含 三、显示加载3.1查询关联实体 四、延时加载4.1 不使用代理进行延迟加载 一、概述 Entity…

【C语言】带你完全理解指针(六)指针笔试题

目录 1. 2. 3. 4. 5. 6. 7. 8. 1. int main() {int a[5] { 1, 2, 3, 4, 5 };int* ptr (int*)(&a 1);printf("%d,%d", *(a 1), *(ptr - 1));return 0; } 【答案】 2&#xff0c;5 【解析】 定义了一个指向整数的指针ptr&#xff0c;并将其初始化为&…

设计编程网站集:动物,昆虫,蚂蚁养殖笔记

入门指南 区分白蚁与蚂蚁 日常生活中&#xff0c;人们常常会把白蚁与蚂蚁搞混淆&#xff0c;其实这两者是有很大区别的&#xff0c;养殖方式差别也很大。白蚁主要食用木质纤维&#xff0c;会给家庭房屋带来较大危害&#xff0c;而蚂蚁主要采食甜食和蛋白质类食物&#xff0c;不…

pytorch 今日小知识3——nn.MaxPool3d 、nn.AdaptiveAvgPool3d、nn.ModuleList

MaxPool3d — PyTorch 2.2 documentation 假设输入维度&#xff08;1,2,3,4,4&#xff09; maxpool torch.nn.MaxPool3d(kernel_size(2, 2, 2), stride(2, 2, 2), padding(1, 0, 0))F 维的 kernel_size 为 2&#xff0c;说明在 F 维的覆盖的 frame 数为 2&#xff0c;也就是…