当前位置: 首页 > news >正文

【Linux网络】简易应用层协议定制

目录

  • 什么是序列化反序列化
  • 网络计算器服务
  • json完成序列化反序列化
  • 理解OSI模型中的上三层

什么是序列化反序列化

我们在使用socket编程时,以TCP为例,TCP是面向字节流的,全双工的,它并不关心应用层想要传输的数据是什么,它只是负责将数据及时可靠的以字节流的方式接收发送,TCP属于传输层,被写入了操作系统内部,所以我们可以给予其信任,放心地将数据交给它,但是面向字节流的方式让接收端接收的数据有着不确定性(如果只是低并发,少量数据的收发这点可能看不出来),我们的接收方某一个时刻从TCP的接收缓冲区中拷贝上来了一段数据,这段数据完整吗?有可能发送方的数据过大所以只发了一部分过来,也有可能很小所以缓冲区一下接收了好几次发送的数据,这些都有可能,接收方又该如何正确读取呢,另外,正确读取到用户层缓冲区了,我们又该怎样解析数据,正确提取信息呢?不用想,这肯定是要靠应用层的协议。

协议是一套规则、标准和约定的集合,它规定了通信实体之间如何交换信息、如何解读信息,以确保双方能够相互理解、成功协作。我们定好发来的数据有哪些,怎么排列,数据之间以什么为分隔。这种根据协议将程序运行时内存中的对象或数据结构转换成一个可以存储或传输的格式的过程就叫序列化。反序列化则是序列化的逆过程,即根据协议将之前序列化得到的字节序列或字符串重新构建成内存中完全相等的对象或数据结构。

序列化完之后,我们还要注意怎么将一个个序列化玩的数据分隔开,因为在TCP中,数据都是字节流,这样的方式会导致我们无法划定消息的开头和结尾从而将其从字节流上截取下来,这时就需要定界了。网络传输中的定界,指的是在通信过程中,接收方如何从连续不断的数据流中准确地识别出一个完整消息(或数据包)的开始和结束位置。

网络计算器服务

事实上,应用层协议的定制在实际项目开发中并不多见,企业在能够使用成熟方案的情况下是不会自定义应用层协议的,成熟的应用层协议定制也绝非易事。我们这里只是出于想要学习理解应用层协议和序列化反序列化的过程,所以写了一个网络版本的计算器(只支持两数运算),基于它做了一个简易的应用层协议定制。

首先我们先使用socket快速的搭建一个TCP服务的简易框架。

socket.hpp

#include<iostream>#include<string>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include<unistd.h>#include<string.h>std::string defaultip = "127.0.0.1";
int16_t defaultport = 8080;enum
{SOCKETERR = 1,LISTENERR,BINDERR,ACCEPTERR,CONNECTERR
};class sock
{
public:sock(){}void my_socket(){_sockfd = socket(AF_INET, SOCK_STREAM, 0);if(_sockfd < 0){perror("socket");exit(SOCKETERR);}int opt = 1;setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt)); // 防止偶发性的服务器无法进行立即重启(tcp协议的时候再说)}void my_bind(int16_t port){sockaddr_in cli_in;memset((void*)&cli_in, 0, sizeof(cli_in));cli_in.sin_addr.s_addr = inet_addr(defaultip.c_str());cli_in.sin_family = AF_INET;cli_in.sin_port = htons(port);int ret = bind(_sockfd, (const sockaddr*)&cli_in, sizeof(cli_in));if(ret < 0){perror("bind");exit(BINDERR);}}void my_listen(){int ret = listen(_sockfd, 5);if(ret < 0){perror("listen");exit(LISTENERR);}}int my_accept(std::string& clientip,  int16_t& clientport){sockaddr_in get;memset((void*)&get, 0, sizeof(get));socklen_t glen = sizeof(get);int ret = accept(_sockfd, (sockaddr*)&get, &glen);if(ret < 0){perror("accept");return -1;}char ipstr[64] = {0};clientip = inet_ntop(AF_INET, (const sockaddr*)&get.sin_addr, ipstr, sizeof(ipstr));clientip = ipstr;clientport = get.sin_port;return ret;}bool my_connect(const std::string& ip, const int16_t& port){sockaddr_in ser_in;memset((void*)&ser_in, 0, sizeof(ser_in));ser_in.sin_addr.s_addr = inet_addr(ip.c_str());ser_in.sin_family = AF_INET;ser_in.sin_port = htons(port);int ret = connect(_sockfd, (const sockaddr*)&ser_in, sizeof(ser_in));if(ret < 0){perror("accept");return false;}return true;}~sock(){}void my_close(){close(_sockfd);}int get_sockfd(){return _sockfd;}private:int _sockfd;
};

