当前位置: 首页 > news >正文

【Linux】:自定义协议(应用层)

朋友们、伙计们,我们又见面了,本期来给大家带来应用层自定义协议相关的知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个  人  主  页 :stackY、

C + + 专 栏   :C++

Linux 专 栏  :Linux

目录

1. 协议

2. 自定义协议

2.1 预备工作 

3. 序列与反序列化

3.1 报头的添加与解析

3.2 计算业务

4. 功能完善

4.1 服务端

4.2 客户端 

5. 成熟的序列反序列化方案


1. 协议

前面说过,协议其实就是一种约定,我们实现的tcp通信时,都是按照字符串的方式进行发送的,那么对方收到的也是字符串,那么如果我们需要发送一些具体化的数据呢?

就比如:现在使用的这些聊天软件,我们在发送数据时,有昵称、时间、具体的消息内容,因此,在发送数据时,不仅仅是将消息内容发送过去,而是将这三样东西发送过去了,这是一种结构化的数据;

  • 所以在发送类似与这种结构化字段的数据就要制定一种协议;
  • 协议其实就是双方在通信时约定好的一种结构化字段;

在应用层这里我们发送时并不是直接将这个结构化的字段发送给对方:

  • 因为在应用层很可能双方系统有所差异,对于结构体的计算不统一,导致数据的不准确;
  • 所以,在应用层这里,我们要发送结构化的字段,必须要将结构化字段进行序列化成为字节流(“字符串”),将字节流发送给对方,对方通过反序列化将字节流转化为结构化字段;
  • 序列化的目的是为了更好的网络发送,反序列化的目的是为了上层更好的对数据进行有效字段的提取;
  • 序列化和反序列化的方式双方可以进行统一的约定;

2. 自定义协议

在自定协议这里我们直接实现一个网络版本的计算器来提现一下自定义协议的过程;

我们采用分模块来实现:

  • Socket.hpp:对网络套接字进行封装
  • TcpServer.hpp:实现Tcp的服务器
  • TcpServerMain.cc:测试Tcp服务器
  • TcpClientMain.cc:完成客户端
  • Protocol.hpp:自定义协议
  • Calculate.hpp:实现计算的业务

2.1 预备工作 

既然要进行网络通信,那么就少不了需要套接字接口,前面已经写过好多次套接字的接口了,这里对套接字进行封装,将服务器和客户端各自使用的接口整合在一起,我们对封装好的套接字提供一些我们需要的接口接口;

我们之前发送数据使用的read和write,其实还有两个接口:


