再谈“协议” 【网络版计算器】

协议的概念

协议,网络协议的简称,网络协议是通信计算机双方必须共同遵从的一组约定,比如怎么建立连接、怎么互相识别等。

为了使数据在网络上能够从源到达目的,网络通信的参与方必须遵循相同的规则,我们将这套规则称为协议(protocol),而协议最终都需要通过计算机语言的方式表示出来。只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流。

结构化数据的传输

通信双方在进行网络通信时:

  • 如果需要传输的数据是一个字符串,那么直接将这一个字符串发送到网络当中,此时对端也能从网络当中获取到这个字符串。
  • 但如果需要传输的是一些结构化的数据,此时就不能将这些数据一个个发送到网络当中。

比如现在要实现一个网络版的计算器,那么客户端每次给服务端发送的请求数据当中,就需要包括左操作数、右操作数以及对应需要进行的操作,此时客户端要发送的就不是一个简单的字符串,而是一组结构化的数据。

如果客户端将这些结构化的数据单独一个个的发送到网络当中,那么服务端从网络当中获取这些数据时也只能一个个获取,此时服务端还需要纠结如何将接收到的数据进行组合。因此客户端最好把这些结构化的数据打包后统一发送到网络当中,此时服务端每次从网络当中获取到的就是一个完整的请求数据,客户端常见的“打包”方式有以下两种。

将结构化的数据组合成一个字符串

约定方案一:

  1. 客户端发送一个形如“1+1”的字符串。
  2. 这个字符串中有两个操作数,都是整型。
  3. 两个数字之间会有一个字符是运算符。
  4. 数字和运算符之间没有空格。

客户端可以按某种方式将这些结构化的数据组合成一个字符串,然后将这个字符串发送到网络当中,此时服务端每次从网络当中获取到的就是这样一个字符串,然后服务端再以相同的方式对这个字符串进行解析,此时服务端就能够从这个字符串当中提取出这些结构化的数据。

定制结构体+序列化和反序列化

约定方案二:

  1. 定制结构体来表示需要交互的信息。
  2. 发送数据时将这个结构体按照一个规则转换成网络标准数据格式,接收数据时再按照相同的规则把接收到的数据转化为结构体。
  3. 这个过程叫做“序列化”和“反序列化”。

客户端可以定制一个结构体,将需要交互的信息定义到这个结构体当中。客户端发送数据时先对数据进行序列化,服务端接收到数据后再对其进行反序列化,此时服务端就能得到客户端发送过来的结构体,进而从该结构体当中提取出对应的信息。

注意: 无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据, 在另一端能够正确的进行解
析, 就是ok的. 这种约定, 就是 应用层协议

序列化和反序列化

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

比如我们发送信息的过程中有昵称,时间,消息内容 三条消息 , 将它们转换为 昵称-时间-信息内容 一条消息的字符串,然后打个包,这种这叫序列化。

  • 反序列化是把字节序列恢复为对象的过程。

消息被读取,然后将昵称-时间-信息内容 解包为昵称,时间,消息内容 三条消息 ,这叫反序列化。

序列化和反序列化的目的

  • 在网络传输时,序列化目的是为了方便网络数据的发送和接收,无论是何种类型的数据,经过序列化后都变成了二进制序列,此时底层在进行网络数据传输时看到的统一都是二进制序列。
  • 序列化后的二进制序列只有在网络传输时能够被底层识别,上层应用是无法识别序列化后的二进制序列的,因此需要将从网络中获取到的数据进行反序列化,将二进制序列的数据转换成应用层能够识别的数据格式。

我们可以认为网络通信和业务处理处于不同的层级,在进行网络通信时底层看到的都是二进制序列的数据,而在进行业务处理时看得到则是可被上层识别的数据。如果数据需要在业务处理和网络通信之间进行转换,则需要对数据进行对应的序列化或反序列化操作。

在这里插入图片描述

网络版计算器

下面实现一个网络版的计算器,主要目的是感受一下什么是协议。

对套接字进行封装Socket.hpp

用namespace Net_Work进行封装是为了更好的使用

//socket.hpp
#define Convert(addrptr) ((struct sockaddr *)addrptr)

namespace Net_Work
{
    const static int defaultsockfd = -1;
    const int backlog = 5;

