计算机网络(五) —— 自定义协议简单网络程序

news2024/11/26 12:44:01

目录

一,关于“协议”

1.1 结构化数据

1.2 序列化和反序列化

二,网络版计算器实现准备

2.1 套用旧头文件

2.2 封装sock API

三,自定义协议

3.1 关于自定义协议

3.2  实现序列化和反序列化

3.3 测试

三,服务器实现

3.1 逻辑梳理

3.2 各头文件实现

四,客户端实现


一,关于“协议”

1.1 结构化数据

两个主机通过网络和协议进行通信时,发送的数据有两种形式:

  • 如果传输的数据直接就是一个字符串,那么把这个字符串发出去,对方也能得到这个字符串
  • 如果需要传输的是一个struct结构体,那么不能将结构体数据一个个发送到网络中

比如我要实现一个网络版的计算器,那么客户端给服务器发送的数据,就要包含左操作数,运算符和右操作数,那么这就不仅仅是一个字符串了,而是一组数据

所以客户端不能把这些数据一个个发送过去,需要把这些数据“打个包”,统一发到网络中,此时服务器就能获取到一个完整的数据请求,“打包”方式有两种:

方案一:将结构化的数据结合成一个大的字符串

  • 比如我要发送“1+1”,用户输入的是“整型”,“字符”,“整型”
  • 我们先用to_string函数把整型转为字符串,然后用strcat或者C++/string的 "+="运算符重载将这三个字符拼接成一个长字符串,然后就可以直接发送
  • 最后服务器收到了长字符串,再以相同的方式进行拆分,用stoi函数将字符串转整型,就可以提取这些结构化的数据

方案二:定制结构化数据,实现序列化和反序列化 

  • 客户端可以定制一个结构体,将需要交互的信息放到结构体种
  • 客户端发送前,将结构体的数据进行序列化,服务器收到数据后进行反序列化,此时服务器就能得到客户端发送过来的结构体,下面我们来详细讲讲序列化和反序列化

1.2 序列化和反序列化

  •  序列化是将对象的状态信息转换为可以存储或传输的形式(字节序列)的过程
  • 反序列化就是把序列化的字节序列恢复为对象的过程

OSI七层模型中表示层的作用,就是“实现数据格式和网络标准数据格式的转换”。前者数据格式就是指数据再应用层上的格式,后者就是指序列化之后可以进行网络传输的数据格式 

  •  序列化的目的,是为了方便网络数据的发送和接收,序列化后数据就全变成了二进制数据,此时底层在进行数据传输时看到的统一都是二进制序列
  • 我发的是二进制数据,所以对方收到的也是二进制数据,所以需要进行反序列化,将二进制数据转化为上层能够识别的比如字符串,整型数据

二,网络版计算器实现准备

前置博客:计算机网络(三) —— 简单Udp网络程序-CSDN博客

计算机网络(四) —— 简单Tcp网络程序-CSDN博客

下面我们来全程手搓一个网络版计算器服务,并且我们自己实现一个自定义协议,主要是为了感受一下协议的实现,后面我们就不会再自定义协议了,直接用现成的

2.1 套用旧头文件

源代码下载:计算机网络/自定义协议——网络版计算器 · 小堃学编程/Linux学习 - 码云 - 开源中国 (gitee.com)

网络版计算器我们要用到的头文件有以下几个:

 

 我们先把前面写的头文件套用一下:

makefile

.PHONY:all
all:servercal clientcal

Flag=#-DMySelf=1
Lib=-ljsoncpp #这个是后面使用json头文件时要用的

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

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>
#include <stdlib.h>

#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"

class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }
    void Enable(int method)
    {
        printMethod = method;
    }
    std::string levelToString(int level)
    {
        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); // "log.txt"
        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); // "log.txt.Debug/Warning/Fatal"
        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);

        // printf("%s", logtxt); // 暂时打印
        printLog(level, logtxt);
    }

private:
    int printMethod;
    std::string path;
};

Log log;

Deamon.hpp

#pragma once

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string nullfile = "/dev/null";

