再谈“协议”

1.认识协议

之前我们使用TCP的方式实现了一个服务器,而TCP是面向字节流的,而UDP是面向数据报的,接下来通过一个例子区分两种的区别。

  • UDP面向数据报:就如同发快递,你发多少个快递,对面就收到多少个快递(不考虑丢包情况),并且每个快递之间都是有明显的分割的。你很容易就能区别出不同的快递。
  • TCP面向字节流:数据在传输或处理时都会被拆分成一系列的字节,然后按照特定的顺序进行读取或写入。就像水流一样,你打开水龙头时,你并不知道哪些水是自来水厂第一次给你传的,哪些是第二次给你传的。

所以我们之前写的TCP服务器有个很大的问题,就是你并不能保证读取到的就是一个完整的数据。例如我们的英汉互译词典,如果用户输入的是:carriage,但是我们接受到的是car,最后返回的就是car的汉文,那这肯定是不对的,如何解决这个问题呢,我们需要制定协议。

协议就是一种约定,网络中的通讯双方都必须遵守。

2.结构化数据

在网络传输中,我们难免会遇到需要传输结构化数据的情况,例如网络聊天中,我们不仅仅需要传输别人发的消息,还包含头像,名称,时间等消息。那我们直接传送一个结构体吗。

我们使用send,recv等IO函数时是可以直接传递一个结构化的数据的,但是在实际中我们选择将这个结构体转化成一个字符串再发送给对方,为什么呢?

  1. 跨平台兼容性:字符串是一种非常基础且通用的数据类型,几乎所有的编程语言和平台都支持字符串。相比之下,结构体可能因编程语言和平台的差异而具有不同的表示方式,导致在不同的系统上传输和解析时出现问题。使用字符串作为传输介质可以避免这类跨平台兼容性问题
  2. 简化编码与解码过程:在网络传输中,需要将数据编码为可以在网络上传输的格式。对于字符串,编码和解码过程通常相对简单,因为字符串本身就是一种序列化的数据形式。而结构体在编码为可传输的格式时可能需要考虑字段顺序、大小端字节序等问题,增加了编码和解码的复杂性。

当我们知道了网络传输中应该使用字符串的方式,那我们如何构建这个字符串呢,我们以网络版本计算器举例子。

struct request
{
    int _data_x;
    char _oper;
    int _data_y;
};

方案一:直接发送

例如:“12+32”,但是这种方式就会出现我们之前说的问题,我们无法保证读取到的是一个完整的报文,并且报文与报文之间没有分割,所以可能实际独到的是“12+3224/12”(其中24/12是第二次发送的)。

方法二:制定协议,序列化与反序列化

  • 序列化的意思就是把一个结构化的数据转化成可以存储或传输的形式(字符串)。
  • 反序列化就是把一个字节序列转化成结构化的数据

然后我们可以制定一个协议,比如每次发送都已\n结尾,我们就可以通过\n将两个报文分开了,例如当我们收到"12+32\n24/12\n"就知道这是两次报文了。

因为是协议,双方都要遵守,所以,发送端会在每次发送的报文后面加一个\n,接收端知道读到了\n就是这个报文的结尾,这就是协议的作用。

接下来,通过一个代码,带大家更加深刻的了解协议。

3.网络版本计算器

3.1准备工作

3.1.1封装socket

在每次使用socket时,我们写的代码都差不多,所以我们可以封装一下socket。

我们可以使用一个模板方法设计模式。

class Socket
{
public:
    virtual ~Socket() {}
    virtual void CreateSocket() = 0;
    virtual void BindSocket(uint16_t port) = 0;
    virtual void ListenSocket() = 0;
    virtual Socket *AcceptConnect(InetAddr& addr) = 0;
    virtual bool ConnectServer(InetAddr& addr) = 0;
    virtual int Recvfrom(std::string& buffer, InetAddr* addr) = 0;
    virtual int Sendto(std::string buffer, InetAddr& addr) = 0;
    virtual int GetSockfd() = 0;
    virtual void SetSockfd(int _sockfd) = 0;
    virtual void CloseSockfd() = 0;

public:
    void CreateBindListenSocket(uint16_t port)
    {
        CreateSocket();
        BindSocket(port);
        ListenSocket();
    }

    bool CreateSocketAndConnect(InetAddr& addr)
    {
        CreateSocket();
        return ConnectServer(addr);
    }
};

父类如上所示。

const int defaultbacklog = 10;
const int defaultSockfd = -1;

class TcpSocket : public Socket
{
public:
    TcpSocket(int sockfd = defaultSockfd)
        : _sockfd(sockfd)
    {
    }

    ~TcpSocket()
    {
    }

    void CreateSocket() override
    {
        if (_sockfd > 0)
            return;

        _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            lg.LogMessage(Error, "Create listensocket error");
            exit(CREATESOCKETERROR);
        }
        int opt = 0;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

