TCP定制协议,序列化和反序列化

目录

前言

1.理解协议

2.网络版本计算器

2.1设计思路

2.2接口设计

2.3代码实现:

2.4编译测试

总结


前言

        在之前的文章中,我们说TCP是面向字节流的,但是可能对于面向字节流这个概念,其实并不理解的,今天我们要介绍的是如何理解TCP是面向字节流的,通过编码的方式,自己定制协议,实现序列化和反序列化,相信看完这篇文章之后,关于TCP面向字节流的这个概念,你将会有一个清晰的认识,下面我们就一起来看看。

1.理解协议

        前面,我们通俗的介绍过协议,在网络中协议是属于一种约定,今天要说的是,数据在网络中传输的时候,协议又是如何体现的。

根据我们之前写的TCP服务器实现数据通信知道socket api的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?

什么是结构化的数据呢?

举个简单的例子,比如在微信上发送信息的时候,除了有发送的信息之外还包含有昵称,时间,头像这些信息,这些合起来就称为是结构化的数据。

所以我们将结构化的数据打包形成一个字符串的过程就称为是序列化,将打包形成的一个字符串转化为结构化数据的过程就称为是反序列化

如图所示:

TCP发送和接受数据的流程

如图所示:

作为程序员,在应用层定义一个缓冲区,然后send接口将数据发送,在这里发送不是将数据直接发送到网络里了,而是调用send接口将数据拷贝到传输层操作系统维护的缓冲区中,而read是将传输层的数据拷贝到应用层,当数据拷贝到传输层之后,剩下数据如何继续发送是由操作系统进行维护的,所以将TCP协议称为是传输控制协议,又因为TCP协议既可以是客户端向服务端发送信息,也可以是服务端向客户端发送信息,所以TCP是全双工的。

了解了TCP协议发送和接受数据的流程之后,因为TCP是面向字节流的,思考当数据由客户端发送给服务端的时候,有没有可能服务端的接受缓冲区中不足一个报文,有没有可能上层来不及处理导致服务端传输层接受缓冲区中有多个报文的情况,此时如何正确的拿到一个完整的报文呢?

因为这些问题的存在,所以我们要定制协议,明确一个完整报文大小,明确一个报文和一个报文的边界,所以我们要采取定制协议的方案获取到一个正确的报文。

一般采取的策略有三种:

1.定长

2.特殊符号

3.自描述的方式

下面我们按照上述的三种方式实现编码上的协议定制。

2.网络版本计算器

说明:为了演示协议定制和序列化以及反序列化在编码上如何实现,以及如何在编码上体现TCP面向字节流的特性,我们通过实现一个网络版本计算器为大家进行介绍

实现网络版本计算机约定:

客户端发送一个形如"1+1"的字符串;
这个字符串中有两个操作数, 都是整形;
两个数字之间会有一个字符是运算符, 运算符只能是 + ;
数字和运算符之间没有空格;

2.1设计思路

        客户端将想要计算的请求按照序列化的方式打包成一个字符串,然后发送给服务端,服务端按照定制协议的方式准确收到客户端的请求,然后服务端进行反序列化获取到结构化的数据,然后进行处理业务逻辑计算结果,结果计算完成之后,服务端将计算结果序列化打包形成一个字符串发送给客户端,然后客户端按照定制协议的方式准确获取到服务端发送过来的一个完整报文,至此就基于TCP协议实现了一个网络版本的计算器

2.2接口设计

要向完成上述的要求,就必须要包含几个接口:

a.请求的序列化和反序列化

b.响应的序列化和反序列化

c.协议定制

d.计算业务逻辑

e.准确获取一个报文

f.客户端和服务端编写

2.3代码实现:

1.请求的序列化和反序列化