Socket.hpp:

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define Convert(addrptr) ((struct sockaddr *)addrptr)  // 套接字中对于地址类型强转的宏

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

    enum // 对于一些错误码的设置
    {
        SocketError = 1,
        BindError,
        ListenError,
    };

    // 封装一个基类,Socket接口类
    // 设计模式:模版方法类
    class Socket
    {
    public:
        virtual ~Socket() {}
        virtual void CreateSocketorDie() = 0;      // 创建套接字
        virtual void BindSocketorDie(uint16_t port) = 0;  // 绑定
        virtual void ListenSocketorDie(int backlog) = 0;  // 监听
        virtual Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) = 0; // 获取连接
        virtual bool ConnectServer(std::string &serverip, uint16_t &serverport) = 0; // 建立连接
        virtual int GetSockFd() = 0;   // 获取套接字
        virtual void SetSockFd(int sockfd) = 0;  // 设置套接字
        virtual void CloseSocket() = 0;   // 关闭套接字
        virtual bool Recv(std::string *buffer, int size) = 0;  // 读取信息
        virtual void Send(std::string &send_str) = 0;    // 发送信息

    public:
        // 创建监听套接字----Server
        void BuildListenSocketMethod(uint16_t port, int blacklog)
        {
            CreateSocketorDie();
            BindSocketorDie(port);
            ListenSocketorDie(blacklog);
        }
        // 创建连接套接字---Client
        bool BuildConnectSocketMethod(std::string &serverip, uint16_t serverport)
        {
            CreateSocketorDie();
            return ConnectServer(serverip, serverport);
        }
        void BuildNormalSocketMethod(int sockfd)
        {
            SetSockFd(sockfd);
        }
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket(int sockfd = defaultsockfd) : _sockfd(sockfd)
        {
        }
        ~TcpSocket() {}
        void CreateSocketorDie() override
        {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
                exit(SocketError);
        }
        void BindSocketorDie(uint16_t port) override
        {
            struct sockaddr_in local;
            local.sin_family = AF_INET;
            local.sin_port = htons(port);
            local.sin_addr.s_addr = INADDR_ANY;
            int n = ::bind(_sockfd, Convert(&local), sizeof(local));
            if (n < 0)
                exit(BindError);
        }
        void ListenSocketorDie(int backlog) override
        {
            int n = ::listen(_sockfd, backlog);
            if (n < 0)
                exit(ListenError);
        }
        Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) override
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            uint16_t newsockfd = ::accept(_sockfd, Convert(&peer), &len);
            if (newsockfd < 0)
                return nullptr;
            *peerip = inet_ntoa(peer.sin_addr);
            *peerport = ntohs(peer.sin_port);
            Socket *s = new TcpSocket(newsockfd);
            return s;
        }
        bool ConnectServer(std::string &serverip, uint16_t &serverport) override
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_addr.s_addr = inet_addr(serverip.c_str());
            server.sin_port = htons(serverport);
            int n = ::connect(_sockfd, Convert(&server), sizeof(server));
            if (n == 0)
                return true;
            else
                return false;
        }
        int GetSockFd() override
        {
            return _sockfd;
        }

        void SetSockFd(int sockfd) override
        {
            _sockfd = sockfd;
        }
        void CloseSocket() override
        {
            if (_sockfd > defaultsockfd)
                ::close(_sockfd);
        }
        bool Recv(std::string *buffer, int size) override
        {
            char inbuffer[size];
            ssize_t n = recv(_sockfd, inbuffer, size - 1, 0);
            if(n > 0)
            {
                inbuffer[n] = 0;
                *buffer += inbuffer;
                return true;
            }
            else if (n == 0) return false;
            else return false;
        }
        void Send(std::string &send_str) override
        {
            // 发送信息这里我们简略的写了;
            send(_sockfd, send_str.c_str(), send_str.size(), 0);
        }
    private:
        int _sockfd;
    };

}

为了先测试一下客户端与服务器,我们先来简单的定制一下协议:

我们想要实现一个网络版本的计算器,来自己实现一下自定义协议;

协议方法:

  • 请求:参数1 运算符号 参数2
  • 响应:运算结果 运算状态(结果是否可靠)

请求和相应是两个结构化的字段,因为今天在同一台主机上进行测试,所以我们先直接发送结构化的字段,未来客户端和服务器分别include这个协议,至此双方都可以看到同一份结构化字段,这就是一种自定义的协议;

另外,我们定制好的协议我也想给他们设置一个工厂模式,为了后面方便使用;

接下来我们对客户端和服务器进行简单的实现,因为服务器与客户端在前面UDP和TCP通信那里细致的说多了,这里就直接展示代码了:

TcpServer.hpp:

 对于服务器我们想采用多线程的方式对任务进行处理;

TcpServerMain.cc:

这里对与任务函数的编写就简单一点,为了测试能否正常通信,以及直接传递结构化字段;

客户端:

TcpClientMain.cc:

客户端这里我们也是直接发送结构化字段;

网络版本计算器的基本流程:

服务器启动之后,先创建连接套接字,构建一个计算的请求,然后向服务器发起请求;

因为是本地测试,所以我们可以直接传递结构体,先完成基本的通信,后面再实现序列化的过程;

服务器启动之后,先创建监听套接字设置回调方法,然后获取新连接,使用多线程执行任务;

基本测试:

上面的代码通信时直接发送的是结构体字段,这种发送方式只在限定情况下可以使用,就比如我们上面的测试代码是在本地上演示的,所以不会有什么问题,为了考虑更全面,接下来就需要进行序列、反序列操作; 