        lg.LogMessage(Normal, "Create listensocket success: %d", _sockfd);
    }

    void BindSocket(uint16_t port) override
    {
        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = INADDR_ANY;

        if (::bind(_sockfd, (sockaddr *)&addr, sizeof(addr)) < 0)
        {
            lg.LogMessage(Error, "Bind error");
            exit(BINDERROR);
        }

        lg.LogMessage(Normal, "Bind success");
    }

    void ListenSocket() override
    {
        if (::listen(_sockfd, defaultbacklog) < 0)
        {
            lg.LogMessage(Error, "Listen error");
            exit(LISTENERROR);
        }
        lg.LogMessage(Normal, "Listen success");
    }

    Socket *AcceptConnect(InetAddr& addr) override
    {
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int sockfd = ::accept(_sockfd, (sockaddr*)&peer, &len);
        if (sockfd < 0)
            return nullptr;

        addr.Init(peer);
        TcpSocket* newsock = new TcpSocket(sockfd);

        return newsock;
    }

    bool ConnectServer(InetAddr& addr) override
    {
        sockaddr_in peer = addr.GetAddr();
        int n = ::connect(_sockfd, (sockaddr*)&peer, sizeof(peer));
        if (n < 0)
            return false;
        else 
            return true;
    }

    int Recvfrom(std::string& buffer, InetAddr* addr) override
    {
        sockaddr_in peer;
        socklen_t peerlen = sizeof(peer);
        char temp[4096];
        ssize_t n = recvfrom(_sockfd, temp, sizeof(temp) - 1, 0, (sockaddr*)&peer, &peerlen);
        if (n > 0)
        {
            temp[n] = 0;
            buffer += temp;
            addr->Init(peer);
        }
        return n;
    }

    int Sendto(std::string buffer, InetAddr& addr) override
    {
        sockaddr_in peer = addr.GetAddr();
        ssize_t n = sendto(_sockfd, buffer.c_str(), buffer.size(), 0, (sockaddr*)&peer, sizeof(peer));
        return n;
    }
    
    int GetSockfd() override
    {
        return _sockfd;
    }

    void SetSockfd(int sockfd) override
    {
        _sockfd = sockfd;
    }

    void CloseSockfd() override
    {
        close(_sockfd);
    }

private:
    int _sockfd;
};

子类TcpSocket继承父类,将来我们直接创建TcpSocket对象,即可调用创建套接字,绑定,监听等函数。

3.1.2封装sockaddr_in

每次创建sockaddr_in,我们都需要将ip转成点分十进制的字符串,port网络转主机等操作,我们可以封装一个InetAddr对象帮我们管理。

#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

class InetAddr
{
public:
    InetAddr()
    {
    }

    InetAddr(const sockaddr_in addr)
        : _addr(addr)
    {
        Init(addr);
    }

    void Init(const sockaddr_in& addr)
    {
        _ip = inet_ntoa(addr.sin_addr);
        _port = ntohs(addr.sin_port);

        _user += _ip;
        _user += " : ";
        _user += std::to_string(_port);
        _addr = addr;
    }

    std::string &GetUser()
    {
        return _user;
    }

    std::string &GetIp()
    {
        return _ip;
    }

    uint16_t &GetPort()
    {
        return _port;
    }

    sockaddr_in &GetAddr()
    {
        return _addr;
    }

private:
    std::string _ip;
    uint16_t _port;
    sockaddr_in _addr;
    std::string _user;
};

3.2服务器

#pragma once
#include <iostream>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

#include "InetAddr.hpp"
#include "LogMessage.hpp"
#include "Socket.hpp"
#include "Protocol.hpp"

using callback_t = std::function<bool(std::string &, std::string *)>;

class TcpServer;

struct ThreadData
{
public:
    ThreadData(TcpSocket *&serverSock, TcpServer *self)
        : _serverSock(serverSock), _self(self)
    {
    }

public:
    TcpSocket *_serverSock;
    TcpServer *_self;
};

class TcpServer
{
public:
    TcpServer(uint16_t port, callback_t cb)
        : headler_request(cb)
    {
        _listenSock = new TcpSocket();
        _listenSock->CreateBindListenSocket(port);
    }

    ~TcpServer()
    {
        _listenSock->CloseSockfd();
        delete _listenSock;
    }

    void Start()
    {
        while (true)
        {
            InetAddr addr;
            TcpSocket *serverSock = (TcpSocket *)_listenSock->AcceptConnect(addr);
            lg.LogMessage(Normal, "%s connect success, serverSock: %d", addr.GetUser().c_str(), serverSock->GetSockfd());

            pthread_t p;
            ThreadData *data = new ThreadData(serverSock, this);
            pthread_create(&p, nullptr, Routine, data);
        }
    }

    static void *Routine(void *arg)
    {
        pthread_detach(pthread_self());
        ThreadData *data = static_cast<ThreadData *>(arg);

        std::string buffer;
        InetAddr addr;
        while (true)
        {
            ssize_t n = data->_serverSock->Recvfrom(buffer, &addr);
            if (n > 0)
            {
                // 对请求进行构建相应
                std::string out;
                data->_self->headler_request(buffer, &out);

                // 将数据发送回去
                data->_serverSock->Sendto(out, addr);
            }
            else if (n == 0)
            {
                lg.LogMessage(Normal, "you close connect, me too");
                break;
            }
            else
            {
                lg.LogMessage(Warning, "recvfrom error");
                break;
            }
        }

        data->_serverSock->CloseSockfd();
        delete data->_serverSock;
        delete data;
    }

private:
    TcpSocket *_listenSock;

public:
    callback_t headler_request;
};

