应用层自定义协议

接下来我们实现一个网络计算器功能。

自定义协议和序列化

前面我们的UDP套接字编程TCP套接字编程都实现了应用层,但没有实现协议。是否会疑惑有没有协议有何不同?

事实上我们这里要详细谈一谈何为面向字节流 ,以及引出的为什么read、write、recv、send等函数支持全双工。毕竟我们之前实现的TCP的echo时都是read之后直接write,你们不觉得很诡异吗?难道不会覆盖别的用户的数据?

面向字节流

我们之前说过,所谓协议就是大家都约定好的、都看得懂的结构化数据。但是我们传输过程是面向字节流的,因此这些结构化数据要全部转化成字节串,这个过程称为序列化。随后获取数据之后,也要从序列化数据反序列化得到相应的结构化数据,这个过程称为反序列化。

全双工

如上图,这些系统调用能支持全双工的根本原因就是他们有两个内核缓冲区。接收和发送调用的是不同的缓冲区,因此不会发生相互覆盖的现象。

所以我们得出以下结论:

  1. read、write、recv、send本质就是拷贝函数
  2. 发送数据的本质是:从发送方的发送缓冲区把数据通过协议栈和网络拷贝给接收方的接收缓冲区
  3. tcp协议支持全双工和传输控制的原因如上
  4. 每个缓冲区都有人写入和读取,这其实就是一个生产者消费者模型
  5. 那么IO函数阻塞目的就是维持同步

自定义协议

我们的发送和接收消息过程是这样,但我们有个问题。

这些信息在缓冲区中什么时候发送、发送多少、出错了怎么办?

这些都是由TCP控制的。

但是这样面向字节流传输数据就会出现一些问题,因为我们写入缓冲区的都是一些字节流,TCP不会知道完整报文是多大,所以读取的时候会出现报文不完整的现象。

因此我们要确保读取的时候是完整的报文,否则不做处理。

那么这样该如何实现呢?

这就要依靠我们应用层的协议了,我们要给报文加上报头,通过报头确定我们的报文大小,然后读取报文。

例如我们设计一个很简单的报头:

"len"\r\n

这里len是有效载荷的大小,\r\n则是标识符。Find到标识符证明读取到了报头,那么我们再根据len来读取有效载荷:

根据上面逻辑,我们写入数据和读取数据前应该做:

序列化

上面添加报头和去报头都是针对字节流的,那么我们如何将结构化数据序列化呢?

这个过程并不困难,我们也可以自己实现。

当然也可以调用已经实现的库函数,这里调用jsoncpp来序列化和反序列化。

Jsoncpp 是一个用于处理JSON数据的C++库。它提供了将JSON数据序列化为字符串以及从字符串反序列化为C++数据结构的功能。Jsoncpp是开源的,广泛用于各种需要处理JSON数据的C++项目中。

特性:

  1. 简单易用:Jsoncpp提供了直观的API,使得处理JSON数据变得简单。
  2. 高性能:Jsoncpp的性能经过优化,能够高效地处理大量JSON数据。
  3. 全面支持:支持JSON标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和null。
  4. 错误处理:在解析JSON数据时,Jsoncpp提供了详细的错误信息和位置,方便开发者调试。

那么我们要现在Ubuntu上安装这个第三方库:

shell 复制代码
sudo apt-get install libjsoncpp-dev

具体函数调用接口我们以后再详谈,这里直接使用吧。

可以看到我们序列化的逻辑非常直观。

反序列化逻辑依旧显然:

就这样我们实现结构化的发送数据和接收数据封装:

Request:

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

        _x = root["x"].asInt();
        _y = root["y"].asInt();
        _oper = root["oper"].asInt();

        return res;
    }
    void Print()
    {
        std::cout << _x << std::endl;
        std::cout << _y << std::endl;
        std::cout << _oper << std::endl;
    }
    int X()
    {
        return _x;
    }
    int Y()
    {
        return _y;
    }
    char Oper()
    {
        return _oper;
    }
    void SetValue(int x, int y, char oper)
    {
        _x = x;
        _y = y;
        _oper = oper;
    }

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

Response:

cpp 复制代码
class Response
{
public:
    Response() : _result(0), _code(0), _desc("success") {}
    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);

        _result = root["result"].asInt();
        _code = root["code"].asInt();
        _desc = root["desc"].asString();

        return res;
    }

    void PrintResult()
    {
        std::cout << "result: " << _result << ", code: " << _code << ", desc: " << _desc << std::endl; 
    }

    ~Response()
    {
    }