在进行序列化之前再来对TCP协议进行一下深入的概念性了解:

  • 其实我们在进行TCP通信的时候,我们使用的发送(write/send)和接收(read/recv)的接口,并不是直接将数据通过网络发送给对方,因为这些接口是用户层接口,在内核层双方还会存在两个缓冲区:一个是发送缓冲区,一个是接收缓冲区;

  • write/send和read/recv接口只是将数据从用户缓冲区拷贝到内核缓冲区,本质就是一个拷贝函数;
  • 那么至于什么时候发送,发多少,怎么发,发送出错了怎么办等等,这些都不需要用户去考虑,这是由内核决定的,换句话说是由TCP协议决定的!
  • TCP协议在进行通信的时候,将发送缓冲区中的数据通过网络拷贝至对方接收缓冲区中,其实,是双方的OS之间进行通信
  • 这也就解释了,write或者read在某些条件下会发生阻塞的问题;当接收缓冲区中没有数据时,read就会阻塞,因为他不具备接收条件;当发送缓冲区写满的时候write就会阻塞,因为他不具备发送条件;
  • 像上面这种有人写就有人拿的模型其实就是一种生产者消费者模型
  • 因为双方在接收和发送时是两个独立的模块,所以可以进行同时通信,所以说TCP协议是全双工协议;

在TCP通信中,在发送时,对方发送了多少数据,并不意味着我就要接收多少数据,这完全由TCP协议来决定;

那么这就存在一种问题:我要读取对方发送的数据,怎么保证我就能读到对方发送的一个完整的数据报文呢?

此时就需要明确报文与报文之前的边界(代码中体现)

3. 序列与反序列化

为了完整我们的代码,我们就需要在发送数据与解析数据时进行序列与反序列化的工作;

序列反序列化也是双方进行的一种约定,也就是自定义的一种协议;

在这里我们想定制的协议是:

  • 未来要发送数据时将结构化字段全部转化为一个字符串“_data_x op _data_y”;
  • 这里需要注意op的长度是固定的(+ - * /)但是两边的操作数的长度是不固定的,所以为了反序列化更方便,我们需要添加报头,其中报头表示的含义就是这个字符串有效内容的长度“len_data_x op _data_y”,这个报头就叫做报文的自描述字段;
  • 为了让报头和有效载荷易于区分,并且为了让报文与报文之间易于区分,我们需要在报头和有效载荷的中间添加特殊字符(\n),在报文末尾添加特殊字符(\n)
  • “len\n_data_x op _data_y\n”;
  • 未来在读取报文的时候,首先读到的就是报头,读到\n时就知道前面的是报头,根据报头所表示的有效内容的长度,再向后读取指定大小的字符即可;
  • 这里添加\n是为了应付多种场景,我们现在的场景是四则运算,有效载荷中不可能出现\n,但是如果场景是一个聊天信息呢,里面可能会出现\n,但是这个消息的长度不可能有\n,所以用\n来区分报头与有效载荷的边界,当然也可以使用其他特殊字符;
  • 另外,我们在报文最后添加的\n不仅仅用于区分报文和报文之间的边界,还可以帮助我们在写代码的时候打印调试;
  • 上面是对请求进行的序列化,对于响应也是一样的“len\n_result _code”。

因为请求和相应都需要添加报头,所以序列化与报头我们分开处理;

Request的序列化与反序列化:

未来的客户端与服务器都需要遵守这样的约定来进行数据的交互与处理,这就是一种自定义的协议,有用户来决定的;

Response序列化与反序列化:

3.1 报头的添加与解析

添加报头:

未来我们相对这种类型"_data_x op _data_y"的字符串添加报头,所以我们依旧采用字符串的操作,这里就不详细解释了;

拼接特殊字符即可;

解析报头:

因为我们不确定报文的完整性,所以在使用解析报头时我们采用循环调用的方式;

首先我们需要找到区分报头和有效载荷的特殊字符;

