「网络编程」第三讲:认识协议及简单的协议定制

news2024/11/13 15:12:54

「前言」文章内容是关于协议的,大致内容是再次认识协议及简单协议的定制,目的是帮助理解协议,下面开始讲解! 

「归属专栏」网络编程

「笔者」枫叶先生(fy)

「座右铭」前行路上修真我

「枫叶先生有点文青病」「句子分享」 

我与我周旋久,宁作我。

——烽火戏诸侯《剑来》

目录

一、再谈协议

1.1 结构化数据

1.2 序列化和反序列化

二、网络版本的计算器

2.1 服务端

2.2  定制协议

2.3 客户端

2.4 全部代码

2.5 代码测试

三、序列化和反序列化


一、再谈协议

协议是一种 "约定",双方都需要遵守。

在计算机网络中,协议(protocol)用于规定数据传输、通信和交互的一系列规则和约定。网络协议定义了计算机之间进行通信的方式、数据格式、传输速率、错误检测和纠正等细节,从而确保网络中的设备能够相互理解和正确地进行数据交换。

1.1 结构化数据

socket的api接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的,如果我们要传输一些"结构化的数据" 怎么办呢?

什么是结构化的数据??

比如QQ聊天,就不能单纯发送消息过去,还要把头像url、时间昵称等打包形成一个报文,把这个报文的数据一起发送给对方,这个打包形成的报文就是一个结构化的数据

1.2 序列化和反序列化

序列化和反序列化:

  • 序列化是将数据结构或对象转换为字节流的过程。在序列化过程中,对象的状态信息被转换为字节序列,可以将其存储在文件中或通过网络传输
  • 反序列化是将字节流或其他存储形式转换回数据结构或对象的过程。在反序列化过程中,字节序列被重新转换为对象的状态信息,以便可以重新创建对象并使用其数据

序列化和反序列化的目的

  • 数据持久化:通过序列化,可以将对象的状态保存到文件或数据库中,以便在程序重新启动或重新加载时可以从中恢复对象的状态 
  • 数据传输:通过序列化,可以将对象转换为字节流,以便在网络传输中进行传递
  • 跨平台和跨语言交互:通过序列化,可以将对象转换为通用的字节流格式,使得不同平台和不同编程语言之间可以进行数据交换和共享。无论是Java、Python、C++还是其他编程语言,只要能够进行序列化和反序列化操作,就可以实现跨平台和跨语言的数据交互

发送报文到网络时候,报文首先需要进行序列化,然后再发送,报文通过协议栈发送给对方后,接收报文的一方也需要对报文进行反序列化,才能正常使用该报文

二、网络版本的计算器

下面实现一个网络版的计算器,主要目的是感受一下什么是协议,以及了解简单的业务协议定制,序列化和反序列化的过程,重点不在计算器上

2.1 服务端

代码直接采用socket套接字TCP多线程版的,前面已经讲解过了,就不再解释

初始化服务器initServer函数步骤大致如下:

  • 调用socket函数,创建套接字。
  • 调用bind函数,为服务端绑定一个端口号
  • 调用listen函数,将套接字设置为监听状态

启动服务器start函数步骤大致如下:

  • 调用accept函数,获取新链接
  • 为客户端提供服务

除了为客户端提供服务需要我们重新写,其他代码都是之前的

tcpServer.hpp      

注:代码太多,只贴出一小部分 

static const int gbacklog = 5;
typedef std::function<void(const Request &req, Response &resp)> func_t;

// 错误类型枚举
enum
{
    UAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR
};

// 业务处理 -- 解耦
void handlerEntery(int sockefd, func_t func)
{
    
}

class tcpServer; // 声明
class ThreadDate
{
public:
    ThreadDate(int sockfd, func_t func)
        : _sockfd(sockfd), _func(func)
    {}

public:
    int _sockfd;
    func_t _func;
};

class calServer
{
public:
    calServer(const uint16_t &port)
        : _listensock(-1), _port(port)
    {}

    // 初始化服务器
    void initServer()
    {}

    // 启动服务器
    void start(func_t func)
    {
        for (;;)
        {
            // 5. 为sockfd提供服务,即为客户端提供服务
            // 多线程版
            pthread_t tid;
            ThreadDate *td = new ThreadDate(sockfd, func);
            pthread_create(&tid, nullptr, threadRoutine, td);
        }
    }

    static void *threadRoutine(void *args)
    {
        pthread_detach(pthread_self()); // 线程分离
        ThreadDate *td = static_cast<ThreadDate *>(args);
        handlerEntery(td->_sockfd, td->_func); // 业务处理
        close(td->_sockfd);                    // 必须关闭,由新线程关闭
        delete td;
        return nullptr;
    }

