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));
            // 程序遇到严重错误,退出并返回错误码 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
{
    // 定义一个类型别名,用于表示接受一个整型参数(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;
            else
                op_ = str[left + SPACE_LEN];
            return true;
#else// 使用JsonCpp反序列化:创建Json对象,使用Reader解析字符串,然后从Json对象中提取数据
            Json::Value root;
            Json::Reader reader;
            reader.parse(str, root);
            x_ = root["x"].asInt();
            y_ = root["y"].asInt();
            op_ = root["op"].asInt();
            return true;
#endif
        }

    public:
        Request() {} // 默认构造函数
        Request(int x, int y, char op) : x_(x), y_(y), op_(op) {} // 构造函数
        ~Request() {} // 析构函数

    public:
        //格式: x_ op y_ 或 y_ op x_
        int x_;   // 未知整数
        int y_;   // 未知整数
        char op_; // 运算符:'+' '-' '*' '/' '%'
    };

    // 定义响应类,用于封装响应相关的数据和序列化/反序列化方法
    class Response
    {
    public:
        // 序列化方法,生成 "code_ result_" 格式的字符串
        std::string Serialize()
        {
#ifdef MYSELF
            std::string s;
            s = std::to_string(code_);
            s += SPACE;
            s += std::to_string(result_);

            return s;
#else
            Json::Value root;
            root["code"] = code_;
            root["result"] = result_;
            root["xx"] = x_;
            root["yy"] = y_;
            root["zz"] = op_;
            Json::FastWriter writer;
            return writer.write(root);
#endif
        }
        // 反序列化字符串 "code_ result_",例如 "111 100"
        bool Deserialized(const std::string &s)
        {
#ifdef MYSELF
            std::size_t pos = s.find(SPACE);
            if (pos == std::string::npos)
                return false;
            code_ = atoi(s.substr(0, pos).c_str());
            result_ = atoi(s.substr(pos + SPACE_LEN).c_str());
            return true;
#else
            Json::Value root;
            Json::Reader reader;
            reader.parse(s, root);
            code_ = root["code"].asInt();
            result_ = root["result"].asInt();
            x_ =  root["xx"].asInt();
            y_ =  root["yy"].asInt();
            op_ =  root["zz"].asInt();
            return true;
#endif
        }

    public:
        Response() {} // 默认构造函数
        Response(int result, int code, int x, int y, char op) 
            : result_(result), code_(code), x_(x), y_(y), op_(op) {} // 构造函数
        ~Response() {} // 析构函数

    public:
        // result_:计算结果
        // code_:计算结果的状态码(例如:0, 1, 2, 3)
        int result_;
        int code_;

        int x_;
        int y_;
        char op_;
    };

    // 接收完整的报文
    bool Recv(int sock, std::string *out)
    {
        // 创建缓冲区接收数据
        char buffer[1024];

        // 使用recv接收数据
        ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 例如:接收到 "9\r\n123+789\r\n"

        if (s > 0)
        {
            buffer[s] = 0;
            *out += buffer; // 将接收到的数据添加到输出字符串中
        }
        else if (s == 0)
        {
            return false;
        }
        else
        {
            // std::cout << "recv error" << std::endl;
            return false;
        }
        return true;
    }

    void Send(int sock, const std::string str)
    {
        int n = send(sock, str.c_str(), str.size(), 0);
        if (n < 0)
            std::cout << "send error" << std::endl;
    }

    // 解码缓冲区中的报文,格式为 "length\r\nmessage\r\n..."
    // 示例:解码 "10\r\nabc"
    std::string Decode(std::string &buffer)
    {
        std::size_t pos = buffer.find(SEP);
        if(pos == std::string::npos) return ""; // 未找到分隔符,返回空字符串

        int size = atoi(buffer.substr(0, pos).c_str()); // 提取长度字段
        int surplus = buffer.size() - pos - 2*SEP_LEN; // 计算剩余缓冲区长度

        if(surplus >= size) // 如果剩余缓冲区长度大于等于报文长度
        {
            // 至少存在一个完整的报文,可以进行提取
            buffer.erase(0, pos + SEP_LEN); // 移除长度字段及分隔符
            std::string s = buffer.substr(0, size); // 提取报文
            buffer.erase(0, size + SEP_LEN); // 移除已处理的报文及分隔符
            return s; // 返回解码后的报文
        }
        else
        {
            return ""; // 剩余缓冲区不足以构成一个完整报文,返回空字符串
        }
    }

    // 对字符串进行编码,添加长度前缀和分隔符,格式为 "length\r\nmessage\r\n"
    // 示例:编码 "XXXXXX" 为 "123\r\nXXXXXX\r\n"
    std::string Encode(std::string &s)
    {
        std::string new_package = std::to_string(s.size());
        new_package += SEP;
        new_package += s;
        new_package += SEP;
        return new_package;
    }

}

