运用自定义协议设计与实现“跨网络计算器”

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 运用自定义协议设计与实现“跨网络计算器”

一、设计方案

1. 日志模块 (Log.hpp)

日志模块提供了一个简单的日志记录功能,允许将日志输出到控制台、单个文件或按日志级别分类的文件中。它定义了不同级别的日志(Info, Debug, Warning, Error, Fatal),并允许通过Enable方法切换日志输出方式。

2. 协议模块 (Protocol.hpp)

协议模块定义了请求和响应的数据格式。Request类封装了计算请求的数据,包括操作数和操作符。Response类封装了计算结果和错误代码。这两个类都提供了序列化和反序列化的方法,以便将数据转换为网络传输的格式。

3. 服务器模块 (ServerCal.hpp、Socket.hpp 和 TcpServer.hpp )

服务器模块由ServerCalSocket.hppTcpServer类组成。

  1. ServerCal类负责处理计算请求,它定义了一个Calculator方法来执行实际的算术运算,并返回结果和错误代码。
  2. Socket类封装了基本的socket操作,如连接、读取和写入。
  3. TcpServer类负责网络通信,它监听指定端口,接受客户端连接,并使用回调函数来处理接收到的数据。

4. 客户端模块 (ClientCal.cpp)

客户端模块提供了与服务器通信的能力,ClientCal类使用Socket类来向服务器发送计算请求,并接收响应。

客户端程序首先检查命令行参数是否正确,然后创建一个套接字并连接到服务器。程序将随机生成两个数字和一个操作符,创建一个请求对象,并将其序列化为字符串。然后,程序将这个字符串编码为网络字节流,并通过套接字发送给服务器。接收到服务器的响应后,程序将其解码并反序列化为响应对象,然后打印出请求和响应的详细信息。这个过程将重复10次,每次请求后程序会暂停1秒。最后,程序关闭套接字并退出。

二、日志模块、makefile文件

✅Log.hpp

// 预处理指令,确保头文件只被包含一次
#pragma once

// 引入必要的头文件
#include <iostream> // 标准输入输出流
#include <time.h>    // 时间函数
#include <stdarg.h>  // 可变参数列表
#include <sys/types.h> // 文件系统类型
#include <sys/stat.h> // 文件状态
#include <fcntl.h>   // 文件控制
#include <unistd.h>   // UNIX标准函数
#include <stdlib.h>   // 标准库

// 定义常量SIZE,用于缓冲区大小
#define SIZE 1024

// 定义日志级别,分别对应不同的日志重要性
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

// 定义日志输出方式,分别对应控制台输出和文件输出
#define Screen 1
#define Onefile 2
#define Classfile 3

// 定义日志文件名
#define LogFile "log.txt"

// 日志类Log的声明
class Log
{
public:
    // 构造函数,初始化日志输出方式为控制台输出,日志路径为当前目录下的log文件夹
    Log()
    {
        printMethod = Screen; // 默认输出方式为控制台输出
        path = "./log/";     // 默认日志路径
    }

    // 设置日志输出方式的函数
    void Enable(int method)
    {
        printMethod = method; // 根据传入的参数设置输出方式
    }

    // 将日志级别转换为字符串的函数
    std::string levelToString(int level)
    {
        // 使用switch语句根据日志级别返回对应的字符串
        switch (level)
        {
            case Info:
                return "Info";
            case Debug:
                return "Debug";
            case Warning:
                return "Warning";
            case Error:
                return "Error";
            case Fatal:
                return "Fatal";
            default:
                return "None";
        }
    }

    // 打印日志的函数
    void printLog(int level, const std::string &logtxt)
    {
        // 根据输出方式选择不同的打印方法
        switch (printMethod)
        {
            case Screen:
                std::cout << logtxt << std::endl; // 控制台输出
                break;
            case Onefile:
                printOneFile(LogFile, logtxt); // 单文件输出
                break;
            case Classfile:
                printClassFile(level, logtxt); // 分类文件输出
                break;
            default:
                break;
        }
    }

    // 单文件日志输出的实现
    void printOneFile(const std::string &logname, const std::string &logtxt)
    {
        // 拼接完整的日志文件路径
        std::string _logname = path + logname;
        // 打开文件,如果失败则直接返回
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
        if (fd < 0)
            return;
        // 将日志文本写入文件
        write(fd, logtxt.c_str(), logtxt.size());
        // 关闭文件描述符
        close(fd);
    }

