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.mzph.cn/news/46103.shtml

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

相关文章

nginx反向代理后实现nginx和apache两种web服务器能够记录客户端的真实IP地址

一.构建环境 二.配置反向代理 1.基于源码安装的nginx环境下修改nginx.conf&#xff08;设备1&#xff09; 2.通过windows powershell进行修改hosts文件并测试 3.设备2和设备3上查看日志&#xff0c;可以看到访问来源都是代理服务器&#xff08;2.190&#xff09;而不是真实…

WebSocket服务端数据推送及心跳机制(Spring Boot + VUE)

一、WebSocket简介 HTML5规范在传统的web交互基础上为我们带来了众多的新特性&#xff0c;随着web技术被广泛用于web APP的开发&#xff0c;这些新特性得以推广和使用&#xff0c;而websocket作为一种新的web通信技术具有巨大意义。WebSocket是HTML5新增的协议&#xff0c;它的…

Python Web开发 Django 简介

今天来为大家介绍 Python 另一个 Web 开发框架 Django&#xff0c;它是一个基于 Python 定制的开源 Web 应用框架&#xff0c;最早源于一个在线新闻 Web 网站&#xff0c;后于2005年开源。Django 的功能大而全&#xff0c;它提供的一站式解决的思路&#xff0c;能让开发者不用在…

支持https访问

文章目录 1. 打开自己的云服务器的 80 和 443 端口2. 安装 nginx3. 安装 snapd4. 安装 certbot5. 生成证书6. 拷贝生成的证书到项目工作目录7. 修改 main.go 程序如下8. 编译程序9. 启动程序10. 使用 https 和端口 8081 访问页面成功11. 下面修改程序&#xff0c;支持 https 和…

C++中function,bind,lambda

c11之前&#xff0c;STL中提供了bind1st以及bind2nd绑定器 首先来看一下他们如何使用&#xff1a; 如果我们要对vector中的元素排序&#xff0c;首先会想到sort&#xff0c;比如&#xff1a; void output(const vector<int> &vec) {for (auto v : vec) {cout <&l…

js 中的原型

JavaScript规定&#xff0c;每一个构造函数都有一个prototype属性&#xff0c;指向另一个对象&#xff0c;所以我们也称为原型对象。这个对象可以挂载函数&#xff0c;对象实例化不会多次创建原型上函数&#xff0c;节约内存。我们可以把那些不变的方法&#xff0c;直接定义在p…

【Linux】传输层协议:UDP和TCP

争做西格玛男人 文章目录 一、UDP协议1.端口号2.理解UDP报头3.UDP的特点&#xff08;面向数据报&#xff0c;全双工&#xff09; 二、TCP协议1.理解TCP报头某些TCP的策略1.1 TCP报头字段&#xff08;TCP的黏包问题&#xff09;1.2 网络协议栈和linux系统的联系&#xff08;以p…

聚观早报|京东称在技术投入没有止境;木蚁机器人完成B2轮融资

【聚观365】8月18日消息 京东零售CEO表示在技术上投入没有止境 木蚁机器人完成B2轮超亿元融资 耐能推出AI芯片KL730 三星电子泰勒晶圆厂首家客户是AI半导体厂商 韩国新能源汽车7月出口额同比大增36% 京东零售CEO表示在技术上投入没有止境 近日&#xff0c;京东零售CEO辛利…

0基础学习VR全景平台篇 第88篇:智慧眼-成员管理

一、功能说明 成员管理&#xff0c;是指管理智慧眼项目的成员&#xff0c;拥有相关权限的人可以进行添加成员、分配成员角色、设置成员分类、修改成员以及删除成员五项操作。但是仅限于管理自己的下级成员&#xff0c;上级成员无权管理。 二、前台操作页面 登录智慧眼后台操…

Maven之Servlet 版本问题

maven-archetype-webapp 骨架的 Servlet 版本问题 通过 maven-archetype-webapp 骨架去创建 java web 项目时&#xff0c;自动生成的 web.xml 配置文件所使用的 Servlet 的版本比较低&#xff08;2.3&#xff09;&#xff0c;而在低版本的 Servlet 中 EL 表达式默认是关闭的。…