void Daemon(const std::string &cwd = "")
{
    // 1. 忽略其他异常信号
    signal(SIGCLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);

    // 2. 将自己变成独立的会话
    if (fork() > 0)
        exit(0);
    setsid();

    // 3. 更改当前调用进程的工作目录
    if (!cwd.empty())
        chdir(cwd.c_str());

    // 4. 标准输入,标准输出,标准错误重定向至/dev/null
    int fd = open(nullfile.c_str(), O_RDWR);
    if (fd > 0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
}

2.2 封装sock API

在Udp和Tcp服务器编写时,可以发现在使用sock API以及填装sockaddr结构体时,步骤都非常相似,所以我们可以把这些相似的步骤都封装起来,下面是Socket.hpp的代码:

#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"

#include <cstring>

enum

{
    SocketErr = 2,
    BindErr,
    ListenErr,
};

const int backlog = 10;

class Sock
{
public:
    Sock()
    {
    }
    ~Sock()
    {
    }

public:
    void Socket() // 创建套接字
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            log(Fatal, "socket error, %s: %d", strerror(errno), errno);
            exit(SocketErr);
        }
    }
    void Bind(uint16_t port) // 绑定套接字
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0) // 如果小于0就绑定失败
        {
            log(Fatal, "bind error, %s: %d", strerror(errno), errno);
            exit(BindErr);
        }
    }

    void Listen() // 监听套接字
    {
        if (listen(_sockfd, backlog) < 0) // 如果小于0就代表监听失败
        {
            log(Fatal, "listen error, %s: %d", strerror(errno), errno);
            exit(ListenErr);
        }
    }

    int Accept(std::string *clientip, uint16_t *clientport) // 获取连接,参数做输出型参数
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int newfd = accept(_sockfd, (struct sockaddr *)(&peer), &len);
        if (newfd < 0) // 获取失败
        {
            log(Warning, "accept error, %s: %d", strerror(errno), errno);
            return -1;
        }
        char ipstr[64];
        inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr)); // 把网络字节序列转化为字符串保存在ipstr数组里供用户读取
        *clientip = ipstr;
        *clientport = ntohs(peer.sin_port);

        return newfd;
    }

    bool Connect(const std::string &ip, const uint16_t port)
    {
        struct sockaddr_in peer;
        memset(&peer, 0, sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));

        int n = connect(_sockfd, (struct sockaddr *)&peer, sizeof(peer));
        if (n == -1)
        {
            std::cerr << "connect to " << ip << ":" << port << "error" << std::endl;
            return false;
        }
        return true;
    }

    void Close()
    {
        close(_sockfd);
    }

    int Fd()
    {
        return _sockfd;
    }

private:
    int _sockfd;
};

三,自定义协议

3.1 关于自定义协议

在之前的文章中介绍过,任何的网络协议,都要提供两种功能,下面是博客的截图:计算机网络(一) —— 网络基础入门_计算机网络基础教程-CSDN博客

网络版计算器,用户会在命令行输入三个字符:"1","+","1",然后我们可以拼接成一个长字符串:"1 + 1",数字与运算符通过一个空格隔开,

但是,如果客户端连续发了两个字符串,那么最终服务器收到的报文就是“1 + 12 + 1”,可以发现,两个字符串粘在了一起,所以我们的自定义协议,不仅仅要提供将报文和有效载荷分离的能力,也要提供将报文与报文分开的能力,有下面两种方法:

  • 方案一,用特殊字符隔开报文与报文 --> "1 + 1" \n "2 + 2"
  • 方案二,在报文前面加上报文的长度,也就是报头 --> "9"\n"100 + 200"\n,这样就为一个完整的报文(其实只要有长度就可以了,这里增加\n是为了可读性,也是为了方便后面打印)

所以下面来梳理一下我们自定义协议的序列化和反序列化全流程:

3.2  实现序列化和反序列化

这个部分就是具体实现Protocol.hpp头文件了,这个文件具体包含下面几个内容:

  1. "100","+","200" --> "100 + 200"
  2. "100 + 200" --> "9"\n"100 + 200"
  3.  "9"\n"100 + 200" --> "100 + 200"
  4. "100 + 200" --> "100","+","200"