    enum
    {
        SocketError = 1,
        BindError,
        ListenError,
    };

    // 封装一个基类,Socket接口类
    // 设计模式:模版方法类
    class Socket//抽象类
    {
    public:
        virtual ~Socket() {}
        virtual void CreateSocketOrDie() = 0;
        virtual void BindSocketOrDie(uint16_t port) = 0;
        virtual void ListenSocketOrDie(int backlog) = 0;
        virtual Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) = 0;
        virtual bool ConnectServer(std::string &serverip, uint16_t serverport) = 0;
        virtual int GetSockFd() = 0;
        virtual void SetSockFd(int sockfd) = 0;
        virtual void CloseSocket() = 0;
        virtual bool Recv(std::string *buffer, int size) = 0;
        virtual void Send(std::string &send_str) = 0;
        // TODO
    public:
        void BuildListenSocketMethod(uint16_t port, int backlog)
        {
            CreateSocketOrDie();
            BindSocketOrDie(port);
            ListenSocketOrDie(backlog);
        }
        bool BuildConnectSocketMethod(std::string &serverip, uint16_t serverport)
        {
            CreateSocketOrDie();
            return ConnectServer(serverip, serverport);
        }
        void BuildNormalSocketMethod(int sockfd)
        {
            SetSockFd(sockfd);
        }
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket(int sockfd = defaultsockfd) : _sockfd(sockfd)
        {
        }
        ~TcpSocket()
        {
        }
        void CreateSocketOrDie() override
        {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
                exit(SocketError);
        }
        void BindSocketOrDie(uint16_t port) override
        {
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_addr.s_addr = INADDR_ANY;
            local.sin_port = htons(port);

            int n = ::bind(_sockfd, Convert(&local), sizeof(local));
            if (n < 0)
                exit(BindError);
        }
        void ListenSocketOrDie(int backlog) override
        {
            int n = ::listen(_sockfd, backlog);
            if (n < 0)
                exit(ListenError);
        }
        Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) override
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int newsockfd = accept(_sockfd, Convert(&peer), &len);
            if (newsockfd < 0)
                return nullptr;
            *peerport = ntohs(peer.sin_port);
            *peerip = inet_ntoa(peer.sin_addr);
            Socket *s = new TcpSocket(newsockfd);
            return s;
        }
        bool ConnectServer(std::string &serverip, uint16_t serverport) override
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_addr.s_addr = inet_addr(serverip.c_str());
            server.sin_port = htons(serverport);

            int n = connect(_sockfd, Convert(&server), sizeof(server));
            if (n == 0)
                return true;
            else
                return false;
        }
        int GetSockFd() override
        {
            return _sockfd;
        }
        void SetSockFd(int sockfd) override
        {
            _sockfd = sockfd;
        }
        void CloseSocket() override
        {
            if (_sockfd > defaultsockfd)
                close(_sockfd);
        }
        bool Recv(std::string *buffer, int size) override
        {
            char inbuffer[size];
            ssize_t n = recv(_sockfd, inbuffer, size-1, 0);
            if(n > 0)
            {
                inbuffer[n] = 0;
                *buffer += inbuffer; // 故意拼接的
                return true;
            }
            else if(n == 0) return false;
            else return false;
        }
        void Send(std::string &send_str) override
        {
            send(_sockfd, send_str.c_str(), send_str.size(), 0);
        }
    private:
        int _sockfd;
    };
}

服务端代码

TcpServer封装的最初代码,

//TcpSetver.hpp
using func_t = std::function<std::string(std::string &, bool *error_code)>; // &必须的

class TcpServer
{
public:
    TcpServer(uint16_t port, func_t handler_request)
        : _port(port), _listensocket(new Net_Work::TcpSocket()), _handler_request(handler_request)
    {
        _listensocket->BuildListenSocketMethod(_port, Net_Work::backlog);
    }
    
    ~TcpServer()
    {
        delete _listensocket;
    }

private:
    int _port;
    Net_Work::Socket *_listensocket;//用封装套接字创建的_listensocket

public:
    func_t _handler_request;
};

采用多线程

  • 当前服务器采用的多线程的方案,你也可以选择采用多进程的方案或是将线程池接入到多线程当中。
  • 服务端创建新线程时,需要将调用accept获取到套接字作为参数传递给该线程,为了避免该套接字被下一次获取到的套接字覆盖,最好在堆区开辟空间存储该文件描述符的值。
