应用层自定义协议与序列化

前言

在计算机网络中,数据的传输需要多层协议栈的处理,而程序员主要关注的是应用层,应用层涉及用户需求业务逻辑。因此编写网络程序时,需要设计或者选择合适的协议,以保证客户端和服务端双方通信时,数据的正确和高效传输!其中现成的协议有很多,如:HTTPFTPSMTP等,但是我们今天不使用这些,想自己实现一个简单的应用层协议,一是理解他们的工作流程和共性,二是为后面的标准协议的学习打基础!

🎉 目录

前言

一、序列化和反序列化

1.1 协议的重要性

1.2 序列化和反序列化

1.3 结合OS的理解

二、实现网络版本计算器

2.1 封装 socket

2.2 服务端

获取连接层

数据 IO 层_V1

自定义协议层

应用层

数据 IO 层_V2

2.3 客户端

单进程收发

多线程收发


一、序列化和反序列化

1.1 协议的重要性

在【网络基础入门】就说过,协议 是一种约定,是通信双方都约定好(认识)的结构化字段!当时还举了快递单的例子。如果没有协议,主机的数据发送的再完美,对方可能都不认识你发的啥,就更谈不上处理了!所以协议是双方通信的基石

例如我们要实现一个简单的 【网络版本计算机】,需要客户端把 操作数操作符 发给 服务端,然后服务端计算后把结果返回给客户端。

我们双方约定定制应用层协议):

客户端 发送的数据必须是两个操作数和一个操作符

服务端 发送的必须是两整数(结果和退出码)和一个退出码字符串

struct request
{
    // 客户端 字段
    int x;     // 操作数1
    int y;     // 操作数1
    char oper; // 操作符
};


struct response
{
    // 服务端 字段
    int result;       // 结果
    int code;         // 退出码
    std::string desc; // 退出码描述
};

此时只要双方都遵守这个约定(协议),双方就一定能收到对方的数据!

现在的问题是 双方如何传递这些数据?有两种方案:

方案一:将这些数据拼接成一个字符串发,送给对方字符串

方案二:将发送给的数据定义一个结构体对象,发送对象结构体对象

// 方案一:发送字符串
"xyoper" 

// 方案二:封装成一个结构体发送
struct msg
{
    int x;
    int y;
    char oper;
};

无论是哪一种方案,都是有问题的!前者是有可能当对方收到后无法解析成功、后者是平台问题而导致结构体也无法解析成功。因此,为了让遵守协议的双方正确的获取数据,我们需要对发送的数据进行 序列化和反序列化 的处理

1.2 序列化和反序列化

序列化将一条或者多条需要传递的数据,按照一定的格式,拼接为一条数据

反序列化将收到的一条数据,按照双方约定的格式进行解析

上面的例子我们也知道,双方发送的这些数据都是一个整体,并不是一条条分开发送的,而是当成一个字符串整体发送的,当对方收到这一个整体后将字符串按照约定的格式打散,就可以获得各个数据了!则这个过程就是序列化和反序列化

所以有了序列化和反序列化,之后我们我们对于方案一,可以在每一个数据后面 加一个 空格来分割,解析的时候也按照 空格解析即可,这样双方就拿到一样的数据了。方案二,我们可以不用发送方结构体对象,而是将结构体中的数据按照序列化的格式变成一个字符串,然后将字符串发送过去,因为上层双方用的同一个协议(认识双方的结构体),所以解析后也就知道对方发送的每个字段是啥意思。这里我们采用第二种实现

下面就是大致的网络版本计算器的执行过程:

其实以上的过程可以看成是两层:一层是协议定制层、另一层是序列化和反序列化

所以,为什么需要序列化和反序列化呢?

把需要发送的信息按照特定的格式由多变一,方便网络的发送!

综上,应用层协议序列化和反序列化 是 一起配合使用的

1.3 结合OS的理解

我们前面介绍TCP 套接字编程的时候,简单的介绍过 TCP面向连接的。也就是TCP 通信时服务端,会进行 accpet 成功,返回一个 fd 用于当前客户端的 IO 通信。

也就是说,一个 fd 代表一个连接!而传输层属于OS内核, 在OS内核中会给 accpet 的 每一个fd 创建两个缓冲区,一个发送缓冲区、一个接收缓冲区

我们每次进行通信的时候,发送数据并不是直接发送到网络中的,而是通过 read/write/recv/sendIO 的系统调用,将上层(应用层)的数据拷贝到TCP的发送缓冲区,最后由OS逐层封包和定期向网路中刷新(类似文件)。所以,发送数据的本质是从发送方的发送缓冲区把数据通过协议栈和网络拷贝给接收方的接收缓冲区read/write/recv/send 的 本质是拷贝函数!