该文件包含两个类,一个类是请求类,是客户端发给服务器用到的类;另一个类是响应类,是服务器处理完后,返回给客户端的类;此外还包括两个方法,分别是封装报头将报头和有效载荷分离

Request类:

#pragma once
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

#define MySelf 0 // 去掉注释就是用我们自己的序列化和反序列化,加上注释就是用json库提供的

const std::string blank_space = " "; // 分隔符
const std::string protocol_sep = "\n";

class Request // 计算的请求
{
public:
    Request(int data1, int data2, char oper)
        : x(data1), y(data2), op(oper)
    {
    }
    Request()
    {
    }
    ~Request()
    {
    }

public:
    bool Serialize(std::string *out) // 序列化
    {
#ifdef MySelf
        // 1,构建报文的有效载荷
        //  需要把结构化的数据转化为字符串 struct --> string, "x op y"
        std::string s = std::to_string(x);
        s += blank_space;
        s += op;
        s += blank_space;
        s += std::to_string(y);
        // 走到这里的时候就是字符串 "x op y"
        // 但是在传输的时候可能发过来的不是完整的一个报文:"10 + 20",而是只有半个报文:"10 + "
        // 解决方案一:用特殊字符隔开报文与报文 --> "10 + 20" \n "20 + 40"
        // 解决方案二:再在报文前面加一个字符串的长度也就是报头,例如s.size()
        // 结合起来就是"9"\n"100 + 200"\n,为一个完整的报文,其实只要有长度就可以了,这里增加\n是为了可读性,也是为了方便后面

        // 2,封装报头
        *out = s;
        return true;
#else
        Json::Value root;
        root["x"] = x;
        root["y"] = y;
        root["op"] = op;
        Json::FastWriter w;
        *out = w.write(root);
        return true;

#endif
    }
    bool DeSerialize(const std::string &in) // 反序列化  "9"\n"10 + 20"
    {
#ifdef MySelf
        std::size_t left = in.find(blank_space); // 找空格的左边,"10 + 20",也就是找10的右边位置
        if (left == std::string::npos)           // 没找到空格,说明当前解析错误
        {
            return false;
        }
        std::string part_x = in.substr(0, left); // 截取第一个数字,也就是10

        std::size_t right = in.rfind(blank_space); // 逆向再次找空格,"10 + 20",找20左边的位置
        if (right == std::string::npos)            // 没找到空格,说明当前解析错误
        {
            return false;
        }
        std::string part_y = in.substr(right + 1); // 截取后面的数字,也就是20,+1是因为找到的是空格的右边,+1跳过空格才是数字
        if (left + 2 != right)
            return false;  // 数字中间还有运算符,所以left+2就应该是right的左边那个空格的左边位置,如果不是那么就是解析错误
        op = in[left + 1]; // 拿到运算符
        // op = in[right - 1]; //一样的

        x = std::stoi(part_x); // 拿到数字
        y = std::stoi(part_y);

        return true;
#else
        Json::Value root;
        Json::Reader r;
        r.parse(in, root);

        x = root["x"].asInt();
        y = root["y"].asInt();
        op = root["op"].asInt();
        return true;
#endif
    }
    void DebugPrint()
    {
        std::cout << "新请求构建完成:  " << x << " " << op << " " << y << "=?" << std::endl;
    }

public:
    int x;
    int y;
    char op; // 运算符
};