public:
    int _result;
    int _code;
    std::string _desc;
};

这里为了方便操作,我们将Response的成员变量设成公有的。

然后我们还可以实现一个工厂模式快速生成智能指针:

IO模块

socket封装

在实现IO模块前,我们可以先对Socket进行封装,毕竟我们用socket很多都是固定的方法。

我们这里采用模板方法类的形式封装不同协议下的套接字类:

其中using SockSPtr = std::shared_ptr<Socket>;

接下来我们实现Tcp的套接字封装:

CreateSocketOrDie

cpp 复制代码
void CreateSocketOrDie() override
{
    _sockfd=::socket(AF_INET,SOCK_STREAM,0);
    if(_sockfd<0)
    {
        LOG(FATAL,"socket create error\n");
        exit(SOCKET_ERROR);
    }
    LOG(INFO,"socket create success,sockfd:%d\n",_sockfd);
}

CreateBindOrDie

cpp 复制代码
void CreateBindOrDie(uint16_t port)override
{
    struct sockaddr_in local;
    memset(&local,0,sizeof(local));
    local.sin_family=AF_INET;
    local.sin_port=htons(port);
    local.sin_addr.s_addr=INADDR_ANY;

    if(::bind(_sockfd,(struct sockaddr*)&local,sizeof(local))<0)
    {
        LOG(FATAL,"bind error\n");
        exit(BIND_ERROR);
    }
    LOG(INFO,"bind success,sockfd:%d\n",_sockfd);
}

CreateListenOrDie

cpp 复制代码
void CreateListenOrDie(int backlog = gblcklog)override
{
    if(::listen(_sockfd,gblcklog)<0)
    {
        LOG(FATAL,"listen error\n");
        exit(LISTEN_ERR);
    }
    LOG(INFO,"listen success\n");
}

Accepter

cpp 复制代码
SockSPtr Accepter(InetAddr *cliaddr) override
{
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    int sockfd = ::accept(_sockfd, (struct sockaddr *)&client, &len);
    if (sockfd < 0)
    {
        LOG(WARNING, "accept error\n");
        return nullptr;
    }
    *cliaddr = InetAddr(client);
    LOG(INFO, "get a new link ,client info :%s,sockfd is :%d\n", cliaddr->AddrStr().c_str(), sockfd);
    return std::make_shared<TcpSocket>(sockfd);
}

Conntecor

cpp 复制代码
bool Conntecor(const std::string &peerip, uint16_t peerport) override
{
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(peerport);
    ::inet_pton(AF_INET, peerip.c_str(), &server.sin_addr);

    if (::connect(_sockfd, (struct sockaddr *)&server, sizeof(server)) < 0)
    {
        return false;
    }
    return true;
}

Recv

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

需要注意是+=,对应读取报文的逻辑。

Send

cpp 复制代码
ssize_t Send(const std::string &in) override
{
    return ::send(_sockfd, in.c_str(), in.size(), 0);
}

然后我们还可以实现一些集成的方法:

Server

封装完Socket就可以实现服务端逻辑了。这个和以前我们实现的类似,就是改用我们封装过的Socket罢了。

首先还是简单的初始化:

然后我们实现的是多线程版本,还需要封装一个内部类:

cpp 复制代码
class ThreadData
{
 public:
     SockSPtr _sockfd;
     TcpServer *_self;
     InetAddr _addr;
 public:
     ThreadData(SockSPtr sockfd, TcpServer *self, const InetAddr &addr):_sockfd(sockfd), _self(self), _addr(addr)
     {}
 };

最后就是执行逻辑:

很好,那么我们待会实现具体的service逻辑。先处理一下Main函数逻辑:

那么接下来我们先实现service的读取报文逻辑。

service

首先我们要确定的我们的回调函数:

传入指令,然后返回对应的结果。

然后根据我们刚才实现的顺序进行io:

cpp 复制代码
void IOExcute(SockSPtr sock, InetAddr &addr)
{
   std::string packagestreamqueue;
   while(true)
   {
       //1.负责读取
       ssize_t n=sock->Recv(&packagestreamqueue);
       if(n<=0)
       {
           LOG(INFO,"client %s quit or recv error\n",addr.AddrStr().c_str());
           break;
       }
       //2.报文解析,提取报头和有效载荷
       std::string package = Decode(packagestreamqueue);
       if(package.empty())continue;
       //此时读到了完整报文
       auto req=Factory::BuildRequestDefault();
       //3.反序列化
       req->Deserialize(package);
       //4.业务处理
       auto resp=_process(req);
       //5.序列化应答
       std::string respjson;
       resp->Serialize(&respjson);
       //6.添加报头
       respjson=Encode(respjson);
       //7.发送回去
       sock->Send(respjson);
   }
}