然后截取特殊字符前面的报头来确定有效载荷的长度;

因为报文的不确定性,所以我们需要通过前面对有效载荷的长度以及报文长度的已知值来确定出一个完整报文的总长度;

然后根据传入的package与这个长度比较,想要至少有一个完整的报文那么就必须大于或者等于这个总长度;

然后通过特殊字符的位置进行截取到有效载荷的信息;

然后将我们已经截取到的完整报文丢弃掉,继续处理下一个报文;

3.2 计算业务

有了序列化与反序列化,接下来就需要对数据进行业务处理了,我们拿到数据先对数据进行处理,获取到其中的运算符(+ - * / %),然后根据不同的运算符来截取对应的操作数执行运算,然后将结果返回,所以在使用计算业务的时候,传入的是一个请求,返回的是一个相应;

4. 功能完善

4.1 服务端

上面实现的添加报头与解包分用其实就可以进行通信了,但是,我们想实现的是,把发送数据和接收数据放在TcpServer底层,此时我只负责发送和接收,不管发送和接收的数据是什么,将网络和业务进行解耦;

此时就需要对执行任务的函数进行简单的调整,未来我们发送一个字节流,对字节流进行业务处理,然后将处理完成的结果再序列化为字节流再返回给我即可,还可以再带一个参数,表示的是业务执行过程中是否出错;

首先我们来实现一下这个业务处理的函数HandlerRequest,在调用时,传入一个待处理的字节流,我们需要对这个字节流进行处理,获取到一个完整的报文,然后对报文反序列化,将有效载荷进行业务处理,处理完成之后的结果我们需要再进行序列化以及添加报头,然后返回出去;

紧接着我们需要在ThreadRun函数中对数据进行接收和发送的操作:

4.2 客户端 

客户端这里的代码就不封装了,直接编写实现通信;

在客户端这里我们首先需要构建一些需要计算的请求,然后对其进行序列化并添加报头,然后发送给服务器:

我们可以来梳理一下这个解耦的逻辑:

 我们既然能发送,当然也可以进行读取我们发送之后计算完成的结果,所以需要对返回的响应进行解析并反序列化得到最终的结构化字段的Response:

#include "Protocol.hpp"
#include "Socket.hpp"

#include <iostream>
#include <ctime>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>

using namespace Protocol;

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "Usage: " << " serverip serverport" << std::endl;
        return 1;
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    // 创建套接字
    Net_Work::Socket *conn = new Net_Work::TcpSocket();

    if (!conn->BuildConnectSocketMethod(serverip, serverport))
    {
        std::cerr << "connect " << serverip << ":" << serverport << " failed" << std::endl;
    }
    std::cout << "connect " << serverip << ":" << serverport << " success" << std::endl;

    // 使用工厂模式
    std::unique_ptr<Protocol::Factory> factory = std::make_unique<Protocol::Factory>();
    srand(time(nullptr) ^ getpid()); // 建立随机数的种子
    const std::string opers = "+-*/%";
    while (true)
    {
        // 构建请求,遵守协议
        int x = rand() % 100; // [0, 99)
        usleep(rand() % 1234);
        int y = rand() % 100; // [0, 99)
        char op = opers[rand() % opers.size()];
        // 创建请求
        std::shared_ptr<Protocol::Request> req = factory->BuildRequest(x, y, op);
        // 对请求序列化
        std::string request_str;
        req->Serialize(&request_str);
        std::cout << request_str << std::endl;

        // for test
        std::string testreq = request_str;
        testreq += " ";
        testreq += "= ";

        // 添加报头
        request_str = Encode(request_str);
        // 发送请求
        conn->Send(request_str);
        
        std::string response_str;
        while(true)
        {
            // 读取响应
            if(!conn->Recv(&response_str, 1024)) break;
            // 解析响应报文
            std::string response;
            if(!Decode(response_str, &response))
                continue;
            // 反序列化
            auto resp = factory->BuildResponse();
            resp->Deserialize(response);
            // 得到了结果
            std::cout << testreq << resp->GetResult() << "[" << resp->GetCode() << "]" << std::endl;

            break;
        }
        sleep(1);
    }
    

    conn->CloseSocket();
    return 0;
}