class Response // 计算的应答
{
public:
    Response(int res, int c)
        : result(res), code(c)
    {
    }
    Response()
    {
    }
    ~Response()
    {
    }

public:
    bool Serialize(std::string *out) // 序列化
    {
#ifdef MySelf
        // 1,构建报文的有效载荷
        //"len"\n"result code"
        std::string s = std::to_string(result);
        s += blank_space;
        s += std::to_string(code);

        *out = s;
        return true;
#else
        Json::Value root;
        root["result"] = result;
        root["code"] = code;
        // Json::FastWriter w;
        Json::StyledWriter w;
        *out = w.write(root);
        return true;
#endif
    }
    bool DeSerialize(const std::string &in) // 反序列化
    {
#ifdef MySelf
        // 对服务器发过来的结果报文做解析: "result code"
        std::size_t pos = in.find(blank_space); // 找空格的左边
        if (pos == std::string::npos)           // 没找到空格,说明当前解析错误
        {
            return false;
        }
        std::string part_left = in.substr(0, pos);   // 截取第一个数字,也就是result
        std::string part_right = in.substr(pos + 1); // 截取后面第二个数字,也就是code

        result = std::stoi(part_left);
        code = std::stoi(part_right);
        return true;
#else
        Json::Value root;
        Json::Reader r;
        r.parse(in, root);
        result = root["result"].asInt();
        code = root["code"].asInt();
        return true;
#endif
    }

    void DebugPrint()
    {
        std::cout << "结果响应完成, result: " << result << ", code: " << code << std::endl;
    }

public:
    int result; // x op y
    int code;   // 错误码,为0时结果正确,为其它数时对应的数表示对应的原因
};

 Response类:

class Response // 计算的应答
{
public:
    Response(int res, int c)
        : result(res), code(c)
    {
    }
    Response()
    {
    }
    ~Response()
    {
    }

public:
    bool Serialize(std::string *out) // 序列化
    {
#ifdef MySelf
        // 1,构建报文的有效载荷
        //"len"\n"result code"
        std::string s = std::to_string(result);
        s += blank_space;
        s += std::to_string(code);

        *out = s;
        return true;
#else
        Json::Value root;
        root["result"] = result;
        root["code"] = code;
        // Json::FastWriter w;
        Json::StyledWriter w;
        *out = w.write(root);
        return true;
#endif
    }
    bool DeSerialize(const std::string &in) // 反序列化
    {
#ifdef MySelf
        // 对服务器发过来的结果报文做解析: "result code"
        std::size_t pos = in.find(blank_space); // 找空格的左边
        if (pos == std::string::npos)           // 没找到空格,说明当前解析错误
        {
            return false;
        }
        std::string part_left = in.substr(0, pos);   // 截取第一个数字,也就是result
        std::string part_right = in.substr(pos + 1); // 截取后面第二个数字,也就是code

        result = std::stoi(part_left);
        code = std::stoi(part_right);
        return true;
#else
        Json::Value root;
        Json::Reader r;
        r.parse(in, root);
        result = root["result"].asInt();
        code = root["code"].asInt();
        return true;
#endif
    }

    void DebugPrint()
    {
        std::cout << "结果响应完成, result: " << result << ", code: " << code << std::endl;
    }

public:
    int result; // x op y
    int code;   // 错误码,为0时结果正确,为其它数时对应的数表示对应的原因
};

 添加和去掉报头函数:

std::string Encode(const std::string &content) // 添加报头
{
    std::string packge = std::to_string(content.size()); // 加报头
    packge += protocol_sep;                              // 加\n
    packge += content;                                   // 加正文
    packge += protocol_sep;                              // 再加\n

    return packge;
}

bool Decode(std::string &package, std::string *content) // 解析并去掉报头 "9"\n"10 + 20"\n -->"10 + 20"  俗称解包,但是只是去掉了报头,没有做报文的具体解析
{
    std::size_t pos = package.find(protocol_sep); // 找到\n的左边
    if (pos == std::string::npos)
        return false;                             // 解析失败
    std::string len_str = package.substr(0, pos); // 从开始截到我找到的\n处,把前面的9给截出来
    std::size_t len = std::stoi(len_str);         // 把截出来的报头转化为size_t,也就是把字符串9转化成数字9

    // packge的长度 = 报头长度len_str + 有效载荷长度content_str + 两个\n 2
    std::size_t total_len = len_str.size() + len + 2;
    // ①找到了第一个\n说明一定有长度,如果没找到\n就说明连报头都没有
    // ②有了长度报头,你也还得保证后面的内容也是完整的,如果不完整也就是长度不一样,那我也就不玩了
    if (package.size() < total_len)
        return false;

    // 走到这一步说明我们能保证报文是完整的,开始拿有效载荷
    *content = package.substr(pos + 1, len); // pos现在是第一个\n左边的位置,+1后面的就是正文内容,正文内容长度为len

    // 移除一个报文,该功能需要和网络相结合
    package.erase(0, total_len);

    return true;
}

