【在Linux世界中追寻伟大的One Piece】应用层自定义协议|序列化

news2024/10/19 7:39:30

目录

1 -> 应用层

2 -> 网络版计算器

3 -> 序列化与反序列化

4 -> 重新理解read、write、recv、send和tcp为什么支持全双工

5 -> 开始实现

5.1 -> 定制协议

5.2 -> 关于流式数据的处理


1 -> 应用层

应用层是OSI模型或TCP/IP模型中的最高层,它直接为用户的应用程序提供网络服务。应用层的主要功能包括:

  • 用户交互:提供用户与计算机网络交互的界面,允许用户访问网络资源、发送和接收数据、运行应用程序等。
  • 数据处理:允许用户在端系统上进行文档编辑、数据存储和处理等操作。
  • 网络通信:通过网络接口与计算机网络进行通信,发送和接收数据,与其他端系统或网络设备进行交互。
  • 支持多种网络应用模型:如客户/服务器模型(C/S模型)和对等网络模型(P2P模型),这些模型定义了应用程序之间通信和服务提供的方式。
  • 提供网络服务:如域名解析系统(DNS)、文件传输协议(FTP)、电子邮件传输协议(SMTP、POP3、IMAP)和超文本传输协议(HTTP)等。
  • 数据表示和转换:确保不同系统和应用程序之间的数据能够正确理解和处理,包括数据格式转换、字符编码、数据压缩和数据加密。
  • 会话管理:管理应用程序之间的会话,包括会话的建立、维护和终止。
  • 错误处理和恢复:处理通信过程中的错误,并提供相应的恢复机制。
  • 用户接口:提供图形用户界面(GUI)或命令行界面(CLI),使用户能够方便地使用网络服务。

2 -> 网络版计算器

例如,我们需要实现一个服务器版的加法器。我们需要客户端把要计算的两个加数发过去,然后由服务器进行计算,最后再把结果返回给客户端。

约定方案一:

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

约定方案二:

  • 定义结构体来表示我们需要交互的信息。
  • 发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转化回结构体。
  • 这个过程叫做"序列化"和"反序列化"。

3 -> 序列化与反序列化

无论我们采用方案一,还是方案二,还是其他的方案,只要保证,一端发送时构造的数据,在另一端能够正确的进行解析,就是OK的。这种约定,就是应用层协议

但是,为了让我们深刻理解协议,我们打算自定义实现一下协议的过程。

  • 采用方案2,我们也要体现协议定制的细节。
  • 引入序列化和反序列化。
  • 要对socket进行字节流的读取处理。

4 -> 重新理解read、write、recv、send和tcp为什么支持全双工

  • 在任何一台主机上,TCP连接既有发送缓冲区,又有接受缓冲区,所以,在内核中,可以在发消息的同时,也可以收消息,即全双工
  • 这就是为什么一个tcp sockfd读写都是它的原因。
  • 实际数据什么时候发,发多少,出错了怎么办,由TCP控制,所以TCP叫做传输控制协议

5 -> 开始实现

代码结构

Calculate.hpp Makefile Socket.hpp TcpServer.hpp
Daemon.hpp Protocol.hpp TcpClientMain.cc TcpServerMain.cc
// 简单起见,可以直接采用自定义线程
// 建议不用用户输入,直接 client<<->>server 通信,这样可以省去编写没有干货的代码

Socket封装

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;
		// TODO
	public:
		void BuildListenSocketMethod(uint16_t port, int backlog)
		{
			CreateSocketOrDie();
			BindSocketOrDie(port);
			ListenSocketOrDie(backlog);
		}

		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;
			memset(&local, 0, sizeof(local));

			local.sin_family = AF_INET;
			local.sin_addr.s_addr = INADDR_ANY;
			local.sin_port = htons(port);

			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);
			int newsockfd = ::accept(_sockfd, Convert(&peer),
				&len);
			if (newsockfd < 0)
				return nullptr;

			*peerport = ntohs(peer.sin_port);
			*peerip = inet_ntoa(peer.sin_addr);
			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;
	};
}

