【网络编程】序列化与反序列化

news/2024/11/20 10:22:48/

文章目录

  • 一、网络协议
  • 二、序列化和反序列化
    • 1. 结构化数据
    • 2. 序列化和反序列化
  • 三、网络版计算器
    • 1. 协议定制
    • 2. 客户端处理收到的数据
    • 3. 整体代码


一、网络协议

网络协议 是通信计算机双方必须共同遵从的一组约定,为了使数据在网络上能够从源地址到目的地址,网络通信的参与方必须遵循相同的规则,因此我们将这套规则称为协议,协议最终需要通过计算机语言的方式表示出来。


二、序列化和反序列化

1. 结构化数据

如果需要传输的数据是字符串,那么我们可以直接将这个字符串发送到网络当中。但是如果我们传输的是一些结构化的数据,此时就不能直接将这些数据发送到网络当中。

如果服务器将这些结构化的数据单独一个一个发送到网络当中,那么服务器从网络当中获取这些数据时也只能一个一个获取,并且结构化的数据往往具有特定的格式和规范,例如数据库表格或者特定的数据模型。如果直接将这些数据发送到网络,服务端可能需要处理大量复杂的格式转换和数据清洗工作,而且还有可能会影响数据的正确性。所以客户端最好把这些结构化的数据打包后统一发送到网络当中,此时服务端每次从网络当中获取到的就是一个完整的请求数据。

常见的打包方式:

  1. 将结构化的数据组合成一个字符串:
    客户端可以按照某种方式将这些结构化的数据组合成一个字符串,然后将该字符串发送到网络当中,当服务器接收到这个字符串时,以相同的方式将这个字符串进行解析。就可以从这个字符串中提取出这些结构化的数据。

  2. 定制结构体 + 序列化与反序列化
    客户端可以定制一个结构体,将需要交互的信息定义到这个结构体当中。客户端发送数据时先对数据进行序列化,服务端接收到数据后在对其进行反序列化,此时服务端就能得到客户端发送过来的结构体了。


2. 序列化和反序列化

  • 序列化: 将对象的状态信息转换为可以存储或传输的形式的过程。
  • 反序列化: 将字节序列恢复为的过程。

OSI七层模型中表示层的作用就是,实现设备固有数据格式和网络标准数据格式的转换。其中设备固有的数据格式指的是数据在应用层上的格式,而网络标准数据格式则指的是序列化之后可以进行网络传输的数据格式。

💕 序列化和反序列化的目的:

序列化的目的是方便网络数据的发送和接收,无论何种类型的数据,经过序列化后都变成了二进制序列,此时底层在进行网络传输时看到的都是二进制序列。

序列化后的二进制序列只有在网络传输时能够被底层识别,上层应用是无法识别序列化后的二进制数据的,因此需要将从网络中获取到的数据进行反序列化,将二进制序列的数据转换成应用层能够识别的数据格式。

在这里插入图片描述


三、网络版计算器

1. 协议定制

💕 请求数据和响应数据

这里我们实现一个请求结构体和一个响应结构体。

  • 请求数据结构体中包含两个操作数和一个操作符,操作数表示需要进行计算的两个数,操作符表示是要进行 +-*/ 中的哪一个操作。
  • 响应数据结构体需要包含一个计算结果和一个状态码,表示本次计算的结果和计算状态。

状态码所表示的含义:

  • 0 表示计算成功
  • 1 表示出现除0错误
  • 2 表示模0错误
  • 3 表示非法计算

因为协议定制好以后要被客户端和服务端同时遵守,所以需要将它写到一个头文件中,同时被客户端和服务端包含该头文件。

// 请求
class Request
{
public:Request(){}Request(int x, int y, char op):_x(x), _y(y), _op(op){}
public:	int _x;int _y;char _op;
};// 响应
class Response
{
public:Response(){}Response(int result, int code):_result(result), _code(code){}
public:int _result;int _code;
};

💕 自定义序列化和反序列化

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

请求序列化

在这里插入图片描述
请求反序列化

在这里插入图片描述

响应序列化

在这里插入图片描述

响应反序列化

在这里插入图片描述