3.3 测试

我们可以在ServerCal.cc文件里测试上面我们的序列化和反序列化操作

先测试Request:

ServerCal.cc:

#include "Log.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "ServerCal.hpp"
#include "Deamon.hpp"

int main()
{
    // Request测试--------------------
     Request req(10, 20, '+');
     std::string s;
     req.Serialize(&s);
     std::cout << "有效载荷为: " << s << std::endl;
     s = Encode(s);
     std::cout << "报文为:" << s;

    std::string content;
    bool r = Decode(s, &content); //分离报头和有效载荷
    std::cout << "分离报头后的有效载荷为: "<< content << std::endl;
    Request temp;

    temp.DeSerialize(content); //解析有效载荷
    std::cout<< "有效载荷分离后, x为: " << temp.x << " 运算符为:\"" << temp.op << "\"  y为: " << temp.y << std::endl;

    return 0;
}

然后是Response的测试: 

ServerCal.cc:

#include "Log.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "ServerCal.hpp"
#include "Deamon.hpp"

int main()
{
    // Response测试--------------------
    Response resp(10, 20);
    std::string s;
    resp.Serialize(&s);
    std::cout << "有效载荷为: " << s << std::endl;
    std::string package = Encode(s); //分离报头和有效载荷
    std::cout << "报文为: " << package;
    s = "";
    bool r = Decode(package, &s);
    std::cout << "分离报头后的有效载荷为: " << s << std::endl;

    Response temp;
    temp.DeSerialize(s); // 解析有效载荷
    std::cout << "解析有效载荷: " << std::endl;
    std::cout << "结果为: " << temp.result << std::endl;
    std::cout << "错误码为: " << temp.code << std::endl;

    return 0;
}

三,服务器实现

3.1 逻辑梳理

服务器涉及两个个头文件和一个源文件,有点绕,下面先梳理一下:

有三个文件:

  • 首先,TcpServer.hpp是服务器主函数,ServerCal.cc包含服务器初始化和启动的main函数,ServerCal.hpp是进行计算器运算的头文件
  • 首先构建服务器对象,并在构造函数里将ServerCal.cc里面的运算函数带进去,然后是初始化服务器,执行创建套接字等操作,然后启动服务器
  • 当服务器收到客户端发来的报文后,直接将报文传给运算函数,由运算函数做去掉报头,解析有效载荷等过程,并执行运算,最后把运算结果再次构建成响应报文,以返回值形式返回给服务器运行函数
  • 然后服务器再把响应报文发给客户端,完成一次计算请求处理

3.2 各头文件实现

Server.hpp实现:

#pragma once
#include <iostream>
#include <string>
#include "Protocol.hpp"

enum
{
    Div_Zero = 1,
    Mod_Zero,
    Other_Oper
};

class ServerCal
{
public:
    ServerCal()
    {
    }

    ~ServerCal()
    {
    }
    Response CalculatorHelper(const Request &req)
    {
        Response resp(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)
            {
                resp.code = Div_Zero;
            }
            else
            {
                resp.result = req.x / req.y;
            }
        }
        break;
        case '%':
        {
            if (req.y == 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;
        if (!Decode(package, &content)) // 分离报头和有效载荷:"len"\n"10 + 20"\n
            return "";
        // 走到这里就是完整的报文
        Request req;
        if (!req.DeSerialize(content)) // 反序列化,解析有效载荷 "10 + 20" --> x=10 op="+" y=20
            return "";

        content = "";
        Response resp = CalculatorHelper(req); // 执行计算逻辑
        resp.Serialize(&content);              // 序列化计算结果的有效载荷 result=10, code=0
        content = Encode(content);             // 将有效载荷和报头封装成响应报文 "len"\n"30 0"

        return content;
    }
};

TcpServer.hpp实现:

#pragma once
#include "Log.hpp"
#include "Socket.hpp"
#include <signal.h>
#include <string>
#include <functional>

using func_t = std::function<std::string(std::string &package)>;

class TcpServer
{
public:
    TcpServer(uint16_t port, func_t callback)
        : _port(port), _callback(callback)
    {
    }