5.1 -> 定制协议

基本结构

定制基本的结构化字段,这个就是协议。

class Request
{
private:
	// _data_x _oper _data_y
	// 报文的自描述字段
	// "len\r\nx op y\r\n" : \r\n 不属于报文的一部分,约定
	// 很多工作都是在做字符串处理!
	int _data_x; // 第一个参数
	int _data_y; // 第二个参数
	char _oper; // + - * / %
};

class Response
{
private:
	// "len\r\n_result _code\r\n"
	int _result; // 运算结果
	int _code; // 运算状态
};

protocol.hpp

#pragma once
#include <iostream>
#include <memory>
#include <jsoncpp/json/json.h>

namespace Protocol
{
	// 问题
	// 1. 结构化数据的序列和反序列化
	// 2. 还要解决用户区分报文边界 --- 数据包粘报问题
	// 讲法
	// 1. 自定义协议
	// 2. 成熟方案序列和反序列化
	// "protocol_code\r\nlen\r\nx op y\r\n" : \r\n 不属于报文的一部分,约定
	const std::string ProtSep = " ";
	const std::string LineBreakSep = "\r\n";

	// "len\r\nx op y\r\n" : \r\n 不属于报文的一部分,约定
	std::string Encode(const std::string& message)
	{
		std::string len = std::to_string(message.size());
		std::string package = len + LineBreakSep + message +
			LineBreakSep;

		return package;
	}
	// "len\nx op y\n" : \n 不属于报文的一部分,约定
	// 我无法保证 package 就是一个独立的完整的报文
	// "l
	// "len
	// "len\r\n
	// "len\r\nx
	// "len\r\nx op
	// "len\r\nx op y
	// "len\r\nx op y\r\n"
	// "len\r\nx op y\r\n""len
	// "len\r\nx op y\r\n""len\n
	// "len\r\nx op
	// "len\r\nx op y\r\n""len\nx op y\r\n"
	// "len\r\nresult code\r\n""len\nresult code\r\n"
	bool Decode(std::string &package, std::string *message)
	{
		// 除了解包,我还想判断报文的完整性, 能否正确处理具有"边界"的报文
		auto pos = package.find(LineBreakSep);
		if (pos == std::string::npos)
			return false;

		std::string lens = package.substr(0, pos);
		int messagelen = std::stoi(lens);
		int total = lens.size() + messagelen + 2 *
			LineBreakSep.size();
		if (package.size() < total)
			return false;

		// 至少 package 内部一定有一个完整的报文了!
		*message = package.substr(pos + LineBreakSep.size(),
			messagelen);
		package.erase(0, total);

		return true;
	}

	class Request
	{
	public:
		Request() : _data_x(0), _data_y(0), _oper(0)
		{
		}

		Request(int x, int y, char op) : _data_x(x), _data_y(y),
			_oper(op)
		{
		}

		void Debug()
		{
			std::cout << "_data_x: " << _data_x << std::endl;
			std::cout << "_data_y: " << _data_y << std::endl;
			std::cout << "_oper: " << _oper << std::endl;
		}

		void Inc()
		{
			_data_x++;
			_data_y++;
		}

		// 结构化数据->字符串
		bool Serialize(std::string* out)
		{
			Json::Value root;
			root["datax"] = _data_x;
			root["datay"] = _data_y;
			root["oper"] = _oper;
			Json::FastWriter writer;
			*out = writer.write(root);

			return true;
		}

		bool Deserialize(std::string & in) // "x op y" [)
		{
			Json::Value root;
			Json::Reader reader;
			bool res = reader.parse(in, root);
			if (res)
			{
				_data_x = root["datax"].asInt();
				_data_y = root["datay"].asInt();
				_oper = root["oper"].asInt();
			}

			return res;
		}