//TcpServer.hpp
using func_t = std::function<std::string(std::string &, bool *error_code)>; // &必须的

class TcpServer;

class ThreadData
{
public:
    ThreadData(TcpServer *tcp_this, Net_Work::Socket *sockp) : _this(tcp_this), _sockp(sockp)
    {
    }

public:
    TcpServer *_this;
    Net_Work::Socket *_sockp;
};

class TcpServer
{
public:
    static void *ThreadRun(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        std::string inbufferstream;
        while (true)
        {
            bool ok = true;
            // 读取数据 -- 不关心数据是什么,只读取
            //1.读取报文
            if(!td->_sockp->Recv(&inbufferstream,1024))
                 break;
            //2.报文处理
            std::string send_string = td->_this->_handler_request(inbufferstream,&ok); // 回调不仅仅是调出去,还会回来
            if (ok)
            {
                // 发送数据 -- 不关心数据是什么,只发送
                // 3. 发送数据
                if (!send_string.empty())
                {
                    td->_sockp->Send(send_string);
                }
            }
            else
            {
                break;
            }
        
        } 

        td->_sockp->CloseSocket();
        delete td->_sockp;
        delete td; 

        return nullptr;
    }

    void Loop()
    {
        while (true)
        {
            std::string peerip;
            uint16_t peerport;
            Net_Work::Socket *newsock = _listensocket->AcceptConnection(&peerip, &peerport);
            if (newsock == nullptr) //连接失败也不能退出,继续连接
                continue;
            std::cout << "获取一个新连接, sockfd: " << newsock->GetSockFd() << " client info: " << peerip << ":" << peerport << std::endl;

            pthread_t tid;
            ThreadData *td = new ThreadData(this, newsock);
            pthread_create(&tid, nullptr, ThreadRun, td);
        }
    }
private:
    int _port;
    Net_Work::Socket *_listensocket;

public:
    func_t _handler_request;
};

对于TcpServerMain.cc

using namespace Net_Work;
using namespace Protocol;
using namespace CalCulateNS;


//网络负责IO发送
//HandlerRequest字节流数据解析和调用业务处理方法的
std::string HandlerRequest(std::string &inbufferstream, bool *error_code)
{
    *error_code = true;
    // 0. 计算机对象
    Calculate calculte;

    // 1. 构建响应对象
    std::unique_ptr<Factory> factory = std::make_unique<Factory>();
    auto req = factory->BuildRequest();

    // 2. 分析字节流,看是否有一个完整的报文
    std::string total_resp_string;
    std::string message;
    while (Decode(inbufferstream, &message))
    {
        std::cout << message << "---- messge" << std::endl;

        // 3. 我一定读到了一个完整的报文,可以进行反序列化啦!
        if (!req->Deserialize(message))
        {
            std::cout << "Deserialize error" << std::endl;
            *error_code = false;
            return std::string();
        }
        std::cout << "Deserialize success" << std::endl;
        // 4. 业务处理了
        auto resp = calculte.Cal(req);
        // 5. 序列化response
        std::string send_string;
        resp->Serialize(&send_string); // "result code"
        // 6. 构建完成的字符串级别的响应报文
        send_string = Encode(send_string);
        // 7. 发送
        total_resp_string += send_string;
    }
    return total_resp_string;
}

// ./server port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cout << "Usage : " << argv[0] << " port" << std::endl;
        return 0;
    }
    uint16_t localport = std::stoi(argv[1]);

    std::unique_ptr<TcpServer> svr(new TcpServer(localport,HandlerRequest));
    svr->Loop();

    return 0;
}

协议Protocal.hpp

namespace Protocol
{

    const std::string ProtSep = " ";
    const std::string LineBreakSep = "\n";

    // 问题
    // 1. 结构化数据的序列和反序列化
    // 2. 还要解决用户区分报文边界 --- 数据包粘报问题

    // 讲法
    // 1. 自定义协议
    // 2. 成熟方案序列和反序列化

    // "len\nx op y\n" : \n不属于报文的一部分,约定
    std::string Encode(const std::string &message)
    {
        std::string len = std::to_string(message.size());
        std::string package = len + LineBreakSep + message + LineBreakSep;
        return package;
    }

