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

文章目录

  • [1. 一些理解](#1. 一些理解)
    • [1.1 理解序列化与反序列化](#1.1 理解序列化与反序列化)
    • [1.2 理解协议](#1.2 理解协议)
    • [1.3 重新理解read,write为什么支持全双工](#1.3 重新理解read,write为什么支持全双工)
  • [2. 网络版计算器](#2. 网络版计算器)
    • [2.1 demo v1 (socket封装:引入模板方法模式)](#2.1 demo v1 (socket封装:引入模板方法模式))
      • [2.1.1 源码](#2.1.1 源码)
      • [2.1.2 流程详解](#2.1.2 流程详解)
      • [2.1.3 运行结果](#2.1.3 运行结果)
    • [2.2 demo v2 (自定义协议)](#2.2 demo v2 (自定义协议))
      • [2.2.1 源码](#2.2.1 源码)
      • [2.2.2 流程详解](#2.2.2 流程详解)
    • [2.3 demo v3 (最终版)](#2.3 demo v3 (最终版))
      • [2.3.1 源码](#2.3.1 源码)
      • [2.3.2 流程详解](#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函数也不是从网络中直接读数据,而是等操作系统将数据拷贝到接受缓冲区,然后再去看接收缓冲区有没有数据,如果有数据就将数据再拷贝到用户缓冲区。

结论:

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

2. 网络版计算器

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

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

2.1.1 源码

socket.hpp

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

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

cpp 复制代码
#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函数看起。

  1. 格式化调用方法。
  2. 创建智能指针对象tsvr,指向服务器对象。传参:端口号。
  3. 调用服务器方法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

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

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

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

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

这段代码的核心是:

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

第二个构造参数:lambda 表达式

cpp 复制代码
[&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,进而调用 protocalGetRequest 方法处理客户端请求(比如读取数据、解析协议)。

点我了解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

这个过程就像:

  1. 你去餐厅吃饭,进门时告诉服务员(创建 TcpServer):"等下上菜时,把菜(sock)端给我,我要按自己的方式吃(protocal->GetRequest)"------这是预约规则,此时菜还没做,你只是说了"怎么吃",并没有真的吃;
  2. 等厨师做好菜(Start() 里创建 sock),服务员才把菜端给你(_service(sock, client)),你才按之前说的方式吃(执行 lambda)。

创建 TcpServer 时的 lambda 就是"你告诉服务员的吃法",只是一个"约定",不是"执行";Start() 就是"厨师做菜+服务员端菜",是真正触发约定执行的环节。

总结

  1. 创建 TcpServer 时,lambda 仅被保存_service,函数体(protocal->GetRequest)完全没执行,因此不需要 sock 存在;
  2. 只有调用 tsvr->Start() 后,Start() 里创建了 sock,才会调用 _service(sock, client),此时 lambda 才执行,sock 才有具体指向;
  3. 核心逻辑:lambda 是"待执行的逻辑",不是"立即执行的代码",参数要等 lambda 被调用时才传入。

之前的困惑是误以为"创建 TcpServer 时就执行了 lambda,但实际上 lambda 是"延后执行"的,这也是回调函数的核心特点------定义逻辑,延后触发

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

2.3 demo v3 (最终版)

2.3.1 源码

点我看源码 ^ _ ^

main.cc

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

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

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

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

cpp 复制代码
#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中已经实现了分层设计。不着急我们先从看一下流程。

服务端:

  1. 格式化调用服务端。
  2. 守护进程化。
  3. 打开日志(向文件中写)。
  4. 创建计算模块的对象。
  5. 创建协议对象。
  6. 创建服务器。
  7. 调用服务器。

客户端:

  1. 格式化调用客户端。
  2. 创建客户端套接字。
  3. 建立连接。
  4. 创建协议对象。
  5. 创建应答缓冲区。
  6. 从键盘获取要计算的参数。
  7. 构建请求字符串。
  8. 发送请求。
  9. 创建应答对象。
  10. 获取应答。
  11. 显示结果。
  12. 关闭客户端。

对于服务器来说:

创建计算对象,里面就是一个计算逻辑,没啥可讲的。

创建协议的时候,要绑定匿名函数,指定服务器要执行的逻辑。

捕捉计算对象,传入请求参数,返回计算结果(相应对象)。

对于请求对象:主要实现将请求进行序列化和反序列化。这里就用了Json。

创建服务器。完善了获取请求的逻辑。

首先创建一个请求缓冲区,调用服务器套接字封装的接收请求的方法,将请求放入请求缓冲区中。

recv函数参数意义:从哪获取,获取到哪,获取的大小,接收标志位(默认为0就行)。

序列化与反序列化遇到的问题 因为TCP是面向字节流的,当读取方在读取的时候可能读到一个完整的Json请求,也可能会读到半个。

read本身并不保证读取到的报文的完整性,read只能保证把数据读上来。

由应用层的程序员保证读取数据的完整性。也就是要完善协议(添加报头)。

当我们tcp中读取数据的时候,读取到的报文不完整,或者读多了,导致下一个报文不完整,这样的问题叫做"粘报"问题。

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

创建一个json_package串,将来放一条完整的报文信息。

先做一个Decode,作用是:获取一条问政的请求,并且去除报文头。通过package带出。也就是json_package。

先对照的看一眼封装报文头的逻辑:

每个报文由,请求长度+标志位+请求体本身+标志位组成。

成功获取到一条报文信息后,做反序列化,再调用回调函数。服务器的协议层已经绑定。

以加法为例:

传入一条请求,实例化一个相应。匹配'+',设置结果和返回码。

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

以上就是服务器的执行逻辑。客户端不再赘述。

有任何疑问欢迎给作者留言。: )


相关推荐
Rockbean1 天前
用40行代码搭建自己的无服务器OCR
服务器·python·deepseek
茶杯梦轩1 天前
CompletableFuture 在 项目实战 中 创建异步任务 的核心优势及使用场景
服务器·后端·面试
崔小汤呀1 天前
最全的docker安装笔记,包含CentOS和Ubuntu
linux·后端
何中应1 天前
vi编辑器使用
linux·后端·操作系统
何中应1 天前
Linux进程无法被kill
linux·后端·操作系统
何中应1 天前
rm-rf /命令操作介绍
linux·后端·操作系统
何中应1 天前
Linux常用命令
linux·操作系统
葛立国1 天前
从 / 和 /dev 说起:Linux 文件系统与挂载点一文理清
linux
郑州光合科技余经理2 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
海天鹰2 天前
【免费】PHP主机=域名+解析+主机
服务器