Cal

接下来就是业务处理逻辑。

这个其实很简单,我们学c语言的时候就会做了。

cpp 复制代码
class NetCal
{
public:
    NetCal()
    {
    }
    ~NetCal()
    {
    }
    std::shared_ptr<Response> Calculator(std::shared_ptr<Request> req)
    {
        auto resp = Factory::BuildResponseDefault();
        switch (req->Oper())
        {
        case '+':
            resp->_result = req->X() + req->Y();
            break;
        case '-':
            resp->_result = req->X() - req->Y();
            break;
        case '*':
            resp->_result = req->X() * req->Y();
            break;
        case '/':
        {
            if (req->Y() == 0)
            {
                resp->_code = 1;
                resp->_desc = "div zero";
            }
            else
            {
                resp->_result = req->X() / req->Y();
            }
        }
        break;
        case '%':
        {
            if (req->Y() == 0)
            {
                resp->_code = 2;
                resp->_desc = "mod zero";
            }
            else
            {
                resp->_result = req->X() % req->Y();
            }
        }
        break;
        default:
        {
            resp->_code = 3;
            resp->_desc = "illegal operation";
        }
        break;
        }
        return resp;
    }
};

Client

最后我们实现客户端代码,也是很简单的逻辑

cpp 复制代码
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"

using namespace socket_ns;

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage:" << argv[0] << "server-ip server-port" << std::endl;
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    SockSPtr sock = std::make_shared<TcpSocket>();
    if (!sock->BuildClientSocket(serverip, serverport))
    {
        std::cerr << "connect error" << std::endl;
        exit(1);
    }

    srand(time(nullptr) ^ getpid());
    const std::string opers = "+-*/%&^!";

    int cnt = 3;
    std::string packagestreamqueue;
    while (true)
    {
        // 构建数据
        int x = rand() % 10;
        usleep(x * 1000);
        int y = rand() % 10;
        usleep(x * y * 100);
        char oper = opers[y % opers.size()];

        // 构建请求
        auto req = Factory::BuildRequestDefault();
        req->SetValue(x, y, oper);

        // 1. 序列化
        std::string reqstr;
        req->Serialize(&reqstr);

        // 2. 添加长度报头字段
        reqstr = Encode(reqstr);
        std::cout << "####################################" << std::endl;
        std::cout << "request string: \n" <<  reqstr << std::endl;

        // 3. 发送数据
        sock->Send(reqstr);
        while (true)
        {
            // 4. 读取应答,response
            ssize_t n = sock->Recv(&packagestreamqueue);
            if (n <= 0)
            {
                break;
            }

            // 5. 报文解析,提取报头和有效载荷
            std::string package = Decode(packagestreamqueue);
            if (package.empty())
                continue;
            std::cout << "package: \n" << package << std::endl;

            // 6. 反序列化
            auto resp = Factory::BuildResponseDefault();
            resp->Deserialize(package);

            // 7. 打印结果
            resp->PrintResult();

            break;
        }
        sleep(1);
    }
    sock->Close();

    return 0;
}

运行结果:

完整代码

完整代码奉上

相关推荐
头疼的程序员2 小时前
计算机网络:自顶向下方法(第七版)第一章 学习分享
网络·学习·计算机网络
前端不太难2 小时前
从一次点赞操作,看 RN 列表的渲染扩散路径
网络·react
神的孩子都在歌唱2 小时前
ARP 与 MAC 地址解析:局域网通信的第一步
网络·macos
tzhou644522 小时前
Docker核心功能解析:网络、资源控制、数据卷与镜像构建
网络·docker·eureka
init_23612 小时前
MPLS跨域optionA 配置案例
java·开发语言·网络
G_H_S_3_2 小时前
【网络运维】KVM基础使用
linux·运维·网络·kvm
lkbhua莱克瓦242 小时前
面向编程3-UDP通信程序
java·网络·网络协议·udp
小尧嵌入式2 小时前
CANOpen协议
服务器·网络·c++·windows
代码游侠2 小时前
学习笔记——网络基础
linux·c语言·网络·笔记·学习·算法