    ~calServer()
    {}

private:
    int _listensock; // listen套接字,不是用来数据通信的,是用来监听链接到来
    uint16_t _port;  // 端口号
};

我们需要进进行重新写的函数只有业务处理 handlerEntery

// 业务处理 -- 解耦
void handlerEntery(int sockefd, func_t func)
{}

传输控制(TCP)

 假设左边的主机是服务端,右边的主机是客户端

服务端向客户端发送数据或者反过来:

  • 发送函数(write等)在自己的应用层有自己的应用层缓冲区,调用发送函数实际上是把数据拷贝到传输层的缓冲区中,数据是否发送到网络中,由TCP协议自主决定,所以TCP协议称为传输控制协议,关键字:传输控制
  • 接收函数(read等)在自己的应用层也有自己的应用层缓冲区,调用接收函数实际上是把传输层缓冲区中数据拷贝到自己应用层缓冲区中

所以我们调用的发送、接收函数,本质都是拷贝函数

所以一方在发送,另一方也在发送,双方根本就不会影响,因为它们有成对的缓冲区,一个负责发送,一个负责接收,所以TCP是全双工的

所以TCP在读取数据的时候会出现问题(面向字节流)

 比如,对方一下子发来多个报文,这些报文都堆积在TCP的接收缓冲区中,应用层进行读取报文的时候,如何判断自己读到的是一个完整的报文?又或者只读到半个报文如何处理?又或者读到一个半的报文又如何处理?又或者读到两个报文呢??

所以,要明确报文的大小和报文的边界

解决方法:

  1. 对报文进行定长
  2. 用特殊符号区分
  3. 自描述方式

2.2  定制协议

定制的协议,必须保证通信双方(客户端、服务端)能够遵守协议的约定。

我们可以设计一套简单的协议,数据可以分为请求数据和响应数据,因此我们分别需要对请求数据和响应数据进行做约定,可以用两个结构体进行封装数据的请求和响应

填写好的业务处理函数handlerEntery

该函数大致分五个步骤:

  1. 读取接收到一个完整的报文,对报文进行解包
  2. 对报文反序列化
  3. 真正进行业务处理,计算处理数据
  4. 对数据处理的结果进行序列化
  5. 对报文进行添加报头,最后再发送响应结果的报文
// 业务处理 -- 解耦
void handlerEntery(int sockfd, func_t func)
{
    std::string inbuffer; // 读取的报文全部放在inbuffer里面
    while (true)
    {
        // 1.读取 -- 收取到一个完整报文
        std::string req_text;                          // 用于接收一个完整报文
        if (!recvPackage(sockfd, inbuffer, &req_text)) // 接收一个完整报文
            return;
        std::cout << "带报头的报文:" << req_text << std::endl;
        std::string req_str;          // 获取解包之后的结果
        deLength(req_text, &req_str); // 解包
        std::cout << "解包后的报文:" << req_str << std::endl;

        // 2. 请求request -- 反序列化
        Request req;
        if (!req.deserialize(req_str)) // 请求反序列化
            return;

        // 3. 业务逻辑 -- 处理数据
        Response resp;   // 拿取计算结果
        func(req, resp); // 计算,回调函数

        // 4.响应Response -- 序列化
        std::string resp_str;      // 拿取响应序列化结果
        resp.serialize(&resp_str); // 序列化
        std::cout << "计算完成,响应序列化结果:" << resp_str << std::endl;

        // 5. 发送响应的结果
        std::string send_str = enLength(resp_str); // 构建成为一个完整的报文
        std::cout << "构建成为一个完整的报文:" << send_str << std::endl;
        send(sockfd, send_str.c_str(), send_str.size(), 0); // 发送也有bug,暂时不用理会
    }
}

确保可以读到一个完整的报文 

// 接收一个报文
bool recvPackage(int sockfd, std::string &inbuffer, std::string *text)
{
    char buffer[1024];
    while (true)
    {
        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            buffer[n] = 0;
            inbuffer += buffer;
            auto pos = inbuffer.find(LINE__SEP);
            if (pos == std::string::npos)
                continue; // 报文不完整,继续读取
            std::string text_len_str = inbuffer.substr(0, pos);
            int text_len = std::stoi(text_len_str); // 报文有效载荷大小
            // "content_len" + "\r\n" + "exitcode result" + "\r\n"
            int total_len = text_len_str.size() + 2 * LINE__SEP_LEN + text_len; // 一个完整报文的长度

            if (inbuffer.size() < total_len)
                continue; // 报文不完整,继续读取
            std::cout << "处理前的inbuffer: " << inbuffer << std::endl;
            // 走到这里,至少有一个完整的报文
            *text = inbuffer.substr(0, total_len); // 拿走报文
            inbuffer.erase(0, total_len);          // 删除已拿走的报文
            std::cout << "处理后的inbuffer: " << inbuffer << std::endl;
            return true;
        }
        else
        {
            return false;
        }
    }
}

 定制的协议

  • 请求结构体中成员变量需要包括两个操作数,以及对应操作符
  • 响应结构体中需要包括一个计算结果,除此之外,响应结构体中还需要包括一个状态字段,表示本次计算的状态