class Request
{
public:Request():x(0),y(0),op(char()){}Request(int x_, int y_, char op_) : x(x_), y(y_), op(op_){}bool serialize(std::string *out){*out = "";// 结构化 -> "x op y";std::string x_string = std::to_string(x);std::string y_string = std::to_string(y);*out = x_string;*out += SEP;*out += op;*out += SEP;*out += y_string;return true;}// "x op yyyy";bool deserialize(const std::string &in){// "x op y" -> 结构化auto left = in.find(SEP);auto right = in.rfind(SEP);if (left == std::string::npos || right == std::string::npos)return false;if (left == right)return false;if (right - (left + SEP_LEN) != 1)return false;std::string x_string = in.substr(0, left); // [0, 2) [start, end) , start, end - startstd::string y_string = in.substr(right + SEP_LEN);if (x_string.empty())return false;if (y_string.empty())return false;x = stoi(x_string);y = stoi(y_string);op = in[left + SEP_LEN];return true;}public:int x;int y;char op;
};

序列化结果:将x,y,op - > 转化为 "x y op\r\n"

反序列化结果:"x y op\r\n" - > 转化为 x,y,op

2.响应的序列化和反序列化

#define SEP " "
#define SEP_LEN strlen(SEP) // 不敢使用sizeof()
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) // 不敢使用sizeof()
class Response
{
public:Response():exitcode(0),result(0) {}Response(int exitcode_, int result_) : exitcode(exitcode_), result(result_){}bool serialize(std::string *out){*out = "";std::string ec_string = std::to_string(exitcode);std::string res_string = std::to_string(result);*out = ec_string;*out += SEP;*out += res_string;return true;}bool deserialize(const std::string &in){// "exitcode result"auto mid = in.find(SEP);if (mid == std::string::npos)return false;std::string ec_string = in.substr(0, mid);std::string res_string = in.substr(mid + SEP_LEN);if (ec_string.empty() || res_string.empty())return false;exitcode = std::stoi(ec_string);result = std::stoi(res_string);return true;}
public:int exitcode;int result;
};

序列化结果:将exitcode,result - > 转化为 "exitcode result\r\n"

反序列化结果: "exitcode result\r\n" - > 转化为 exitcode,result

3.协议定制

说明:采用自描述的方式+特殊符号,给一个报文头部加上报文的长度,特殊符号"\r\n"用来区分报文长度和报文数据

#define SEP " "
#define SEP_LEN strlen(SEP) // 不敢使用sizeof()
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) // 不敢使用sizeof()
//enLength 和 deLength:打包和解包,解决服务端和客户端准确拿到数据
// "x op y" -> "content_len"\r\n"x op y"\r\n
// "exitcode result" -> "content_len"\r\n"exitcode result"\r\n
std::string enLength(const std::string &text)
{std::string send_string = std::to_string(text.size());send_string += LINE_SEP;send_string += text;send_string += LINE_SEP;return send_string;
}// "content_len"\r\n"exitcode result"\r\n
bool deLength(const std::string &package, std::string *text)
{auto pos = package.find(LINE_SEP);if (pos == std::string::npos)return false;std::string text_len_string = package.substr(0, pos);int text_len = std::stoi(text_len_string);*text = package.substr(pos + LINE_SEP_LEN, text_len);return true;
}

4.计算业务逻辑

//req是反序列化后的结果,根据res业务处理填充req即可
bool cal(const Request& req,Response& res)
{//req是结构化的数据,可以直接使用// req已经有结构化完成的数据啦,你可以直接使用res.exitcode = OK;res.result = OK;switch (req.op){case '+':res.result = req.x + req.y;break;case '-':res.result = req.x - req.y;break;case '*':res.result = req.x * req.y;break;case '/':{if (req.y == 0)res.exitcode = DIV_ZERO;elseres.result = req.x / req.y;}break;case '%':{if (req.y == 0)res.exitcode = MOD_ZERO;elseres.result = req.x % req.y;}break;default:res.exitcode = OP_ERROR;break;}return true;
}

5.准确获取一个报文