TCP支持全双工的本质?

因为在任何一台机器上, TCP 连接既有发送缓冲区,又有接收缓冲区,所以在内核中,再发消息的同时,也可以接收消息,即 全双工

TCP 称为传输控制协议的原因?

上面说了我们双方通信,是将数据由 IO 系统调用函数,拷贝到内核缓冲区,但是现在的问题是,TCP 咋知道 什么时候发?发多少?出错了咋办?这些问题都不是我们该关心的,这些问题都是由 TCP (OS内核)解决的,所以它叫做 传输控制协议

内核缓冲区的本质是一个生产消费者模型

上层通过 IO 的系统调用将数据向缓冲区中放,OS 从缓冲区中拿,进行封包向网络中发送。所以,内核中的缓冲区就是一个典型的 生产消费模型

为何 IO 函数要阻塞?

有了上面的基础,我们就很容易知道:网络通信的本质是两进程在通信,也就是进程在调用 IO 系统调用函数 进行向缓冲区读写!当缓冲区满了/空 的时候,进程就会阻塞!现在的问题就转换成了:为啥缓冲区在空/满的时候 进程不进行数据的 IO 而进行阻塞呢?其实主要原因是维护数据的同步关系!

如何保证在接收缓冲区中拿到的数据是一个完整的报文?

TCP 面向字节流的,也就是在接收缓冲区中的数据,有可能是一次发的,也有可能是多次发的,还有可能接收缓冲区的数据不够一个完整的数据报文!那TCP如何保证呢?TCP保证不了!TCP只负责发送数据到对方的缓冲区,因为是字节流的所以人家不管如何解析报文!所以,解析一个完整报文的工作是有上层(应用层)的协议保证的!

二、实现网络版本计算器

下面我们来实现一个网络版本的计算器,我们打算自定义协议、实现序列化和反序列化、进行报文解析等工作!也就是走一遍协议的整体流程,主要目的是理解他们!

2.1 封装 socket

在学习 线程 的时候,我们先是使用原生的接口,后来是对他们进行面向对象式的封装。这里socket 也是一样的。为了后面可以直接使用起来方便,我们对 socket 进行封装!这里封装 socket 打算采用 模板方法类 的设计模式

什么是模板方法类的设计模式?

模板方法类 设计模式在C++中是一种非常有用的行为型设计模式。该模式会定义 一个抽象类用于所有情况的算法框架(只是声明)对应的实现类继承该抽象类并重写所需的方法来实现子类所需的功能!

下面举一个简单的例子:

// 抽象类 - 游戏
class Game
{
public:
    // 模板方法 - 游戏的主要流程
    void play()
    {
        initialize();
        startPlay();
        endPlay();
    }

    // 具体步骤 - 初始化游戏
    void initialize()
    {
        std::cout << "Game initialized!" << std::endl;
    }

    // 抽象步骤 - 开始游戏
    virtual void startPlay() = 0;

    // 抽象步骤 - 结束游戏
    virtual void endPlay() = 0;
};

// 具体子类 - 足球游戏
class FootballGame : public Game
{
public:
    // 实现具体步骤 - 开始足球游戏
    void startPlay() override
    {
        std::cout << "Football game started!" << std::endl;
    }

    // 实现具体步骤 - 结束足球游戏
    void endPlay() override
    {
        std::cout << "Football game ended!" << std::endl;
    }
};

// 具体子类 - 篮球游戏
class BasketballGame : public Game
{
public:
    // 实现具体步骤 - 开始篮球游戏
    void startPlay() override
    {
        std::cout << "Basketball game started!" << std::endl;
    }

    // 实现具体步骤 - 结束篮球游戏
    void endPlay() override
    {
        std::cout << "Basketball game ended!" << std::endl;
    }
};

int main()
{
    Game *football = new FootballGame();
    Game *basketball = new BasketballGame();

    std::cout << "Playing football game..." << std::endl;
    football->play();

    std::cout << "\nPlaying basketball game..." << std::endl;
    basketball->play();

    delete football;
    delete basketball;

    return 0;
}

OK,我们下面就来封装 Socket 了,我们这里主要实现封装的是 TCP socket

先来实现一个 父类 Socket 定义所有的方法,后面无论是TCP还是UDP只要继承Socket 重写对应的方法即可!

namespace Socket_ns
{
    const static int g_blcklog = 8; // 全连接队列的长度