注意:序列化和反序列化的过程不属于协议的内容

class Request
{
public:
    Request() : x(0), y(0), op(0){};
    Request(int x_, int y_, char op_) : x(x_), y(y_), op(op_)
    {}

    // 请求序列化 -- 暂时自己写
    bool serialize(std::string *out)
    {
    }

    // 请求反序列化 -- 暂时自己写
    bool deserialize(std::string &in)
    {
    }

public:
    int x;
    int y;
    char op;
};

class Response
{
public:
    Response() : exitcode(0), result(0)
    {}

    Response(int exitcode_, int result_) : exitcode(exitcode_), result(result_)
    {}

    // 响应结果序列化 -- 暂时自己写
    bool serialize(std::string *out)
    {
    }

    // 响应结果反序列化 -- 暂时自己写
    bool deserialize(std::string &in)
    {
    }

public:
    int exitcode;
    int result;
};

注意: 协议定制好后必须要被客户端和服务端同时看到,这样它们才能遵守这个约定

函数介绍 

send函数,用于TCP发送数据

send函数的函数原型如下:

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数说明:

  • sockfd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
  • buf:需要发送的数据。
  • len:需要发送数据的字节个数。
  • flags:发送的方式,一般设置为0,表示阻塞式发送。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

该函数与write函数功能一致

recv函数,用于TCP接受数据

ecv函数的函数原型如下: 

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数说明:

  • sockfd:特定的文件描述符,表示从该文件描述符中读取数据。
  • buf:数据的存储位置,表示将读取到的数据存储到该位置。
  • len:数据的个数,表示从该文件描述符中读取数据的字节数。
  • flags:读取的方式,一般设置为0,表示阻塞式读取。

返回值说明:

  • 如果返回值大于0,则表示本次实际读取到的字节个数。
  • 如果返回值等于0,则表示对端已经把连接关闭了。
  • 如果返回值小于0,则表示读取时遇到了错误。

 该函数的功能与read一致

2.3 客户端

客户端的代码跟前面也差不多,修改start函数的读写数据即可,遵守我们定制的协议

 // 启动客户端
    void start()
    {
        // 客户端需要发起链接,链接服务端
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_serverport);                  // 主机转网络序列
        server.sin_addr.s_addr = inet_addr(_serverip.c_str()); // 1.std::string类型转int类型 2.把int类型转换成网络字节序 (这两个工作inet_addr已完成)

        if (connect(_sockfd, (struct sockaddr *)&server, sizeof(server)) != 0)
        {
            std::cerr << "socket connect error" << std::endl;
        }
        else // 连接成功
        {
            std::string line;
            std::string inbuffer;
            while (true)
            {
                // 发送请求
                std::cout << "Input Cal>> ";
                std::getline(std::cin, line);
                Request req = parseLine(line); // 对输入的字符串做解析
                std::string content;     // 获取序列化的结果
                req.serialize(&content); // 对输入的内容进行序列化
                std::string send_str = enLength(content); // 添加报头
                send(_sockfd, send_str.c_str(), send_str.size(), 0); // 发送,bug,不理会

                std::string package, text;
                if (!recvPackage(_sockfd, inbuffer, &package)) continue;  // 获取一个完整的报文
                if (!deLength(package, &text)) continue; // 对报文进行解包
                Response resp;
                resp.deserialize(text); // 对报文反序列化
                std::cout << "exitcode: " << resp.exitcode << ", result: " << resp.result << std::endl;
            }
        }
    }

2.4 全部代码

代码全部在gitee

code_linux/code_202306_27/protocol · Maple_fylqh/code - 码云 - 开源中国 (gitee.com)

2.5 代码测试

编译没有问题

新建窗口,先运行服务端,再启动客户端,客户端先用本地环回进行测试

 

客户端,正常

输入需要按照协议要求,测试结果正常

上面的代码只为体会什么是协议,序列化和反序列化的过程 