服务端整体逻辑。

  • 1.将来server.cc可以创建一个TcpServer对象,并传入一个端口和一个回调函数,TcpServer的构造函数会自动帮我们创建套接字,绑定,监听。
  • 2.外部通过调用Start函数就可以启动服务器了,Start会accept获取到一个新连接,就创建一个新线程,并且去执行Routine函数,主线程持续获得新连接。
  • 3.Routine函数中,我们先接受对端发来的消息,然后调用回调函数去对数据进行处理,再将数据发送回去。

3.3协议

我们需要一个请求,还需要响应。

//请求
class Request
{
    int _data_x;
    int _data_y;
    char _oper;
};

//响应
class Response
{
    int _result;   //结果
    int _code;     //0成功,1除零错误,2模零错误,3未识别的操作符
};

我们可以再添加一些功能,例如我们需要将请求和响应进行序列化和反序列化,意味着,我们需要将他转化成字节序列,也需要将字节序列转化回结构化数据,这里有两种方案。

3.3.1自定义协议

  • 方案一:我们自己手动定制协议,明确报文与报文之间的间隔。

我们可以定制这样一个协议:len\nx op y\n

len表示x op y的长度,读取时我们先查找\n,如果找到了\n就说明\n之前的就是len,我们可以先将len提取出来。得知len之后就好办了,我们可以通过len查看当前报文是否是一个完整的报文,如果是就将他拆分出来,如果不是就继续读取。

void Serialization(std::string *out)
{
    *out = std::to_string(_data_x) + space + _oper + space + std::to_string(_data_y);
    return;
}

// 添加报头
void EnCode(std::string &input)
{
    std::string str = std::to_string(input.size());
    str += linebreaks + input + linebreaks;
    input = str;
}

将来Serialization函数用于将_data_x,_oper,_data-y序列化成结构化数据,序列化之后我们就可以调用EnCode函数添加报头了。添加完之后就变成了len\nx op y\n的格式了。

bool Deserialization(std::string &inputbuffer)
{
    int left = inputbuffer.find(" ");
    if (left == std::string::npos)
        return false;
    int right = inputbuffer.rfind(" ");
    if (right == std::string::npos || right <= left)
        return false;

    _data_x = atoi(inputbuffer.substr(0, left).c_str());
    _data_y = atoi(inputbuffer.substr(right + space.size()).c_str());
    _oper = inputbuffer.substr(left + space.size(), right - left - space.size())[0];

    return true;
}


bool DeCode(std::string &package, std::string *message)
{
    int pos = package.find(linebreaks);
    if (pos == std::string::npos)
        return false;

    std::string len = package.substr(0, pos);
    int messagelen = atoi(len.c_str());
    int total = len.size() + 2 * linebreaks.size() + messagelen;
    if (total > package.size())
    {
        return false;
    }

    *message = package.substr(pos + linebreaks.size(), messagelen);
    package.erase(0, total);

    return true;
}

Decode函数用于解析字符串,从package函数中提取出一个完整报文放入message中,如果package函数中没有一个完整报文,就返回false,也就是说,如果返回的是true就一定能保证读取到了一个完整的报文。