iPhone卫星通信SOS功能如何在灾难中拯救生命

iPhone上的卫星紧急求救信号功能在从毛伊岛野火中拯救一家人方面发挥了至关重要的作用。这是越来越多的事件的一部分&#xff0c;在这些事件中&#xff0c;iPhone正在帮助人们摆脱危及生命的情况。 卫星提供商国际通信卫星组织负责移动的高级副总裁Mark Rasmussen在接受Lifewir…

docker compose部署zookeeper

单机部署 新建docker-compose.yaml version: 3 services:zookeeper:image: zookeeper:3.5.7container_name: base-zookeeperhostname: zookeeperprivileged: truerestart: alwaysports:- 2181:2181environment:TZ: "Asia/Shanghai"volumes:- ./volumes/zookeeper/d…

Python web实战之Django的国际化和本地化详解

关键词&#xff1a;Django、Python、Web开发、国际化&#xff08;i18n&#xff09;、本地化&#xff08;l10n&#xff09; 今天我要和大家分享一下 Python Web 开发中的一个重要话题——Django 的国际化和本地化。 1. 国际化和本地化 你有没有想过如何让你的网站在全球范围内…

fastadmin 下拉多级分类

要实现下图效果 一、先创建数据表 二、在目标的controll中引入use fast\Tree; public function _initialize() {parent::_initialize();$this->model new \app\admin\model\zxdc\Categorys;$tree Tree::instance();$tree->init(collection($this->model->order(…

框架分析(1)-IT人必须会

框架分析&#xff08;1&#xff09;-IT人必须会 专栏介绍当今主流框架前端框架后端框架移动应用框架数据库框架测试框架 Angular关键特点和功能&#xff1a;组件化架构双向数据绑定依赖注入路由功能强大的模板语法测试友好 优缺点分析优点缺点 总结 专栏介绍 link 主要对目前市…

NineData x SelectDB 完成产品兼容互认证

近日&#xff0c;新一代实时数据仓库厂商 SelectDB 与云原生智能数据管理平台 NineData 完成产品兼容互认证。经过严格的联合测试&#xff0c;双方软件完全相互兼容、功能完善、整体运行稳定且性能表现优异。基于本次的合作&#xff0c;双方将进一步为数据管理与大数据分析业务…

el-table 多个表格切换多选框显示bug

今天写了个功能&#xff0c;点击左侧的树做判断&#xff0c;一级树节点显示系统页面&#xff0c;二级树节点显示数据库页面&#xff0c;三级树节点显示表页面。 数据库页面和表页面分别有2个el-table ,上面的没有多选框&#xff0c;下面的有多选框 现在出现bug&#xff0c;在…

小型双轮差速底盘实现悬崖巡检功能

1. 功能说明 本文示例将实现R023样机小型双轮差速底盘悬崖巡检的功能。在小型双轮差速底盘上安装一个检测装置&#xff0c;它可以由1个 近红外传感器 和1个 灰度传感器 组成。近红外传感器可以识别桌面&#xff0c;灰度传感器可以识别“悬崖”&#xff0c;让机器人沿着“悬崖”…

【MySQL】事务

事务&#xff0c;我们一直没有提到过它&#xff0c;它并不影响我们书写正确的sql语句&#xff0c;但并不意味着事务就不重要 目录 一、事务的概念 二、为什么会出现事务 三、事务的版本支持 四、事务的提交方式 五、事务的常见操作方式 5.1 准备 5.2 正常演示 5.2.1 开…

linux-shell编程

shell编程 1. 变量、条件判断、流程控制、函数 $n &#xff08;功能描述&#xff1a;n为数字&#xff0c;$0代表该脚本名称&#xff0c;$1-$9代表第一到第九个参数&#xff0c;十以上的参数&#xff0c;十以上的参数需要用大括号包含&#xff0c;如${10}&#xff09; $# &…