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

目录

一,关于"协议"

[1.1 结构化数据](#1.1 结构化数据)

[1.2 序列化和反序列化](#1.2 序列化和反序列化)

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

[2.1 套用旧头文件](#2.1 套用旧头文件)

[2.2 封装sock API](#2.2 封装sock API)

三,自定义协议

[3.1 关于自定义协议](#3.1 关于自定义协议)

[3.2 实现序列化和反序列化](#3.2 实现序列化和反序列化)

[3.3 测试](#3.3 测试)

三,服务器实现

[3.1 逻辑梳理](#3.1 逻辑梳理)

[3.2 各头文件实现](#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

bash 复制代码
.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

cpp 复制代码
#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

cpp 复制代码
#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的代码:

cpp 复制代码
#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类:

cpp 复制代码
#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类:

cpp 复制代码
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时结果正确,为其它数时对应的数表示对应的原因
};

添加和去掉报头函数:

cpp 复制代码
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

cpp 复制代码
#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

cpp 复制代码
#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实现:

cpp 复制代码
#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实现:

cpp 复制代码
#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实现:

cpp 复制代码
#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;
}

四,客户端实现

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

cpp 复制代码
#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;
}

效果演示:

相关推荐
七夜zippoe2 小时前
CANN Runtime任务描述序列化与持久化源码深度解码
大数据·运维·服务器·cann
盟接之桥2 小时前
盟接之桥说制造:引流品 × 利润品,全球电商平台高效产品组合策略(供讨论)
大数据·linux·服务器·网络·人工智能·制造
Fcy6483 小时前
Linux下 进程(一)(冯诺依曼体系、操作系统、进程基本概念与基本操作)
linux·运维·服务器·进程
袁袁袁袁满3 小时前
Linux怎么查看最新下载的文件
linux·运维·服务器
代码游侠4 小时前
学习笔记——设备树基础
linux·运维·开发语言·单片机·算法
主机哥哥4 小时前
阿里云OpenClaw部署全攻略,五种方案助你快速部署!
服务器·阿里云·负载均衡
Harvey9034 小时前
通过 Helm 部署 Nginx 应用的完整标准化步骤
linux·运维·nginx·k8s
珠海西格电力科技5 小时前
微电网能量平衡理论的实现条件在不同场景下有哪些差异?
运维·服务器·网络·人工智能·云计算·智慧城市
释怀不想释怀5 小时前
Linux环境变量
linux·运维·服务器
zzzsde5 小时前
【Linux】进程(4):进程优先级&&调度队列
linux·运维·服务器