    class Socket;                             // 前置声明
    using SockSPtr = std::shared_ptr<Socket>; // 定义一个 Socket 类型

    class Socket
    {
    public:
        virtual void CreateSocket() = 0;                                         // 创建套接字
        virtual void CreateBind(uint16_t port) = 0;                              // 绑定
        virtual void CreateListen(int blcklog = g_blcklog) = 0;                  // 监听
        virtual SockSPtr Accepter(Inet_Addr *client) = 0;                        // 连接
        virtual bool Connector(std::string server_ip, uint16_t server_port) = 0; // 获取连接

        virtual int Sockfd() = 0; // 获取 socket
        virtual void Close() = 0; // 关闭 socket

        virtual ssize_t Recv(std::string *out) = 0;      // 发送消息
        virtual ssize_t Send(const std::string &in) = 0; // 接收消息
    public:
        void BuildListenSocket(uint16_t port)// TCP 服务端 创建、绑定、监听
        {
            CreateSocket();
            CreateBind(port);
            CreateListen();
        }

        bool BuildClientSoket(const std::string &server_ip, uint16_t server_port)//  TCP 客户端获取连接
        {
            CreateSocket();
            return Connector(server_ip, server_port);
        }
    };

    class TcpSocket: public Socket
    {

    };

    // class UdpSocket: public Socket
    // {
        
    // };
} // namespace Socket_ns

这里主要是 TCP 所以 UDP 就不再实现了!下面就主要实现 TcpServer

class TcpSocket: public Socket
{
public:
    TcpSocket(int sockfd = -1) : _sockfd(sockfd)
    {
    }
    // 创建套接字
    void CreateSocket() override
    {
    }
    // 绑定
    void CreateBind(uint16_t port) override
    {
    }
    // 监听
    void CreateListen(int blcklog = g_blcklog) override
    {
    }
    // 连接
    SockSPtr Accepter(Inet_Addr *client) override
    {
    }
    // 获取连接
    bool Connector(std::string server_ip, uint16_t server_port) override
    {
    }
    // 获取 socket
    int Sockfd() override
    {
    }
    // 关闭 socket
    void Close() override
    {
    }
    // 发送消息
    ssize_t Recv(std::string *out) override
    {
    }
    // 接收消息
    ssize_t Send(const std::string &in) override
    {
    }

private:
    int _sockfd; // 既可以是 listensockfd 也可以是 IO sockfd
};

这里唯一要理解的是,如何理解这里的 一个 sockfd 可以两用呢?在创建监听套接字时直接用成员属性我可以理解!如何理解 accpet 的 fd 呢?因为我们封装了 SockSPtr 的类型,后面我们在 IO 时可以返回一个 TcpSocket 的对象即可,这样就可以更好的做到面向对象!

下面就时实现这些接口的源码,因为这些代码前面都写过了,所以这里就不再一个个解释了!

class TcpSocket : public Socket
{
public:
    TcpSocket(int sockfd = -1) : _sockfd(sockfd)
    {
    }
    // 创建套接字
    void CreateSocket() override
    {
        _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            LOG(FATAL, "sockfd create error\n");
            exit(SOCKET_ERROR);
        }
        LOG(INFO, "sockfd create success, sockfd is: %d\n", _sockfd);
    }
    // 绑定
    void CreateBind(uint16_t port) override
    {
        struct sockaddr_in local;           // 定义存储ip端口等信息的结构体
        memset(&local, 0, sizeof(0));       // 初始化
        local.sin_family = AF_INET;         // 网络通信
        local.sin_port = htons(port);       // 将端口转为网络序列
        local.sin_addr.s_addr = INADDR_ANY; // 服务端绑定任意 ip

        if (::bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            LOG(FATAL, "bind error\n");
            exit(BIND_ERROR);
        }
        LOG(INFO, "bind success\n");
    }
    // 监听
    void CreateListen(int blcklog = g_blcklog) override
    {
        if (::listen(_sockfd, blcklog) < 0)
        {
            LOG(FATAL, "listen error\n");
        }
        LOG(INFO, "listen success\n");
    }
    // 连接
    SockSPtr Accepter(Inet_Addr *client) override // 这里带上 Inet_Addr 的目的是方便打印
    {
        struct sockaddr_in clientaddr;
        socklen_t len = sizeof(clientaddr);
        int sock = ::accept(_sockfd, (struct sockaddr *)&clientaddr, &len);
        if (sock < 0)
        {
            LOG(FATAL, "accept error");
            return nullptr;
        }
        *client = Inet_Addr(clientaddr);
        LOG(FATAL, "get a new link, client info: %s sockfd is %d\n", client->AddrStr().c_str(), sock);
        return std::make_shared<TcpSocket>(sock); // C++14
    }
    // 获取连接
    bool Connector(std::string server_ip, uint16_t server_port) override
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;                             // 网络通信
        server.sin_port = htons(server_port);                    // 主机端口的序列转为网络序列
        inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr); // 将端口号设置为网络序列

        int n = ::connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
        if (n < 0)
        {
            return false;
        }
        return true;
    }
    // 获取 socket
    int Sockfd() override
    {
        return _sockfd;
    }
    // 关闭 socket
    void Close() override
    {
        if (_sockfd > 0)
            ::close(_sockfd);
    }
    // 发送消息
    ssize_t Recv(std::string *out) override
    {
        char buffer[4096];
        ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = 0;
            *out += buffer;// 注意这里是 +=
        }
        return n;
    }
    // 接收消息
    ssize_t Send(const std::string &in) override
    {
        int n = ::send(_sockfd, in.c_str(), in.size(), 0);
        return n;
    }