💕 添加报头和去除报头

为了保证服务器在接收时能够接收到一个完整的报文,我们需要给已经序列化完成的字符串添加报头。此报头来标识服务器读取到的数据是否是完整的,因此我们在序列化后的字符串前面添加要发送的字符串的长度,以便于服务器读取到完整的报文。

添加报头

在这里插入图片描述

去除报头

在这里插入图片描述

去除报头


2. 客户端处理收到的数据

当客户端收到已经反序列化完成之后的数据后,需要将其分隔到数组里面,然后在转换为结构化的数据类型进行计算。

服务器收到去除字符串报头后的结果,需要将其分隔并反序列化

在这里插入图片描述
客户端收到去除字符串报头后的结果,需要将其分隔并反序列化

在这里插入图片描述


3. 整体代码

💕 Sock.hpp

#pragma once#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Err.hpp"using namespace std;
static const int gbacklog = 32;
static const int defaultfd = -1;class Sock
{
public:Sock():_sock(defaultfd){}Sock(int sock):_sock(sock){}// 创建套接字void Socket(){_sock = socket(AF_INET, SOCK_STREAM, 0);if(_sock < 0){logMessage(Fatal, "socket error, code: %d, errstring: %s", errno, strerror(errno));exit(SOCKET_ERR);}}// 绑定端口号 和 ip地址void Bind(const uint16_t& port){struct sockaddr_in local;local.sin_addr.s_addr = INADDR_ANY; // 绑定任意IP地址local.sin_port = htons(port); // 绑定端口号local.sin_family = AF_INET;if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0){logMessage(Fatal, "bind error, code: %d, errstring: %s", errno, strerror(errno));exit(BIND_ERR);}}// 监听void Listen(){if(listen(_sock, gbacklog) < 0){logMessage(Fatal, "listen error, code: %d, errstring: %s", errno, strerror(errno));exit(LISTEN_ERR);}}// 获取连接int Accept(string* clientip, uint16_t* clientport){struct sockaddr_in temp;socklen_t len = sizeof(temp);int sock = accept(_sock, (struct sockaddr*)&temp, &len);if(sock < 0){logMessage(Warning, "accept error, code: %d, errstring: %s", errno, strerror(errno));}else{*clientip = inet_ntoa(temp.sin_addr);*clientport = ntohs(temp.sin_port);}return sock;}// 客户端和服务器建立连接int Connect(const string& serverip, const uint16_t& serverport){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());return connect(_sock, (struct sockaddr*)&server, sizeof(server));}int Fd(){return _sock;}void Close(){if(_sock != defaultfd)close(_sock);}~Sock(){}private:int _sock;
};