三、序列化和反序列化

对于序列化和反序列化,我们不会自己去写,上面写只是为了体会该过程,序列化和反序列化有相应的库支持,都是现成的方案,我们直接使用库即可,协议就可能需要我们自己写

常见的序列化和反序列化的库:

  1. json
  2. protobuf
  3. xml

其中,json 是简单易上手的,C++、Java,Python等都支持,protobuf 和 json 是C++常用的,xml 是Java常用的

安装json库

install -y jsoncpp-devel

 注意:普通用户需要 sudo 提权

安装完成

一般安装在这个路径下

使用json需要包含头文件

#include <jsoncpp/json/json.h>

 编译需要带该库的名称

文章不太好写,有点水...

--------------------- END ----------------------

「 作者 」 枫叶先生
「 更新 」 2023.6.28
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
          或有谬误或不准确之处,敬请读者批评指正。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/697981.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

qt 调节win声音版本大小

QT4 情况下&#xff0c;运行的&#xff0c;会出错&#xff0c;目前暂时没有办法解决在&#xff0c;win下调节音量大小问题在这里插入代码片 参考资料 QT 对window系统下音量的设置和获取 还有个很好贴子&#xff0c;没有找到

LLM 应用参考架构:ArchGuard Co-mate 实践示例

随着&#xff0c;对于 LLM 应用于架构领域探索的进一步深入&#xff0c;以及 ArchGuard Co-mate 开发进入深入区&#xff0c;我们发现越来越多的通用模式。 在先前的文章里&#xff0c;我们总结了一系列的设计原则&#xff0c;在这篇文章里&#xff0c;我们将介绍 ArchGuard Co…

架构重构|性能和扩展性大幅提升的Share Creators智能数字资产管理软件3.0

作为数字资产管理行业的领军者&#xff0c;Share Creators智能数字资产管理软件持续致力于帮助企业和团队智能化管理数字资产&#xff0c;提升工业化管线制作效率。经过本次重构&#xff0c;Share Creators 3.0版本重装上阵&#xff0c;全面更新的服务架构标志着软件整体性能的…

C语言-数字爆炸游戏

问题&#xff1a; 你好&#xff0c;欢迎来到数字爆炸&#xff0c;系统随机生成一个数字&#xff0c;猜大了&#xff0c;提示猜大了&#xff0c;猜小了&#xff0c;提示猜小了。 思路&#xff1a; 先写游戏大概思路首先&#xff0c;会有菜单吧&#xff0c;所以先写一个菜单函数…

服务启动后能ping通但无法访问

近期业务需要&#xff0c;重启了服务器&#xff08;centos 7.4&#xff09;&#xff0c;但是各类服务启动后&#xff0c;仍然无法访问&#xff0c;Nginx的白页面都访问不到&#xff0c;能ping通&#xff0c;nslookup 域名解析也没问题&#xff0c;考虑是防火墙 -- 查看防火墙启…

《2023 信创软件品牌影响力报告》发布!融云入选「信创生态」代表厂商

信创正由试点实践转向全面推广。关注【融云 RongCloud】&#xff0c;了解协同办公平台更多干货。 由于外围环境收紧和国内数字经济高速发展等原因&#xff0c;信创产业在顶层战略的指引推动下蓬勃发展&#xff0c;相关政策有力促进了芯片、操作系统、数据库、办公软件、工业软…

jquery html页面先加载内容过几秒后显示数据

&#x1f607;作者介绍&#xff1a;一个有梦想、有理想、有目标的&#xff0c;且渴望能够学有所成的追梦人。 &#x1f386;学习格言&#xff1a;不读书的人,思想就会停止。——狄德罗 ⛪️个人主页&#xff1a;进入博主主页 &#x1f5fc;推荐专栏&#xff1a;点击进入查看 &a…

文心一言 VS 讯飞星火 VS chatgpt (50)-- 算法导论6.2 2题

二、参考过程 MAX-HEAPIFY&#xff0c;写出能够维护相应最小堆的 MIN-HEAPIFY(A&#xff0c;i)的伪代码&#xff0c;并比较 MIN-HEAPIFY 与 MAX-HEAPIFY 的运行时间。 文心一言&#xff1a; MIN-HEAPIFY(A, i)的伪代码如下&#xff1a; 1.将节点i及其子树中的最小元素交换到…

linux定时清理三个月前的应用日志

目录 思路find 命令需要用到 -mtime n 参数 shell脚本三种方法方法一方法二方法三 配置计划任务 思路 要想删除三个月前的日志&#xff0c;先得找到三个月前的日志才行&#xff0c;可以通过 find命令查找。 find 命令 1.find命令基本介绍     find顾名思义就是查找&…