private:
    int _sockfd; // 既可以是 listensockfd 也可以是 IO sockfd
};

这样就完成了TcpSocket的封装!下面我们来实现一下 服务端TcpServer.hpp

2.2 服务端

服务端这里我们不再和以前一样,直接所有的任务都在这里处理!我们后面还要加协议等,所以,我们要进行分层处理:

1、在 TcpServer.hpp 中采用多线程的方式进行 获取客户端连接

2、在 Service.hpp 中进行 双方的数据 IO 

3、在 Net_Cal.hpp 中进行 业务处理

4、在 Protocal.hpp 中进行 自定义协议和序列化反序列化

如下图:

获取连接层

OK,先把 TcpServer.hpp 给整出来:

#pragma once

#include <functional>
#include <pthread.h>
#include "Socket.hpp"

using namespace Socket_ns;

using service_t = std::function<void(SockSPtr, Inet_Addr &)>;// 包装的 Service 中的处理 IO 的函数

class TcpServer
{
public:
    TcpServer(service_t service, uint16_t port)
        : _port(port), _isrunning(false), _sockfd(std::make_shared<TcpSocket>()), _service(service)
    {
        _sockfd->BuildListenSocket(_port);
    }
    // 内部类
    class ThreadData
    {
    public:
        SockSPtr _sockfd;
        TcpServer *_self;
        Inet_Addr _addr;

    public:
        ThreadData(SockSPtr sockfd, TcpServer *self, const Inet_Addr &addr)
            : _sockfd(sockfd), _self(self), _addr(addr)
        {
        }
    };

    void Loop()
    {
        _isrunning = true;
        while (_isrunning)
        {
            Inet_Addr client;
            SockSPtr sock = _sockfd->Accepter(&client);
            if (sock == nullptr)
                continue;
            LOG(INFO, "get a new link, client info: %s, sockfd is: %d\n", client.AddrStr().c_str(), sock->Sockfd());
            // 采用多线程
            pthread_t tid;
            ThreadData *td = new ThreadData(sock, this, client);
            pthread_create(&tid, nullptr, Execute, td);
        }
        _isrunning = false;
    }

    static void *Execute(void *args) // 这里是在类的内部,有默认的 this 所以得用static 修饰
    {
        ThreadData *td = static_cast<ThreadData *>(args);
        td->_self->_service(td->_sockfd, td->_addr);
        td->_sockfd->Close();// 回调回来,将 sockfd 关闭
        delete td;
        return nullptr;
    }

private:
    uint16_t _port;     // 端口
    bool _isrunning;    // 运行状态
    SockSPtr _sockfd;   // 父类对象
    service_t _service; // IO 回调的函数
};

这里就更简单了,我们采用多线程连接客户端,并使用 Socket 封装的接口即可!

数据 IO 层_V1

下面我们就来实现以下 IO 的层 即 Service.hpp

#pragma once

#include "Socket.hpp"

using namespace Socket_ns;

class Service
{
public:
    void Service_IO(SockSPtr sockfd, Inet_Addr &addr)
    {
        std::string packagestream;
        while (true)
        {
            // 读取数据
            ssize_t n = sockfd->Recv(&packagestream); // Bug
            if (n <= 0)
            {
                LOG(FATAL, "client %s quit or recv error\n", addr.AddrStr().c_str());
                break;
            }
            
            // 检查是否是完整的报文....
            // 序列化和反序列化 ...


            // 接收成功 发送回去
            n = sockfd->Send(packagestream);
            // ....
        }
    }
};