//从sock中读取数据保存到text中
//continue是因为tcp协议是面向字节流的,传输数据的时候可能不完整
bool recvPackage(int sock,string &inbuffer,string *text)
{char buffer[1024];while (true){ssize_t n = recv(sock, 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_string = inbuffer.substr(0, pos);int text_len = std::stoi(text_len_string);int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;// text_len_string + "\r\n" + text + "\r\n" <= inbuffer.size();std::cout << "处理前#inbuffer: \n" << inbuffer << std::endl;if (inbuffer.size() < total_len){std::cout << "你输入的消息,没有严格遵守我们的协议,正在等待后续的内容, continue" << std::endl;continue;}// 至少有一个完整的报文*text = inbuffer.substr(0, total_len);inbuffer.erase(0, total_len);std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;break;}elsereturn false;}return true;
}

注:看到这里我们就可以理解了TCP是面向字节流的概念了。

6.客户端和服务端实现:

calServer.hpp:

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include "log.hpp"
#include "protocol.hpp" //按照协议约定读取请求
using namespace std;
namespace server
{enum{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR};static const uint16_t gport = 8080;static const int gbacklog = 5;typedef function<bool(const Request& req,Response& res)> func_t;//读取请求,保证解耦void handlerEnter(int sock,func_t fun){string inbuffer;while(true){//1. 读取:"content_len"\r\n"x op y"\r\n// 1.1 你怎么保证你读到的消息是 【一个】完整的请求string req_text, req_str;if (!recvPackage(sock,inbuffer,&req_text))return;std::cout << "带报头的请求:\n" << req_text << std::endl;//req_str:获取报文if (!deLength(req_text, &req_str))return;std::cout << "去掉报头的正文:\n" << req_str << std::endl;// 2. 对请求Request,反序列化// 2.1 得到一个结构化的请求对象Request req;if(!req.deserialize(req_str))return;// 3. 计算机处理,req.x, req.op, req.y --- 业务逻辑// 3.1 得到一个结构化的响应Response res;fun(req,res);//req处理的结果放到res中,采用回调的方式保证上层业务逻辑和服务器的解耦// 4.对响应Response,进行序列化// 4.1 得到了一个"字符串"string resp_str;if(!res.serialize(&resp_str))return;std::cout << "计算完成, 序列化响应: " <<  resp_str << std::endl;// 5. 然后我们在发送响应// 5.1 构建成为一个完整的报文std::string send_string = enLength(resp_str);std::cout << "构建完成完整的响应\n" <<  send_string << std::endl;send(sock, send_string.c_str(), send_string.size(), 0); // 其实这里的发送也是有问题的,不过后面再说}}class CalServer{public:CalServer(const uint16_t &port = gport) : _listensock(-1), _port(port){}void initServer(){// 1. 创建socket文件套接字对象_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){logMessage(FATAL, "create socket error");exit(SOCKET_ERR);}logMessage(NORMAL, "create socket success: %d", _listensock);// 2. bind绑定自己的网络信息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(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind socket error");exit(BIND_ERR);}logMessage(NORMAL, "bind socket success");// 3. 设置socket 为监听状态if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑{logMessage(FATAL, "listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL, "listen socket success");}void start(func_t fun){for (;;){// 4. server 获取新链接// sock, 和client进行通信的fdstruct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(_listensock, (struct sockaddr *)&peer, &len);if (sock < 0){logMessage(ERROR, "accept error, next");continue;}logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // ?// version 2 多进程版(2)pid_t id = fork();if (id == 0) // child{close(_listensock);handlerEnter(sock,fun);close(sock);exit(0);}close(sock);// fatherpid_t ret = waitpid(id, nullptr, 0);if (ret > 0){logMessage(NORMAL, "wait child success"); // ?}}}~CalServer() {}private:int _listensock; // 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的!uint16_t _port;};} // namespace server

calClient.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>
#include "protocol.hpp"#define NUM 1024class CalClient
{
public:CalClient(const std::string &serverip, const uint16_t &serverport): _sock(-1), _serverip(serverip), _serverport(serverport){}void initClient(){// 1. 创建socket_sock = socket(AF_INET, SOCK_STREAM, 0);if (_sock < 0){std::cerr << "socket create error" << std::endl;exit(2);}}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());if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0){std::cerr << "socket connect error" << std::endl;}else{std::string line;std::string inbuffer;while (true){std::cout << "mycal>>> ";std::getline(std::cin, line);  // 1+1Request req = ParseLine(line); // "1+1"std::string content;req.serialize(&content);std::string send_string = enLength(content);send(_sock, send_string.c_str(), send_string.size(), 0); // bug?? 不管std::string package, text;//  "content_len"\r\n"exitcode result"\r\nif (!recvPackage(_sock, inbuffer, &package))continue;if (!deLength(package, &text))continue;// "exitcode result"Response resp;resp.deserialize(text);std::cout << "exitCode: " << resp.exitcode << std::endl;std::cout << "result: " << resp.result << std::endl;}}}Request ParseLine(const std::string &line){// 建议版本的状态机!//"1+1" "123*456" "12/0"int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后int i = 0;int cnt = line.size();std::string left, right;char op;while (i < cnt){switch (status){case 0:{if(!isdigit(line[i])){op = line[i];status = 1;}else left.push_back(line[i++]);}break;case 1:i++;status = 2;break;case 2:right.push_back(line[i++]);break;}}std::cout << std::stoi(left)<<" " << std::stoi(right) << " " << op << std::endl;return Request(std::stoi(left), std::stoi(right), op);}~CalClient(){if (_sock >= 0)close(_sock);}private:int _sock;std::string _serverip;uint16_t _serverport;
};

2.4编译测试

如图所示:我们准确的实现了网络版本计算器

总结

        通过上面代码的编写,包含定制协议,序列化和反序列代码的实现,我们就能够理解协议在网络传输的重要性了,以及理解了TCP是面向字节流的概念。感谢大家的观看,希望能够帮助到大家,我们下次再见。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

派森 #P122. 峰值查找

描述 给定一个长度为n的列表nums&#xff0c;请你找到峰值并返回其索引。数组可能包含多个峰值&#xff0c;在这种情况下&#xff0c;返回任何一个所在位置即可。 &#xff08;1&#xff09;峰值元素是指其值严格大于左右相邻值的元素。严格大于即不能有等于&#xff1b; &…

docker的资源控制及数据管理

docker的资源控制及docker数据管理 一.docker的资源控制 1.CPU 资源控制 1.1 资源控制工具 cgroups&#xff0c;是一个非常强大的linux内核工具&#xff0c;他不仅可以限制被 namespace 隔离起来的资源&#xff0c; 还可以为资源设置权重、计算使用量、操控进程启停等等。 …

kafka晋升之路-理论+场景

kafka晋升之路 一&#xff1a;故事背景二&#xff1a;核心概念2.1 系统架构2.2 生产者&#xff08;Producer&#xff09;2.2.1 生产者分区2.2.2 生产者分区策略 2.3 经纪人&#xff08;Broker&#xff09;2.3.1 主题&#xff08;Topic&#xff09;2.3.2 分区&#xff08;Partit…

07 - 查看、创建、切换和删除分支

查看所有文章链接&#xff1a;&#xff08;更新中&#xff09;GIT常用场景- 目录 文章目录 1. 查看分支2. 创建和切换分支3. 删除分支 1. 查看分支 git branch -va2. 创建和切换分支 第一种&#xff1a; 创建分支&#xff1a; git branch new_branch切换分支&#xff1a; …

代码生成综述

代码生成大模型属于LLM模型的一个子类&#xff0c;理论来讲也是属于语言模型的一种特例。代码本身其实也是一种特殊的语言表示&#xff0c;所以代码模型的实现应该是具备通用自然语言和代码两部分的能力。实际的代码模型也是有两条路径来实现&#xff0c;让训练好的NLP LLM模型…

【2023年11月第四版教材】《第5章-信息系统工程之软件工程(第二部分)》

《第5章-信息系统工程之软件工程&#xff08;第二部分&#xff09;》 1.3 软件设计1.4 软件实现&#xff3b;补充第三版教材内容&#xff3d; 1.5 部署交付 1.3 软件设计 1、结构化设计SD是一种面向数据流的方法&#xff0c;它以SRS和SA阶段所产生的DFD和数据字 典等文档为基础…

无人机精细化巡检方案制定:提高效率与准确性的关键

在当前技术日新月异的时代&#xff0c;无人机在多个领域的应用已成为行业标配。但如何制定出一套有效、细致的无人机巡检方案&#xff0c;确保其最大效能&#xff0c;成为许多组织与公司的核心议题。其中&#xff0c;复亚智能在此领域已展现出了卓越的实力与深入的见解。 1. 精…

TCP中窗口和滑动窗口的含义以及流量控制

一.窗口 在TCP中由于要保证可靠性&#xff0c;所以每发送一条数据后&#xff0c;都需要接收方返回一条应答报文&#xff0c;要是我们每发送一条数据&#xff0c;发送方就等待接收应答报文&#xff0c;收到之后再去发送下一条数据&#xff0c;这样我们就会花费大量的时间在等待应…

一文预览 | 8 月 16 日 NVIDIA 在 WAVE SUMMIT深度学习开发者大会 2023精彩亮点抢先看!

由深度学习技术及应用国家工程研究中心主办&#xff0c;百度飞桨和文心大模型承办的 WAVE SUMMIT深度学习开发者大会2023&#xff0c;将于 8 月 16 日在北京与大家见面。NVIDIA 作为技术合作伙伴&#xff0c;将携手百度飞桨参与这场技术盛会。 在这次大会中&#xff0c;NVIDIA…

【从零开始的rust web开发之路 二】axum中间件和共享状态使用

系列文章目录 第一章 axum学习使用 第二章 axum中间件使用 文章目录 系列文章目录前言一、中间件是什么二、中间件使用常用中间件使用中间件使用TraceLayer中间件实现请求日志打印自定义中间件 共享状态 前言 上篇文件讲了路由和参数相应相关的。axum还有个关键的地方是中间件…

微信小程序 游戏水平评估系统的设计与实现_pzbe0

近年来&#xff0c;随着互联网的蓬勃发展&#xff0c;游戏公司对信息的管理提出了更高的要求。传统的管理方式已无法满足现代人们的需求。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;随着各行业的不断发展&#xff0c;使命召…

【C++习题集】-- 堆

&#xff08;用于复习&#xff09; 目录 树概念及结构 名词概念 二叉树概念及结构 特殊的二叉树 满二叉树 完全二叉树 运算性质 二叉树存储结构 顺序存储 链式存储 堆 - 顺序存储 堆的性质 堆的实现 堆的应用 堆排序 直接建堆法 树概念及结构 概念&#xff1a…

【目标检测中对IoU的改进】GIoU,DIoU,CIoU的详细介绍

文章目录 1、IoU2、GIoU(Generalized Intersection over Union)3、DIoU4、CIoU 1、IoU IoU为交并比&#xff0c;即对于pred和Ground Truth&#xff1a;交集/并集 1、IoU可以作为评价指标使用&#xff0c;也可以用于构建IoU loss 1 - IoU 缺点&#xff1a; 2、对于pred和GT相…

[MySQL]主从服务器布置

配置主服务器 配置文件 /etc/my.cnf 在[mysqld]下进行配置 log_binON //启动二进制日志 log-bin mysql-bin //启用二进制日志&#xff0c;用于记录主服务器的更新操作 server-id 1 // 用来表示mysql服务id,保证集成环境中的唯一性 , 范围 [1,2^32) read-only0 // 1表示只…

ChatGLM-6B微调记录

目录 GLM-130B和ChatGLM-6BChatGLM-6B直接部署基于PEFT的LoRA微调ChatGLM-6B GLM-130B和ChatGLM-6B 对于三类主要预训练框架&#xff1a; autoregressive&#xff08;无条件生成&#xff09;&#xff0c;GPT的训练目标是从左到右的文本生成。autoencoding&#xff08;语言理解…

如何在windows电脑安装多个tomcat服务器和乱码问题

前提条件安装jdk 以17版本为例&#xff0c;将jdk8卸载干净 1.首先进入tomcat官网下载 tomcat网址 这里下载tomcat10为例子 1.1 这里选择方式一 下载解压版 2.解压后拷贝三份 分别命名为 8081、 8082、 8083 3.分别对每个tomcat执行以下操作 3.1 找到tomcat所在webapps文…

java的类和对象详解

一、java是面向对象的编程语言 首先一般的编程语言有两种&#xff0c;一种是面向对象&#xff0c;一种是面向过程。前者更加关注代码中对象与对象之间关系与协作&#xff0c;而后者更加注重代码的执行过程。 举个例子 传统的方式&#xff1a;注重的是洗衣服的过程&#xff0c;…

引领行业高质量发展|云畅科技参编《低代码开发平台创新发展路线图(2023)》

8月8日-9日&#xff0c;中国电子技术标准化研究院于北京顺利召开《低代码开发平台创新发展路线图&#xff08;2023&#xff09;》封闭编制会。云畅科技、浪潮、百度、广域铭岛等来自低代码开发平台解决方案供应商、用户方、科研院所等近30家相关单位的40余位专家参与了现场编制…

CentOS 8.5修改安装包镜像源

1 备份原配置 cd /etc/yum.repos.d mkdir backup mv *.repo backup/2 下载镜像源 2.1 使用wget下载 wget http://mirrors.aliyun.com/repo/Centos-8.repo2.2 使用curl下载 我是安装的最小版本的系统&#xff0c;默认只有curl curl使用方法&#xff1a;https://www.ruanyife…

【分布式共识】Multi-Paxos 算法思想

上一篇文章主要聊了Basic Paxos算法&#xff0c;而Multi-Paxos并不是一种算法&#xff0c;是一种算法思想。具体就是Basic Paxos解决的是对一个值达成共识。而后者是通过执行多次的Basic Paxos算法就多个值达成一致。具体的落地实现有Raft。 Muti-Paxos的问题 在Basic Paxos中…