    bool InitServer()
    {
        // 创建,绑定,监听套接字
        _listensockfd.Socket();
        _listensockfd.Bind(_port);
        _listensockfd.Listen();
        log(Info, "Init server... done");
        return true;
    }

    void Start()
    {
        signal(SIGCHLD, SIG_IGN); // 忽略
        signal(SIGPIPE, SIG_IGN);
        while (true)
        {
            std::string clientip;
            uint16_t clientport;
            int sockfd = _listensockfd.Accept(&clientip, &clientport);

            if (sockfd < 0)
                continue;
            log(Info, "accept a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);

            // 走到了这里就是成功获取发起连接方IP与port,后面就是开始提供服务
            if (fork() == 0)
            {
                _listensockfd.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; // 这里用+=

                        log(Debug, "debug:\n%s", inbuffer_stream.c_str());
                        while (true)
                        {
                            std::string info = _callback(inbuffer_stream);
                            // if (info.size() == 0) //ServerCal.hpp,解析报文失败的话会返回空串
                            if (info.empty()) // 空的话代表inbuffstream解析时出问题,表示不遵守协议,发不合法的报文给我,我直接丢掉不玩了
                                break;        // 不能用continue

                            log(Debug, "debug, response:\n%s", info.c_str());
                            log(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 _listensockfd;
    func_t _callback;
};

ServerCal.cc实现:

#include "Log.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "ServerCal.hpp"
#include "Deamon.hpp"

static void Usage(const std::string &proc)
{
    std::cout << "\nUsage: " << proc << "port\n\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;
    TcpServer *tsvp = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));
    tsvp->InitServer();
    //Daemon();
    //daemon(0, 0);
    tsvp->Start();

    // Request测试--------------------
    // Request req(10, 20, '+');
    // std::string s;
    // req.Serialize(&s);
    // std::cout << "有效载荷为: " << s << std::endl;
    // s = Encode(s);
    // std::cout << "报文为:" << s;

    // std::string content;
    // bool r = Decode(s, &content); //分离报头和有效载荷
    // std::cout << "分离报头后的有效载荷为: "<< content << std::endl;
    // Request temp;

    // temp.DeSerialize(content); //解析有效载荷
    // std::cout<< "有效载荷分离后, x为: " << temp.x << " 运算符为:\"" << temp.op << "\"  y为: " << temp.y << std::endl;

    // Response测试--------------------
    // Response resp(10, 20);
    // std::string s;
    // resp.Serialize(&s);
    // std::cout << "有效载荷为: " << s << std::endl;
    // std::string package = Encode(s); //分离报头和有效载荷
    // std::cout << "报文为: " << package;
    // s = "";
    // bool r = Decode(package, &s);
    // std::cout << "分离报头后的有效载荷为: " << s << std::endl;

    // Response temp;
    // temp.DeSerialize(s); // 解析有效载荷
    // std::cout << "解析有效载荷: " << std::endl;
    // std::cout << "结果为: " << temp.result << std::endl;
    // std::cout << "错误码为: " << temp.code << std::endl;

    return 0;
}

四,客户端实现

客户端的话,为了方便发送计算请求,会采用随机数的方式获取运算数和运算符,如下代码:

#include <iostream>
#include <string>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"

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);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]); //获取IP和端口

    Sock sockfd;
    sockfd.Socket();
    if (!sockfd.Connect(serverip, serverport))
        return 1;

    srand(time(nullptr) ^ getpid()); // 种随机数种子
    int cnt = 1;
    const std::string opers = "+-*/%-=&^";