命名空间 ns_protocol

namespace ns_protocol
{
    // ...
}

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

预处理器宏定义

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

定义了几个预处理器宏,用于简化代码中对特定字符串和其长度的引用:

  • SPACE:表示单个空格字符。
  • SPACE_LEN:计算 SPACE 字符串的长度,即 1。
  • SEP:表示换行符序列 \r\n
  • SEP_LEN:计算 SEP 字符串的长度,即 2。

类 Request

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

std::string Serialize()

        // 1. 自定义序列化方法,格式为 "length\r\nx_ op_ y_\r\n"
        // 2. 使用JsonCpp库进行序列化
        std::string Serialize()
        {
#ifdef MYSELF// 自定义序列化:拼接字符串并返回
            std::string str;
            str = std::to_string(x_);
            str += SPACE;
            str += op_;
            str += SPACE;
            str += std::to_string(y_);
            return str;
#else// 使用JsonCpp序列化:创建Json对象,填充数据,然后使用FastWriter写入字符串并返回
            Json::Value root;
            root["x"] = x_;
            root["y"] = y_;
            root["op"] = op_;
            Json::FastWriter writer;
            return writer.write(root);
#endif
        }

该函数将 Request 对象序列化为字符串形式,以便通过网络传输。根据 MYSELF 宏定义的不同,可以选择两种序列化方式:

  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
    // 自定义反序列化逻辑...
#else
    Json::Value root;
    Json::Reader reader;
    reader.parse(str, root);

    x_ = root["x"].asInt();
    y_ = root["y"].asInt();
    op_ = root["op"].asInt();

    return true;
#endif
}

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

  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);  // 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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SQLite数据库中JSON 函数和运算符(二十七)

返回&#xff1a;SQLite—系列文章目录 上一篇:维护SQLite的私有分支&#xff08;二十六&#xff09; 下一篇&#xff1a;SQLite—系列文章目录 ​1. 概述 默认情况下&#xff0c;SQLite 支持 29 个函数和 2 个运算符 处理 JSON 值。还有两个表值函数可用于分解 JSON 字…

程序员自由创业周记#32:新产品构思

程序员自由创业周记#32&#xff1a;新产品构思 新作品 我时常把自己看做一位木匠&#xff0c;有点手艺&#xff0c;能做一些作品养活自己。而 加一、Island Widgets、Nap 就是我的作品。 接下来在持续维护迭代的同时&#xff0c;要开启下一个作品的创造了。 其实早在2022的1…

【C++初阶】List使用特性及其模拟实现

1. list的介绍及使用 1.1 list的介绍 1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代。 2. list的底层是双向链表结构&#xff0c;双向链表中每个元素存储在互不相关的独立节点中&#xff0c;在节点中通过指针指向其前…

(2022级)成都工业学院数据库原理及应用实验五: SQL复杂查询

写在前面 1、基于2022级软件工程/计算机科学与技术实验指导书 2、成品仅提供参考 3、如果成品不满足你的要求&#xff0c;请寻求其他的途径 运行环境 window11家庭版 Navicat Premium 16 Mysql 8.0.36 实验要求 在实验三的基础上完成下列查询&#xff1a; 1、查询医生…

漆包线行业你了解多少?专业漆包线行业MES生产管理系统

今天就说说漆包线行业&#xff0c;漆包线是工业电机&#xff08;包括电动机和发电机&#xff09;、变压器、电工仪表、电力及电子元器件、电动工具、家用电器、汽车电器等用来绕制电磁线圈的主要材料。 漆包线上游是铜杆行业&#xff0c;下游是各种消费终端&#xff0c;主要是电…

[大模型]Qwen-Audio-chat FastApi 部署调用

Qwen-Audio-chat FastApi 部署调用 Qwen-Audio 介绍 Qwen-Audio 是阿里云研发的大规模音频语言模型&#xff08;Large Audio Language Model&#xff09;。Qwen-Audio 可以以多种音频 (包括说话人语音、自然音、音乐、歌声&#xff09;和文本作为输入&#xff0c;并以文本作为…

Linux-用户管理类命令实训

查看根目录下有哪些内容 进入/tmp目录&#xff0c;以自己的学号建一个目录&#xff0c;并进入该目录 像是目前所在的目录 在当前目录下&#xff0c;建立权限为741的目录test1 在目录test1下建立目录test2/test3/test4 进入test2&#xff0c;删除目录test3/test4 &#xff08;7&…

Python实现在线翻译工具

Python实现在线翻译工具 使用Python的内置的标准库tkinter和webbrowser&#xff0c;实现一个简单Python在线翻译工具。 tkinter库用来创建一个图形用户界面&#xff08;GUI&#xff09;&#xff0c;webbrowser库用来打开网页。 webbrowser 是 Python 的一个标准库&#xff0…

