如何定制自己的应用层协议?|面向字节流|字节流如何解决黏包问题?如何将字节流分成数据报?

news2024/11/24 3:12:26

前言

那么这里博主先安利一些干货满满的专栏了!

首先是博主的高质量博客的汇总,这个专栏里面的博客,都是博主最最用心写的一部分,干货满满,希望对大家有帮助。

高质量干货博客汇总https://blog.csdn.net/yu_cblog/category_12379430.html?spm=1001.2014.3001.5482

该项目GITHUB地址

网络计算器-序列化和反序列化-协议定制icon-default.png?t=N6B9https://github.com/Yufccode/BitCode/tree/main/Linux/%E4%BB%A3%E7%A0%81/0226%E7%BD%91%E7%BB%9C%E8%AE%A1%E7%AE%97%E5%99%A8-%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E5%8D%8F%E8%AE%AE%E5%AE%9A%E5%88%B6

什么是字节流和数据报

在计算机网络理论中,什么是字节流,什么是数据报,他们和TCP,UDP的关系是什么,字节流和数据报的关系是什么?

字节流是一种连续的、无边界的数据流。它将数据视为一个连续的字节序列,没有明确的分组或边界。在字节流中,数据按照发送的顺序进行传输,接收方按照相同的顺序接收数据。字节流是一种面向连接的传输方式,它提供可靠的、有序的、基于错误检测和纠正的数据传输。TCP(传输控制协议)是一个使用字节流传输的协议。

数据报是一种离散的、有边界的数据传输方式。它将数据划分为固定大小的数据包,每个数据包都有自己的头部信息(通常包含源地址、目标地址、序列号等),并独立发送。每个数据包在网络中独立传输,可能沿不同的路径到达接收方。数据报传输通常是无连接的,不保证可靠性和有序性。UDP(用户数据报协议)是一个使用数据报传输的协议。

关于它们和TCP、UDP的关系:

  • TCP使用字节流传输方式,通过TCP连接来提供可靠的、有序的、面向连接的数据传输。TCP使用序号和确认机制来确保数据的可靠性和有序性。
  • UDP使用数据报传输方式,提供了无连接、不可靠的数据传输。UDP适用于对实时性要求较高,但对数据可靠性和顺序性要求不高的应用场景。

字节流和数据报之间没有直接的关系,它们是不同的传输方式。TCP使用字节流传输方式,而UDP使用数据报传输方式。这两种传输方式适用于不同的网络应用场景,具体选择取决于应用的要求和设计。

粘包问题

粘包问题是在网络通信中常见的一个现象,指的是接收方无法准确地将字节流拆分为原始的数据报,导致数据解析错误。为了解决粘包问题,可以采取以下方法:

  • 使用固定长度分割,每个数据报长度固定,接收方按照固定长度提取数据。
  • 使用特定字符分割,定义一个特殊的字符或字符序列作为数据报之间的分隔符,接收方根据分隔符提取数据报。
  • 使用长度字段分割,数据报头部添加表示长度的字段,接收方读取长度字段并提取相应长度的字节作为数据报。

学习计算机网络应用层协议的原理是为了理解网络通信机制和实现自定义协议。定制协议原理的学习能帮助我们根据特殊字符将字节流分割为数据报,并通过反序列化解析出所需报文。这对于开发网络应用、数据交互和错误处理至关重要。

序列化和反序列化

序列化(Serialization)是将对象的状态转换为字节流的过程,以便将其存储在内存、文件或网络中,或者将其传输到其他远程系统。序列化将对象转换为字节序列的形式,使得可以在不同的平台、系统或编程语言之间进行数据交换和持久化存储。

反序列化(Deserialization)是将字节流转换回对象的状态的过程,即从序列化的字节流中恢复对象的属性和数据结构。反序列化将字节流重新转换为原始对象的形式,以便可以使用和操作这些对象。

序列化和反序列化通常在分布式系统、网络通信和持久化存储等场景中使用。通过序列化,可以将对象转换为字节流,在网络传输中发送或存储到磁盘上。然后,通过反序列化,可以将字节流重新还原为原始对象,以便进行处理、操作或者重新恢复对象的状态。

在使用HTTP协议的时候,使用过浏览器的时候,浏览器的后端,服务器的后端,会帮我们做好这些事情,但是今天,博主要带着大家来定制一个自己的协议,带着大家来理解上述的原理。

本项目:实现一个网络版本计算器利用自己定制的协议