    std::string inbuffer_stream;
    while (cnt <= 5)
    {
        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);                  // 形成报文
        int fd = sockfd.Fd();                       // 获取套接字
        write(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; // "len"\n"result code"\n
            std::cout << inbuffer_stream << std::endl;
            std::string content;
            bool r = Decode(inbuffer_stream, &content); // 去掉报头"result code"\n
            assert(r);                                  // r为真说明报头成功去掉
            Response resp;
            r = resp.DeSerialize(content); // 对有效荷载进行反序列化
            assert(r);

            resp.DebugPrint(); // 打印结果
        }
        std::cout << "=================================================" << std::endl;
        sleep(1);
        cnt++;
    }

    sockfd.Close();
    return 0;
}

 效果演示:

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

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

相关文章

【C++ 面试题】构造函数和析构函数你了解多少呢?

文章目录 1. 什么是构造函数和析构函数2. 构造函数和析构函数可以是虚函数吗3. 构造函数有哪几种4. 深拷贝和浅拷贝的区别 1. 什么是构造函数和析构函数 &#x1f427; 构造函数&#xff1a; 构造函数是在创建对象时自动调用的特殊成员函数。 目的&#xff1a;初始化对象的成…

【Redis】主从复制 - 源码

因为主从复制的过程很复杂, 同时核心逻辑主要集中在 replication.c 这个文件中, 避免篇幅过大, 所以将主从复制中涉及这个文件的代码集中到了另一篇文章。 在当前文章主要分析主从复制的大体代码逻辑, 如果需要了解整体的过程, 可以配合 Redis 主从复制 - relication 源码分析 …

中非合作打开非洲14亿人的市场新空间,非洲电商平台有哪些?

中非合作论坛自2000年成立以来&#xff0c;中非贸易额从105亿美元增至2821亿美元&#xff0c;增长近26倍。中国对非投资也从4亿多美元增长至400多亿美元&#xff0c;增幅超过100倍。此次中非合作论坛开幕式上&#xff0c;中国更是宣布未来将为非洲提供约500亿美元的融资&#x…

水库大坝安全监测方案,双重守护,安全无忧

水库作为重要的水利设施&#xff0c;在防洪、灌溉及供水等方面发挥着重要作用。然而随着时间的推移&#xff0c;大坝面临着自然老化、设计标准不足及极端天气等多重挑战&#xff0c;其安全性与稳定性日益受到关注。水库堤坝险情导致的洪涝灾害给人民生命财产和经济社会发展带来…

运动耳机选哪种好用?六条绝妙选购要点避免踩坑

​开放式耳机目前非常流行&#xff0c;它们的设计不侵入耳道&#xff0c;长时间佩戴也不会感到不适&#xff0c;同时还能维护耳部卫生&#xff0c;这使得它们特别受到运动爱好者和耳机发烧友的喜爱。然而&#xff0c;市场上的开放式耳机品牌众多&#xff0c;质量参差不齐&#…

乡村振兴/乡村风貌 乡村建设改造方案设计

[若愚文化STUDIO] 乡村振兴/乡村建设/风貌改造/产业策划 深度参与GD省“百千万”工程&#xff0c; 助力乡村建设。 根据现状实景&#xff0c;充分保留主体建筑物&#xff0c;快速出改造意向图。