当读到了一个完整的报文(我们只需要x op y的这一部分,我们直接交给Deserialization函数,他会自动帮我们完成将每一段的数据拆分出来。

方案二:使用第三方库

3.3.2使用Json库

我们可以使用Json库帮我们完成上面的逻辑,由于是第三方库,所以在使用g++编译时,需要包含库名(-ljsoncpp)

我们先了解一下json库怎么使用

3.3.2.1序列化

1.我们先创建一个Value对象,类似于万能对象

#include <iostream>
#include <jsoncpp/json/json.h>

int main()
{
    Json::Value root;

    return 0;
}

2.将需要的参数以KV的形式保存。

#include <iostream>
#include <jsoncpp/json/json.h>

int main()
{
    Json::Value root;
    root["d1"] = 1;
    root["d2"] = 3.14;
    root["d3"] = "hello world";

    return 0;
}

3.将root写入字符串中

#include <iostream>
#include <jsoncpp/json/json.h>

int main()
{
    // 1.创建Value对象
    Json::Value root;
    // 2.以KV形式保存数据
    root["d1"] = 1;
    root["d2"] = 3.14;
    root["d3"] = "hello world";
    // 3.创建对象写入字符串中
    Json::FastWriter write;
    std::string res = write.write(root);
    std::cout << res << std::endl;


    return 0;
}

运行一下看看。

可以看到成功将我们的数据转化成了字符串。如果你觉得这种方式看着不太方便,也可以使用StyledWriter的方式来写.

Json::StyledWriter write;

序列化的工作完成了,反序列化呢。

3.3.2.3反序列化

1.创建Reader对象

// 1.创建Reader对象
Json::Reader reader;

2.解析字符串和Value

// 1.创建Reader对象
Json::Reader reader;
// 2.解析字符串和Value对象
reader.parse(res, root);

3.读取数据

// 1.创建Reader对象
Json::Reader reader;
// 2.解析字符串和Value对象
reader.parse(res, root);
// 3.读取
int d1 = root["d1"].asInt();
float d2 = root["d2"].asFloat();
std::string d3 = root["d3"].asCString();

我们再来运行一下试试。

最后也就能成功解析出来。

3.3.2.4Json库方案

我们可以使用条件编译的方式,区分是自定义协议还是Json库方案

void Serialization(std::string *out)
{
#ifdef Customize
    *out = std::to_string(_data_x) + space + _oper + space + std::to_string(_data_y);
    return;
#else
    Json::Value root;
    root["datax"] = _data_x;
    root["datay"] = _data_y;
    root["oper"] = _oper;
    Json::FastWriter writer;
    *out = writer.write(root);
#endif
}


bool Deserialization(std::string &inputbuffer)
{
#ifdef Customize
    int left = inputbuffer.find(" ");
    if (left == std::string::npos)
        return false;
    int right = inputbuffer.rfind(" ");
    if (right == std::string::npos || right <= left)
        return false;

    _data_x = atoi(inputbuffer.substr(0, left).c_str());
    _data_y = atoi(inputbuffer.substr(right + space.size()).c_str());
    _oper = inputbuffer.substr(left + space.size(), right - left - space.size())[0];

    return true;
#else
    Json::Value root;
    Json::Reader reader;
    auto res = reader.parse(inputbuffer, root);
    if (res == false)
        return false;

    _data_x = root["datax"].asInt();
    _data_y = root["datay"].asInt();
    _oper = (char)root["oper"].asInt();
    return true;
#endif
}

Result和Response都需要序列化和反序列化的函数,这里就不过多赘述了,逻辑都类似。来看看整体代码

#pragma once

#include <iostream>
#include <memory>
#include <unistd.h>
#include <cstdlib>
#include <jsoncpp/json/json.h>
#include "LogMessage.hpp"

//#define Customize = 1;

namespace Protocol
{
    const std::string space = " ";
    const std::string linebreaks = "\n";

    enum Err_Des
    {
        Success = 0,
        Divide_Zero,
        Modulo_Zero,
        Not_Recognized
    };

    // 添加报头
    void EnCode(std::string &input)
    {
        std::string str = std::to_string(input.size());
        str += linebreaks + input + linebreaks;
        input = str;
    }

    // 解析报头,并放在message中
    // le
    // len
    // len\n
    // len\nx
    // len\nx op
    // len\nx op
    // len\nx op y\n
    // len\nx op y\nlen..
    // 7\n12 + 32\n
    bool DeCode(std::string &package, std::string *message)
    {
        int pos = package.find(linebreaks);
        if (pos == std::string::npos)
            return false;

        std::string len = package.substr(0, pos);
        int messagelen = atoi(len.c_str());
        int total = len.size() + 2 * linebreaks.size() + messagelen;
        if (total > package.size())
        {
            return false;
        }

        *message = package.substr(pos + linebreaks.size(), messagelen);
        package.erase(0, total);

        return true;
    }

    class Request
    {
    public:
        Request()
        {
        }

        Request(int x, char oper, int y)
            : _data_x(x), _data_y(y), _oper(oper)
        {
        }

        //_data_x _oper _data_y
        void Serialization(std::string *out)
        {
#ifdef Customize
            *out = std::to_string(_data_x) + space + _oper + space + std::to_string(_data_y);
            return;
#else
            Json::Value root;
            root["datax"] = _data_x;
            root["datay"] = _data_y;
            root["oper"] = _oper;
            Json::FastWriter writer;
            *out = writer.write(root);
#endif
        }

        //_data_x _oper _data_y
        bool Deserialization(std::string &inputbuffer)
        {
#ifdef Customize
            int left = inputbuffer.find(" ");
            if (left == std::string::npos)
                return false;
            int right = inputbuffer.rfind(" ");
            if (right == std::string::npos || right <= left)
                return false;

            _data_x = atoi(inputbuffer.substr(0, left).c_str());
            _data_y = atoi(inputbuffer.substr(right + space.size()).c_str());
            _oper = inputbuffer.substr(left + space.size(), right - left - space.size())[0];

            return true;
#else
            Json::Value root;
            Json::Reader reader;
            auto res = reader.parse(inputbuffer, root);
            if (res == false)
                return false;

            _data_x = root["datax"].asInt();
            _data_y = root["datay"].asInt();
            _oper = (char)root["oper"].asInt();
            return true;
#endif
        }

        int &GetX()
        {
            return _data_x;
        }

        int &GetY()
        {
            return _data_y;
        }

        char &GetOper()
        {
            return _oper;
        }

    private:
        int _data_x = 0;
        int _data_y = 0;
        char _oper = 0;
    };

    class Response
    {
    public:
        Response()
        {
        }

        Response(int result, int code)
            : _result(result), _code(code)
        {
        }

        void Serialization(std::string *out)
        {
#ifdef Customize
            *out = std::to_string(_result) + space + std::to_string(_code);
            return;
#else
            Json::Value root;
            root["result"] = _result;
            root["code"] = _code;
            Json::FastWriter writer;
            *out = writer.write(root);
#endif
        }

        //_data_x _oper _data_y
        bool Deserialization(std::string &inputbuffer)
        {
#ifdef Customize
            int pos = inputbuffer.find(" ");
            if (pos == std::string::npos)
                return false;

            _result = atoi(inputbuffer.substr(0, pos).c_str());
            _code = atoi(inputbuffer.substr(pos + space.size()).c_str());

            return true;
#else
            Json::Value root;
            Json::Reader reader;
            auto res = reader.parse(std::cin, root);
            if (res == false)
                return false;
            _result = root["result"].asInt();
            _code = root["code"].asInt();
#endif
        }

        int &GetResult()
        {
            return _result;
        }

        int &GetCode()
        {
            return _code;
        }

    private:
        int _result = 0;
        int _code = 0;
    };

    // 工厂模式
    class Factory
    {
    public:
        std::shared_ptr<Request> BulidRequest()
        {
            std::shared_ptr<Request> res = std::make_shared<Request>();
            return res;
        }

        std::shared_ptr<Request> BulidRequest(int x, char ch, int y)
        {
            std::shared_ptr<Request> res = std::make_shared<Request>(x, ch, y);
            return res;
        }

        std::shared_ptr<Response> BulidResponse()
        {
            std::shared_ptr<Response> res = std::make_shared<Response>();
            return res;
        }

        std::shared_ptr<Response> BulidResponse(int result, int code)
        {
            std::shared_ptr<Response> res = std::make_shared<Response>(result, code);
            return res;
        }
    };
}

下面的Factory是工厂模式,用于返回一个堆上创建的对象,也可以不使用这个,未来直接在栈上创建也行。

3.5计算器

#include <iostream>
#include <memory>
#include "Protocol.hpp"
namespace Calculator
{
    class Calculator
    {
    public:
        std::shared_ptr<Protocol::Response> Compute(std::shared_ptr<Protocol::Request> req)
        {
            int x = req->GetX();
            int y = req->GetY();
            char op = req->GetOper();

            Protocol::Factory fact;
            std::shared_ptr<Protocol::Response> resp = fact.BulidResponse();
            resp->GetCode() = Protocol::Success;

            switch (op)
            {
            case '+':
                resp->GetResult() = x + y;
                break;
            case '-':
                resp->GetResult() = x - y;
                break;
            case '*':
                resp->GetResult() = x * y;
                break;
            case '/':
            {
                if (y == 0)
                    resp->GetCode() = Protocol::Divide_Zero;
                else
                    resp->GetResult() = x / y;
                break;
            }
            case '%':
            {
                if (y == 0)
                    resp->GetCode() = Protocol::Modulo_Zero;
                else
                    resp->GetResult() = x % y;
                break;
            }

            default:
                resp->GetCode() = Protocol::Not_Recognized;
                break;
            }

            return resp;
        }
    };
}

逻辑很简单,就是将Request计算出结果之后,再构建一个Response对象返回回去。

3.6回调函数

还记得服务器的整体逻辑吗,前面说过,服务器所做的就是读取用户的数据, 然后调用回调函数处理数据,最后再将数据写回给用户,所以我们的网络版本服务器最核心的内容就是回调函数,如何处理用户数据。

1.准备工作,先创建好对应的对象

bool Headler(std::string& instreambuffer, std::string* out)
{
    //计算器
    Calculator::Calculator cal;

    //构建相应对象
    Protocol::Factory fact;
    auto request = fact.BulidRequest(); 

    return true;
}

2.读取用户数据

bool Headler(std::string& instreambuffer, std::string* out)
{
    //计算器
    Calculator::Calculator cal;

    //构建相应对象
    Protocol::Factory fact;
    auto request = fact.BulidRequest();

    std::string message;
    std::string temp;
    while (Protocol::DeCode(instreambuffer, &message) != false)
    {
        if (request->Deserialization(message) == false)
            return false;

        //读到了一个完整的报文,可以开始处理了
    }

    return true;
}

Decode函数用于解析报文,从instreambuffer中读取一个完整报文放入message当中,所以当进入while函数内部,我们能保证一定读取到了一个完整报文,然后我们对这个报文进行反序列化,就一定能拿到用户传输给我们的x,oper,y这三个数据。

bool Headler(std::string& instreambuffer, std::string* out)
{
    //计算器
    Calculator::Calculator cal;

    //构建相应对象
    Protocol::Factory fact;
    auto request = fact.BulidRequest();

    std::string message;
    std::string temp;
    while (Protocol::DeCode(instreambuffer, &message) != false)
    {
        //读到了一个完整的报文,可以开始处理了
        if (request->Deserialization(message) == false)
            return false;

        std::cout << "Request: " << message << std::endl;
        auto respose = cal.Compute(request);
        respose->Serialization(&temp);
        std::cout << "Response: " << temp << std::endl;

        Protocol::EnCode(temp);
        *out += temp;
    }

    return true;
}

现在我们可以开始计算了,计算的结果就放在response当中,我们再将response反序列化成字符串,并将反序列化的结果放入temp当中,再将temp添加报头,以上就是读到一个完整报文的操作。如果有多个完整报文,操作类似。

我们以一个数据为例.

instreambuffer = "7\n12 + 32\n9123 * 1";

经过Decode函数解析之后,发现前面是一个完整的报文,所以message在处理过后等于"12 + 32"。然后将他反序列化,我们就能拿到数据了,此时request内部。

class request
{
    int _data_x = 12;
    int _data_y = 34;
    char _oper = '+';
};

然后我们就可以使用cal开始计算了,计算后会给我们返回一个Response对象。

class response
{   
    int _result = 46;
    int _code = 0;
};

然后我们将他序列化之后保存进temp当中。temp = "46 0",然后我们就可以添加报头了

temp = "4\n46 0\n"

再将结果放入out中,我们再次使用DeCode函数判断是否能构成一个完整报文,发现不能,函数返回。

3.7客户端

#include <iostream>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "InetAddr.hpp"
#include "Socket.hpp"
#include "LogMessage.hpp"
#include "Protocol.hpp"

void Usage()
{
    std::cout << "Please enter: ./Client ip port" << std::endl;
}


int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage();
        return 4;
    }
    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);

    TcpSocket sockfd;
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(ip.c_str());
    addr.sin_port = htons(port);
    InetAddr peer(addr);
    if (sockfd.CreateSocketAndConnect(peer) == false)
    {
        return 5;
    }

    std::string oper = "+-*/%@!#$&=[]";
    srand(time(nullptr) ^ getpid());
    Protocol::Factory fact;

    std::string send;
    while (true)
    {
        int x = rand() % 100;
        usleep(rand() % 1234);
        int y = rand() % 100;
        usleep(rand() % 321);
        char op = oper[rand() % oper.size()];

        auto req = fact.BulidRequest(x, op, y);
        std::string request;
        req->Serialization(&request);
        std::string total = std::to_string(request.size()) + "\n" + request + "\n";
        std::cout << total << std::endl;

        sockfd.Sendto(total, peer);

        std::string recv;
Start:
        sockfd.Recvfrom(recv, &peer);
        int left = recv.find("\n");
        if (left == std::string::npos)
        {
            goto Start;
        }

        int right = recv.find("\n", left + 1);
        if (right == std::string::npos)
        {
            goto Start;
        }
        //必须确保读到了一个完整的协议
        std::string result = recv.substr(left + 1, right - left - 1);
        std::cout << request << " = " << result << std::endl;

        sleep(1);
    }

    return 0;
}