这就是整体的大框架,由于协议还没有写,所以我们就先暂时这样写!后面有了协议我们加上就好了!下面我们先来写一个简单的 Servermain.cc调用一下,试一下前面的服务端和封装的 Socket 是否正确:

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

// ./calserver local-port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cout << "Usage: " << argv[0] << " local-port" << std::endl;
        exit(1);
    }

    Service svic;
    service_t service_io(std::bind(&Service::Service_IO, &svic, std::placeholders::_1, std::placeholders::_2));

    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<TcpServer> tsvc = std::make_unique<TcpServer>(service_io, port);
    tsvc->Loop();
}

服务端起来了,此时已经可以接收客户端的请求了!OK,没问题!我们下面接着编写 自定义协议与序列化和反序列化层

自定义协议层

协议的本质是双方约定好的结构化字段,也就是结构体/类!所以我们把这些一段定义在一个 Protocal.hpp 中,未来双方各自包含一下,双方就能看到同一份结构体代码了,即达成了约定

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

    ~Request()
    {
    }

    // 序列化
    bool Serialize(std::string *out)
    {
    }
    // 反序列化
    bool Deserialize(const std::string &in)
    {
    }

    void SetValue(int x, int y, char oper)
    {
        _x = x;
        _y = y;
        _oper = oper;
    }

    void SetX(int x)
    {
        _x = x;
    }

    void SetY(int y)
    {
        _y = y;
    }

    void SetOper(char oper)
    {
        _oper = oper;
    }

    int GetX()
    {
        return _x;
    }

    int GetY()
    {
        return _y;
    }

    char GetOper()
    {
        return _oper;
    }

    void Print()
    {
        std::cout << "_x: " << _x << " _y: " << _y << " _oper: " << _oper << std::endl;
    }

private:
    int _x;     // 操作数1
    int _y;     // 操作数2
    char _oper; // 运算符
};


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

    ~Response()
    {
    }

    // 序列化
    bool Serialize(std::string *out)
    {
    }
    // 反序列化
    bool Deserialize(const std::string &in)
    {
    }

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

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

    void SetDesc(const std::string &desc)
    {
        _desc = desc;
    }

    int GetResult()
    {
        return _result;
    }

    int GetCode()
    {
        return _code;
    }

    std::string GetDesc()
    {
        return _desc;
    }

    void Print()
    {
        std::cout << "_result: " << _result << " : _code: " << _code << " : _desc: " << _desc << std::endl;
    }

private:
    int _result;       // 计算结果
    int _code;         // 退出码
    std::string _desc; // 退出码描述
};

这样就做好了约定,即定好了协议!这里我不想把成员的私有属性直接设置成 public 的,所以就和Java一样写了一些 get/set 方法,方便后面的使用~!

下面我们的主要任务就是在,请求 响应 中进行实现序列化和反序列化了!我们可以手写,也可以使用当前常用的序列化反序列化工具:如 jsonxmlprotobuf

我们这里先使用现成的工具,后面我们专门会手写一次!现成的工具这里我们选择的是 Json 原因有两点:1、简单 2、可视化

我们C++这里使用的话需要安装 C++ 的Json库 Jsoncpp

ubuntu 环境

sudo apt-get install libjsoncpp-dev
Centos 环境
sudo yum install jsoncpp-devel

具体的 Jsoncpp 介绍,请参考这篇博文:Jsoncpp的使用介绍  和Jsoncpp的使用

这里就直接使用了!

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

    ~Request()
    {
    }

    // 序列化
    bool Serialize(std::string *out)
    {
        Json::Value root; // 创建一个 万能对象
        root["x"] = _x; // 使用键值对的方式赋值
        root["y"] = _y;
        root["oper"] = _oper;
        Json::FastWriter writer;// 创建一个序列化对象
        std::string s = writer.write(root);// 序列化
        *out = s;// 将序列化结果带出去
        return true;
    }

    // 反序列化
    bool Deserialize(const std::string &in)
    {
        Json::Value root;// 创建一个 万能对象
        Json::Reader reader;// 创建一个反序列化对象
        bool res = reader.parse(in, root);// 反序列化
        if (!res)
            return false;
        _x = root["x"].asInt();// 将反序列化的结果, 给成员属性
        _y = root["y"].asInt();
        _oper = root["oper"].asInt();
        return true;
    }

    // ...

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



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

    ~Response()
    {
    }

     // 序列化
    bool Serialize(std::string *out)
    {
        Json::Value root;// 创建一个 万能对象
        root["result"] = _result;// 使用键值对的方式赋值
        root["code"] = _code;
        root["desc"] = _desc;
        Json::FastWriter writer;// 创建一个序列化对象
        std::string s = writer.write(root);// 序列化
        *out = s;// 将序列化结果带出去
        return true;
    }

    // 反序列化
    bool Deserialize(const std::string &in)
    {
        Json::Value root;// 创建一个 万能对象
        Json::Reader reader;// 创建一个反序列化对象
        bool res = reader.parse(in, root);// 反序列化
        if (!res)
            return false;
        _result = root["result"].asInt();// 将反序列化的结果, 给成员属性
        _code = root["code"].asInt();
        _desc = root["desc"].asString();
        return true;
    }

    // ...

