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

目录

一,关于"协议"

[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;
}

效果演示:

相关推荐
鲸鱼姐8 分钟前
在linux注册服务并开机启动springboot程序
linux·运维·服务器
jyan_敬言25 分钟前
虚拟机centos_7 配置教程(镜像源、配置centos、静态ip地址、Finalshell远程操控使用)
linux·运维·服务器·c语言·数据结构·tcp/ip·centos
Licky1325 分钟前
Centos中dnf和yum区别对比
linux·运维·架构·centos·bash
洁洁!32 分钟前
【计算机网络】数据链路层深度解析
网络·网络协议·计算机网络
Aomnitrix3 小时前
网络协议全景:Linux环境下的TCP/IP、UDP
linux·运维·网络·c++·网络协议·tcp/ip·运维开发
哲伦贼稳妥3 小时前
网络运维故障处理
运维·网络·经验分享·职场和发展
花花花14 小时前
Linux 文件与目录操作命令详解
linux·运维·服务器·文件·目录·命令
小故渊10 小时前
JSON对象
运维·服务器·json
Aileen_0v010 小时前
【Java宝典】——探索数组的奥秘之旅
java·运维·开发语言·windows·后端·docker·golang
阳爱铭11 小时前
指标服务平台:全面解析
java·服务器·网络·hadoop·sql·hbase·数据库架构