    // 分类文件日志输出的实现
    void printClassFile(int level, const std::string &logtxt)
    {
        // 根据日志级别创建对应的文件名
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level);
        // 调用单文件输出函数
        printOneFile(filename, logtxt);
    }

    // 析构函数,目前为空
    ~Log()
    {
    }

    // 重载函数调用运算符,用于格式化输出日志
    void operator()(int level, const char *format, ...)
    {
        // 获取当前时间
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        // 格式化时间字符串
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]",
                 levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        // 使用可变参数列表进行格式化字符串
        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        // 拼接完整的日志文本
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

        // 调用打印日志函数
        printLog(level, logtxt);
    }

private:
    // 日志输出方式
    int printMethod;
    // 日志文件路径
    std::string path;
};

// 定义一个Log类的全局对象lg,用于输出日志
Log lg;

✅makefile

.PHONY:all
all:servercal clientcal

Flag=#-DMySelf=1
Lib=-ljsoncpp

servercal:ServerCal.cc
  g++ -o $@ $^ -std=c++11 $(Lib) $(Flag)
clientcal:ClientCal.cc
  g++ -o $@ $^ -std=c++11 -g $(Lib) $(Flag)


.PHONY:clean
clean:
  rm -f clientcal servercal

三、协议模块(Protocol.hpp)

✅Protocol.hpp

#pragma once // 确保头文件在整个程序中只被包含一次

#include <iostream> // 包含标准输入输出流
#include <string> // 包含字符串类
#include <jsoncpp/json/json.h> // 包含JSONCPP库,用于JSON数据的处理

// 宏定义,用于序列化和反序列化过程中的数据分隔
const std::string blank_space_sep = " "; // 空格分隔符
const std::string protocol_sep = "\n"; // 换行符作为协议的分隔符

// 序列化函数,将内容字符串包装成网络传输的格式
std::string Encode(std::string &content)
{
    std::string package; // 创建一个字符串用于存储包装后的数据
    package = std::to_string(content.size()); // 将内容的长度转换为字符串
    package += protocol_sep; // 添加协议分隔符
    package += content; // 添加内容本身
    package += protocol_sep; // 再次添加协议分隔符,表示数据结束

    return package; // 返回包装后的字符串
}

// 反序列化函数,将接收到的网络数据解析为内容字符串
bool Decode(std::string &package, std::string *content)
{
    std::size_t pos = package.find(protocol_sep); // 查找协议分隔符的位置
    if(pos == std::string::npos) return false; // 如果找不到分隔符,解析失败

    std::string len_str = package.substr(0, pos); // 提取长度字符串
    std::size_t len = std::stoi(len_str); // 将长度字符串转换为数字
    std::size_t total_len = len_str.size() + len + 2; // 计算总长度(长度字符串 + 内容 + 分隔符)
    if(package.size() < total_len) return false; // 如果实际数据长度小于预期,解析失败

    *content = package.substr(pos+1, len); // 提取内容字符串
    package.erase(0, total_len); // 从原始数据中移除已解析的部分

    return true; // 解析成功
}

// 请求数据结构
class Request
{
public:
    // 构造函数,初始化请求的参数
    Request(int data1, int data2, char oper) : x(data1), y(data2), op(oper) {}
    // 默认构造函数
    Request() {}

public:
    // 序列化方法,将请求对象转换为字符串
    bool Serialize(std::string *out)
    {
#ifdef MySelf
        // 使用简单的字符串拼接方式
        std::string s = std::to_string(x); // 将操作数x转换为字符串
        s += blank_space_sep; // 添加空格分隔符
        s += op; // 添加操作符
        s += blank_space_sep; // 再次添加空格分隔符
        s += std::to_string(y); // 将操作数y转换为字符串
        *out = s; // 将拼接后的字符串赋值给输出参数
        return true; // 返回成功
#else
        // 使用JSON格式
        Json::Value root; // 创建JSON值对象
        root["x"] = x; // 添加操作数x
        root["y"] = y; // 添加操作数y
        root["op"] = op; // 添加操作符
        Json::StyledWriter w; // 创建JSON格式化写入器
        *out = w.write(root); // 将JSON对象转换为格式化的字符串
        return true; // 返回成功
#endif
    }