vue的学习之路(Vue中组件(component )

注意&#xff1a;其中添加div的意义就是让template标签有一个根标签 &#xff0c;否则只展示“欢迎进入登录程序” 不加div效果图 &#xff08;2&#xff09;两种开发方式 第一种开发方式 //局部组件登录模板声明 let login { //具体局部组件名称 template:‘ 用户登录 ’…

网络安全工程师能赚多少钱一个月?

&#x1f91f; 基于入门网络安全/黑客打造的&#xff1a;&#x1f449;黑客&网络安全入门&进阶学习资源包 网络安全工程师的月薪取决于多种因素&#xff0c;包括他们的经验、技能、学历、所在地区和行业的需求等。因此&#xff0c;很难给出一个确切的数字作为所有网络安…

STM32的GPIO使用

一、使用流程 1.使用RCC开启GPIO时钟 2.使用GPIO_Init 函数初始化GPIO 3.使用输出或输入函数控制GPIO口 二、RCC的常用函数 函数内容可通过这两个文件进行查看&#xff1a; RCC常用函数如下&#xff1a; void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalS…

掌握Python自动化:探索keymousego库的无限可能!

文章目录 掌握Python自动化&#xff1a;探索keymousego库的无限可能&#xff01;背景&#xff1a;为什么选择keymousego&#xff1f;简介&#xff1a;keymousego是什么&#xff1f;安装指南&#xff1a;如何安装keymousego&#xff1f;快速入门&#xff1a;5个简单函数的使用实…

Java中校验导入字段长度与数据库字段长度一致性

需求&#xff1a;使用EasyExcel导入数据时&#xff0c;根据数据库字段长度校验导入字段的长度。使用的数据库是mysql。若是一般的校验需求&#xff0c; Spring Validation 或 Hibernate Validator 即可满足。 实现步骤&#xff1a; 获取需要校验的表&#xff0c;查询出字段相…

【JAVA基础】实现Tomcat基本功能

文章目录 TCP/IP协议Socket编程ServletTomcat 在搜索了两三天之后&#xff0c;也是大概弄懂了Tomcat是个什么东西&#xff0c;我们在说Tomcat之前&#xff0c;先来了解一下下面这三个东西&#xff1a; TCP/IP协议 TCP/IP 是互联网通信的基础协议。TCP&#xff08;传输控制协议…

C++类和对象3

一.初始化列表 我们之前的构造函数都是在函数体内对数据成员进行赋值 Date(int year, int month, int day) {_year year;_month month;_day day; } 然而我们的构造函数还有另一种初始化的方式&#xff1a;初始化列表 ——初始化列表是以参数表后冒号开始&#xff0c;用数…

数学建模笔记—— 多目标规划

数学建模笔记—— 多目标规划 多目标规划1. 模型原理1.1 多目标规划的一般形式1.2 多目标规划的解1.3 多目标规划的求解 2. 典型例题3. matlab代码实现 多目标规划 多目标规划是数学规划的一个分支。研究多于一个的目标函数在给定区域上的最优化。又称多目标最优化。通常记为 …

VCS(Video Cloud Storage)解决方案研究报告

1.背景 控视频是重要的数据资产和证据链&#xff0c;在银行、交通、司法等行业对视频数据有很高的安全等级。随着监控的重要性不断提升&#xff0c;在能源、电力、校园、厂矿、高星酒店等多场景中对监控存储也有更高要求&#xff0c;体现为海量存储、超长时间和数据安全。为了充…

得物APP助力释放首发经济新活力,解锁年轻潮流密码

在消费升级与高质量发展的时代背景下&#xff0c;我国首发经济正以前所未有的活力蓬勃发展&#xff0c;成为推动市场繁荣、满足个性化消费需求的重要力量。首发&#xff0c;即产品首次在市场亮相&#xff0c;往往代表着最新的设计理念、最尖端的科技应用以及最前沿的潮流趋势。…

C++入门知识(1)

一、namespace 1、用处 可以解决程序里面定义重名变量的问题 namespace是一个命名空间。 定义变量可以在4个域下面定义&#xff0c;全局域&#xff0c;局部域&#xff0c;命名空间域&#xff0c;类域。各个域之间是相互不影响的。命名空间里面的变量可以和外面的变量重名 2…

Stable Diffusion4.9一键安装教程SD(AI绘画软件)

**无套路&#xff01;**文末提供下载方式 Stable Diffusion 是一款革命性的 AI 绘画生成工具&#xff0c;它通过潜在空间扩散模型&#xff0c;将图像生成过程转化为一个逐步去噪的“扩散”过程。 与传统的高维图像空间操作不同&#xff0c;Stable Diffusion 首先将图像压缩到…

样品管理的重要性与实操解决方案,外贸软件一键搞定

在外贸过程中&#xff0c;样品管理是一个重要的环节&#xff0c;它不仅涉及到产品的质量和细节确认&#xff0c;还是与客户沟通的重要桥梁。在选择客户时&#xff0c;通常会优先考虑那些目的明确、意向较强的客户&#xff0c;因为这些客户成交的可能性较大。无论是纺织品、服装…

基于SpringBoot+Vue的学生成绩管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于JavaSpringBootVueMySQL的学生成绩…