UMS攸信入选福建省知识产权优势企业名单,为企业自主创新体系建设增添新动能!

近日&#xff0c;根据福建省市场监管局办公室《关于组织开展2023年度省知识产权优势企业申报及复核工作的通知》&#xff08;闽市监办〔2023〕11号&#xff09;要求&#xff0c;将拟确认的2023年福建省知识产权优势企业名单予以公示。 攸信技术成功入选该名单&#xff0c;荣获2…

chatgpt赋能python:Python退出主程序:如何正确结束你的Python代码

Python退出主程序&#xff1a;如何正确结束你的Python代码 对于Python编程的初学者来说&#xff0c;经常会遇到一个问题&#xff1a;如何正确退出Python程序&#xff1f;在Python中&#xff0c;有许多种方式可以停止运行Python程序&#xff0c;但不是所有的方法都是相同的。如…

vue 访问本地json数据

如果你的项目中需要模拟下json数据&#xff0c;来看下访问速度&#xff0c;那就参照这个试试吧&#xff0c;首先创建test.josn&#xff0c;放在pulic目录下&#xff0c;见下图 定义js // 文件 prodOrder.jsexport function test(data) {return request({url: http://localhost…

无缝数据转换!使用C++ 实现 Excel文件与CSV之间的相互转换

CSV格式是一种通用的文本文件格式&#xff0c;可在多个应用程序之间共享和使用。相比之下&#xff0c;Excel文件是一种电子表格格式&#xff0c;通常只能在Microsoft Excel中编辑和查看。因此&#xff0c;将Excel文件转换为CSV格式可使数据更方便地在其他应用程序中使用&#x…

Linux服务器丢包故障的解决思路及引申的TCP/IP协议栈理论

Linux服务器丢包故障的解决思路及引申的TCP/IP协议栈理论 我们使用Linux作为服务器操作系统时&#xff0c;为了达到高并发处理能力&#xff0c;充分利用机器性能&#xff0c;经常会进行一些内核参数的调整优化&#xff0c;但不合理的调整常常也会引起意想不到的其他问题&#x…

Android 冷启动优化的3个小案例

背景 为了提高App的冷启动耗时&#xff0c;除了在常规的业务侧进行耗时代码优化之外&#xff0c;为了进一步缩短启动耗时&#xff0c;需要在纯技术测做一些优化探索&#xff0c;本期我们从类预加载、Retrofit 、ARouter方面进行了进一步的优化。从测试数据上来看&#xff0c;这…

docker创建mysql容器

步骤 引言执行创建命令设置远程访问使用Navicat连接 引言 只要有开发&#xff0c;就要用数据库&#xff0c;mysql是最简单&#xff0c;也是非常好用的数据库&#xff0c;也要学会用docker创建mysql数据库。 执行创建命令 docker run --name mysql\--restartalways\-p 13306:…

每天一点Python——day43

#第四十三天字典的视图操作&#xff1a; ①keys()获取字典中所有的键 ②values()获取字典中所有的值 ③items()获取字典中所有的键值对#如图&#xff1a; #例&#xff1a;获取所有的键 a{哥哥:18,妹妹:16,姐姐:17}#字典创立 ba.keys()#获取后我们存在变量b中&#xff0c;右边的…

【Mysql】索引数据结构深入研究(二)

前言 在这里需要明确的一点是&#xff0c;数据库的引擎InnoDB或者是MyISAM引擎它们是形容数据表的&#xff0c;不是形容数据库的。 另外&#xff1a;文章中提到的索引的数据结构暂且都默认使用BTree InnoDB引擎 InnoDB的索引数据文件有两个&#xff0c;tableName.frm和table…

Redis7【⑦ Redis哨兵(sentinel)】

Redis哨兵 Redis Sentinel&#xff08;哨兵&#xff09;是 Redis 的高可用性解决方案之一&#xff0c;它可以用于监控和管理 Redis 主从复制集群&#xff0c;并在主节点发生故障时自动将从节点升级为新的主节点&#xff0c;从而保证系统的高可用性和可靠性。 Redis Sentinel …

v8-tc39-ecma262:数组push执行了什么?

v8开发文档介绍 v8新特性 tc39-ecma262-push函数执行步骤 上图步骤&#xff0c;解释如下&#xff1a; 如果是对象&#xff0c;则当作对象调用设置该值如果是类数组&#xff0c;则执行类数组调用设置该值如果参数有多个参数&#xff0c;则&#xff1a;如果&#xff0c;参数长度…