private:
    int _result;       // 计算结果
    int _code;         // 退出码
    std::string _desc; // 退出码描述
};

OK ,此时协议与序列化和反序列化就完成了!剩下的就是 正确的解析报文和正确的封包了!

先说 封包封包很简单,在json串的前面 加上当前json串的长度,在加上特定的格式

这里采用的格式 是 "\r\n", 所以一条信息就是:len +  "\r\n" + jsonstr +  "\r\n"

这里为啥这样设计"\r\n" 的格式?其实为了后面 HTTP 的复用

// 添加报头
std::string EnCode(const std::string& jsonstr)
{
    int len = jsonstr.size();
    std::string lenstr  = std::to_string(len);
    return lenstr + sep + jsonstr + sep;
}

正确的解析一个完整的报文:

因为 封包的格式是 : len +  "\r\n" + jsonstr +  "\r\n"

len :表示的是要发送数据json串的长度,而它是以 "\r\n" 结尾的,所以我们可以通过,"\r\n" 为分割,先拿到len 然后再跳过 "\r\n" ,截取len个字符就是实际的json串,但是还要加上最后一个"\r\n" ,所以一个报文的长度 total 是:记录 len 的字符串的长度 + jasonstr.size() + sep*2!

如果当前的报文小于 total 则证明不够一个报文!否则就是至少一个报文,则提取一个完整的!最后已经获取的 jsonstr 删除即可!

// 获取一个完成的报文
std::string DeCode(std::string& packagestream)
{
    auto pos = packagestream.find(sep);
    if(pos == std::string::npos)// 只有长度,没有报文
        return "";
    // 获取报文中的json串长度
    std::string lenstr = packagestream.substr(0, pos);
    int len = std::stoi(lenstr);
    // 求出一个完整报文的长度
    int total = lenstr.size() + len + 2*sep.size();
    if(packagestream.size() < total) // 当前的数据流中,不够一个报文
        return "";
    // 获取一个完整的报文
    std::string jsonstr = packagestream.substr(pos+sep.size(), len);
    // 删除拿走的报文
    packagestream.erase(0,total);
    return jsonstr;
}

为了后面快速的获取请求和响应的对象,我们这里设计一个简单的 工厂类,采用智能指针实现!

// 构建一个简单的工厂
class Factory
{
public:
    static std::shared_ptr<Request> BuildRequest()
    {
        return std::make_shared<Request>();
    }

    static std::shared_ptr<Response> BuildResponse()
    {
        return std::make_shared<Response>();
    }
};

OK ,到这里协议层以及序列化和反序列化就已经完成了!

应用层

应用层是啥呢?我们今天的业务是 进行简单的网络版本的计算器

我们实现再 Net_Cal.hpp 中,因为这里没啥技术含量,就是简单的 switch-case,就直接写了

#pragma once
#include <iostream>
#include <memory>

#include "Protocal.hpp"

class Net_Cal
{
public:
    std::shared_ptr<Response> Calculator(std::shared_ptr<Request> &req)// 用于Service层回调的
    {
        std::shared_ptr<Response> resp = Factory::BuildResponse();
        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(1);
                    resp->SetDesc("div zero");
                }
                else
                {
                    resp->SetResult(req->GetX() / req->GetY());
                }
            }
            break;
            case '%':
            {
                if (req->GetY() == 0)
                {
                    resp->SetCode(2);
                    resp->SetDesc("mod zero");
                }
                else
                {
                    resp->SetResult(req->GetX() % req->GetY());
                }
            }
            break;
            default :
            {
                resp->SetCode(3);
                resp->SetDesc("illegal operation");
            }
            break;
        }
        return resp;
    }
};

下面我们需要干的是,将完善 数据 IO 层 的代码:

数据 IO 层_V2