整体逻辑:随机创建x,y和op,然后序列化并添加报文之后发送给客户端。然后就开始接受服务端消息。(Start后面的部分用于确定读到的是一个完整报文)。

4.运行结果

最终也是可以成功完成计算器功能,而且我们也不怕遇到数据粘包问题了。

我们再试试Json库的方案。

5.源码

5.1TcpServer.hpp

#pragma once
#include <iostream>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

#include "InetAddr.hpp"
#include "LogMessage.hpp"
#include "Socket.hpp"
#include "Protocol.hpp"

using callback_t = std::function<bool(std::string &, std::string *)>;

class TcpServer;

struct ThreadData
{
public:
    ThreadData(TcpSocket *&serverSock, TcpServer *self)
        : _serverSock(serverSock), _self(self)
    {
    }

public:
    TcpSocket *_serverSock;
    TcpServer *_self;
};

class TcpServer
{
public:
    TcpServer(uint16_t port, callback_t cb)
        : headler_request(cb)
    {
        _listenSock = new TcpSocket();
        _listenSock->CreateBindListenSocket(port);
    }

    ~TcpServer()
    {
        _listenSock->CloseSockfd();
        delete _listenSock;
    }

    void Start()
    {
        while (true)
        {
            InetAddr addr;
            TcpSocket *serverSock = (TcpSocket *)_listenSock->AcceptConnect(addr);
            lg.LogMessage(Normal, "%s connect success, serverSock: %d", addr.GetUser().c_str(), serverSock->GetSockfd());

            pthread_t p;
            ThreadData *data = new ThreadData(serverSock, this);
            pthread_create(&p, nullptr, Routine, data);
        }
    }