tcpserver.hpp

#include"socket.hpp"#include"servercal.hpp"#include<signal.h>#define SIZE 1024typedef std::string(*func)(std::string&);class tcpserver
{
public:tcpserver(const int16_t& port, func slove) : _port(port), _slove(slove){}void init(){_listen_sock.my_socket();_listen_sock.my_bind(_port);_listen_sock.my_listen();}void start(){signal(SIGPIPE, SIG_IGN);signal(SIGCHLD, SIG_IGN);while(1){std::string ip;int16_t port;int sockfd = _listen_sock.my_accept(ip, port);if(fork() == 0){//后续填充服务端代码}close(sockfd);}}~tcpserver(){}
private:sock _listen_sock;int16_t _port;func _slove;
};

都是一些最基本的socket接口,不做过多赘述。

之后我们就来制定一下应用层的协议,首先要明确的一点就是我们写的是什么项目,我们写的是计算器,客户端向服务端发送计算式,服务端返回答案。那么发送的数据有两种,分服务端和客户端。我们先从客户端说起,客户要发送计算式,计算式中包含三个信息——数字1、运算符、数字2。对于服务端,我们发送的信息有两个,运算结果和结果状态,因为客户端可能发送过来一些错误数据,我们应该额外添加一个数据表示运算结果是否正确才对。这两个数据我们都可以定义成结构体。

struct Request
{int _a;int _b;char _op;
};
struct Response
{int _result = 0;int _status_code = 0;
};

单纯定义成结构体,直接发送就行了吗?答案是不是,因为即使是结构体,不同的主机,不同的系统之间也会因为内存对齐等原因从而大小不一,所以我们不能直接发送,我们应该将其序列化。序列化成什么呢?最简单的,字符串就行。

我们首先考虑发送的一条条字符串之间要怎么分隔开,也就是定界,这里给出两个方法,一个是字符串结尾加上一个特殊字符表示结束,这样我们可以通过是否读到了这个特殊字符来判断是否已经读完了一个字符串,我们也可以在开头再加一个数字表示这条字符串的大小,从而直接根据大小直接读取。再者就是数字之间的分隔,也就是序列化,我们采用特殊字符分隔就好了。

我们利用c++的类将序列化反序列化的函数和要发送的结构体合并成类,

#include<iostream>#include<jsoncpp/json/json.h>#define Separator_Spa ' '#define Separator_Pro '\n'std::string encode(std::string msg) // 定界,加尺寸,开头结尾进一步封装。a + b -> 5\na + b\n
{std::string ret = std::to_string(msg.size());ret += Separator_Pro;ret += msg;ret += Separator_Pro;return ret;
}bool decode(std::string& msg, std::string& content) // 提取尺寸,截取字符串。5\na + b\n…… -> a + b
{size_t pos = msg.find(Separator_Pro);if(pos == std::string::npos) return false;std::string szstr = msg.substr(0, pos);size_t sz = std::stoi(szstr);size_t totalsz = sz + szstr.size() + 2;if(totalsz < msg.size()) return false;content = msg.substr(pos + 1, sz);msg.erase(0, totalsz);return true;
}class Request
{
public:bool Serialization(std::string& message) // 序列化{message += std::to_string(_a);message += Separator_Spa;message += _op;message += Separator_Spa;message += std::to_string(_b);return true;}bool Deserialization(std::string& msg) // 反序列化{size_t left = msg.find(Separator_Spa);if(left == std::string::npos) return false;_a = std::stoi(msg.substr(0, left));size_t right = msg.find(Separator_Spa, left + 1);if(right == std::string::npos) return false;_op = msg[left + 1];_b = std::stoi(msg.substr(right + 1));return true;}int _a;int _b;char _op;
};class Response
{
public:bool Serialization(std::string& message){message += std::to_string(_result);message += Separator_Spa;message += std::to_string(_status_code);return true;}bool Deserialization(std::string& msg){size_t mid = msg.find(Separator_Spa);if(mid == std::string::npos) return false;_result = std::stoi(msg.substr(0, mid));_status_code = std::stoi(msg.substr(mid + 1));return true;}int _result = 0;int _status_code = 0;
};

这里使用了开头加上字符串大小的方式截取一个个字符串,但是我同样也在字符串的开头结尾加上了\n,开头加上\n是因为要将字符串大小的数字和字符串分割,结尾加上/n是为了方便调试打印,其实可以不加的。

序列化反序列化的过程不用多说,分隔符我用的是空格。

协议定完我们来实现一下计算器的主逻辑,

class Calculator
{
public:static Response Calculation(const Request& rq){Response rp;switch (rq._op){case '+':rp._result = rq._a + rq._b;break;case '-':rp._result = rq._a - rq._b;break;case '*':rp._result = rq._a * rq._b;break;case '/':if (rq._a != 0) rp._result = rq._a / rq._b;else rp._status_code = DIVERR;break;case '%':if (rq._a != 0) rp._result = rq._a % rq._b;else rp._status_code = MODERR;break;default:rp._status_code = OPERR;}return rp;}static std::string server_cal(std::string& message){std::string content;bool back = decode(message, content);if(!back) return "";Request rq;back = rq.Deserialization(content);if(!back) return "";Response rp = Calculation(rq);std::string ret;back = rp.Serialization(ret);if(!back) return "";ret = encode(ret);return ret;}
};

server_cal是服务端accept到连接套接字后fork开的服务进程所直接调用的函数,负责在运算前做好准备工作,因为毕竟只是读上来了一条字符串,先对其反序列化,而后调用Calculation运算,之后再进行善后工作,序列化完返回。

如此一来,我们就能补齐tcpserver空缺的代码了,

void start()
{signal(SIGPIPE, SIG_IGN);signal(SIGCHLD, SIG_IGN);while(1){std::string ip;int16_t port;int sockfd = _listen_sock.my_accept(ip, port);// std::cout << sockfd << std::endl;if(fork() == 0){_listen_sock.my_close();std::string message;while(1){char buffer[SIZE] = {0};ssize_t ret = read(sockfd, buffer, SIZE);message += buffer;// std::cout << buffer << std::endl;// std::cout << message << std::endl;if(ret > 0){// while(1)// {std::string back = _slove(message);// std::cout << back << std::endl;if(back.empty()) break;write(sockfd, back.c_str(), back.size());// }}else break;}exit(0);}close(sockfd);}
}

然后我们在main函数中启动服务端程序,

#include"tcpserver.hpp"int main()
{daemon(0, 0);int16_t port = 8080;tcpserver ts(port, Calculator::server_cal);ts.init();ts.start();return 0;
}

服务端的代码就完成了。

最后我们来写一下客户端的代码,

#include<iostream>#include"protocol.hpp"#include"socket.hpp"#define SIZE 1024#include<time.h>void String_Parsing(Request& rq, const std::string& str)
{ssize_t left = str.find(' ');rq._a = std::stoi(str.substr(0, left));ssize_t pos = left;while(1){if(str[pos] == ' ') pos++;else break;}rq._op = str[pos];ssize_t right = str.find(' ', pos);rq._b = std::stoi(str.substr(right));
}int main()
{srand(time(NULL));sock sk;sk.my_socket();sk.my_connect("127.0.0.1", 8080);// std::string opstr = "+-*/%=&";char buffer[SIZE] = {0};int cnt = 5;while(cnt--){// char buffer[SIZE] = {0};sleep(1);Request rq;std::string message;// rq._a = rand()%100;// rq._b = rand()%100;// rq._op = opstr[rand()%7];// std::cout << rq._a << " " << rq._op << " " << rq._b << std::endl;std::getline(std::cin, message);String_Parsing(rq, message);message = "";rq.Serialization(message);std::string content = encode(message);write(sk.get_sockfd(), content.c_str(), content.size());ssize_t ret = read(sk.get_sockfd(), buffer, SIZE);if(ret > 0){buffer[ret] = 0;std::string buf = buffer;Response rp;std::string tmp;decode(buf, tmp);rp.Deserialization(tmp);std::cout << rp._result << " " << rp._status_code << std::endl;}}return 0;
}

会写服务端,客户端肯定也不在话下。

这样我们自己实现的一个简单的网络计算器小服务就完成了。
在这里插入图片描述

json完成序列化反序列化

除了手动完成序列化之外,我们也能使用json完成序列化反序列化。什么是json呢?JSON(JavaScript Object Notation,JavaScript 对象表示法)是一种轻量级的数据交换格式,它的核心作用是在不同的系统(如前后端、不同服务之间)传递和存储有结构的数据。json因为其轻量且易于阅读和编写的特点,已经脱胎于JavaScript,成为全球通用的标准,所有主流编程语言都支持它。

我们要在Linux中的c++程序上使用它,我们首先要安装第三方库,centeos中使用指令

sudo yun install -y jsoncpp-devel

来进行下载,之后我们查看系统路径下的对应文件,有了说明安装成功了。
在这里插入图片描述
之后要使用json进行序列化反序列化,我们包头文件

#include<jsoncpp/json/json.h>

就可以了。

首先使用

Json::Value 对象名

创建一个Json::Value对象,这是JsonCpp的核心类。

这样我们就能向Json::Value中添加键值对,添加方法也非常简单,

root["name"] = "XXX";

直接用[]添加就行,类中进行了运算符重载。

如此一来我们就能将我们上面的数据添加进Json::Value中了。

之后我们创建对象

Json::StyledWriter w

Json::FastWriter w;

之后调用函数

virtual std::string Json::FastWriter::write(const Json::Value &root);
virtual std::string Json::StyledWriter::write(const Json::Value &root);

完成序列化,转换成字符串,这两个类的用处是一样的,但是生成的字符串格式不一样。FastWriter更直接紧凑,StyledWriter有格式易读。

#include<iostream>#include<jsoncpp/json/json.h>using namespace std;int main()
{Json::Value root;root["a"] = 1;root["b"] = 2;root["c"] = 3;Json::FastWriter w;std::string str = w.write(root);std::cout << str << std::endl;return 0;
}

在这里插入图片描述

#include<iostream>#include<jsoncpp/json/json.h>using namespace std;int main()
{Json::Value root;root["a"] = 1;root["b"] = 2;root["c"] = 3;Json::StyledWriter w;std::string str = w.write(root);std::cout << str << std::endl;return 0;
}

在这里插入图片描述
转化成的字符串又该怎么重新变回JSON::Value对象呢。我们创建JSON::Reader类类型对象,使用函数

bool Json::Reader::parse(const std::string &document, Json::Value &root, bool collectComments = true)

进行转化。

转化成JSON::Value对象后,我们可以使用

int a = root["a"].asInt();

的方式提取对象中的键值对数据,as系的函数有很多种,可以提取各种数据,我们这里只会用到asInt();

有了这些最基本的使用方法我们就能改造之前的协议了,

#include<iostream>#include<jsoncpp/json/json.h>#define Separator_Spa ' '#define Separator_Pro '\n'std::string encode(std::string msg)
{std::string ret = std::to_string(msg.size());ret += Separator_Pro;ret += msg;ret += Separator_Pro;return ret;
}bool decode(std::string& msg, std::string& content)
{size_t pos = msg.find(Separator_Pro);if(pos == std::string::npos) return false;std::string szstr = msg.substr(0, pos);size_t sz = std::stoi(szstr);size_t totalsz = sz + szstr.size() + 2;if(totalsz < msg.size()) return false;content = msg.substr(pos + 1, sz);msg.erase(0, totalsz);return true;
}class Request
{
public:bool Serialization(std::string& message){// message += std::to_string(_a);// message += Separator_Spa;// message += _op;// message += Separator_Spa;// message += std::to_string(_b);// return true;return Json_Serialization(message);}bool Json_Serialization(std::string& message){Json::Value v;v["a"] = _a;v["op"] = _op;v["b"] = _b;Json::StyledWriter w;message = w.write(v);return true;}bool Deserialization(std::string& msg){// size_t left = msg.find(Separator_Spa);// if(left == std::string::npos) return false;// _a = std::stoi(msg.substr(0, left));// size_t right = msg.find(Separator_Spa, left + 1);// if(right == std::string::npos) return false;// _op = msg[left + 1];// _b = std::stoi(msg.substr(right + 1));// return true;return Json_Deserialization(msg);}bool Json_Deserialization(std::string& msg){Json::Value v;Json::Reader r;r.parse(msg, v);_a = v["a"].asInt();_b = v["b"].asInt();_op = v["op"].asInt();return true;}int _a;int _b;char _op;
};class Response
{
public:bool Serialization(std::string& message){// message += std::to_string(_result);// message += Separator_Spa;// message += std::to_string(_status_code);// return true;return Json_Serialization(message);}bool Json_Serialization(std::string& message){Json::Value v;v["result"] = _result;v["status_code"] = _status_code;Json::StyledWriter w;message = w.write(v);return true;}bool Deserialization(std::string& msg){// size_t mid = msg.find(Separator_Spa);// if(mid == std::string::npos) return false;// _result = std::stoi(msg.substr(0, mid));// _status_code = std::stoi(msg.substr(mid + 1));// return true;return Json_Deserialization(msg);}bool Json_Deserialization(std::string& msg){Json::Value v;Json::Reader r;r.parse(msg, v);_result = v["result"].asInt();_status_code = v["status_code"].asInt();return true;}int _result = 0;int _status_code = 0;
};

json是序列化反序列化最为常用的一种方式了,当然市面上还有诸如protobuffer等方式可以进行序列化,这里不做介绍了。

理解OSI模型中的上三层

OSI模型中的上三层是会话层表示层应用层。

会话层建立、管理和终止两个应用程序之间的会话。其实它就对应着我们accept到连接套接字之后开启进程进行数据收发的过程,只不过我这写的很简单,就只是阻塞式地等待,实际项目中会更复杂,比如设置成非阻塞等待,客户端超过多少秒没有发送请求就断开连接,又或者在长时间的数据传输中,会话层可以插入检查点,如果网络中断,可以从最后一个检查点恢复数据传输,而不必从头开始。

表示层其主要职责是处理两个系统之间交换信息的语法(Syntax)问题,而不涉及数据的语义(Semantics)。它确保从应用层发出的、具有特定本地表示形式的信息,能够被对端主机的应用层所理解。它本质上是应用程序的数据格式翻译器。他就对应着我们协议自定义序列反序列化的过程。

应用层直接为应用程序进程提供访问OSI环境的手段和分布式信息服务。该层包含了用户为了实现特定任务(如文件传输、电子邮件)所需的各种应用服务元素和应用实体。他对应着我们写的计算器程序,也就是处理反序列化出来的数据。

看完这三层的作用以及对应着我们写的小项目中的部分,我们不难看出,这三层都是会随着场景的变化而变化的,不同的项目会需要不同的会话层、表示层、应用层,这就是为什么这三层没有被写进操作系统的原因,因为没办法写,它是一直变化的。而且我们也不难发现,这三层其实层与层之间存在着不小的联系,有些功能也说不清是在那一层,层与层之间的界限模糊,功能与具体应用逻辑绑定太深,无法抽象出一个干净的通用层。强行分离原本就耦合的功能势必增加了很多不必要的复杂性,且报文每经过一层都要加上一层报头,带来了额外的性能开销,所以种种原因导致了OSI模型尽管很好,很经典,清晰地指出了网络通信该是什么样,但是最后还是TCP/IP模型得到了广泛的应用,因为它将会话表示应用层同意合并成应用层,大大降低了开发难度。


文章转载自:

http://R7SUzkMA.bnmrp.cn
http://ISt7lzNC.bnmrp.cn
http://F7S2kd6a.bnmrp.cn
http://gl2KiVYc.bnmrp.cn
http://nH68TZyp.bnmrp.cn
http://XUsKdbcu.bnmrp.cn
http://UPXpURSH.bnmrp.cn
http://AKxUPDlL.bnmrp.cn
http://hSrPyjHG.bnmrp.cn
http://2kUq7blR.bnmrp.cn
http://YMr3orta.bnmrp.cn
http://oMlqJIoG.bnmrp.cn
http://uE2afpjK.bnmrp.cn
http://nhdpMKJQ.bnmrp.cn
http://bE4kcI78.bnmrp.cn
http://yR9CgkAk.bnmrp.cn
http://DiyECA1e.bnmrp.cn
http://ocZyIxh1.bnmrp.cn
http://XGL6j1ry.bnmrp.cn
http://1qOA6PkQ.bnmrp.cn
http://8tYNrymw.bnmrp.cn
http://En8lzfWH.bnmrp.cn
http://R8Cz1CUC.bnmrp.cn
http://oNnsDlpd.bnmrp.cn
http://IBVWde6C.bnmrp.cn
http://9b7nVuTp.bnmrp.cn
http://fdMylkAK.bnmrp.cn
http://8HwbCSaX.bnmrp.cn
http://vd2vxc8Z.bnmrp.cn
http://VFmNvsBM.bnmrp.cn
http://www.dtcms.com/a/381962.html

相关文章:

  • 剪/染前如何降低“想象错位”的风险:一次线上试发的记录(工具:RightHair)
  • 【数据结构与算法Trip第4站】摩尔投票法
  • Java的8 种基本类型 + 包装类,缓存池机制
  • AI 辅助完成复杂任务的亲身体验:使用Qoder 3 天完成 OneCode UI 升级
  • 二叉树基础学习(图文并茂)万字梳理
  • Qt 工程中 UI 文件在 Makefile 中的处理
  • Champ-基于3D的人物图像到动画视频生成框架
  • 深入探索 C++ 元组:从基础到高级应用
  • 第5节-连接表-Cross-Join连接
  • 2025年8月月赛 T2 T3
  • 在Linux上无法访问usb视频设备
  • AI行业应用全景透视:从理论到实践的深度探索
  • [硬件电路-192]:基级与发射极两端的电压超过1.5v可能是什么原因
  • OpenTenBase应用落地实践:从MySQL到OpenTenBase的平滑迁移
  • Redis常用数据结构及其底层实现
  • 深度卷积生成对抗网络
  • 打造精简高效的 uni-app 网络请求工具
  • 基于ZIGBEE的智能太阳能路灯系统设计(论文+源码)
  • Linux 磁盘I/O高占用进程排查指南:从定位到分析的完整流程
  • 20250913-02: Langchain概念:表达式语言(LCEL)
  • 【YOLO目标检测】获取COCO指标
  • React 18 过渡更新:并发渲染的艺术
  • node.js卸载并重新安装(超详细图文步骤)
  • 【CSS学习笔记3】css特性
  • k8s-Sidecar容器学习
  • 坦克大战的学习
  • 如何进行WEB安全性测试
  • 使用UV工具安装和管理Python环境
  • WPS中接入DeepSeek:方法与实践
  • hexo文章