这里我们当收到一个数据后不再是,直接拿着去处理任务,然后发送回去了!这里我们先是,得解析是否是一个完整的报文,如果不是就继续读取(这就是我们前面再Recv将 = 换成 += 的原因),否则构建一个请求对象,进行反序列化,然后将反序列化的数据进行业务处理,最后会返回一个响应对象!响应对象对处理的结果进行,序列化+添加报头然后进行发送(响应)给用户!

这里业务处理的函数,实际上就是包装的 应用层的 Calculator

#pragma once
#include <functional>

#include "Protocal.hpp"
#include "Socket.hpp"

using namespace Socket_ns;

// 用于 IO 完成之后去,执行业务的回调函数
using process_t = std::function<std::shared_ptr<Response>(std::shared_ptr<Request> &)>;

class Service
{
public:
    Service(process_t process)
        : _process(process)
    {
    }

    ~Service()
    {
    }

    void Service_IO(SockSPtr sock, Inet_Addr &addr)
    {
        std::string packagestreamqueue;
        while (true)
        {
            // 1、读取消息
            ssize_t n = sock->Recv(&packagestreamqueue);
            if (n <= 0)
            {
                LOG(FATAL, "client %s quit or recv error\n", addr.AddrStr().c_str());
                break;
            }
            // 2、解析有效载荷
            // 此时 recv 接收到的不一定是一个完整的报文
            std::string package = DeCode(packagestreamqueue);
            if (package.empty()) // 如果 jsonstr 是空的 说明当前的数据流不是一个完整的报文
                continue;        // 继续读取
            std::cout << "package =>  " << package << std::endl;
            // 3、反序列化
            auto req = Factory::BuildRequest();
            req->Deserialize(package);
            // 4、业务处理
            auto resp = _process(req);
            // 5、序列化响应
            std::string jsonstr;
            resp->Serialize(&jsonstr);
            std::cout << "respjson: " << jsonstr << std::endl;
            // 6、添加报头
            jsonstr = EnCode(jsonstr);
            std::cout << "add header done, jsonstr is:\n"
                      << jsonstr;
            // 7、发送回去
            n = sock->Send(jsonstr);
            if (n < 0)
            {
                LOG(FATAL, "send error\n");
                break;
            }
            std::cout << "==============================================" << std::endl;// 方便看清楚每一个报文
        }
    }

private:
    process_t _process;
};

修改 ServerMain.cc

Service 是 IO 层,前面没有业务的处理,验证是对的,而这里已经加上了业务,所以我们得再 IO 层接收完数据后去执行业务,而我们当时就在 Service 中有一个回调的为业务函数!这里只需在绑定给 Service 一个可调用对象即可!

#include <iostream>
#include <memory>
#include "TcpServer.hpp"
#include "Service.hpp"
#include "Net_Cal.hpp"

// ./calserver local-port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cout << "Usage: " << argv[0] << " local-port" << std::endl;
        exit(1);
    }

    uint16_t port = std::stoi(argv[1]);

    Net_Cal cal;
    Service svic(std::bind(&Net_Cal::Calculator, &cal, std::placeholders::_1));
    service_t service_io(std::bind(&Service::Service_IO, &svic, std::placeholders::_1, std::placeholders::_2));
   
    std::unique_ptr<TcpServer> tsvc = std::make_unique<TcpServer>(service_io, port);
    tsvc->Loop();
}

OK,整体的服务端就涉及到这里!

思考为什么我们要这样的设计呢?

我们只是前面告诉你,要这样设计!但是为啥呢?其实这种设计思想我们再一开始就介绍过!

他就是赫赫有名的 OSI 七层模型 的 应用、会话、表示层TCP/IP合成了一层)!

所以,我们当时说:OSI 是参考模型,但是设计的很好!后来很多工程师在实现的时候,发现

应用、会话、表示层 这三层是相互依赖的,不能单独分开!所以实际落地的时候就是 TCP/IP 五层协议!

用一张图总结服务端就是:


2.3 客户端

上面服务端设计完成之后,这里就很简单了!还是先搭建一个框架出来!

构造这里我加了一个简单的 断线重连 的检测!

class TcpClient
{
public:
    TcpClient(std::string ip, uint16_t port)
        : _server_ip(ip), _server_port(port), _sock(std::make_shared<TcpSocket>())
    {
        // 断线重连
        int cnt = 5;
        while (cnt)
        {
            if (_sock->BuildClientSoket(_server_ip, _server_port))
            {
                break;
            }
            std::cout << "连接失败正在重连...., 重连剩余次数:" << --cnt << std::endl;
            sleep(1);
        }

        if (cnt == 0)
        {
            std::cout << "connect failed" << std::endl;
            exit(CONN_ERROR);
        }
    }