    static void *Routine(void *arg)
    {
        pthread_detach(pthread_self());
        ThreadData *data = static_cast<ThreadData *>(arg);

        std::string buffer;
        InetAddr addr;
        while (true)
        {
            ssize_t n = data->_serverSock->Recvfrom(buffer, &addr);
            if (n > 0)
            {
                // 对请求进行构建相应
                std::string out;
                data->_self->headler_request(buffer, &out);

                // 将数据发送回去
                data->_serverSock->Sendto(out, addr);
            }
            else if (n == 0)
            {
                lg.LogMessage(Normal, "you close connect, me too");
                break;
            }
            else
            {
                lg.LogMessage(Warning, "recvfrom error");
                break;
            }
        }

        data->_serverSock->CloseSockfd();
        delete data->_serverSock;
        delete data;
    }

private:
    TcpSocket *_listenSock;

public:
    callback_t headler_request;
};

5.2Server.cc

#include <iostream>
#include <memory>

#include "TcpServer.hpp"
#include "LogMessage.hpp"
#include "InetAddr.hpp"
#include "Calculator.hpp"
#include "Protocol.hpp"

void Usage()
{
    std::cout << "./Server port" << std::endl;
}

//
bool Headler(std::string& instreambuffer, std::string* out)
{
    //计算器
    Calculator::Calculator cal;

    //构建相应对象
    Protocol::Factory fact;
    auto request = fact.BulidRequest();

    std::string message;
    std::string temp;
    while (Protocol::DeCode(instreambuffer, &message) != false)
    {
        //读到了一个完整的报文,可以开始处理了
        if (request->Deserialization(message) == false)
            return false;

        std::cout << "Request: " << message << std::endl;
        auto respose = cal.Compute(request);
        respose->Serialization(&temp);
        std::cout << "Response: " << temp << std::endl;

        Protocol::EnCode(temp);
        *out += temp;
    }

    return true;
}

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        Usage();
        exit(4);
    }

    uint16_t port = atoi(argv[1]);
    TcpServer* http = new TcpServer(port, Headler);
    http->Start();

    delete http;

    return 0;
}