    // 反序列化方法,将字符串转换为请求对象
    bool Deserialize(const std::string &in)
    {
#ifdef MySelf
        // 使用简单的字符串拼接方式
        std::size_t left = in.find(blank_space_sep); // 查找第一个空格分隔符
        if (left == std::string::npos) return false; // 如果找不到分隔符,解析失败
        std::string part_x = in.substr(0, left); // 提取操作数x的字符串

        std::size_t right = in.rfind(blank_space_sep); // 查找最后一个空格分隔符
        if (right == std::string::npos) return false; // 如果找不到分隔符,解析失败
        std::string part_y = in.substr(right + 1); // 提取操作数y的字符串

        if (left + 2 != right) return false; // 如果分隔符之间的长度不符合预期,解析失败
        op = in[left + 1]; // 提取操作符
        x = std::stoi(part_x); // 将操作数x的字符串转换为数字
        y = std::stoi(part_y); // 将操作数y的字符串转换为数字
        return true; // 返回成功
#else
        // 使用JSON格式
        Json::Value root;
        Json::Reader r;
        if (!r.parse(in, root)) return false; // 解析JSON字符串,如果失败则返回

        x = root["x"].asInt(); // 提取操作数x
        y = root["y"].asInt(); // 提取操作数y
        op = root["op"].asInt(); // 提取操作符
        return true; // 返回成功
#endif
    }

    // 调试方法,打印请求对象的内容
    void DebugPrint()
    {
        std::cout << "新请求构建完成:  " << x << op << y << "=?" << std::endl; // 打印操作数和操作符
    }

public:
    // 请求的数据成员
    int x; // 操作数x
    int y; // 操作数y
    char op; // 操作符,可以是 + - * / %
};

// 响应数据结构
class Response
{
public:
    // 构造函数,初始化响应的参数
    Response(int res, int c) : result(res), code(c) {}
    // 默认构造函数
    Response() {}

public:
    // 序列化方法,将响应对象转换为字符串
    bool Serialize(std::string *out)
    {
#ifdef MySelf
        // 使用简单的字符串拼接方式
        std::string s = std::to_string(result); // 将结果转换为字符串
        s += blank_space_sep; // 添加空格分隔符
        s += std::to_string(code); // 将错误代码转换为字符串
        *out = s; // 将拼接后的字符串赋值给输出参数
        return true; // 返回成功
#else
        // 使用JSON格式
        Json::Value root; // 创建JSON值对象
        root["result"] = result; // 添加结果
        root["code"] = code; // 添加错误代码
        Json::StyledWriter w; // 创建JSON格式化写入器
        *out = w.write(root); // 将JSON对象转换为格式化的字符串
        return true; // 返回成功
#endif
    }

    // 反序列化方法,将字符串转换为响应对象
    bool Deserialize(const std::string &in)
    {
#ifdef MySelf
        // 使用简单的字符串拼接方式
        std::size_t pos = in.find(blank_space_sep); // 查找空格分隔符
        if (pos == std::string::npos) return false; // 如果找不到分隔符,解析失败
        std::string part_left = in.substr(0, pos); // 提取结果字符串
        std::string part_right = in.substr(pos + 1); // 提取错误代码字符串

        result = std::stoi(part_left); // 将结果字符串转换为数字
        code = std::stoi(part_right); // 将错误代码字符串转换为数字
        return true; // 返回成功
#else
        // 使用JSON格式
        Json::Value root;
        Json::Reader r;
        if (!r.parse(in, root)) return false; // 解析JSON字符串,如果失败则返回

        result = root["result"].asInt(); // 提取结果
        code = root["code"].asInt(); // 提取错误代码
        return true; // 返回成功
#endif
    }

    // 调试方法,打印响应对象的内容
    void DebugPrint()
    {
        std::cout << "结果响应完成, result: " << result << ", code: " << code << std::endl; // 打印结果和错误代码
    }

public:
    int result; // 计算结果
    int code; // 错误代码,0表示成功,非0表示错误
};

四、服务端模块

✅ServerCal.hpp

// 预处理指令,确保头文件只被包含一次
#pragma once

// 引入必要的头文件
#include <iostream>
// 引入自定义的协议头文件
#include "Protocol.hpp"

// 定义枚举类型,用于表示不同类型的操作和错误
enum
{
    Div_Zero = 1, // 除数为零的错误代码
    Mod_Zero,    // 模数为零的错误代码
    Other_Oper     // 其他操作或者错误
};