    ~TcpClient()
    {
    }

    void Loop()
    {
        // 进行 读取和接收
    }

private:
    std::string _server_ip; // 服务端 ip
    uint16_t _server_port;  // 服务端 port
    SockSPtr _sock;         //  // 父类对象
};

单进程收发

下面我们就来实现一下, Loop 即客户端的收发数据!

这里的收发都是长服务,先得向服务端发送请求,然后接收服务端的响应!

1、先让用户输入数据,然后构建请求对象,进行序列化+添加报头,最后发送给服务端!

2、然后进行接收客户端的消息,接收到后进行完整报文的解析,完成后构建响应对象进行反序列化,最后给用户(这里简单打印即可)!

void Loop()
{
    while (true)
    {
        int x = 0, y = 0;
        char oper = '+';
        std::cout << "请输入 x, oper, y:> ";
        std::cin >> x >> oper >> y;

        auto req = Factory::BuildRequest();
        req->SetValue(x, y, oper);

        // 序列化
        std::string jsonstr;
        req->Serialize(&jsonstr);

        // 添加报头
        jsonstr = EnCode(jsonstr);
        // 发送给服务端
        _sock->Send(jsonstr);

        // 读取响应
        std::string packageStream;
        while (true)
        {
            ssize_t n = _sock->Recv(&packageStream);
            if (n <= 0)
                break;

            // 提取有效载荷
            std::string package = DeCode(packageStream);
            if (package.empty())
                continue;
            
            // 反序列化
            auto resp = Factory::BuildResponse();
            resp->Deserialize(package);
            
            resp->Print();
            break;
        }

        std::cout << "----------------------------------------" << std::endl;
    }
    _sock->Close();
}

这里我们下来写一个 ClientMain.cc 验证一下:

#include <iostream>
#include <memory>

#include "TcpClient.hpp"


int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(1);
    }

    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);

    std::unique_ptr<TcpClient> tcsv = std::make_unique<TcpClient>(ip, port);
    tcsv->Loop();
    
    return 0;
}

OK是没有问题的!

多线程收发

上面是每一个客户端都是由主线程进行发送请求、也是主线程进行接收响应的!其实我们可以把主线程中的收发分开给到两个线程去做,玩法和我们之前再 UDP 那里完的类似!这里我们可以使用之前的封装好的线程 Thread.hpp

先来修改,客户端的 Loop 变成两个函数 recvMsgsendMsg

void recvMsg(std::string)
    {
        while (true)
        {
            int x = 0, y = 0;
            char oper = '+';
            std::cout << "请输入 x, oper, y:> ";
            std::cin >> x >> oper >> y;

            auto req = Factory::BuildRequest();
            req->SetValue(x, y, oper);

            // 序列化
            std::string jsonstr;
            req->Serialize(&jsonstr);

            // 添加报头
            jsonstr = EnCode(jsonstr);
            // 发送给服务端
            _sock->Send(jsonstr);
            usleep(1000);
        }
        _sock->Close();
    }

    void sendMsg(std::string)
    {
        // 读取响应
        std::string packageStream;
        while (true)
        {
            ssize_t n = _sock->Recv(&packageStream);
            if (n <= 0)
                break;

            // 提取有效载荷
            std::string package = DeCode(packageStream);
            if (package.empty())
                continue;
            // std::cout << "package => " << package << std::endl;
            // 反序列化
            auto resp = Factory::BuildResponse();
            resp->Deserialize(package);
            // std::cout << resp->GetResult() << " " << resp->GetCode() << " " << resp->GetDesc() << std::endl;
            resp->Print();
            std::cout << "----------------------------------------" << std::endl;
        }
        _sock->Close();
    }

ClientMain.cc 修改

#include <iostream>
#include <memory>
#include <functional>

#include "TcpClient.hpp"

#include "Thread.hpp"

using namespace ThreadModule;

/*多线程版本*/
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(1);
    }

    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);

    std::unique_ptr<TcpClient> tcsv = std::make_unique<TcpClient>(ip, port);

    Thread send("send-thread", std::bind(&TcpClient::sendMsg,tcsv.get(), std::placeholders::_1));
    Thread recv("recv-thread", std::bind(&TcpClient::recvMsg,tcsv.get(), std::placeholders::_1));

    send.start();
    recv.start();

    send.join();
    recv.join();
    return 0;
}

全部源码:自定义协议与序列化的全部源码


OK,本期分享就到这里,好兄弟我是 cp 我们下期再见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值