Github地址

网络计算器-序列化和反序列化-协议定制icon-default.png?t=N6B9https://github.com/Yufccode/BitCode/tree/main/Linux/%E4%BB%A3%E7%A0%81/0226%E7%BD%91%E7%BB%9C%E8%AE%A1%E7%AE%97%E5%99%A8-%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E5%8D%8F%E8%AE%AE%E5%AE%9A%E5%88%B6

底层服务器准备

底层使用TCP连接,在博主实现的代码中,博主简单实用了一下多线程,写了一个多线程的版本,这里博主就不展示底层服务器的代码了,小伙伴可以直接看博主github上的那个就行。

制定序列化反序列化规则和报文规则

规定一个运算包括三个字段:

x,y,op 

x代表第一个操作数

y代表第二个操作数

op代表运算符

1+2 即 x=1,y=2,op='+'

规定,客户端输入格式如下所示:

 序列化后为:

Request报文格式

length\r\n__x __op __y\r\n

即第一个字段是报文长度,紧接着两个特殊字符\r\n 然后是x,y和op,中间用空格分开,最后再加上一个\r\n。

这个就是我们的报文! 

步骤如下:

当用户输入到客户端之后,客户端首先会将三个字段用结构体Request存起来,然后调用序列化的接口,把结构体序列化成一个Request字符串,就是上面我们展示的报文的格式。

当服务端收到这个Request报文之后,再调用反序列化的接口得到三个字段,得到一个结Request构体。

然后通过这个Request结构体的内容,计算得到结果后,形成Respone结构体,对Respone进行序列化,发送给客户端,客户端再把Respone反序列化,得到最后的结果。

Respones报文格式

length\r\n__x __op __y\r\n

Protocol.hpp



#ifndef __Yufc_Protocol_For_Cal
#define __Yufc_Protocol_For_Cal

#include <iostream>
#include <string>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"

/* 协议的本质 --> 约定! */

#define RECV_MAX_SIZE 1024

/* 请求协议是必须自带序列化功能的 一个结构体是很难发送的 需要序列化成string */
/* 上次0222定制的协议是不完善的!我们需要定制报文*/

// 协议
/* length\r\n__x __op __y\r\n*/
/* length这些就叫做协议报头 */