5. 成熟的序列反序列化方案

上面我们是手写的序列反序列化,这样子写也可以,但是毕竟是我们手写的,我们可以使用一下成熟的方案,比如:json、protobuf、xml;

我们想使用一下json来替换我们手写的序列和反序列化;

想细致了解json的使用可以去搜一些博客看一下,这里我们先使用json进行简单的演示:

// ubuntu 安装jsoncpp

sudo apt-get install libjsoncpp-dev

演示代码:

#include <iostream>
#include <string>

#include "jsoncpp/json/json.h"

int main()
{
    // 创建Json对象
    // Json::Value 万能的类型
    Json::Value root;

    // 添加kv映射数据
    root["k1"] = 100;
    root["k2"] = 100;
    root["hello"] = "world";
    root["bit"] = 8;

    // 序列化
    Json::FastWriter writer;
    std::string s = writer.write(root);
    std::cout << s << std::endl;

    // 反序列化
    int k1, k2, bit;
    std::string hello;
    Json::Value _root;
    Json::Reader reader;
    if (reader.parse(s, _root))
    {
        k1 = _root["k1"].asInt();
        k2 = _root["k2"].asInt();
        hello = _root["hello"].asCString();
        bit = _root["bit"].asInt();
    }

    std::cout << k1 << " " << k2 << " " << hello << " " << bit << std::endl;
    return 0;
}

接下来我们就将json引入到我们的代码中:

我们使用条件编译,也可以将我们自己实现的序列反序列化的过程保留下来

这到这里我们的代码已经完结了,其实也可以将服务器变成守护进程;

源码链接: https://gitee.com/yue-sir-bit/linux/tree/master/Network_version_calculator

当我们自己手写协议之后,再回头看一下OSI定义的七层网络协议栈,就可以与我们本节实现的代码可以结合起来了:

相关文章:

  • 信息学奥赛一本通 1831:【03NOIP提高组】神经网络 | 洛谷 P1038 [NOIP 2003 提高组] 神经网络
  • 数据结构(C\C++)——顺序表
  • 【MySQL数据库】存储过程与自定义函数(含: SQL变量、分支语句、循环语句 和 游标、异常处理 等内容)
  • 前端iView面试题及参考答案
  • 【大模型基础_毛玉仁】3.3 思维链
  • `fetch` 和 `axios`的前端使用区别
  • 基于Spring Boot的流浪动物救助平台的设计与实现(LW+源码+讲解)
  • Jetson Nano 三个版本(B01 4GB、Orin 4GB、Orin 8GB)本地部署Deepseek等大模型的测评
  • 机器学习开发完整流程
  • STM32HAL库,解决串口UART中断接收到的第一个字节数据丢失
  • 危化品经营单位考试:从基础夯实到能力提升的进阶之路​
  • 《Keras 3 : AI神经网络开发人员指南》
  • Linux 文件操作-标准IO函数4-fseek设置文件偏移量、ftell获取当前偏移量、rewind使文件偏移量(为0)定位到开头
  • 英语词性--形容词
  • 智能宠物饮水机WTL580微波雷达感应模块方案;便捷管理宠物饮水
  • 三维仿射变换-简略版
  • 【合新通信】---射频光模块
  • Deepseek使用技巧大全
  • Redis主从复制(Master-Slave Replication)解析与搭建操作指南
  • L2TP实验 作业
  • 黄仁勋访华期间表示希望继续与中国合作,贸促会回应
  • 春暖花开,为何皮肤却闹起了小情绪?
  • 幸福航空取消“五一”前航班,财务人员透露“没钱飞了”
  • 云南鲁甸县一河滩突然涨水致4死,有人在救人过程中遇难
  • “今日海上”对话“今日维也纳”,东西方艺术在上海碰撞
  • 特朗普签署行政命令推动深海采矿,被指无视国际规则,引发环境担忧