// 声明ServerCal类,用于处理客户端的计算请求
class ServerCal
{
public:
    // 构造函数
    ServerCal()
    {
    }

    // 计算助手函数,根据请求计算结果并返回响应对象
    Response CalculatorHelper(const Request &req)
    {
        Response resp(0, 0); // 创建一个响应对象,初始化结果和错误代码为0
        // 根据请求中的操作符进行计算
        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 (req.y == 0) // 如果除数为0,设置错误代码
                    resp.code = Div_Zero;
                else // 否则进行除法运算
                    resp.result = req.x / req.y;
            }
            break;
            case '%':
            {
                if (req.y == 0) // 如果模数为0,设置错误代码
                    resp.code = Mod_Zero;
                else // 否则进行模运算
                    resp.result = req.x % req.y;
            }
            break;
            default: // 如果操作符不是预定义的几种
                resp.code = Other_Oper; // 设置错误代码为其他操作
                break;
        }

        return resp; // 返回计算结果和错误代码
    }

    // 计算函数,解析请求字符串,并返回计算结果
    std::string Calculator(std::string &package)
    {
        std::string content; // 用于存储解码后的内容
        // 解析请求包的长度和内容
        bool r = Decode(package, &content);
        if (!r)
            return ""; // 如果解码失败,返回空字符串

        // 从解码后的内容中反序列化请求对象
        Request req;
        r = req.Deserialize(content);
        if (!r)
            return ""; // 如果反序列化失败,返回空字符串

        // 清空content,准备存储响应内容
        content = "";
        // 调用助手函数进行计算
        Response resp = CalculatorHelper(req);
        // 将计算结果和错误代码序列化到content
        resp.Serialize(&content);
        // 编码响应内容,添加长度前缀
        content = Encode(content);

        return content; // 返回响应字符串
    }

    // 析构函数
    ~ServerCal()
    {
    }
};

✅Socket.hpp

// 预处理指令,确保头文件只被包含一次
#pragma once

// 引入必要的头文件
#include <iostream>
// 引入自定义的协议头文件
#include "Protocol.hpp"

// 定义枚举类型,用于表示不同类型的操作和错误
enum
{
    Div_Zero = 1, // 除数为零的错误代码
    Mod_Zero,    // 模数为零的错误代码
    Other_Oper     // 其他操作或者错误
};

// 声明ServerCal类,用于处理客户端的计算请求
class ServerCal
{
public:
    // 构造函数
    ServerCal()
    {
    }

    // 计算助手函数,根据请求计算结果并返回响应对象
    Response CalculatorHelper(const Request &req)
    {
        Response resp(0, 0); // 创建一个响应对象,初始化结果和错误代码为0
        // 根据请求中的操作符进行计算
        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 (req.y == 0) // 如果除数为0,设置错误代码
                    resp.code = Div_Zero;
                else // 否则进行除法运算
                    resp.result = req.x / req.y;
            }
            break;
            case '%':
            {
                if (req.y == 0) // 如果模数为0,设置错误代码
                    resp.code = Mod_Zero;
                else // 否则进行模运算
                    resp.result = req.x % req.y;
            }
            break;
            default: // 如果操作符不是预定义的几种
                resp.code = Other_Oper; // 设置错误代码为其他操作
                break;
        }

        return resp; // 返回计算结果和错误代码
    }

    // 计算函数,解析请求字符串,并返回计算结果
    std::string Calculator(std::string &package)
    {
        std::string content; // 用于存储解码后的内容
        // 解析请求包的长度和内容
        bool r = Decode(package, &content);
        if (!r)
            return ""; // 如果解码失败,返回空字符串

        // 从解码后的内容中反序列化请求对象
        Request req;
        r = req.Deserialize(content);
        if (!r)
            return ""; // 如果反序列化失败,返回空字符串

        // 清空content,准备存储响应内容
        content = "";
        // 调用助手函数进行计算
        Response resp = CalculatorHelper(req);
        // 将计算结果和错误代码序列化到content
        resp.Serialize(&content);
        // 编码响应内容,添加长度前缀
        content = Encode(content);

        return content; // 返回响应字符串
    }

    // 析构函数
    ~ServerCal()
    {
    }
};

✅TcpServer.hpp

// 预处理指令,确保头文件只被包含一次
#pragma once

// 引入必要的头文件
#include <functional> // 用于支持 std::function
#include <string>     // 用于支持 std::string
#include <signal.h>   // 用于处理信号
#include "Log.hpp"     // 自定义的日志头文件
#include "Socket.hpp"  // 自定义的套接字头文件