5.3Client.cc

#include <iostream>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "InetAddr.hpp"
#include "Socket.hpp"
#include "LogMessage.hpp"
#include "Protocol.hpp"

void Usage()
{
    std::cout << "Please enter: ./Client ip port" << std::endl;
}


int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage();
        return 4;
    }
    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);

    TcpSocket sockfd;
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(ip.c_str());
    addr.sin_port = htons(port);
    InetAddr peer(addr);
    if (sockfd.CreateSocketAndConnect(peer) == false)
    {
        return 5;
    }

    std::string oper = "+-*/%@!#$&=[]";
    srand(time(nullptr) ^ getpid());
    Protocol::Factory fact;

    std::string send;
    while (true)
    {
        int x = rand() % 100;
        usleep(rand() % 1234);
        int y = rand() % 100;
        usleep(rand() % 321);
        char op = oper[rand() % oper.size()];

        auto req = fact.BulidRequest(x, op, y);
        std::string request;
        req->Serialization(&request);
        std::string total = std::to_string(request.size()) + "\n" + request + "\n";
        std::cout << total << std::endl;

        sockfd.Sendto(total, peer);

        std::string recv;
Start:
        sockfd.Recvfrom(recv, &peer);
        int left = recv.find("\n");
        if (left == std::string::npos)
        {
            goto Start;
        }

        int right = recv.find("\n", left + 1);
        if (right == std::string::npos)
        {
            goto Start;
        }
        //必须确保读到了一个完整的协议
        std::string result = recv.substr(left + 1, right - left - 1);
        std::cout << request << " = " << result << std::endl;

        sleep(1);
    }

    return 0;
}

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

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

相关文章

探索React Router:实现动态二级路由

我有一个路由配置的二维数组&#xff0c;想根据这个数组结合路由组件来动态生成路由&#xff0c;应该怎么样实现。在 React Router 6 中渲染二级路由的方式跟 React Router 65相比有一些变化,但核心思路仍然是利用 Route 组件和路由嵌套的方式。下面是具体的步骤: 定义路由数组…

OpenCompass 大模型评测实战——作业

OpenCompass 大模型评测实战——作业 一、基础作业1.1、使用 OpenCompass 评测 internlm2-chat-1_8b 模型在 C-Eval 数据集上的性能1.1.1、安装基本环境1.1.2、解压数据集1.1.3、查看支持的数据集和模型1.1.4、启动评测 二、进阶作业2.1、将自定义数据集提交至OpenCompass官网 …

WIFISKY 7层流控路由器 confirm.php RCE漏洞复现

0x01 产品简介 WIFISKY-7层流控路由器是一款可用于家庭或办公环境的无线路由器,具备流控功能以优化网络流量和提供更稳定的网络连接。该路由器采用了7层流控技术,能够依据网络数据包的内容进行智能管理,从而实现对网络流量的精细化控制和优化。这种技术可以提升网络的整体性…

vscode 使用文件模板功能来添加版权信息

vscode 新建文件的时候&#xff0c;自动填充作者及版权信息 无需使用插件&#xff0c;操作如下&#xff1a; 选择 “首选项(Preferences)”。在搜索框中输入 “file template” 或者 “文件模板”&#xff0c;然后选择相关的设置项。 {"C_Cpp.clang_format_fallbackSt…

ctfshow web入门 SQl注入 web191--web200

web191 多了一个正则绕过 上脚本布尔盲注 用ord #author:yu22x import requests import string url"http://70adf0cb-2208-4974-b064-50a4f4103541.challenge.ctf.show/api/index.php" sstring.ascii_lettersstring.digits flag for i in range(1,45):print(i)for j…

【熵与特征提取】从近似熵,到样本熵,到模糊熵,再到排列熵,包络熵,散布熵,究竟实现了什么?(第六篇)——“散布熵”及其MATLAB实现

今天讲散布熵&#xff0c;之前用了几篇文章分别讲述了功率谱熵、奇异谱熵、能量熵、近似熵、样本熵、模糊熵、排列熵、包络熵这8种类型的熵&#xff1a; Mr.看海&#xff1a;【熵与特征提取】基于“信息熵”的特征指标及其MATLAB代码实现&#xff08;功率谱熵、奇异谱熵、能量…

脚手架搭建项目package.json配置中依赖的版本问题