💕 Protocol.hpp

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <jsoncpp/json/json.h>
#include "Util.hpp"
// 给网络版本计算器定制协议// #define MYSELF 1// 给网络版本计算器定制协议
namespace protocol_ns
{#define SEP " "#define SEP_LEN strlen(SEP) // 注意不能写成 sizeof#define HEADER_SEP "\r\n"#define HEADER_SEP_LEN strlen("\r\n")// 添加报头后 —— "长度"\r\n""_x _op _y"\r\n string AddHeader(const string& str){std::cout << "AddHeader 之前:\n"<< str << std::endl;string s = to_string(str.size());s += HEADER_SEP;s += str;s += HEADER_SEP;std::cout << "AddHeader 之后:\n"<< s << std::endl;return s;}// 去除报头 "长度"\r\n""_x _op _y"\r\n —— _x _op _ystring RemoveHeader(const std::string &str, int len){std::cout << "RemoveHeader 之前:\n"<< str << std::endl;string res = str.substr(str.size() - HEADER_SEP_LEN - len, len);return res;std::cout << "RemoveHeader 之后:\n"<< res << std::endl;}int ReadPackage(int sock, string& inbuffer, string*package){std::cout << "ReadPackage inbuffer 之前:\n"<< inbuffer << std::endl;// 边读取char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer - 1), 0);if(s <= 0)return -1;buffer[s] = 0;inbuffer += buffer;std::cout << "ReadPackage inbuffer 之中:\n"<< inbuffer << std::endl;// 边读取边分析, "7"\r\n""10 + 20"\r\nauto pos = inbuffer.find(HEADER_SEP); // 查找 \r\nif(pos == string::npos)return 0;string lenStr = inbuffer.substr(0, pos); // 获取头部字符串int len = Util::toInt(lenStr);int targetPackageLen = lenStr.size() + len + 2 * HEADER_SEP_LEN; // 获取到的完整字符串的长度if(inbuffer.size() < targetPackageLen)return 0;*package = inbuffer.substr(0, targetPackageLen);inbuffer.erase(0, targetPackageLen);		std::cout << "ReadPackage inbuffer 之后:\n"<< inbuffer << std::endl;return len; // 返回有效载荷的长度}// Request && Response都要提供序列化和反序列化功能——自己手写// 请求class Request{public:Request(){}Request(int x, int y, char op):_x(x), _y(y), _op(op){}// 协议的序列化 struct -> stringbool Serialize(string* outStr){*outStr = "";
#ifdef MYSELFstring x_string = to_string(_x);string y_string = to_string(_y);// 手动序列化*outStr = x_string + SEP + _op + SEP + y_string;std::cout << "Request Serialize:\n"<< *outStr << std::endl;
#elseJson::Value root; // Value 是一种万能对象, 可以接受任意的kv类型root["x"] = _x;root["y"] = _y;root["op"] = _op;// Json::FastWriter writer; // Writer 是用来序列化的, struct -> stringJson::StyledWriter writer;*outStr = writer.write(root);
#endifreturn true;}// 协议的反序列化 string -> structbool Deserialize(const string& inStr){
#ifdef MYSELF// 将inStr分隔到数组里面 -> [0]=>10 [1]=>+ [2]=>20vector<string> result;Util::StringSplit(inStr, SEP, &result);if(result.size() != 3)return false;if(result[1].size() != 1)return false;_x = Util::toInt(result[0]);_y = Util::toInt(result[2]);_op = result[1][0];
#elseJson::Value root;Json::Reader reader; // Reader是用来进行反序列化的reader.parse(inStr, root);_x  = root["x"].asInt();_y = root["y"].asInt();_op = root["op"].asInt();
#endifreturn true;}~Request(){}public:	int _x; // 操作数 _xint _y; // 操作数 _ychar _op;// 操作符 _op};// 响应class Response{public:Response(){}Response(int result, int code):_result(result), _code(code){}bool Serialize(string* outStr){*outStr = "";
#ifdef MYSELFstring res_string = to_string(_result);string code_string = to_string(_code);// 手动序列化*outStr = res_string + SEP + code_string;std::cout << "Response Serialize:\n"<< *outStr << std::endl;
#elseJson::Value root;root["result"] = _result;root["code"] = _code;// Json::FastWriter writer;Json::StyledWriter writer;*outStr = writer.write(root);
#endifreturn true;}bool Deserialize(const string& inStr){
#ifdef MYSELF// 将inStr分隔到数组里面 -> [0]=>10 [1]=>+ [2]=>20vector<string> result;Util::StringSplit(inStr, SEP, &result);if(result.size() != 2)return false;_result = Util::toInt(result[0]);_code = Util::toInt(result[1]);
#elseJson::Value root;Json::Reader reader;reader.parse(inStr, root);_result = root["result"].asInt();_code = root["code"].asInt();
#endifPrint();return true;}void Print(){std::cout << "_result: " << _result << std::endl;std::cout << "_code: " << _code << std::endl;}public:int _result;int _code;};
}

💕 CalculatorClient.cc

