欢迎光临
我们一直在努力

自定义协议:实现网络计算器

文章目录

  • 1. 一些理解
    • 1.1 理解序列化与反序列化
    • 1.2 理解协议
    • 1.3 重新理解read,write为什么支持全双工
  • 2. 网络版计算器
    • 2.1 demo v1 (socket封装:引入模板方法模式)
      • 2.1.1 源码
      • 2.1.2 流程详解
      • 2.1.3 运行结果
    • 2.2 demo v2 (自定义协议)
      • 2.2.1 源码
      • 2.2.2 流程详解
    • 2.3 demo v3 (最终版)
      • 2.3.1 源码
      • 2.3.2 流程详解

1. 一些理解

1.1 理解序列化与反序列化

模拟一个场景: 我们在进行QQ聊天的时候,聊天时的界面不只是显示我们聊天的内容(本质是string),还会显示昵称(本质是string),头像(本质是图片的路径)。这3个数据进行网络传输的时候,不是分开传输的。 而是将三个短的字符串合并成一个长的字符串进行网络传输。该过程称为序列化。 接收方接收到数据后将长字符串拆解成短字符串再使用。该过程称为反序列化。

好处:

  • 保证报文的完整性。
  • 方便网络传输。

1.2 理解协议

在这里插入图片描述 所谓的协议定制,本质其实就是在定制双方都能认识的,符合通信和业务需要的结构化数据(结构化数据就是struct,class)。

Q:网络传输数据的时候能不能不转成长字符串,直接把结构体传输过去也就是直接发送二进制对象? A:可以但是不建议。 理由一:客户端和服务器双方的OS不一定相同,不相同就会导致结构体内存对齐问题不一样。导致无法正确解析传来的二进制。 理由二:客户端和服务器双方使用的语言不一定相同,结构体的定义也就不相同了。

结论:如果要进行网络协议式通信,在应用层,建议使用序列化和反序列化方案,至于直接传输结构体方案,除非特殊场景,否则不建议。

1.3 重新理解read,write为什么支持全双工

在这里插入图片描述

Q:write作用是发送数据,write是不是直接把数据发送到网络中? A:不是的,write没有直接控制硬件的权限,而是将数据拷贝到操作系统的发送缓冲区中。 所以write本质上其实是一个拷贝函数。至于什么时候发,发多少,出错了怎么办?这些都由操作系统中的TCP传输协议控制,也就是由操作系统内核控制。

操作系统把数据发送网络中,本质上也是拷贝。所以在计算机世界里:通信即拷贝。

read函数也不是从网络中直接读数据,而是等操作系统将数据拷贝到接受缓冲区,然后再去看接收缓冲区有没有数据,如果有数据就将数据再拷贝到用户缓冲区。