// 定义一个类型别名,用于简化函数参数
using func_t = std::function<std::string(std::string &package)>;

// 声明TcpServer类,用于创建和运行TCP服务器
class TcpServer
{
public:
    // 构造函数,初始化服务器的端口号和回调函数
    TcpServer(uint16_t port, func_t callback) : port_(port), callback_(callback)
    {
    }
    
    // 初始化服务器的函数,创建套接字,绑定端口,开始监听
    bool InitServer()
    {
        // 创建监听套接字
        listensock_.Socket();
        // 绑定端口到套接字
        listensock_.Bind(port_);
        // 开始监听连接请求
        listensock_.Listen();
        // 记录服务器初始化信息
        lg(Info, "init server .... done");
        return true;
    }

    // 启动服务器的主函数,处理客户端连接请求
    void Start()
    {
        // 忽略子进程退出和管道信号
        signal(SIGCHLD, SIG_IGN);
        signal(SIGPIPE, SIG_IGN);
        // 循环处理连接请求
        while (true)
        {
            // 接受客户端的连接请求,获取客户端的IP地址和端口号
            std::string clientip;
            uint16_t clientport;
            int sockfd = listensock_.Accept(&clientip, &clientport);
            // 如果接受连接失败,则继续下一次循环
            if (sockfd < 0)
                continue;
            // 记录客户端连接信息
            lg(Info, "accept a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);
            // 处理客户端请求,创建子进程提供服务
            if (fork() == 0)
            {
                // 关闭监听套接字
                listensock_.Close();
                std::string inbuffer_stream;
                // 循环读取客户端发送的数据
                while (true)
                {
                    // 读取数据到缓冲区
                    char buffer[1280];
                    ssize_t n = read(sockfd, buffer, sizeof(buffer));
                    // 如果读取到数据
                    if (n > 0)
                    {
                        // 将读取的数据添加到输入缓冲区
                        buffer[n] = 0;
                        inbuffer_stream += buffer;
                        // 记录调试信息
                        lg(Debug, "debug:\n%s", inbuffer_stream.c_str());
                        // 循环处理输入缓冲区中的数据
                        while (true)
                        {
                            // 调用回调函数处理数据,并获取响应信息
                            std::string info = callback_(inbuffer_stream);
                            // 如果没有响应信息,则跳出循环
                            if (info.empty())
                                break;
                            // 记录调试信息
                            lg(Debug, "debug, response:\n%s", info.c_str());
                            // 记录调试信息
                            lg(Debug, "debug:\n%s", inbuffer_stream.c_str());
                            // 将响应信息发送回客户端
                            write(sockfd, info.c_str(), info.size());
                        }
                    }
                    // 如果客户端关闭连接,则跳出循环
                    else if (n == 0)
                        break;
                    // 如果读取失败,则跳出循环
                    else
                        break;
                }
                // 子进程服务结束后退出
                exit(0);
            }
            // 关闭客户端套接字
            close(sockfd);
        }
    }

    // 析构函数
    ~TcpServer()
    {
    }

private:
    // 服务器的端口号
    uint16_t port_;
    // 监听套接字
    Sock listensock_;
    // 回调函数,用于处理客户端请求
    func_t callback_;
};

✅ServerCal.cpp

// 引入必要的头文件
#include "TcpServer.hpp" // 引入自定义的TCP服务器头文件
#include "ServerCal.hpp"  // 引入自定义的计算器逻辑头文件
#include <unistd.h>      // 引入UNIX标准函数库,用于系统调用如sleep等
// #include "Daemon.hpp" // 引入自定义的守护进程头文件(当前被注释)

// 定义Usage函数,用于输出程序的使用方法
static void Usage(const std::string &proc)
{
    std::cout << "\nUsage: " << proc << " port\n" << std::endl; // 输出程序的使用方法
}

// 主函数,程序的入口点
int main(int argc, char *argv[])
{
    // 检查命令行参数数量是否正确
    if(argc != 2)
    {
        Usage(argv[0]); // 如果参数不正确,输出使用方法并退出程序
        exit(0);
    }
    // 将命令行参数转换为端口号
    uint16_t port = std::stoi(argv[1]);
    // 创建计算器逻辑对象
    ServerCal cal;
    // 创建TCP服务器对象,绑定端口号和计算器逻辑对象的Calculator方法
    TcpServer *tsvp = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));
    // 初始化服务器
    tsvp->InitServer();
    // 调用守护进程相关函数,将程序转为守护进程运行(当前被注释)
    // Daemon();
    daemon(0, 0); // 调用守护进程函数,参数设置为0表示不进行标准输出和错误输出的重定向
    // 启动服务器,开始监听和处理客户端请求
    tsvp->Start();
    return 0; // 程序正常结束
}