#include "Sock.hpp"
#include "Protocol.hpp"#include <iostream>
#include <string>using namespace std;
using namespace protocol_ns;static void usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}enum
{LEFT,OPER,RIGHT
};Request ParseLine(const std::string &line)
{std::string left, right;char op;int status = LEFT;int i = 0;while(i < line.size()){switch (status){case LEFT:if (isdigit(line[i]))left.push_back(line[i++]);elsestatus = OPER;break;case OPER:op = line[i++];status = RIGHT;break;case RIGHT:if (isdigit(line[i]))right.push_back(line[i++]);break;}}Request req;req._x = std::stoi(left);req._y = std::stoi(right);req._op = op;return req;
}int main(int argc, char* argv[])
{if(argc != 3){usage(argv[0]);exit(USAGE_ERR);}string serverip = argv[1];uint16_t serverport = atoi(argv[2]);Sock sock;sock.Socket();int n = sock.Connect(serverip, serverport);if(n != 0) return 1;string buffer;while(true){cout << "Enter# ";string line;getline(cin, line);Request req = ParseLine(line);cout << "test:" << req._x << req._op << req._y << endl;// 序列化string sendString;req.Serialize(&sendString);// 添加报头sendString = AddHeader(sendString);// sendsend(sock.Fd(), sendString.c_str(), sendString.size(), 0);// 获取响应string package;int n = 0;START:n = ReadPackage(sock.Fd(), buffer, &package);if(n == 0)goto START;else if(n < 0)break;else{}// 去掉报头package = RemoveHeader(package, n);// 反序列化Response resp;resp.Deserialize(package);cout << "result: " << resp._result << "[code: " << resp._code << "]" << endl;}sock.Close();return 0;
}

💕 TcpServer.hpp

#pragma once
#include <iostream>
#include <functional>
#include <cstring>
#include <pthread.h>
#include "Sock.hpp"
#include "Protocol.hpp"using namespace std;namespace tcpserver_ns
{using namespace protocol_ns;using func_t = function<Response(const Request&)>;class TcpServer;class ThreadData{public:ThreadData(int sock, string ip, uint16_t port, TcpServer* tsvrp):_sock(sock), _ip(ip), _port(port), _tsvrp(tsvrp){}~ThreadData(){}public:int _sock;string _ip;uint16_t _port;TcpServer *_tsvrp;};class TcpServer{public:TcpServer(func_t func, uint16_t port):_func(func), _port(port){}// 初始化服务器void InitServer(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();logMessage(Info, "init server done, listensock: %d, errstring: %s", errno, strerror(errno));}// 运行服务器void Start(){while(true){string clientip;uint16_t clientport;int sock = _listensock.Accept(&clientip, &clientport);if(sock < 0) continue;logMessage(Debug, "get a new client, client info : [%s:%d]", clientip.c_str(), clientport);pthread_t tid; // 创建多线程ThreadData *td = new ThreadData(sock, clientip, clientport, this);pthread_create(&tid, nullptr, ThreadRoutine, td);}}static void* ThreadRoutine(void* args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);td->_tsvrp->ServiceIO(td->_sock, td->_ip, td->_port);logMessage(Debug, "thread quit, client quit ...");delete td;return nullptr;}// 服务器对客户端的数据进行IO处理void ServiceIO(int sock, const std::string &ip, const uint16_t &port){string inbuffer;while(true){// 保证自己读到一个完整的字符串报文 "7"\r\n""10 + 20"\r\nstring package;int n = ReadPackage(sock, inbuffer, &package);if(n == -1)break;else if(n == 0)continue;else{// 已经得到了一个"7"\r\n""10 + 20"\r\n// 1. 提取有效载荷package = RemoveHeader(package, n);// 2. 已经读到了一个完整的stringRequest req;req.Deserialize(package);// 3. 直接提取用户的请求数据Response resp = _func(req); // 业务逻辑// 4. 给用户返回响应——序列化string send_string;resp.Serialize(&send_string);// 5. 添加报头send_string = AddHeader(send_string);// 6. 发送send(sock, send_string.c_str(), send_string.size(), 0);}}close(sock);}~TcpServer(){}private:uint16_t _port; // 端口号Sock _listensock; // 监听套接字func_t _func;};
}

💕 CalculatorServer.cc

#include "TcpServer.hpp"
#include <memory>
using namespace tcpserver_ns;Response calculate(const Request &req)
{// 走到这里,一定保证req是有具体数据的!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 (req._y == 0)resp._code = 1;elseresp._result = req._x / req._y;break;case '%':if (req._y == 0)resp._code = 2;elseresp._result = req._x % req._y;break;default:resp._code = 3;break;}return resp;
}int main()
{uint16_t port = 8080;unique_ptr<TcpServer> tsvr(new TcpServer(calculate, port));tsvr->InitServer();tsvr->Start();return 0;
}

💕 Util.hpp

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>using namespace std;class Util
{
public:static bool StringSplit(const string&str, const string& sep, vector<string>* result){size_t start = 0;while(start < str.size()){auto pos = str.find(sep, start);if(pos == string::npos) break;result->push_back(str.substr(start, pos - start));start = pos + sep.size();}if(start < str.size())result->push_back(str.substr(start));return true;}static int toInt(const string& s){return atoi(s.c_str());}
};

💕 效果演示

在这里插入图片描述

整体源码



http://www.ppmy.cn/news/1158387.html

相关文章

Notes/Domino 14 Early Access Drop3发布

大家好&#xff0c;才是真的好。 其实上周&#xff0c;就是国庆假期的时候&#xff0c;HCL Notes/Domino 14 Early Access Drop3&#xff08;以下简称EA3&#xff09;就已经发布&#xff0c;而且和传说中的一样&#xff0c;带来了数项惊人的新特性。 我们先讲讲这一版本新特性…

分类算法-逻辑回归与二分类

1、逻辑回归的应用场景 广告点击率是否为垃圾邮件是否患病金融诈骗虚假账号 看到上面的例子&#xff0c;我们可以发现其中的特点&#xff0c;那就是都属于两个类别之间的判断。逻辑回归就是解决二分类问题的利器。 2、 逻辑回归的原理 2.1 输入 逻辑回归的输入就是一个线性…

Unity AI Muse 基础教程

Unity AI Muse 基础教程 Unity AI 内测资格申请Unity 项目Package ManagerMuse Sprite 安装Muse Texture 安装 Muse Sprite 基础教程什么是 Muse Sprite打开 Muse Sprite 窗口Muse Sprite 窗口 参数Muse Sprite Generations 窗口 参数Muse Sprite Generations 窗口 画笔Muse Sp…

基于RuoYi-Flowable-Plus的若依ruoyi-nbcio支持自定义业务表单流程(三)

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 相应的后端也要做一些调整 1、启动流程修改如下&#xff1a; /*** 启动流程实例*/private R startProce…

【应用程序代理对象ApplicationDelegate-应用程序启动过程介绍 Objective-C语言】

一、那我们接着昨天的内容,继续往下讲 1.有人对昨天最后这块儿内容有点儿晕,再捋一下吧, 1)我们刚开始的时候,是不是在Main.storyboard里面,放了一个按钮 2)我呢,想在点击按钮的时候,执行一些操作,对吧, 所以呢,我给它拖了一个事件, 拖到类实现里面, 3)那,首…