7.MMD 法线贴图的设置与调教

前期准备 人物 导入温迪模型导入ray.x和ray_controler.pmx导入天空盒time of day调成模型绘制顺序&#xff0c;将天空盒调到最上方给温迪模型添加main.fx材质在自发光一栏&#xff0c;给天空盒添加time of lighting材质 打开材质里的衣服&#xff0c;发现只有一个衣服文件 …

SpringBoot集成FTP

1.加入核心依赖 <dependency><groupId>commons-net</groupId><artifactId>commons-net</artifactId><version>3.8.0</version></dependency> 完整依赖 <dependencies><dependency><groupId>org.springfra…

什么是时间序列分析

时间序列分析是现代计量经济学的重要内容&#xff0c;广泛应用于经济、商业、社会问题研究中&#xff0c;在指标预测中具有重要地位&#xff0c;是研究统计指标动态特征和周期特征及相关关系的重要方法。 一、基本概念 经济社会现象随着时间的推移留下运行轨迹&#xff0c;按…

Linux网络编程--网络传输

Linux网络编程--网络传输 Linux网络编程TCP/IP网络模型网络通信的过程局域网通信跨网络通信&#xff1a;问题总结&#xff1a; Linux网络编程 TCP/IP网络模型 发送方&#xff08;包装&#xff09;&#xff1a; 应用层&#xff1a;HTTP HTTPS SSH等 —> 包含数据&#xff0…

二维码门楼牌管理应用平台建设:助力场所整改与消防安全

文章目录 前言一、二维码门楼牌管理应用平台的构建背景二、二维码门楼牌管理应用平台在场所整改中的作用三、二维码门楼牌管理应用平台的意义与价值四、二维码门楼牌管理应用平台的未来展望 前言 随着城市管理的日益精细化&#xff0c;二维码门楼牌管理应用平台的建设成为了提…

正则表达式中 “$” 并不是表示 “字符串结束”

△△请给“Python猫”加星标 &#xff0c;以免错过文章推送 作者&#xff1a;Seth Larson 译者&#xff1a;豌豆花下猫Python猫 英文&#xff1a;Regex character “$” doesnt mean “end-of-string” 转载请保留作者及译者信息&#xff01; 这篇文章写一写我最近在用 Python …

【Django】学习笔记

文章目录 [toc]MVC与MTVMVC设计模式MTV设计模式 Django下载Django工程创建与运行创建工程运行工程 子应用创建与注册安装创建子应用注册安装子应用 数据模型ORM框架模型迁移 Admin站点修改语言和时区设置管理员账号密码模型注册显示对象名称模型显示中文App显示中文 视图函数与…

英文面试中如何回答为什么离职?柯桥英语口语学校

面试中&#xff0c;面试官可能会问到如下问题&#xff1a; 1.Why are you looking for another job? 你为什么要换工作&#xff1f; 2.May I ask why you left the company? 可以问一下你为什么要离开那家公司吗&#xff1f; 3.What’s the reason for you to leave your …

基于通达信---做T专用算法

什么是做T? 股票做T是股票市场中常见的一种投资策略,也就是股票进行T+0操作,通过当天买进的股票,在当天卖出,是股市中常见的一种超短线的操作。其中T就是指交易日,利用交易日中的股票涨跌来赚取差价。股票做T常见的类型就是正T和倒T。 1、正T 股票做正t就是指先买后卖,…

历史融资额高达 2.44 亿美元的 Monad,是何方神圣?

以并行 EVM 为特点的 Monad&#xff0c;又一个具备竞争力的“以太坊杀手” 在今年 4 月初&#xff0c;Monad Labs&#xff08;Monad 的开发团队&#xff09; 获得了一笔由 Paradigm 领投的高达 2.25 亿美元的巨额融资&#xff0c;本轮融资的其他投资者还包括&#xff1a; Elec…

云服务器需要多少流量?评估支持最大并发量?

一 需要购买多大的流量&#xff1f; 项目上线时&#xff0c;我们需要购买多大的流量的带宽&#xff1f;支持多少设备&#xff08;支持多少并发量&#xff0c;在设计阶段会计算&#xff09;&#xff1f;作为架构师我们必须清楚与明确。 二 清楚服务器的流量计算 常见的云服务主机…

Android安卓写入WIFI热点自动连接NDEF标签

本示例使用的发卡器&#xff1a;Android Linux RFID读写器NFC发卡器WEB可编程NDEF文本/网址/海报-淘宝网 (taobao.com) package com.usbreadertest;import android.os.Bundle; import android.view.MenuItem; import android.view.View; import android.widget.EditText; impo…