namespace yufc_ns_protocol
{
#define MYSELF true
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP)
    class Request
    {
    public:
        std::string Serialize()
        {
#if MYSELF
            std::string str;
            str = std::to_string(__x);
            str += SPACE;
            str += __op; // BUG
            str += SPACE;
            str += std::to_string(__y);
            return str;
#else
            /* 使用别人的序列化方案 */
            #include <jsoncpp/json/json.h>
            Json::Value root;
            root["x"] = __x;
            root["y"] = __y;
            root["op"] = __op;
            Json::FastWriter writer;
            return writer.write(root);
#endif
        }
        bool Deserialize(const std::string &str)
        {
#if MYSELF
            std::size_t left = str.find(SPACE);
            if (left == std::string::npos)
            {
                return false; // 反序列化失败
            }
            std::size_t right = str.rfind(SPACE);
            if (right == std::string::npos)
            {
                return false; // 反序列化失败
            }
            __x = atoi(str.substr(0, left).c_str()); // 拿到__x的字符串之后直接 atoi 就行
            __y = atoi(str.substr(right + SPACE_LEN).c_str());
            if (left + SPACE_LEN > str.size())
                return false;
            __op = str[left + SPACE_LEN];
            return true;
#else
            #include <jsoncpp/json/json.h>
            Json::Value root;
            Json::Reader reader;
            reader.parse(str, root);
            __x = root["x"].asInt();
            __y = root["y"].anInt();
            __op = root["op"].asInt();
            return true;
#endif
        }

    public:
        Request() {}
        Request(int x, int y, char op) : __x(x), __y(y), __op(op) {}
        ~Request() {}

    public:
        int __x;
        int __y;
        char __op; // '+' ...
    };

    class Response
    {
    public:
        /* "code result" */
        std::string Serialize()
        {
#if MYSELF
            std::string s;
            s = std::to_string(__code);
            s += SPACE;
            s += std::to_string(__result);
            return s;
#else
            #include <jsoncpp/json/json.h>
            Json::Value root;
            root["code"] = __code;
            root["result"] = __result;
            Json::FastWriter writer;
            return writer.write(root);
#endif
        }
        bool Deserialize(const std::string &s)
        {
#if MYSELF
            // std::cerr << "s: " << s << std::endl;
            std::size_t pos = s.find(SPACE);
            if (pos == std::string::npos)
                return false;
            __code = atoi(s.substr(0, pos).c_str());
            __result = atoi(s.substr(pos + SPACE_LEN).c_str());
            return true;
#else   
            Json::Value root;
            Json::Reader reader;
            reader.parse(s, root);
            __code = root["code"].asInt();
            __result = root["result"].asInt();
            return true;
#endif
        }

    public:
        Response() {}
        Response(int result, int code) : __result(result), __code(code) {}
        ~Response() {}

    public:
        int __result; // 计算结果
        int __code;   // 计算结果的状态码
        /* 0表示计算结果可信任 */
    };

    // 临时方案
    bool Recv(int sock, std::string *out)
    {
        char buffer[RECV_MAX_SIZE];
        memset(buffer, 0, sizeof buffer);
        ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if (s > 0)
        {
            buffer[s] = 0;
            *out += buffer;
        }
        else if (s == 0)
        {
            logMessage(NORMAL, "client quit");
            return false;
        }
        else
        {
            std::cerr << "recv error" << std::endl;
            return false;
        }
        return true;
    }

    // 临时方案
    void Send(int sock, const std::string sendStr)
    {
        send(sock, sendStr.c_str(), sendStr.size(), 0);
    }

    // "length\r\n__x __op __y\r\n"
    std::string Decode(std::string &buffer)
    {
        std::size_t pos = buffer.find(SEP);
        if (pos == std::string::npos) // 没有找到 返回空串
            return "";
        int size = atoi(buffer.substr(0, pos).c_str());
        int surplus = buffer.size() - pos - SEP_LEN - SEP_LEN; // 如果这个surplus大于报文长度
        // 则说明 这个报文长度是可以保证我们整体解析出一个合法字符串的
        if (surplus >= size)
        {
            // 至少具有一个合法报文,可以动手提取了
            buffer.erase(0, pos + SEP_LEN); // 此时"__x __op __y\r\n"
            std::string s = buffer.substr(0, size);
            buffer.erase(0, size + SEP_LEN);
            // 此时我们就把合法的部分截取出来了 就是s
            return s;
        }
        else
        {
            return "";
        }
    }

    std::string Encode(std::string &s)
    {
        // 添加报头
        std::string length = std::to_string(s.size());
        std::string new_package = length;
        new_package += SEP;
        new_package += s;
        new_package += SEP;
        return new_package;
    }

}

#endif

设置服务器进程为守护进程

 

CalServer.cc

#include "Protocol.hpp"
#include "TcpServer.hpp"
#include <memory>
#include <signal.h>
#include "Daemon.hpp"

static void Usage(const std::string &proc)
{
    std::cout << "\nUsage: " << proc << " port\n"
              << std::endl;
}

void debug(int sock)
{
    std::cout << "service for debug, sock: " << sock << std::endl;
}

static yufc_ns_protocol::Response calHelper(const yufc_ns_protocol::Request &req)
{
    yufc_ns_protocol::Response resp(0, 0);
    switch (req.__op)
    {
    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 (0 == req.__y)
            resp.__code = 1;
        else
            resp.__result = req.__x / req.__y;
        break;
    case '%':
        if (0 == req.__y)
            resp.__code = 2;
        else
            resp.__result = req.__x % req.__y;
        break;
    default:
        resp.__code = 3;
        break;
    }
    return resp;
}