五、客户端模块

✅ClientCal.cpp

// 引入所需的头文件
#include <iostream> // 用于输入输出流
#include <string>   // 用于字符串类
#include <ctime>    // 用于时间相关函数
#include <cassert>   // 用于断言检查
#include <unistd.h>  // 用于UNIX标准函数,如sleep
#include "Socket.hpp" // 自定义的套接字操作库
#include "Protocol.hpp" // 自定义的通信协议库

// 定义Usage函数,用于输出程序的使用方法
static void Usage(const std::string &proc)
{
    std::cout << "\nUsage: " << proc << " serverip serverport\n"
              << std::endl;
}

// 主函数,程序的入口点
int main(int argc, char *argv[])
{
    // 检查命令行参数数量是否正确
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    // 从命令行参数获取服务器的IP地址和端口号
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 创建套接字对象
    Sock sockfd;
    // 创建套接字
    sockfd.Socket();
    // 尝试连接到服务器
    bool r = sockfd.Connect(serverip, serverport);
    if(!r) return 1; // 如果连接失败,则退出程序

    // 初始化随机数生成器
    srand(time(nullptr) ^ getpid());
    // 定义测试次数
    int cnt = 1;
    // 定义操作符字符串
    const std::string opers = "+-*/%=-=&^";

    // 定义输入缓冲区流
    std::string inbuffer_stream;
    // 循环发送请求并接收响应,直到发送了10次
    while(cnt <= 10)
    {
        // 输出测试次数信息
        std::cout << "===============第" << cnt << "次测试....., " << "===============" << std::endl;
        // 生成随机数作为请求的参数
        int x = rand() % 100 + 1;
        usleep(1234); // 微秒级的暂停
        int y = rand() % 100;
        usleep(4321); // 微秒级的暂停
        // 随机选择一个操作符
        char oper = opers[rand() % opers.size()];
        // 创建请求对象
        Request req(x, y, oper);
        // 打印请求的详细信息(调试用)
        req.DebugPrint();

        // 序列化请求对象到字符串
        std::string package;
        req.Serialize(&package);

        // 将请求字符串编码为网络字节流
        package = Encode(package);

        // 通过套接字发送请求
        write(sockfd.Fd(), package.c_str(), package.size());
        // 读取服务器的响应
        char buffer[128];
        ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer)); // 读取响应到缓冲区
        if(n > 0)
        {
            // 确保读取的字符串以空字符结尾
            buffer[n] = 0;
            // 将读取的内容添加到输入缓冲区流
            inbuffer_stream += buffer;
            // 输出接收到的完整响应
            std::cout << inbuffer_stream << std::endl;
            // 从输入缓冲区流中解码出响应内容
            std::string content;
            bool r = Decode(inbuffer_stream, &content); // 解码响应内容
            assert(r); // 断言解码成功

            // 反序列化响应内容到Response对象
            Response resp;
            r = resp.Deserialize(content);
            assert(r); // 断言反序列化成功

            // 打印响应的详细信息(调试用)
            resp.DebugPrint();
        }

        // 输出测试分隔线
        std::cout << "=================================================" << std::endl;
        // 暂停一秒
        sleep(1);

        // 增加测试次数
        cnt++;
    }

    // 关闭套接字
    sockfd.Close();
    // 程序正常退出
    return 0;
}

六、设计方案总结

  1. 模块化:代码被分为不同的模块,每个模块负责一个特定的功能。这种设计使得代码易于理解和维护。
  2. 日志记录:通过Log类,服务器和客户端可以记录操作信息和错误信息,有助于调试和监控。
  3. 自定义协议:通过Protocol类,定义了一个简单的文本协议来传输请求和响应。协议的设计简单明了,易于理解和实现。
  4. 错误处理:服务器和客户端都实现了基本的错误处理逻辑,能够处理一些常见的错误情况。
  5. 多进程模型TcpServer类使用多进程模型来处理并发连接,每个客户端连接都在自己的进程中运行。
  6. 安全性:虽然代码中没有明确提到安全性措施,但在实际部署时,应该考虑使用加密通信、身份验证等安全措施来保护数据。
  7. 性能优化:在生产环境中,可能需要进一步优化服务器的性能,比如使用多线程或异步I/O代替多进程模型,或者使用专门的高性能网络库。
  8. 可扩展性:代码设计允许未来添加新的功能,比如支持更多的操作符或增加新的操作类型。