		int GetX() 
		{ 
			return _data_x; 
		}

		int GetY() 
		{ 
			return _data_y; 
		}

		char GetOper() 
		{ 
			return _oper; 
		}

	private:
		// _data_x _oper _data_y
		// 报文的自描述字段
		// "len\r\nx op y\r\n" : \r\n 不属于报文的一部分,约定
		// 很多工作都是在做字符串处理!
		int _data_x; // 第一个参数
		int _data_y; // 第二个参数
		char _oper; // + - * / %
	};

	class Response
	{
	public:
		Response() : _result(0), _code(0)
		{
		}

		Response(int result, int code) : _result(result),
			_code(code)
		{
		}

		bool Serialize(std::string * out)
		{
			Json::Value root;
			root["result"] = _result;
			root["code"] = _code;
			Json::FastWriter writer;
			*out = writer.write(root);

			return true;
		}

		bool Deserialize(std::string & in) // "_result _code" [)
		{
			Json::Value root;
			Json::Reader reader;
			bool res = reader.parse(in, root);
			if (res)
			{
				_result = root["result"].asInt();
				_code = root["code"].asInt();
			}

			return res;
		}

		void SetResult(int res) 
		{ 
			_result = res; 
		}

		void SetCode(int code) 
		{ 
			_code = code; 
		}

		int GetResult() 
		{ 
			return _result; 
		}

		int GetCode() 
		{ 
			return _code; 
		}

	private:
		// "len\r\n_result _code\r\n"
		int _result; // 运算结果
		int _code; // 运算状态
	};

	// 简单的工厂模式,建造类设计模式
	class Factory
	{
	public:
		std::shared_ptr<Request> BuildRequest()
		{
			std::shared_ptr<Request> req =
				std::make_shared<Request>();

			return req;
		}

		std::shared_ptr<Request> BuildRequest(int x, int y, char
			op)
		{
			std::shared_ptr<Request> req =
				std::make_shared<Request>(x, y, op);

			return req;
		}

		std::shared_ptr<Response> BuildResponse()
		{
			std::shared_ptr<Response> resp =
				std::make_shared<Response>();

			return resp;
		}

		std::shared_ptr<Response> BuildResponse(int result, int
			code)
		{
			std::shared_ptr<Response> req =
				std::make_shared<Response>(result, code);

			return req;
		}
	};
}

期望的报文格式

5.2 -> 关于流式数据的处理

  • 如何保证你每次读取就能读完请求缓冲区的所有内容?
  • 怎么保证读取完毕或者读取没有完毕的时候,读到的就是一个完整的请求呢?
  • 处理TCP缓冲区中的数据,一定要保证正确处理请求。
const std::string ProtSep = " ";
const std::string LineBreakSep = "\n";
// "len\nx op y\n" : \n 不属于报文的一部分,约定

std::string Encode(const std::string& message)
{
	std::string len = std::to_string(message.size());
	std::string package = len + LineBreakSep + message +
		LineBreakSep;

	return package;
}

// "len\nx op y\n" : \n 不属于报文的一部分,约定
// 我无法保证 package 就是一个独立的完整的报文
// "l
// "len
// "len\n
// "len\nx
// "len\nx op
// "len\nx op y
// "len\nx op y\n"
// "len\nx op y\n""len
// "len\nx op y\n""len\n
// "len\nx op
// "len\nx op y\n""len\nx op y\n"
// "len\nresult code\n""len\nresult code\n"
bool Decode(std::string& package, std::string* message)
{
	// 除了解包,我还想判断报文的完整性, 能否正确处理具有"边界"的报文
	auto pos = package.find(LineBreakSep);
	if (pos == std::string::npos)
		return false;

	std::string lens = package.substr(0, pos);
	int messagelen = std::stoi(lens);
	int total = lens.size() + messagelen + 2 *
		LineBreakSep.size();
	if (package.size() < total)
		return false;

	// 至少 package 内部一定有一个完整的报文了!
	*message = package.substr(pos + LineBreakSep.size(),
		messagelen);
	package.erase(0, total);

	return true;
}