void Calculator(int sock)
{
    /* 此处我们就要去定制协议 */
    std::string inbuffer;
    while (true)
    {
        // 1. 读取数据
        bool res = yufc_ns_protocol::Recv(sock, &inbuffer); // 在这里我们读到了一个请求
        // 2. 协议解析 -- 需要保证得到一个完整的报文
        if (!res)
            break;                                                // 读取失败
        std::string package = yufc_ns_protocol::Decode(inbuffer); // 这里Decode保证返回的是一个完整的字节流,是正确的,是可以序列化反序列化的!
        // 如果这个package是空 表示Decode没有给我们返回完整报文
        if (package.empty())
            continue;
        // 3. 保证该报文是一个完整的报文
        yufc_ns_protocol::Request req;
        // 4. 反序列化
        req.Deserialize(package);
        // 5. 业务逻辑
        yufc_ns_protocol::Response resp = calHelper(req);
        // 6. 序列化
        std::string respString = resp.Serialize(); // 序列化

        // 7. 在发送之前,添加报头
        // "length\r\ncode result\r\n"
        respString = yufc_ns_protocol::Encode(respString);
        // 8. 发送(暂时这样写,高级IO的时候,我们再来谈发送的逻辑)
        yufc_ns_protocol::Send(sock, respString); // 发送
    }
}

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    signal(SIGPIPE, SIG_IGN);
    /*
    一般经验: server 在编写的时候都要有严谨的判断逻辑
    一般的服务器,都是要忽略SIGPIPE信号的!防止运行中出现非法写入的问题
    */
    std::unique_ptr<yufc_tcpServer::TcpServer> server(new yufc_tcpServer::TcpServer(atoi(argv[1])));
    server->BindService(Calculator);
    MyDaemon();
    server->Start();
    return 0;
}

运行现象

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

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

相关文章

基于ssm的社区生活超市的设计与实现

博主介绍&#xff1a;专注于Java技术领域和毕业项目实战。专注于计算机毕设开发、定制、文档编写指导等&#xff0c;对软件开发具有浓厚的兴趣&#xff0c;工作之余喜欢钻研技术&#xff0c;关注IT技术的发展趋势&#xff0c;感谢大家的关注与支持。 技术交流和部署相关看文章…

SpringBoot拦截器

一、SpringBoot拦截器介绍 Spring Boot中的拦截器是一种用于在处理请求之前或之后执行特定操作的组件。拦截器通常用于实现对请求进行预处理、日志记录、权限验证等功能。 在Spring Boot中&#xff0c;可以使用HandlerInterceptor接口来定义自己的拦截器&#xff0c;并通过配…

流水灯实现

文章目录 一、流水灯二、代码实现三、引脚分配 一、流水灯 流水灯指的是LED像水流一样点亮&#xff0c;即LED依次点亮但不立刻熄灭&#xff0c;等到4个LED都点亮后&#xff0c;再把所有灯一次性熄灭。 二、代码实现 module horse_led(input wire clk,input wire rst_n,output…

记录管理系统

简单的记录管理系统&#xff0c;适用于保存表格数据&#xff0c;可以用来替代Excel软件保存数据&#xff0c;提供可视化拖动组件用于自定义数据列&#xff0c;数据存到数据库&#xff0c;相比于Excel&#xff0c;更易保存&#xff0c;易搜索。 例如创建合同记录数据&#xff0…

【电子学会】2023年05月图形化四级 -- 计算圆的面积和周长

计算圆的面积和周长 编写程序计算圆的面积和周长。输入圆的半径&#xff0c;程序计算出圆的面积和周长&#xff0c;圆的面积等于3.14*半径*半径&#xff1b;圆的周长等于2*3.14*半径。 1. 准备工作 &#xff08;1&#xff09;保留舞台中的小猫角色和白色背景&#xff1b; 2…

MySQL数据表高级操作

一、克隆/复制数据表二、清空表&#xff0c;删除表内的所有数据删除小结 三、创建临时表四、MySQL中6种常见的约束1、外键的定义2、创建外键约束作用3、创建主表test44、创建从表test55、为主表test4添加一个主键约束。主键名建议以"PK_”开头。6、为从表test5表添加外键&…

Html利用Canvas绘制图形

今天接到粉丝私信&#xff0c;询问是否可以通过Canvas绘制一些图形&#xff0c;然后根据粉丝提供的模板图&#xff0c;通过Canvas进行模拟绘制&#xff0c;通过分析发现&#xff0c;图形虽然相对简单&#xff0c;但是如果不借助相应的软件&#xff0c;纯代码绘制还是稍微费些时…

机器学习:self supervised learning- Recent Advances in pre-trained language models

背景 Autoregressive Langeuage Models 不完整的句子&#xff0c;预测剩下的空的词语 sentence completion Transformer-based ALMs Masked language models-MLMs 预训练模型能将输入文本转成hidden feature representation 模型参数最开始是从预训练模型中拿到&#xf…

如何快速制作一个奶茶店小程序商城

如果你是一个奶茶店的老板&#xff0c;你可能会考虑开设一个小程序商城来增加销售渠道和提升品牌形象。那么&#xff0c;如何快速制作一个奶茶店小程序商城呢&#xff1f;下面我们将介绍一个简单的步骤供你参考。 首先&#xff0c;你需要登录乔拓云平台进入商城后台管理页面。在…