    // "len\nx op y\n" : \n不属于报文的一部分,约定
    // 我无法保证package就是一个独立的完整的报文
    // "l
    // "len
    // "len\n
    // "len\nx
    // "len\nx op
    // "len\nx op y
    // "len\nx op y\n"
    // "len\nx op y\n""len
    // "len\nx op y\n""len\n
    // "len\nx op
    // "len\nx op y\n""len\nx op y\n"
    // "len\nresult code\n""len\nresult code\n"
    bool Decode(std::string &package, std::string *message)
    {
        // 除了解包,我还想判断报文的完整性, 能否正确处理具有"边界"的报文
        auto pos = package.find(LineBreakSep);
        if (pos == std::string::npos)
            return false;
        std::string lens = package.substr(0, pos);
        int messagelen = std::stoi(lens);
        int total = lens.size() + messagelen + 2 * LineBreakSep.size();
        if (package.size() < total)
            return false;
        // 至少package内部一定有一个完整的报文了!
        *message = package.substr(pos + LineBreakSep.size(), messagelen);
        package.erase(0, total);
        return true;
    }

    class Request
    {
    public:
        Request() : _data_x(0), _data_y(0), _oper(0)
        {
        }

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

        // 结构化数据->字符串
        bool Serialize(std::string *out)
        {
            *out = std::to_string(_data_x) + ProtSep + _oper + ProtSep + std::to_string(_data_y);
            return true;
        }

        bool Deserialize(std::string &in) // "x op y"
        {
            auto left = in.find(ProtSep);
            if (left == std::string::npos)
                return false;
            auto right = in.rfind(ProtSep);
            if (right == std::string::npos)
                return false;

            _data_x = std::stoi(in.substr(0, left));
            _data_y = std::stoi(in.substr(right + ProtSep.size()));
            std::string oper = in.substr(left + ProtSep.size(), right - (left + ProtSep.size()));
            if (oper.size() != 1)
                return false;
            _oper = oper[0];
            return true;
        }

        int GetX() { return _data_x; }
        int GetY() { return _data_y; }
        char GetOper() { return _oper; }

    private:
        //_data_x _oper _data_y;
        // 报文的自描述字段
        // "len\n x op y\n" : \n不属于报文的一部分,约定
        // 很多工作都是在做字符串处理!
        int _data_x; // 第一个参数
        int _data_y; // 第二个参数
        char _oper;  // + - * / %
    };

    class Response
    {
    public:
        Response() : _result(0), _code(0)
        {
        }

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

        bool Serialize(std::string *out)
        {
            *out = std::to_string(_result) + ProtSep + std::to_string(_code);
            return true;
        }

        bool Deserialize(std::string &in) // "x op y" [)
        {
            auto pos = in.find(ProtSep);
            if (pos == std::string::npos)
                return false;
            _result = std::stoi(in.substr(0, pos));
            _code = std::stoi(in.substr(pos + ProtSep.size()));
            return true;
        }
        void SetResult(int res) { _result = res; }
        void SetCode(int code) { _code = code; }
        int GetResult() { return _result; }
        int GetCode() { return _code; }

    private:
        // "len\n_result _code\n"
        int _result; // 运算结果
        int _code;   // 运算状态
    };

    // 简单的工厂模式,建造类设计模式
    class Factory
    {
    public:
        std::shared_ptr<Request> BuildRequest()
        {
            std::shared_ptr<Request> req = std::make_shared<Request>();
            return req;
        }
        std::shared_ptr<Request> BuildRequest(int x, int y, char op)
        {
            std::shared_ptr<Request> req = std::make_shared<Request>(x, y, op);
            return req;
        }
        std::shared_ptr<Response> BuildResponse()
        {
            std::shared_ptr<Response> resp = std::make_shared<Response>();
            return resp;
        }
        std::shared_ptr<Response> BuildResponse(int result, int code)
        {
            std::shared_ptr<Response> req = std::make_shared<Response>(result, code);
            return req;
        }
    };

}

业务Calculate.hpp

// 业务
namespace CalCulateNS
{
    enum
    {
        Success = 0,
        DivZeroErr,
        ModZeroErr,
        UnKnowOper
    };