所以,完整的处理过程应该是:


感谢各位大佬支持!!!

互三啦!!!

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

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

相关文章

【C++贪心】2712. 使所有字符相等的最小成本|1791

本文涉及知识点 C贪心 LeetCode2712. 使所有字符相等的最小成本 给你一个下标从 0 开始、长度为 n 的二进制字符串 s &#xff0c;你可以对其执行两种操作&#xff1a; 选中一个下标 i 并且反转从下标 0 到下标 i&#xff08;包括下标 0 和下标 i &#xff09;的所有字符&am…

软件设计模式------简单工厂模式

简单工厂模式&#xff08;Simple factory Pattern&#xff09;&#xff0c;又称静态工厂方法(Static Factory Method),属于创新型模式&#xff0c;但它不属于GoF23个设计模式其一。 一、模式动机&#xff1a; 有时需要创建一些来自相同父类的类的实例。 二、定义&#xff1a…

Java基于SpringBoot微信小程序的跳蚤市场系统设计与实现(lw+数据库+讲解等)

项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念&#xff0c;提供了一套默认的配置&#xff0c;让开发者可以更专注于业务逻辑而不是配置文件。Spring Boot 通过自动化配置和约…

【数据分享】中国历史学年鉴(1979-2001)

数据介绍 目录如下&#xff1a; 特稿 2000年国际历史科学大会 史学研究 史学理论 西周春秋战国史 秦汉史 魏晋南北朝史 隋唐五代史 宋史 辽西夏金史 蒙元史 明史 清史 晚清政治史 近代文化史 中外关系史 近代经济史 近代社会史 近代思想史 民国政治史 世…

Navigation2 算法流程

转自 https://zhuanlan.zhihu.com/p/405670882 此文仅作学习笔记 启动流程 在仿真环境中启动导航包的示例程序&#xff0c;执行nav2_bringup/bringup/launch/tb3_simulation_launch.py文件。ROS2的launch文件支持采用python语言来编写以支持更加复杂的功能&#xff0c;本文件…

React高级Hook

useReducer useReducer 是 React 提供的一个 Hook&#xff0c;用于在函数组件中使用 reducer 函数来管理组件的 state。它类似于 Redux 中的 reducer&#xff0c;但仅用于组件内部的状态管理。useReducer 可以使复杂的状态逻辑更加清晰和可维护。 基本用法 useReducer 接收…

【SQL实验】数据库、表、模式的SQL语句操作

完整代码在文章末尾 1、数据库的建立、删除和修改操作 &#xff08;1&#xff09;使用SQL语句创建数据库EDUC&#xff0c;并进行如下设置&#xff1a; 数据库文件和日志文件的逻辑名称分别为&#xff1a;Student_data和Student_log&#xff1b;数据文件的物理文件名为‘C:\DA…

【Linux】:线程概念

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家带来线程概念相关代码和知识点&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通 数…

Oracle T5-2 ILOM配置

ILOM管理口ip地址配置 连接控制器&#xff08;SP&#xff09;串口&#xff08;RJ45)&#xff0c;进行系统设置 (缺省&#xff1a;9600&#xff0c;8-n-1&#xff0c;root/changeme) …………………. ORACLESP-AK02566506 login: root Password: Detecting screen size; pl…

CSS3 动画相关属性实例大全(1)(@keyframes ,background属性,border 属性)

CSS3 动画相关属性实例大全&#xff08;1) (CSS 中有动画效果的属性图例大全) 本文目录&#xff1a; 零、时光宝盒 一、CSS3 动画基本概念 &#xff08;1&#xff09;、CSS3的动画基本属性 &#xff08;2&#xff09;、keyframes 规则和所有动画属性 二、CSS 中有动画效果…

SQLI LABS | Less-1 GET - Error based - Single Quotes - String