通过上述设计方案,我们可以得到一个基础的TCP服务器和客户端,它们可以根据自定义协议处理客户端请求,并支持基本的算术运算。这个系统可以作为“跨网络计算器”的基础,通过定义合适的回调函数和协议格式来实现计算器的功能。

目录
相关文章
|
23天前
|
负载均衡 网络协议 算法
|
3月前
|
域名解析 存储 网络协议
深入解析网络通信关键要素:IP 协议、DNS 及相关技术
本文详细介绍了IP协议报头结构及其各字段的功能,包括版本、首部长度、服务类型、总长度、标识、片偏移、标志、生存时间(TTL)、协议、首部检验和等内容。此外,还探讨了IP地址的网段划分、特殊IP地址的应用场景,以及路由选择的大致流程。最后,文章简要介绍了DNS协议的作用及其发展历史,解释了域名解析系统的工作原理。
130 5
深入解析网络通信关键要素:IP 协议、DNS 及相关技术
用MASM32按Time Protocol(RFC868)协议编写网络对时程序中的一些有用的函数代码
用MASM32按Time Protocol(RFC868)协议编写网络对时程序中的一些有用的函数代码
|
3月前
|
缓存 算法 物联网
基于AODV和leach协议的自组网络平台matlab仿真,对比吞吐量,负荷,丢包率,剩余节点个数,节点消耗能量
本系统基于MATLAB 2017b,对AODV与LEACH自组网进行了升级仿真,新增运动节点路由测试,修正丢包率统计。AODV是一种按需路由协议,结合DSDV和DSR,支持动态路由。程序包含参数设置、消息收发等功能模块,通过GUI界面配置节点数量、仿真时间和路由协议等参数,并计算网络性能指标。 该代码实现了节点能量管理、簇头选举、路由发现等功能,并统计了网络性能指标。
166 73
|
14天前
|
网络协议 网络安全 网络虚拟化
本文介绍了十个重要的网络技术术语,包括IP地址、子网掩码、域名系统(DNS)、防火墙、虚拟专用网络(VPN)、路由器、交换机、超文本传输协议(HTTP)、传输控制协议/网际协议(TCP/IP)和云计算
本文介绍了十个重要的网络技术术语,包括IP地址、子网掩码、域名系统(DNS)、防火墙、虚拟专用网络(VPN)、路由器、交换机、超文本传输协议(HTTP)、传输控制协议/网际协议(TCP/IP)和云计算。通过这些术语的详细解释,帮助读者更好地理解和应用网络技术,应对数字化时代的挑战和机遇。
51 3
|
17天前
|
网络虚拟化
生成树协议(STP)及其演进版本RSTP和MSTP,旨在解决网络中的环路问题,提高网络的可靠性和稳定性
生成树协议(STP)及其演进版本RSTP和MSTP,旨在解决网络中的环路问题,提高网络的可靠性和稳定性。本文介绍了这三种协议的原理、特点及区别,并提供了思科和华为设备的命令示例,帮助读者更好地理解和应用这些协议。
36 4
|
25天前
|
网络协议 安全 Go
Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
【10月更文挑战第28天】Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
49 13
|
24天前
|
存储 缓存 网络协议
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点,GET、POST的区别,Cookie与Session
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点、状态码、报文格式,GET、POST的区别,DNS的解析过程、数字证书、Cookie与Session,对称加密和非对称加密
|
25天前
|
网络协议 算法 网络性能优化
计算机网络常见面试题(一):TCP/IP五层模型、TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议
计算机网络常见面试题(一):TCP/IP五层模型、应用层常见的协议、TCP与UDP的区别,TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议、ARP协议
|
2月前
|
安全 网络协议 算法
HTTPS网络通信协议揭秘:WEB网站安全的关键技术
HTTPS网络通信协议揭秘:WEB网站安全的关键技术
172 4
HTTPS网络通信协议揭秘:WEB网站安全的关键技术