优秀的推荐系统架构与应用:从YouTube到Pinterest、Flink和阿里巴巴

文章目录 &#x1f31f; 业界经典&#xff1a;YouTube深度学习推荐系统的经典架构长什么样&#xff1f;&#x1f34a; 基础架构&#x1f34a; 深度学习模型&#x1f34a; 额外组件 &#x1f31f; 图神经网络&#xff1a;Pinterest如何应用图神经网络的&#xff1f;&#x1f34a…

【Linux基础】详谈Shell运行原理------王婆传媒(高重复率面试题)

目录 &#x1f4a7;前言 &#x1f4a6;Shell的运行原理 &#x1f449;Shell的基本概念与作用 &#x1f449;原理的展示与剖析 &#x1f449;Shell外壳感性理解【一门亲事】 &#x1f4a7;总结 &#x1f4a7;共勉 &#x1f4a7;前言 在之前的 Linux 讲解中&#xff0c;主要说…

讲解 CSS 过渡和动画 — transition/animation (很全面)

前言 由于用户越来越注重 Web应用 的使用体验&#xff0c;随之而来的是 Web应用 需要提供了更加完善的 Web 动画 效果来实现以平滑的状态贯穿于用户的整个使用过程中。现在&#xff0c;这已经是司空见惯了&#xff0c;用户潜意识是希望可以获得更快的反馈响应和更友好的用户界…