结论:

  • read/write 本质是进行拷贝。
  • 主机间的通信本质:把发送方的发送缓冲区内部的数据拷贝到对端的接收缓冲区。
  • TCP通信是全双工的原因是:有两对接收和发送的缓冲区。
  • 2. 网络版计算器

    思路:实现网络版计算器,支持客户远端向服务器发送数据,服务器执行计算任务,返回计算结果给客户端。

    2.1 demo v1 (socket封装:引入模板方法模式)

    2.1.1 源码

    socket.hpp

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

    namespace SocketModule
    {
    using namespace LogModule;
    const static int gbacklog = 16;
    // 基类套接字
    // 模板方法模式:大部分方法为纯虚方法
    class Socket
    {
    public:
    virtual ~Socket() {};
    virtual void SocketOrDie() = 0;
    virtual void BindOrDie(uint16_t port) = 0;
    virtual void ListenOrDie(int backlog) = 0;
    virtual bool Accept() = 0;

    public:
    void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
    {
    SocketOrDie();
    BindOrDie(port);
    ListenOrDie(backlog);
    }

    // void BuildUdpSocketMethod()
    // {
    // SocketOrDie();
    // BindOrDie();
    // }
    };

    class TcpSocket : public Socket
    {
    public:
    ~TcpSocket() {};
    void SocketOrDie() override
    {
    _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (_sockfd < 0)
    {
    LOG(LogLevel ::FATAL) << "socket error";
    exit(SOCKET_ERR);
    }
    LOG(LogLevel ::INFO) << "socket success";
    }

    void BindOrDie(uint16_t port) override
    {
    InetAddr localadd(port);
    int n = ::bind(_sockfd, localadd.NetAddrPtr(), localadd.NetAddrLen());
    if (n < 0)
    {
    LOG(LogLevel ::FATAL) << "bind error";
    exit(BIND_ERR);
    }
    LOG(LogLevel ::INFO) << "bind success";
    }

    void ListenOrDie(int backlog) override
    {
    int n = ::listen(_sockfd, backlog);
    if (n < 0)
    {
    LOG(LogLevel ::FATAL) << "listen error";
    exit(LISTEN_ERR);
    }
    LOG(LogLevel ::INFO) << "listen success";
    }

    bool Accept() override
    {
    return true;
    }

    private:
    int _sockfd; // 有可能是listen套接字也有可能是普通的socket
    };

    // class UdpSocket : public Socket
    // {
    // public:
    // ~UdpSocket() {};
    // int SocketOrDie() override
    // {
    // return 0;
    // }
    // bool BindOrDie() override
    // {
    // return true;
    // }
    // bool ListenOrDie() override
    // {
    // return true;
    // }
    // bool Accept() override
    // {
    // return true;
    // }

    // private:
    // };

    };

    tcpserver.hpp

    #include "Socket.hpp"
    #include <iostream>
    #include <memory>

    using namespace SocketModule;
    using namespace LogModule;

    class TcpServer
    {
    public:
    TcpServer(uint16_t port) : _port(port),
    _listensockptr(std::make_unique<TcpSocket>())
    {
    _listensockptr->BuildTcpSocketMethod(_port);
    }

    void Start()
    {
    _isrunning = true;
    while (_isrunning)
    {
    sleep(1);
    _listensockptr->Accept();
    LOG(LogLevel ::DEBUG) << "server is running…";
    }
    _isrunning = false;
    }
    ~TcpServer()
    {
    }

    private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensockptr;
    bool _isrunning;
    };

    main.cc

    #include "TcpServer.hpp"
    #include <memory>

    void Usage(std::string proc)
    {
    std::cerr << "Usage: " << proc << "port" << std::endl;
    }

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

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]));
    tsvr->Start();

    return 0;
    }

    2.1.2 流程详解

    在这里插入图片描述 从main函数看起。

  • 格式化调用方法。
  • 创建智能指针对象tsvr,指向服务器对象。传参:端口号。
  • 调用服务器方法Start。
  • 在这里插入图片描述 先看一眼服务器的成员变量:

    • 端口号
    • 监听套接字指针
    • 运行状态

    在这里插入图片描述

    再看到服务器的构造函数, main函数创建TcpServer对象时,会先创建一个基类对象(_listensockptr),然后调用TcpServer构造方法的初始化列表时候,再去创建虚基类指向的子类对象(TcpSocket)。 这时候,std::make_unique() 做的是在堆上实例化 TcpSocket 类对象,分配内存、初始化类成员变量 _sockfd,这是C++ 层面的对象构造,不会调用任何系统套接字接口,也不会生成操作系统的文件描述符。

    当执行构造方法BuildTcpSocketMethod时:SocketOrDie() 内部执行了系统调用 ::socket(),这是操作系统内核层面创建网络套接字,返回文件描述符 _sockfd,这是真正用于网络通信的内核资源。

    这里的socket已经封装,我们先看一下封装的思路。 在这里插入图片描述 这里采用的就是模板方法模式。 因为UDP/TCP都需要创建套接字,所以将套接字抽象出来做成模板化,一样的方法直接做成虚方法,放在具体子类中去实现。

    二者都需要创建套接字,所以直接放在公共部分实现BuildTcpSocketMethod,里面只需要填各自的逻辑就行。 我们这里只讨论TCP协议。

    再回到TcpServer的构造方法,初始化列表完成后,就调用构造方法BuildTcpSocketMethod。完成了创建,绑定,监听。

    在这里插入图片描述 再回到main函数,创建好TcpServer对象后,调用TcpServer的Start方法。

    在这里插入图片描述 修改运行状态,从监听队列中取出一个已完成连接的客户端,执行相关业务,这里就是打印了一句日志用作测试。 业务完毕后修改服务器状态。

    2.1.3 运行结果

    在这里插入图片描述

    2.2 demo v2 (自定义协议)

    2.2.1 源码

    main.cc

    #include "Protocol.hpp"
    #include "TcpServer.hpp"
    #include <memory>

    void Usage(std::string proc)
    {
    std::cerr << "Usage: " << proc << "port" << std::endl;
    }

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

    std::unique_ptr<Protocol> protocal = std::make_unique<Protocol>();
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]),
    [&protocal](std::shared_ptr<Socket> &sock, InetAddr &client)
    {
    protocal->GetRequest(sock, client);
    });
    tsvr->Start();

    return 0;
    }

    server.hpp

    #include "Socket.hpp"
    #include <iostream>
    #include <memory>
    #include <sys/wait.h>
    #include <functional>

    using namespace SocketModule;
    using namespace LogModule;

    using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;

    class TcpServer
    {
    public:
    TcpServer(uint16_t port, ioservice_t service) : _port(port),
    _listensockptr(std::make_unique<TcpSocket>()),
    _isrunning(false),
    _service(service)
    {
    _listensockptr->BuildTcpSocketMethod(_port);
    }

    void Start()
    {
    _isrunning = true;
    while (_isrunning)
    {
    InetAddr client;
    auto sock = _listensockptr->Accept(&client);
    if (sock == nullptr)
    {
    continue;
    }
    LOG(LogLevel ::DEBUG) << "accept success…";

    pid_t id = fork();
    if (id < 0)
    {
    LOG(LogLevel::FATAL) << "fock error";
    exit(FORK_ERR);
    }
    else if (id == 0)
    {
    _listensockptr->Close();
    if (fork() > 0)
    exit(0);
    _service(sock, client);
    exit(OK);
    }
    else
    {
    sock->Close();
    pid_t rid = ::waitpid(id, nullptr, 0);
    (void)rid;
    }
    }
    _isrunning = false;
    }
    ~TcpServer()
    {
    }

    private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensockptr;
    bool _isrunning;
    ioservice_t _service;
    };

    socket.hpp

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

    namespace SocketModule
    {
    using namespace LogModule;
    const static int gbacklog = 16;
    // 基类套接字
    // 模板方法模式:大部分方法为纯虚方法
    class Socket
    {
    public:
    virtual ~Socket() {};
    virtual void SocketOrDie() = 0;
    virtual void BindOrDie(uint16_t port) = 0;
    virtual void ListenOrDie(int backlog) = 0;
    virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
    virtual void Close() = 0;

    public:
    void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
    {
    SocketOrDie();
    BindOrDie(port);
    ListenOrDie(backlog);
    }
    };

    const static int defaultfd = 1;
    class TcpSocket : public Socket
    {
    public:
    TcpSocket() : _sockfd(defaultfd)
    {
    }

    TcpSocket(int fd) : _sockfd(fd)
    {
    }

    ~TcpSocket()
    {
    }

    void SocketOrDie() override
    {
    _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (_sockfd < 0)
    {
    LOG(LogLevel ::FATAL) << "socket error";
    exit(SOCKET_ERR);
    }
    LOG(LogLevel ::INFO) << "socket success";
    }

    void BindOrDie(uint16_t port) override
    {
    InetAddr localadd(port);
    int n = ::bind(_sockfd, localadd.NetAddrPtr(), localadd.NetAddrLen());
    if (n < 0)
    {
    LOG(LogLevel ::FATAL) << "bind error";
    exit(BIND_ERR);
    }
    LOG(LogLevel ::INFO) << "bind success";
    }

    void ListenOrDie(int backlog) override
    {
    int n = ::listen(_sockfd, backlog);
    if (n < 0)
    {
    LOG(LogLevel ::FATAL) << "listen error";
    exit(LISTEN_ERR);
    }
    LOG(LogLevel ::INFO) << "listen success";
    }

    std::shared_ptr<Socket> Accept(InetAddr *client) override
    {
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int fd = ::accept(_sockfd, CONV(peer), &len);
    if (fd < 0)
    {
    LOG(LogLevel ::WARNING) << "accept warning …";
    return nullptr;
    }
    client->SetAddr(peer);
    return std::make_shared<TcpSocket>(fd);
    }

    void Close()
    {
    if (_sockfd >= 0)
    ::close(_sockfd);
    }

    private:
    int _sockfd; // 有可能是listen套接字也有可能是普通的socket
    };
    };

    protocol.hpp

    #pragma once
    #include <string>
    #include <iostream>
    #include <memory>
    #include "Socket.hpp"

    using namespace SocketModule;

    // 约定好各个字段的含义,本质上就是约定好协议。
    // client -> server
    class Request
    {
    public:
    Request()
    {}

    Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper)
    {}

    std::string Serialize()
    {}

    bool Deserialize(std::string &in)
    {}

    ~Request()
    {}

    private:
    int _x;
    int _y;
    char _oper;
    };

    // server -> client
    class Response
    {
    public:
    Response()
    {}

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

    std::string Serialize()
    {}

    bool Deserialize(std::string &in)
    {}

    ~Response()
    {}

    private:
    int _result;
    int _code; // 表示运算结果的退出码
    };

    // 协议(基于TCP)需要解决的两个问题:
    // 1. request和response 必须得有序列化和反序列化功能
    // 2. 读取的时候要读取到完整的请求

    class Protocol
    {
    public:
    Protocol()
    {}

    void GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client)
    {}

    ~Protocol()
    {}

    private:
    // Request _req;
    // Response _resp;
    };

    2.2.2 流程详解

    这里再捋一遍思路,先从main函数看起 在这里插入图片描述

    主要是两个智能指针的。

    先看到18行,使用智能指针创建了协议对象,这里实现了自定义协议的封装。我们转到protocol 在这里插入图片描述


    在这里插入图片描述


    在这里插入图片描述 协议中封装了三个类:请求服务类,响应类,和协议类。

    协议类作用:对前两个做封装,使面向对象化。

    请求类中的成员变量就是运算数和运算符,响应类中的成员变量是结果和退出码。

    重要的是二者要包括序列化和反序列化的成员函数。 以request类来说: 他的序列化函数的作用是:将请求中的两个操作数和一个操作符做强转(转成string)后再做字符串的拼接(将三个小的字符串拼接成一个长字符串),再在网络中传输。 反序列化:要将服务器传来的长字符串做分割,将字符串转化为结构化数据。

    这里的序列化和反序列化可以自己实现(造轮子)也可以使用开源库Json。具体实现代码我们放在最终版demo中展示。

    点我了解Json ^ _ ^


    再回到我们的流程,创建好协议对象后,我们现在要关心的是如何让服务器和协议相勾连起来。 这里的方法是使用lambda表达式。 在这里插入图片描述 这段代码的核心是:

  • 通过 std::make_unique 创建一个 TcpServer 类型的 std::unique_ptr 智能指针(独占所有权);
  • 初始化 TcpServer 时,传入两个参数:
    • 第一个参数:将命令行参数 argv[1] 转为整数(服务器监听的端口号);
    • 第二个参数:一个 lambda 表达式(作为回调函数),用于处理客户端连接事件(当有客户端连入时,调用 protocal->GetRequest 处理请求)。
  • 第二个构造参数:lambda 表达式

    [&protocal](std::shared_ptr<Socket> &sock, InetAddr &client)
    {
    protocal->GetRequest(sock, client);
    }

    这是代码的核心逻辑,拆解如下:

    部分含义
    [&protocal] 捕获列表:按引用捕获外部变量 protocal,使 lambda 内可以访问/调用 protocal 的成员函数;⚠️ 注意:需保证 protocal 在 lambda 执行时仍有效(比如 protocal 是全局/类成员,或生命周期长于 TcpServer)。
    (std::shared_ptr<Socket> &sock, InetAddr &client) lambda 参数列表:- std::shared_ptr<Socket> &sock:指向客户端套接字的共享智能指针(Socket 是自定义套接字类,封装 fd 等);- InetAddr &client:客户端的地址结构体(封装 IP、端口等);参数用引用(&)避免拷贝,提升效率。
    { protocal->GetRequest(sock, client); } lambda 函数体:当 TcpServer 检测到新客户端连接时,会调用这个 lambda,进而调用 protocal 的 GetRequest 方法处理客户端请求(比如读取数据、解析协议)。

    点我了解Lambda 表达式 ^ _ ^


    总的来讲就是创建了一个TcpServer类对象,第一个参数是端口号,第二个参数是服务器要执行的业务,也就是参数中的Lambda表达式。Lambda表达式 = _service

    这里又使用function包装了一下。 在这里插入图片描述 Lambda表达式的函数体是GetRequest

    在这里插入图片描述 Start函数创建client和sock,传递给Lambda表达式做参数,Lambda表达式又将参数传给GetRequest 在这里插入图片描述 当调用Start时,第47行,就会执行调用匿名函数也就是GetRequest,是一个回调的逻辑。 在这里插入图片描述


    okok,我终于弄通了,现在重新捋一遍: 在这里插入图片描述 本质就是创建一个TcpServer对象,第一个参数端口号,第二个参数:匿名函数。 创建TcpServer对象时,调用TcpServer的构造: 在这里插入图片描述

    走初始化列表:又去创建了一个监听套接字,而这里的监听套接字是一个子类。

    主要是第二个参数:匿名函数。 参数sock和client是在24行Start函数创建的(这里也太阴了…) Q:那不对啊 start函数是在TcpServer对象创建之后才调用的为什么在创建TcpServer的时候就能用start里创建的sock了?

    emm…详细解释如下:

    创建 TcpServer 时并没有“使用” Start() 里的 sock,只是“约定了如何使用”,真正的使用发生在 Start() 调用之后。

    创建 TcpServer 时传入的 lambda,本质是“预约一个规则”:

    • 此时 lambda 只是一个“待执行的逻辑模板”,并没有真正执行,也没有访问任何 sock;
    • 只有当你调用 tsvr->Start() 后,Start() 循环里创建了 sock,才会触发 lambda 执行,这时才真正把 sock 传给 lambda 使用。

    我把 main 函数和 TcpServer 的执行步骤按时间顺序列出来,

    执行步骤代码行为关键说明
    1 std::unique_ptr<Protocol> protocal = … 创建 protocal 对象(准备好业务逻辑)
    2 std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(…) 执行 TcpServer 构造函数:1. 保存端口号 _port;2. 创建监听套接字 _listensockptr 并绑定端口;3. 把 lambda 保存为成员变量 _service(仅保存,不执行);⚠️ 此时 lambda 里的 protocal->GetRequest(sock, client) 一行代码都没执行!
    3 tsvr->Start() 调用 Start() 函数,进入循环:1. 调用 Accept() 创建 sock(客户端连接对象);2. fork 子进程;3. 子进程中执行 _service(sock, client) → 触发 lambda 执行;4. 此时才把 Start() 里的 sock 传给 lambda,执行 protocal->GetRequest

    这个过程就像:

  • 你去餐厅吃饭,进门时告诉服务员(创建 TcpServer):“等下上菜时,把菜(sock)端给我,我要按自己的方式吃(protocal->GetRequest)”——这是预约规则,此时菜还没做,你只是说了“怎么吃”,并没有真的吃;
  • 等厨师做好菜(Start() 里创建 sock),服务员才把菜端给你(_service(sock, client)),你才按之前说的方式吃(执行 lambda)。
  • 创建 TcpServer 时的 lambda 就是“你告诉服务员的吃法”,只是一个“约定”,不是“执行”;Start() 就是“厨师做菜+服务员端菜”,是真正触发约定执行的环节。

    总结

  • 创建 TcpServer 时,lambda 仅被保存为 _service,函数体(protocal->GetRequest)完全没执行,因此不需要 sock 存在;
  • 只有调用 tsvr->Start() 后,Start() 里创建了 sock,才会调用 _service(sock, client),此时 lambda 才执行,sock 才有具体指向;
  • 核心逻辑:lambda 是“待执行的逻辑”,不是“立即执行的代码”,参数要等 lambda 被调用时才传入。
  • 之前的困惑是误以为“创建 TcpServer 时就执行了 lambda,但实际上 lambda 是“延后执行”的,这也是回调函数的核心特点——定义逻辑,延后触发。

    这里的回调太妙了,传一个lambda表达式,相当于把协议给注册到服务器里面!!! 当要执行服务器业务时,再返回去执行回调函数(协议中)。

    2.3 demo v3 (最终版)

    2.3.1 源码

    点我看源码 ^ _ ^

    main.cc

    #include "Protocol.hpp"
    #include "TcpServer.hpp"
    #include "NetCal.hpp"
    #include "Daemon.hpp"
    #include <memory>

    void Usage(std::string proc)
    {
    std::cerr << "Usage: " << proc << "port" << std::endl;
    }

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

    std::cout << "服务器已经启动,已经是一个守护进程" << std::endl;
    Daemon(0, 0);
    Enable_File_Log_Strategy();

    // 1. 顶层
    std::unique_ptr<Cal> cal = std::make_unique<Cal>();

    // 2. 协议层
    std::unique_ptr<Protocol> protocal = std::make_unique<Protocol>([&cal](Request &req) -> Response
    { return cal->Execute(req); });

    // 3. 服务器层
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]),
    [&protocal](std::shared_ptr<Socket> &sock, InetAddr &client)
    {
    protocal->GetRequest(sock, client);
    });
    tsvr->Start();

    return 0;
    }

    protocol.hpp

    #pragma once
    #include <string>
    #include <iostream>
    #include <memory>
    #include <jsoncpp/json/json.h>
    #include <functional>
    #include "Socket.hpp"

    using namespace SocketModule;

    // 约定好各个字段的含义,本质上就是约定好协议。
    // client -> server
    class Request
    {
    public:
    Request()
    {
    }

    Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper)
    {
    }

    std::string Serialize()
    {
    std::string s;
    Json::Value root;
    root["x"] = _x;
    root["y"] = _y;
    root["oper"] = _oper;

    Json::FastWriter writer;
    s = writer.write(root);
    return s;
    }

    bool Deserialize(std::string &in)
    {
    Json::Value root;
    Json::Reader reader;
    bool ok = reader.parse(in, root);
    if (ok)
    {
    _x = root["x"].asInt();
    _y = root["y"].asInt();
    _oper = root["oper"].asInt();
    }
    return ok;
    }

    ~Request()
    {
    }
    int X() { return _x; }
    int Y() { return _y; }
    char Oper() { return _oper; }

    private:
    int _x;
    int _y;
    char _oper;
    };

    // server -> client
    class Response
    {
    public:
    Response()
    {
    }

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

    std::string Serialize()
    {
    Json::Value root;
    root["result"] = _result;
    root["code"] = _code;

    Json::FastWriter writer;
    return writer.write(root);
    }

    bool Deserialize(std::string &in)
    {
    Json::Value root;
    Json::Reader reader;
    bool ok = reader.parse(in, root);
    if (ok)
    {
    _result = root["result"].asInt();
    _code = root["code"].asInt();
    }
    return ok;
    }

    void SetResult(int res)
    {
    _result = res;
    }

    void SetCode(int code)
    {
    _code = code;
    }

    void ShowResult()
    {
    std::cout << "计算结果是:" << _result << "[" << _code << "]" << std::endl;
    }

    ~Response()
    {
    }

    private:
    int _result;
    int _code; // 表示运算结果的退出码
    };

    // 协议(基于TCP)需要解决的两个问题:
    // 1. request和response 必须得有序列化和反序列化功能
    // 2. 读取的时候要读取到完整的请求

    const std::string sep = "\\r\\n";
    using func_t = std::function<Response(Request &req)>;

    class Protocol
    {
    public:
    Protocol()
    {
    }

    Protocol(func_t func) : _func(func)
    {
    }

    // 应用层封装报文(添加报头)
    std::string Encode(const std::string &jsonstr)
    {
    std::string len = std::to_string(jsonstr.size());
    return len + sep + jsonstr + sep;
    }

    // 1. 判读报文完整性
    // 2. 如果包含至少一个完整请求,提取他,并从接收缓冲区中移除
    bool DeCode(std::string &buffer, std::string *package)
    {
    ssize_t pos = buffer.find(sep);
    if (pos == std::string::npos)
    return false;

    std::string package_len_str = buffer.substr(0, pos);
    int package_len_int = std::stoi(package_len_str);
    int target_len = package_len_str.size() + package_len_int + 2 * sep.size();
    if (buffer.size() < target_len)
    return false;

    *package = buffer.substr(pos + sep.size(), package_len_int);
    buffer.erase(0, target_len);

    return true;
    }

    void GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client)
    {
    std::string buffer_queue;
    while (true)
    {
    int n = sock->Recv(&buffer_queue);
    if (n > 0)
    {
    std::cout << "———-request_buffer————" << std::endl;
    std::cout << buffer_queue << std::endl;
    std::cout << "————————————" << std::endl;

    // 1. 拿完整报文
    std::string json_package;

    // 2. 做反序列化
    while (DeCode(buffer_queue, &json_package))
    {
    std::cout << "———–request_json————-" << std::endl;
    std::cout << json_package << std::endl;
    std::cout << "————————————" << std::endl;

    std::cout << "———–request_buffer———–" << std::endl;
    std::cout << buffer_queue << std::endl;
    std::cout << "————————————" << std::endl;
    Request req;
    bool ok = req.Deserialize(json_package);
    if (!ok)
    continue;
    // 3. 完成计算功能(通过req得到resp)
    Response resp = _func(req);

    // 4. 序列化
    std::string json_str = resp.Serialize();

    // 5. 加报头
    std::string send_str = Encode(json_str);

    // 6. 发送
    sock->Send(send_str);
    }
    }
    else if (n == 0)
    {
    LOG(LogLevel::INFO) << "client:" << client.StringAddr() << "Quit!";
    break;
    }
    else
    {
    LOG(LogLevel::WARNING) << "client:" << client.StringAddr() << ", recv error";
    break;
    }
    }
    sock->Close();
    }

    bool GetResponse(std::shared_ptr<Socket> &client, std::string &resp_buff, Response *resp)
    {
    std::string resp_str;
    while (true)
    {
    int n = client->Recv(&resp_str);
    if (n > 0)
    {
    std::cout << "————resp_buffer————-" << std::endl;
    std::cout << resp_buff << std::endl;
    std::cout << "————————————" << std::endl;

    // 1. 拿完整报文
    std::string json_package;
    while (DeCode(resp_str, &json_package))
    {

    std::cout << "———–response json————" << std::endl;
    std::cout << json_package << std::endl;
    std::cout << "————————————" << std::endl;

    std::cout << "———–response json————" << std::endl;
    std::cout << resp_buff << std::endl;
    std::cout << "————————————" << std::endl;

    // 2. 做反序列化
    resp->Deserialize(json_package);
    }
    return true;
    }
    else if (n == 0)
    {
    std::cout << "server quit " << std::endl;
    return false;
    }
    else
    {
    std::cout << "recv error" << std::endl;
    return false;
    }
    }
    }

    std::string BuildRequestString(int x, int y, char oper)
    {
    // 1. 构建请求
    Request req(x, y, oper);

    // 2. 序列化
    std::string josn_req = req.Serialize();
    // 2.1 debug
    std::cout << "———-json_req string———–" << std::endl;
    std::cout << josn_req << std::endl;
    std::cout << "————————————" << std::endl;

    // 3. 加报头
    return Encode(josn_req);
    }

    ~Protocol()
    {
    }

    private:
    // Request _req;
    // Response _resp;
    func_t _func;
    };

    tcserver.hpp

    #include "Socket.hpp"
    #include <iostream>
    #include <memory>
    #include <sys/wait.h>
    #include <functional>

    using namespace SocketModule;
    using namespace LogModule;

    using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;

    class TcpServer
    {
    public:
    TcpServer(uint16_t port, ioservice_t service) : _port(port),
    _listensockptr(std::make_unique<TcpSocket>()),
    _isrunning(false),
    _service(service)
    {
    _listensockptr->BuildTcpSocketMethod(_port);
    }

    void Start()
    {
    _isrunning = true;
    while (_isrunning)
    {
    InetAddr client;
    auto sock = _listensockptr->Accept(&client);
    if (sock == nullptr)
    {
    continue;
    }
    LOG(LogLevel ::DEBUG) << "accept success…" << client.StringAddr();

    pid_t id = fork();
    if (id < 0)
    {
    LOG(LogLevel::FATAL) << "fork error";
    exit(FORK_ERR);
    }
    else if (id == 0)
    {
    _listensockptr->Close();
    if (fork() > 0)
    exit(0);
    _service(sock, client);
    exit(OK);
    }
    else
    {
    sock->Close();
    pid_t rid = ::waitpid(id, nullptr, 0);
    (void)rid;
    }
    }
    _isrunning = false;
    }
    ~TcpServer()
    {
    }

    private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensockptr;
    bool _isrunning;
    ioservice_t _service;
    };

    tcpclient.cc

    #include "Socket.hpp"
    #include "Common.hpp"
    #include "Protocol.hpp"
    #include <iostream>
    #include <string>
    #include <memory>

    using namespace SocketModule;

    void Usage(std::string proc)
    {
    std::cerr << "Usage: " << proc << " server_ip server_port" << std::endl;
    }

    void GetDataFromStdin(int *x, int *y, char *oper)
    {
    std::cout << "Please Enter x: ";
    std::cin >> *x;
    std::cout << "Please Enter y: ";
    std::cin >> *y;
    std::cout << "Please Enter oper: ";
    std::cin >> *oper;
    }

    // ./tcpclient server_ip server_port
    int main(int argc, char *argv[])
    {
    if (argc != 3)
    {
    Usage(argv[0]);
    exit(USAGE_ERR);
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    std::shared_ptr<Socket> client = std::make_shared<TcpSocket>();
    client->BuildTcpClientSocketMethod();

    if (client->Connect(server_ip, server_port) != 0)
    {
    std::cerr << "connect errot" << std::endl;
    exit(CONNECT_ERR);
    }

    std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>();
    std::string resp_buffer;

    // 成功连接服务器
    while (true)
    {
    int x, y;
    char oper;

    // 1. 获取数据
    GetDataFromStdin(&x, &y, &oper);

    // 2. 构建请求字符串
    std::string req_str = protocol->BuildRequestString(x, y, oper);
    std::cout << "———–encode string————" << std::endl;
    std::cout << req_str << std::endl;
    std::cout << "————————————" << std::endl;

    // 3. 发送请求
    client->Send(req_str);

    // 4. 获取应答
    Response resp;
    bool res = protocol->GetResponse(client, resp_buffer, &resp);
    if (res == false)
    break;

    // 5. 显示结果
    resp.ShowResult();
    }
    client->Close();
    return 0;
    }

    socket.hpp

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

    namespace SocketModule
    {
    using namespace LogModule;
    const static int gbacklog = 16;
    // 基类套接字
    // 模板方法模式:大部分方法为纯虚方法
    class Socket
    {
    public:
    virtual ~Socket() {};
    virtual void SocketOrDie() = 0;
    virtual void BindOrDie(uint16_t port) = 0;
    virtual void ListenOrDie(int backlog) = 0;
    virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
    virtual void Close() = 0;
    virtual int Recv(std::string *out) = 0;
    virtual int Send(const std::string &message) = 0;
    virtual int Connect(const std::string &server_ip, uint16_t port) = 0;

    public:
    void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
    {
    SocketOrDie();
    BindOrDie(port);
    ListenOrDie(backlog);
    }

    void BuildTcpClientSocketMethod()
    {
    SocketOrDie();
    }
    };

    const static int defaultfd = 1;
    class TcpSocket : public Socket
    {
    public:
    TcpSocket() : _sockfd(defaultfd)
    {
    }

    TcpSocket(int fd) : _sockfd(fd)
    {
    }

    ~TcpSocket()
    {
    }

    void SocketOrDie() override
    {
    _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (_sockfd < 0)
    {
    LOG(LogLevel ::FATAL) << "socket error";
    exit(SOCKET_ERR);
    }
    LOG(LogLevel ::INFO) << "socket success";
    }

    void BindOrDie(uint16_t port) override
    {
    InetAddr localadd(port);
    int n = ::bind(_sockfd, localadd.NetAddrPtr(), localadd.NetAddrLen());
    if (n < 0)
    {
    LOG(LogLevel ::FATAL) << "bind error";
    exit(BIND_ERR);
    }
    LOG(LogLevel ::INFO) << "bind success";
    }

    void ListenOrDie(int backlog) override
    {
    int n = ::listen(_sockfd, backlog);
    if (n < 0)
    {
    LOG(LogLevel ::FATAL) << "listen error";
    exit(LISTEN_ERR);
    }
    LOG(LogLevel ::INFO) << "listen success";
    }

    std::shared_ptr<Socket> Accept(InetAddr *client) override
    {
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int fd = ::accept(_sockfd, CONV(peer), &len);
    if (fd < 0)
    {
    LOG(LogLevel ::WARNING) << "accept warning …";
    return nullptr;
    }
    client->SetAddr(peer);
    return std::make_shared<TcpSocket>(fd);
    }

    int Recv(std::string *out) override
    {
    char buffer[1024];
    ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) 1, 0); // 文件描述符来源:accept创建的
    if (n > 0)
    {
    buffer[n] = 0;
    *out += buffer;
    }
    return n;
    }

    int Send(const std::string &message) override
    {
    return ::send(_sockfd, message.c_str(), message.size(), 0);
    }

    void Close() override
    {
    if (_sockfd >= 0)
    ::close(_sockfd);
    }

    int Connect(const std::string &server_ip, uint16_t port) override
    {
    InetAddr server(server_ip, port);
    return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen());
    }

    private:
    int _sockfd; // 有可能是listen套接字也有可能是普通的socket
    };
    };

    2.3.2 流程详解

    emm…终于到最后一遍了。

    宏观上来看,最终版的demo中已经实现了分层设计。不着急我们先从看一下流程。

    服务端: 在这里插入图片描述

  • 格式化调用服务端。
  • 守护进程化。
  • 打开日志(向文件中写)。
  • 创建计算模块的对象。
  • 创建协议对象。
  • 创建服务器。
  • 调用服务器。
  • 客户端: 在这里插入图片描述

  • 格式化调用客户端。
  • 创建客户端套接字。
  • 建立连接。
  • 创建协议对象。
  • 创建应答缓冲区。
  • 从键盘获取要计算的参数。
  • 构建请求字符串。
  • 发送请求。
  • 创建应答对象。
  • 获取应答。
  • 显示结果。
  • 关闭客户端。

  • 对于服务器来说: 创建计算对象,里面就是一个计算逻辑,没啥可讲的。

    在这里插入图片描述 创建协议的时候,要绑定匿名函数,指定服务器要执行的逻辑。 捕捉计算对象,传入请求参数,返回计算结果(相应对象)。 对于请求对象:主要实现将请求进行序列化和反序列化。这里就用了Json。 在这里插入图片描述

    创建服务器。完善了获取请求的逻辑。 在这里插入图片描述 首先创建一个请求缓冲区,调用服务器套接字封装的接收请求的方法,将请求放入请求缓冲区中。 在这里插入图片描述 recv函数参数意义:从哪获取,获取到哪,获取的大小,接收标志位(默认为0就行)。

    序列化与反序列化遇到的问题 因为TCP是面向字节流的,当读取方在读取的时候可能读到一个完整的Json请求,也可能会读到半个。 read本身并不保证读取到的报文的完整性,read只能保证把数据读上来。 由应用层的程序员保证读取数据的完整性。也就是要完善协议(添加报头)。 当我们tcp中读取数据的时候,读取到的报文不完整,或者读多了,导致下一个报文不完整,这样的问题叫做“粘报”问题。

    这里获取的请求可能是很多条,或者半条…反正先都放在缓冲区中。通过out带出。回到GetRequest,就是放在了buffer_queue中。

    创建一个json_package串,将来放一条完整的报文信息。 在这里插入图片描述 先做一个Decode,作用是:获取一条问政的请求,并且去除报文头。通过package带出。也就是json_package。 在这里插入图片描述 先对照的看一眼封装报文头的逻辑: 在这里插入图片描述 每个报文由,请求长度+标志位+请求体本身+标志位组成。

    在这里插入图片描述

    成功获取到一条报文信息后,做反序列化,再调用回调函数。服务器的协议层已经绑定。 以加法为例: 在这里插入图片描述 传入一条请求,实例化一个相应。匹配‘+’,设置结果和返回码。

    在这里插入图片描述

    然后再对计算好的相应序列化,加报头,最后发送。这里都是封装好的,体现面向对象。

    以上就是服务器的执行逻辑。客户端不再赘述。 有任何疑问欢迎给作者留言。: )


    赞(0)
    未经允许不得转载:171主机测评 » 自定义协议:实现网络计算器
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址