数据结构真题

数据结构真题 1. A. Bills of Paradise 线段树并查集四个操作&#xff1a; D x。标记大于等于 x 的第一个未标记的 a i a_i ai​&#xff1b;若没有&#xff0c;则不操作.F x。查询大于等于 x 的第一个未标记的 a i a_i ai​&#xff1b;若没有&#xff0c;则输出 1 0 12…

《UNUX环境高级编程》(9)进程关系

1、前言 2、终端登录 在早期的UNIX系统&#xff0c;用户用哑终端&#xff08;用硬连接到主机&#xff09;进行登录&#xff0c;因为连接到主机上的终端设备数是固定的&#xff0c;所以同时登录数也就有了已知的上限。 随着位映射图像终端的出现&#xff0c;开发出了窗口系统&…

数学分析:对偶映射

这个其实就是我们一致讨论的对偶映射&#xff0c;换了个马甲&#xff0c;差点认不出来了。本来是V->R 要变成U->R&#xff0c;就需要一个反向的V*->U*的映射。 注意这个式子&#xff0c;t属于U&#xff0c;phit转到了V&#xff0c;但是坐标也发生了变化&#xff0c;这…

2023西南赛区ciscn -- do you like read

Attack 打开后一个商城页面 在login as admin那里有个登录页面&#xff0c;账号admin&#xff0c;密码爆破即可得到admin123 也可以在book.php?bookisbn1进行sql注入得到密码&#xff0c;这里发现是没有注入waf的 登录进来是一个Book List的管理页面&#xff0c;同时在审计源…

【C语言】初阶指针(详细版)

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前正在回炉重造C语言&#xff08;2023暑假&#xff09; ✈️专栏&#xff1a;【C语言航路】 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你…

RSA原理

RSA的历史 RSA加密算法是一种非对称加密算法&#xff0c;在公开密钥加密和电子商业中被广泛使用。RSA是由罗纳德李维斯特&#xff08;Ron Rivest&#xff09;、阿迪萨莫尔&#xff08;Adi Shamir&#xff09;和伦纳德阿德曼&#xff08;Leonard Adleman&#xff09;在1977年一…

本地推理,单机运行,MacM1芯片系统基于大语言模型C++版本LLaMA部署“本地版”的ChatGPT

OpenAI公司基于GPT模型的ChatGPT风光无两&#xff0c;眼看它起朱楼&#xff0c;眼看它宴宾客&#xff0c;FaceBook终于坐不住了&#xff0c;发布了同样基于LLM的人工智能大语言模型LLaMA&#xff0c;号称包含70亿、130亿、330亿和650亿这4种参数规模的模型&#xff0c;参数是指…

Lightening Network for Low-Light Image Enhancement 论文阅读笔记

这是2022年TIP期刊的一篇有监督暗图增强的文章 网络结构如图所示&#xff1a; LBP的网络结构如下&#xff1a; 有点绕&#xff0c;其基于的理论如下。就是说&#xff0c;普通的暗图增强就只是走下图的L1红箭头&#xff0c;从暗图估计一个亮图。但是其实这个亮图和真实的亮图…

54 # 可写流基本用法

内部也是基于 events 模块&#xff0c;fs.open、fs.write&#xff0c;如果文件不存在就会创建文件&#xff0c;默认会清空文件并写入 注意点&#xff1a;可写流的 highWaterMark 表示预期占用的内存&#xff08;达到或者超过预期后返回的值就是false&#xff09;&#xff0c;默…

确认应答机制与超时重发机制【TCP原理(笔记一)】

文章目录 通过序列号与确认应答提高可靠性正常的数据传输数据包丢失的情况确认应答丢失的情况发送的数据 重发超时如何确定 通过序列号与确认应答提高可靠性 在TCP中&#xff0c;当发送端的数据到达接收主机时&#xff0c;接收端主机会返回一个已收到消息的通知。这个消息叫做…

TCP的三次握手以及以段为单位发送数据【TCP原理(笔记二)】

文章目录 连接管理TCP以段为单位发送数据 连接管理 TCP提供面向有连接的通信传输。面向有连接是指在数据通信开始之前先做好通信两端之间的准备工作。 UDP是一种面向无连接的通信协议&#xff0c;因此不检查对端是否可以通信&#xff0c;直接将UDP包发送出去。TCP与此相反&am…