    class Calculate
    {
    public:
        Calculate() {}
        std::shared_ptr<Protocol::Response> Cal(std::shared_ptr<Protocol::Request> req)
        {
            std::shared_ptr<Protocol::Response> resp = factory.BuildResponse();
            resp->SetCode(Success);
            switch (req->GetOper())
            {
            case '+':
                resp->SetResult(req->GetX() + req->GetY());
                break;
            case '-':
                resp->SetResult(req->GetX() - req->GetY());
                break;
            case '*':
                resp->SetResult(req->GetX() * req->GetY());
                break;
            case '/':
            {
                if (req->GetY() == 0)
                {
                    resp->SetCode(DivZeroErr);
                }
                else
                {
                    resp->SetResult(req->GetX() / req->GetY());
                }
            }
            break;
            case '%':
            {
                if (req->GetY() == 0)
                {
                    resp->SetCode(ModZeroErr);
                }
                else
                {
                    resp->SetResult(req->GetX() % req->GetY());
                }
            }
            break;
            default:
                resp->SetCode(UnKnowOper);
                break;
            }

            return resp;
        }
        ~Calculate() {}

    private:
        Protocol::Factory factory;
    };
} // namespace CalCulate

客户端代码TcClientMain.cc

客户端在向服务端发送或接收数据时,可以使用write或read函数进行发送或接收,也可以使用send或recv函数对应进行发送或接收。

send函数的函数原型如下:

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数说明:

  • sockfd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
  • buf:需要发送的数据。
  • len:需要发送数据的字节个数。
  • flags:发送的方式,一般设置为0,表示阻塞式发送。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

recv函数的函数原型如下:

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数说明:

  • sockfd:特定的文件描述符,表示从该文件描述符中读取数据。
  • buf:数据的存储位置,表示将读取到的数据存储到该位置。
  • len:数据的个数,表示从该文件描述符中读取数据的字节数。
  • flags:读取的方式,一般设置为0,表示阻塞式读取。

返回值说明:

  • 如果返回值大于0,则表示本次实际读取到的字节个数。
  • 如果返回值等于0,则表示对端已经把连接关闭了。
  • 如果返回值小于0,则表示读取时遇到了错误。
using namespace Protocol;

// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "Usage : " << argv[0] << " serverip serverport" << std::endl;
        return 0;
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    Net_Work::Socket* conn = new Net_Work::TcpSocket();
    if(!conn->BuildConnectSocketMethod(serverip,serverport))
    {
        std::cerr << "Connect " << serverip << " : " << serverport << "faild" << std::endl;
    }
    std::cerr << "Connect " << serverip << " : " << serverport << " success" << std::endl;
   
    std::unique_ptr<Factory> factory = std::make_unique<Factory>();

    srand(time(nullptr) ^ getpid());
    const std::string opers = "+-*/%^=&";
    while (true)
    {
        // 1. 构建一个请求,遵守协议
        int x = rand() % 100; //[0, 99]
        usleep(rand() % 7777);
        int y = rand() % 100; //[0,99]
        char oper = opers[rand() % opers.size()];
        std::shared_ptr<Request> req = factory->BuildRequest(x, y, oper);

        // 2. 对请求进行序列化
        std::string requeststr;
        req->Serialize(&requeststr); //

        std::cout << requeststr << std::endl;


        // 3. 添加自描述报头
        requeststr = Encode(requeststr);
        std::cout << requeststr << std::endl;

        // 4. 发送请求
        conn->Send(requeststr);
        std::string responsestr;
        while (true)
        {
            // 5. 读取响应
            if(!conn->Recv(&responsestr, 1024)) break;
            // 6. 报文进行解析
            std::string response;
            if (!Decode(responsestr, &response))
                continue; // 我就不连续的处理了

            // 7.response "result code"
            auto resp = factory->BuildResponse();
            resp->Deserialize(response);

            // 8. 得到了计算结果,而且是一个结构化的数据
            std::cout << resp->GetResult() << "[" << resp->GetCode() << "]" << std::endl;
            break;
        }
        sleep(1);
    }

    conn->CloseSocket();
    return 0;
}

代码测试

运行服务端后再让客户端连接服务端,此时服务端就会对客户端发来的计算请求进行处理,并会将计算后的结果响应给客户端。
在这里插入图片描述
此时我们就以这样一种方式约定出了一套应用层的简单的网络计算器,这就叫做协议。

  • 11
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值