脚手架搭建项目package.json配置中依赖的版本问题 问题描述&#xff1a;项目刚搭建好&#xff0c;运行没有问题&#xff0c;为什么过一段时间&#xff0c;删除node_modules&#xff0c;或者重新安装包依赖&#xff0c;然后项目某些地方出现莫名的错误&#xff08;依赖库的地方…

希捷HDD最新财报:销售同比下降11%,环比增长6%,4Q24前景看好

Seagate Technology Holdings plc公布了截至2024年3月29日的第三财季财务业绩。 “随着云需求改善、我们强大的运营纪律和价格执行&#xff0c;希捷3月季度的营收增长了6%&#xff0c;非GAAP每股收益较上一季度翻了一番多。这种组合为我们市场复苏时回归目标利润率奠定了基础。…

C++:类与对象完结篇

hello&#xff0c;各位小伙伴&#xff0c;本篇文章跟大家一起学习《C&#xff1a;运算符重载》&#xff0c;感谢大家对我上一篇的支持&#xff0c;如有什么问题&#xff0c;还请多多指教 &#xff01; 文章目录 重新认识构造函数1.初始化列表2.explicit关键字 static成员1.sta…

面试:ThreadLocal

目录 1、ThreadLocal可以实现〔资源对象】的线程隔离&#xff0c;让每个线程各用各的【资源对象】&#xff0c;避免争用引发的线程安全问题 2、ThreadLocal同时实现了线程内的资源共享 3、原理 4、为什么ThreadLocalMap 中的 key (即 ThreadLocal &#xff09;要设计为弱引用…

configure: error: library ‘crypto‘ is required for OpenSSL

1、执行命令./configure --prefix/usr/local/pgsql/postgresql-14.2 --with-openssl 报错configure: error: library crypto is required for OpenSSL 2、解决办法 yum install openssl openssl-devel

pom文件依赖报红问题

dependencyManagement标签下依赖报红 如图 dependencyManagement标签下依赖报红问题&#xff0c;原因是dependencyManagement标签下的包不会被下载&#xff0c;repository里根本没有 解决方法 &#xff1a;将依赖复制到dependencies标签下&#xff0c;再reload pom文件&#x…

Leetcode算法训练日记 | day35

专题九 贪心算法 一、柠檬水找零 1.题目 Leetcode&#xff1a;第 860 题 在柠檬水摊上&#xff0c;每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品&#xff0c;&#xff08;按账单 bills 支付的顺序&#xff09;一次购买一杯。 每位顾客只买一杯柠檬水&#xff0c;然…

为什么建议游戏工作室使用海外住宅IP防封?

当谈到游戏工作室时&#xff0c;它们通常以多开游戏账号来获取收益为主要目标。这种商业模式在游戏产业中已经成为一个独特而且颇具潜力的领域。然而&#xff0c;随之而来的是防封问题&#xff0c;特别是当游戏工作室试图通过多开账号来赚取更多收益时。因此&#xff0c;我们有…

【第6节】Lagent AgentLego 智能体应用搭建

目录 1 基础课程2 安装环境2.1 教程要求2.2 安装 Lagent 和 AgentLego 3 实践操作3.1 Lagent&#xff1a;轻量级智能体框架3.1.1 Lagent Web Demo 使用3.1.2 用 Lagent 自定义工具 3.2 AgentLego&#xff1a;组装智能体“乐高”3.2.1 AgentLego 直接使用部分3.2.2 AgentLego We…

【Harmony3.1/4.0】笔记二

概述 列表是一种复杂的容器&#xff0c;当列表项达到一定数量&#xff0c;内容超过屏幕大小时&#xff0c;可以自动提供滚动功能。它适合用于呈现同类数据类型或数据类型集&#xff0c;例如图片和文本。在列表中显示数据集合是许多应用程序中的常见要求&#xff08;如通讯录、…

linux——yum工具详解

yum是linux中自动解决软件包依赖关系的管理器 同时&#xff0c;yum也是一个rpm软件 这里使用yum install nginx安装nginx

Windows SMBGhost CVE-2020-0796 Elevate Privileges

SMBGhost CVE-2020-0796 Microsoft Windows 10 (1903/1909) - ‘SMBGhost’ SMB3.1.1 ‘SMB2_COMPRESSION_CAPABILITIES’ Local Privilege Escalation https://www.exploit-db.com/exploits/48267 Github https://github.com/danigargu/CVE-2020-0796 修改载荷[可选] 生成 c# …

用c++实现起泡排序、哈密顿回路问题、TSP问题

5.3.2 起泡排序 【问题】 起泡排序(bubble sort)的基本思想是&#xff1a;两两比较相邻记录&#xff0c;如果反序则交换&#xff0c;直至没有反序的记录&#xff0c;如图5.8所示。【想法】下表给出了一个起泡排序的例子&#xff08;方括号括起来的为无序区&#xff09;&#x…

深入理解JavaScript:对象什么时候创建

&#x1f31f; 我们在chrome浏览器中debug程序。为了好debug我们会写一些在日常开发中基本不会采用的代码书写方式。 JavaScript中创建对象有3中方式&#xff1a; 1、对象字面量&#xff1b; 2、new&#xff1b; 3、Object.create(对象)&#xff1b; 1、使用new创建对象 fun…