关注这个靶场的其它相关笔记&#xff1a;SQLI LABS —— 靶场笔记合集-CSDN博客 0x01&#xff1a;过关流程 输入下面的链接进入靶场&#xff08;如果你的地址和我不一样&#xff0c;按照你本地的环境来&#xff09;&#xff1a; http://localhost/sqli-labs/Less-1/ 靶场提示 …

CyberRT通信介绍与基于Reader、Writer的通信实践(apollo9.0)

目录 数据通信场景 CyberRT中的通信方式 ​编辑 通信模式 话题通信 服务通信 参数通信 protobuf protobuf简介 protobuf文件编写 topic通信实验 实验环境 实验准备 代码编写 定义消息格式 发送消息 接收消息 定义编译规则 程序编译 运行程序 数据通信场景 …

STM32-Modbus协议(一文通)

Modbus协议原理 RT-Thread官网开源modbus RT-Thread官方提供 FreeModbus开源。 野火有移植的例程。 QT经常用 libModbus库。 Modbus是什么&#xff1f; Modbus协议&#xff0c;从字面理解它包括Mod和Bus两部分&#xff0c;首先它是一种bus&#xff0c;即总线协议&#xff0c;和…

开发一个微信小程序要多少钱?

在当今数字化时代&#xff0c;微信小程序成为众多企业和个人拓展业务、提供服务的热门选择。那么&#xff0c;开发一个微信小程序究竟需要多少钱呢&#xff1f; 开发成本主要取决于多个因素。首先是功能需求的复杂程度。如果只是一个简单的信息展示小程序&#xff0c;功能仅限…

使用HIP和OpenMP卸载的Jacobi求解器

Jacobi Solver with HIP and OpenMP offloading — ROCm Blogs (amd.com) 作者&#xff1a;Asitav Mishra, Rajat Arora, Justin Chang 发布日期&#xff1a;2023年9月15日 Jacobi方法作为求解偏微分方程&#xff08;PDE&#xff09;的基本迭代线性求解器在高性能计算&#xff…

Java实现油画滤镜效果【参数可调】

油画滤镜的基本原理 油画滤镜的基本思想是通过改变图像的像素&#xff0c;将每个像素用周围随机选择的像素来代替&#xff0c;从而产生类似油画笔触的效果。这种处理方式可以模糊图像的细节&#xff0c;使得图像的色块更加连贯&#xff0c;从而模仿油画的艺术效果。 核心步骤…

后台管理员登录实现--系统篇

我的小系统后台原来就有一个上传图片的功能还夹带个删除图片的功能&#xff0c;还嵌到了一个菜单里面。之前效果如下 那么现在为了加大安全力度&#xff0c;想增加一个登录页面。通过登录再到这个页面。看着貌似很简单&#xff0c;但是听我细细说来&#xff0c;要新增些什么东西…

OpenLayers:构建现代Web地图应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 OpenLayers&#xff1a;构建现代Web地图应用 文章目录 OpenLayers&#xff1a;构建现代Web地图应用1. 简介2. 为什么选择 OpenLa…

Redis 高可用:从主从到集群的全面解析

目录 一、主从复制 (基础)1. 同步复制a. 全量数据同步b. 增量数据同步c. 可能带来的数据不一致 2. 环形缓冲区a. 动态调整槽位 3. runid4. 主从复制解决单点故障a. 单点故障b. 可用性问题 5. 注意事项a. Replica 主动向 Master 建立连接b. Replica 主动向 Master 拉取数据 二、…

腾讯云宝塔面板前后端项目发版

后端发版 1. 打开“网站”页面&#xff0c;找到java项目&#xff0c;点击状态暂停服务 2.打开“文件”页面&#xff0c;进入jar包目录&#xff0c;删除原有的jar包&#xff0c;上传新jar包 3. 再回到第一步中的网站页面&#xff0c;找到jar项目&#xff0c;启动项目即可 前端发…