【Linux网络与网络编程】05.应用层自定义协议序列化和反序列化
前言
本篇博客通过网络计算器的实现来帮助各位理解应用层自定义协议以及序列化和反序列化。
一、认识自定义协议&&序列化和反序列化
我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序都是在应用层。前面我们说到:协议是一种 "约定"。socket api 的接口在读写数据时,都是按 "字符串" 的方式来发送接收的。如果我们要传输一些 "结构化的数据" 怎么办呢?
例如,我们需要实现一个服务器版的计算器。我们需要客户端把要计算的两个加数发过去,然后由服务器进行计算,最后再把结果返回给客户端。
约定方案一:
• 客户端发送一个形如"1+1"的字符串
• 这个字符串中有两个整型操作数
• 两个数字之间会有一个字符是运算符
• 数字和运算符之间没有空格
约定方案二:
• 定义结构体来表示我们需要交互的信息
• 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体
这个过程叫做 "序列化" 和 "反序列化"。
无论我们采用方案一还是方案二,抑或是还是其他的方案。只要保证一端发送时构造的数据,在另一端能够正确的进行解析就是可以的。这种约定就是应用层协议。
二、理解 tcp 全双工&&面向字节流

在我们创建sockfd时,操作系统会自动创建两个缓冲区——发送缓冲区和接收缓冲区。所以,发送消息的本质是把数据拷贝到发送缓冲区,接收消息的本质就是把数据从接收缓冲区拷贝拿到。而这两个动作是可以同时进行的,即TCP全双工。
源码剖析:


TCP协议即传输控制协议,它控制着实际数据什么时候发,发多少,出错了怎么办,故而它是面向字节流的。
三、自定义协议实现网络计算器
首先我们需要定制协议+序列化与反序列化。

上面要实现序列化和反序列化,有两种方案:
1. 自己做:x + oper(+ - * /) + y,做空格的字符串分割就行
2. xml && json && protobuf
这里我们为了增加可读性,建议将结构化数据转化为 json(jsoncpp) 的字符串,这篇文章主要是关于第二种方案
//Protocol.hpp
#pragma once
#include <string>
#include <jsoncpp/json/json.h>
#include "Log.hpp"using namespace LogMudule;// 接收
class Request
{
public:Request() = default;Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper){}// 序列化bool Serialize(std::string &out_string){Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;out_string = root.toStyledString();return true;}// 反序列化bool Deserialize(std::string &in_string){Json::Value root;Json::Reader reader;bool parsingSuccessful = reader.parse(in_string, root);if (!parsingSuccessful){LOG(LogLevel::ERROR) << "Failed to parse JSON: " << reader.getFormattedErrorMessages();return false;}_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;}int X() const { return _x; }int Y() const { return _y; }int Oper() const { return _oper; }~Request(){}private:int _x;int _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_string){Json::Value root;root["result"] = _result;root["code"] = _code;out_string = root.toStyledString();// LOG(LogLevel::DEBUG)<<out_string;return true;}// 反序列化bool Deserialize(std::string &in_string){Json::Value root;Json::Reader reader;bool parsingSuccessful = reader.parse(in_string, root);if (!parsingSuccessful){LOG(LogLevel::ERROR) << "Failed to parse JSON: " << reader.getFormattedErrorMessages();return false;}_result = root["result"].asInt();_code = root["code"].asInt();return true;}int Result() { return _result; }int Code() { return _code; }void SetResult(int result){_result=result;}void SetCode(int code){_code=code;}~Response() {}private:int _result; // 结果int _code; // 错误码
};const static std::string sep = "\r\n";// 封包
bool EnCode(std::string &message, std::string *package)
{if (message.size() == 0)return false;//转成17\r\nmessage\r\n的格式*package = std::to_string(message.size()) + sep + message + sep;return true;
}
// 解包
bool Decode(std::string &package, std::string *content)
{auto pos = package.find(sep);if (pos == std::string::npos)return false;std::string content_length_str = package.substr(0, pos);int content_length = std::stoi(content_length_str);int full_length = content_length_str.size() + content_length + 2 * sep.size();if (package.size() < full_length)return false;*content = package.substr(pos + sep.size(), content_length);// package erasepackage.erase(0, full_length);return true;
}
完成协议的编写之后,我们顺手写我们的计算逻辑:
//Calculator.hpp
#pragma once
#include <string>
#include "Protocol.hpp"class Calculator
{
public:Calculator(){}Response Execute(const Request &req){Response resp;switch (req.Oper()){case '+':resp.SetResult(req.X() + req.Y());break;case '-':resp.SetResult(req.X() - req.Y());break;case '*':resp.SetResult(req.X() * req.Y());break;case '/':{if (req.Y() == 0){resp.SetCode(1); // 1 就是除0}else{resp.SetResult(req.X() / req.Y());}}break;case '%':{if (req.Y() == 0){resp.SetCode(2); // 2 就是mod 0}else{resp.SetResult(req.X() % req.Y());}}break;default:resp.SetCode(3); // 3 用户发来的计算类型,无法识别break;}return resp;}~Calculator(){}
};
接下来编写我们的服务端代码:
这里的服务端教之前的TCP服务端相比只有执行的方法不同,其他并无二异。
//TCPSever.hpp
#pragma once
#include <string>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "ThreadPool.hpp"using namespace LogMudule;
using namespace ThreadPoolModual;const static uint16_t defaultport = 8888;//回调函数
using work_t =std::function<std::string(std::string&)>;class TCPSever
{void Service(int sockfd){//package充当缓冲区std:: string package;char buff[1024];while(true){int n=::recv(sockfd,buff,sizeof(buff)-1,0);if(n>0){buff[n]=0;package+=buff;//必须是+=,这样才能保证发送过来的数据被加入到缓冲区//读取到的数据计算返回结果std::string result=_work(package);if(result.empty()) continue;//这里表明上面的报文不完整没法解析::send(sockfd,result.c_str(),result.size(),0);}else if(n==0){//表示读到了文件末尾LOG(LogLevel::INFO)<<"Client Quit……";break;}else{LOG(LogLevel::ERROR)<<"read error";break;}}}// 线程分离管理struct ThreadData{int _sockfd;TCPSever *_self;};static void *Handler(void *args){pthread_detach(pthread_self());ThreadData *data = (ThreadData *)args;data->_self->Service(data->_sockfd);return nullptr;}public:TCPSever(work_t work ,uint16_t port = defaultport) : _work(work),_addr(port){// 创建套接字int n = _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (n < 0){LOG(LogLevel::FATAL) << "socket failed";exit(1);}LOG(LogLevel::INFO) << "socket succeed";// 绑定n = ::bind(_listensockfd, _addr.NetAddr(), _addr.Len());if (n < 0){LOG(LogLevel::FATAL) << "bind failed";exit(1);}LOG(LogLevel::INFO) << "bind succeed";// 开始监听n = ::listen(_listensockfd, 5);if (n < 0){LOG(LogLevel::FATAL) << "listen failed";exit(1);}LOG(LogLevel::INFO) << "listen succeed";}void Run(){while (true){// 获取连接struct sockaddr_in connected_addr;socklen_t len = sizeof(connected_addr);int sockfd = ::accept(_listensockfd, (struct sockaddr *)&connected_addr, &len);if (sockfd < 0){LOG(LogLevel::ERROR) << "accept failed";continue;}InetAddr peer(connected_addr);LOG(LogLevel::INFO) << "accept succeed connected is " << peer.Addr() << " sockfd is " << sockfd;ThreadData *data = new ThreadData;data->_sockfd = sockfd;data->_self = this;pthread_t tid;pthread_create(&tid, nullptr, Handler, data);}}~TCPSever(){::close(_listensockfd);}private:int _listensockfd;InetAddr _addr;work_t _work;
};
而服务端的主函数这里我们需要注入执行方法:
//TCPSever.cc
#include "TCPSever.hpp"
#include "Protocol.hpp"
#include "Calculator.hpp"std::string Work(std::string& package)
{std::string message;std::string ret;//解包,循环获取直到不能解析为止while(Decode(package,&message)){if(message.empty()) break;//反序列化Request req;if(!req.Deserialize(message))break;//计算结果Response res=Calculator().Execute(req);//序列化res.Serialize(message);//封包EnCode(message,&message);//添加到结果缓存ret+=message;}return ret;
}int main()
{std::unique_ptr<TCPSever> ts_ptr = std::make_unique<TCPSever>(Work);ts_ptr->Run();return 0;
}
完成服务端之后我们继续客户端的编写,这里我将前面的客户端代码进行抽离,头文件中仅仅增加了执行方法的注入,由主函数传递方法交由Run函数执行:
//TCPClient.hpp
#pragma once
#include <functional>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include "Log.hpp"
#include "InetAddr.hpp"using namespace LogMudule;
const static std::string defaultip="127.0.0.1";
const static int defaultport=8888;using work_t=std::function<void(int)>;class TCPClient
{
public:TCPClient(work_t work,std::string ip,uint16_t port):_work(work),_dst_addr({ip,port}){//创建套接字_sockfd=::socket(AF_INET,SOCK_STREAM,0);if(_sockfd<0){LOG(LogLevel::FATAL)<<"socket failed";exit(1);}LOG(LogLevel::INFO)<<"socket succeed";//不需要绑定}void Run(){int n=::connect(_sockfd,_dst_addr.NetAddr(),_dst_addr.Len());if(n<0){LOG(LogLevel::ERROR)<<"connect failed";exit(3);}LOG(LogLevel::INFO)<<"connect succeed";while(true){_work(_sockfd);}}~TCPClient(){::close(_sockfd);}private:int _sockfd;InetAddr _dst_addr;work_t _work;
};
//TCPClient.cc
#include <memory>
#include "TCPClient.hpp"
#include "Protocol.hpp"void Work(int sockfd)
{// 获取输入int x, y;char oper;std::cout << "Please input x:";std::cin >> x;std::cout << "Please input y:";std::cin >> y;std::cout << "Please input oper:";std::cin >> oper;// 序列化Request req(x, y, oper);std::string package;req.Serialize(package);//封包std::string message;EnCode(package,&message);// 发送消息::send(sockfd, message.c_str(), message.size(), 0);// 接收结果char buff[1024];int n = ::recv(sockfd, buff, sizeof(buff), 0);if (n > 0){buff[n] = 0;std::string result = buff;//解包Decode(result,&message);// 反序列化Response res;res.Deserialize(message);LOG(LogLevel::DEBUG)<<"result:"<<res.Result()<<":code:"<<res.Code();}
}int main(int argc, char *argv[])
{if (argc != 3){std::cout << "Usgae Error" << std::endl;exit(-1);}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);std::unique_ptr<TCPClient> c_ptr = std::make_unique<TCPClient>(Work,ip, port);c_ptr->Run();return 0;
}
其实编写实现之后我们发现其逻辑不过如下图:

相关文章:
【Linux网络与网络编程】05.应用层自定义协议序列化和反序列化
前言 本篇博客通过网络计算器的实现来帮助各位理解应用层自定义协议以及序列化和反序列化。 一、认识自定义协议&&序列化和反序列化 我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序都是在应用层。前面我们说到:协议是一种…...
Android Gradle、Android Gradle Plugin、BuildTool关系
1. Gradle 的定位:通用构建工具 Gradle 是一个通用的跨平台构建工具,支持多种语言(如 Java、Kotlin、C)和项目类型 它的核心功能包括: 任务自动化:通过 Groovy/Kotlin DSL 脚本定义编译、测试、打包等…...
Java的Selenium的特殊元素操作与定位之时间日期控件
分为两种情况: 控件没有限制手动输入,则直接调用sendKeys方法写入时间数据 //时间日期控件处理 chromeDriver.get ("https://www,fliggy,com/?ttidsem.000000736&hlreferidbaidu.082076&route sourceseo"); chromeDriver.findElement (By.xpat…...
Flutter之页面布局二
目录: 1、列表布局1.1、基础列表1.2、水平滑动的列表1.3、网格列表1.3、不同列表项的列表1.4、包含间隔的列表1.6、长列表 2、滚动2.1、浮动的顶栏2.2、平衡错位滚动 1、列表布局 1.1、基础列表 import package:flutter/material.dart;void main() > runApp(con…...
RCE漏洞的小点总结
RCE简介与危害:包括远程代码执行和远程命令执行漏洞。 在很多web应用中,开发人员会使用一些函数,这些函数以一些字符串作为输入,功能是将输入的字符串当作代码或者命令来进行执行。当用户可以控制这些函数的输入时,就…...
设计模式简述(十)责任链模式
责任链模式 描述基本使用使用 描述 如果一个请求要经过多个类似或相关处理器的处理。 可以考虑将这些处理器添加到一个链上,让请求逐个经过这些处理器进行处理。 通常,在一个业务场景下会对整个责任链进行初始化,确定这个链上有哪些Handler…...
主相机绑定小地图
资源初始化:在类中通过 property 装饰器定义主相机、小地图相机、小地图精灵等资源属性,便于在编辑器中赋值。在 start 方法里,当确认这些资源存在后,创建渲染纹理并设置其大小,将渲染纹理与小地图相机关联,…...
单片机实现多线程的方法汇总
在单片机上实现“多线程”的方法有几种,下面按照从简单到复杂、从轻量到系统性来列出常见的方案: 🧵 一、伪多线程(最轻量) 方法:主循环 状态机 / 定时器轮询 主循环中轮流调用各个任务的处理函数&#x…...
Java八股文-List集合
集合的底层是否加锁也就代表是否线程安全 (一)List集合 一、数组 array[1]是如何通过索引找到堆内存中对应的这块数据的呢? (1)数组如何获取其他元素的地址值 (2)为什么数组的索引是从0开始的,不可以从1开始吗 (3)操作数组的时间复杂度 ①查找 根据索引查询 未…...
快手Python开发面经及参考答案
目录 Python 的深浅拷贝有什么区别?请举例说明。 Python 函数声明中有三种类型的参数,分别说明它们的区别。 Python 中的迭代器是怎么使用的? Python2 和 Python3 之间的区别有哪些(例如 range 和 xrange 等方面)? Python 的线程同步问题是怎样的?详细讲解 GIL 的原…...
谈谈策略模式,策略模式的适用场景是什么?
一、什么是策略模式? 策略模式(Strategy Pattern)属于行为型设计模式。核心思路是将一组可替换的算法封装在独立的类中,使它们可以在运行时动态切换,同时使客户端代码与具体算法解耦。它包含三个…...
从零构建大语言模型全栈开发指南:第四部分:工程实践与部署-4.2.3行业案例:智能客服中的图文交互系统
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 从零构建大语言模型全栈开发指南-第四部分:工程实践与部署4.2.3 行业案例:智能客服中的图文交互系统1. 图文交互系统的核心挑战与价值2. 系统架构设计2.1 分层架构2.2 Adapter技术应用3. 行业应用案例…...
华为IP(4)
VRRP(虚拟路由冗余协议) 前言: 局域网中的用户终端通常采用配置一个默认网关的形式访问外部网络,如果默认网关设备发生故障,那么所有用户终端访问外部网络的流量将会中断。可以通过部署多个网关的方式来解决单点故障…...
计算机网络中科大 - 第1章 结构化笔记(详细解析)
博主主页 目录 **1. 计算机网络概述****1.1 计算机网络的定义****1.2 计算机网络的发展** **2. 计算机网络的组成与分类****2.1 计算机网络的组成****2.2 计算机网络的分类****按地理范围****按拓扑结构****按交换方式** **3. 计算机网络的性能指标****4. 计算机网络体系结构**…...
【神经网络】python实现神经网络(三)——正向学习的模拟演练
有了之前的经验(【神经网络】python实现神经网络(二)——正向推理的模拟演练),我们继续来介绍如何正向训练神经网络中的超参(包含权重以及偏置),本章大致的流程图如下: 一.损失函数 神经网络以某个指标为基准寻求最优权重参数,而这个指标即可称之为 “损失函数” 。(…...
PPTAgent:一款开源免费生成和评估幻灯片的项目
这篇文章介绍一下PPTAgent,一个从文档自动生成演示文稿的创新系统。该系统从人类的展示创作方法中汲取灵感,采用两步流程来确保卓越的整体质量。此外,本文还介绍了PPTEval,这是一个综合评估框架,可以跨多个维度评估演示…...
配置管理:夯实软件开发与运维根基
配置管理是对系统配置信息进行管理的活动,以下从定义、目的、主要活动、实施流程等方面为你详细介绍: 一、定义 配置管理是通过技术或行政手段对软件产品及其开发过程和生命周期进行控制、规范的一系列措施。配置管理的目标是记录软件产品的演化过程&a…...
Java 大视界 -- Java 大数据在智能供应链库存优化与成本控制中的应用策略(172)
💖亲爱的朋友们,热烈欢迎来到 青云交的博客!能与诸位在此相逢,我倍感荣幸。在这飞速更迭的时代,我们都渴望一方心灵净土,而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识,也…...
Cocos Creator 进行 Web 发布后,目录结构解析
在使用 Cocos Creator 进行 Web 发布后,生成的目录结构通常包含以下内容,下面为你详细介绍: 1. index.html 这是 Web 项目的入口 HTML 文件,它会加载所需的 JavaScript 文件和资源,从而启动游戏或应用程序。示例代码…...
解决Spring Boot Test中的ByteBuddy类缺失问题
目录 解决Spring Boot Test中的ByteBuddy类缺失问题前奏问题描述问题解决第一步:移除ByteBuddy的特定版本号第二步:更新maven-surefire-plugin配置第三步:清理并重新构建项目 结语 解决Spring Boot Test中的ByteBuddy类缺失问题 前奏 今天&…...
Postman参数化设置如何设置?
在 Postman 里,参数化设置能让你用不同的数据多次运行同一个请求,这对测试不同输入时的 API 响应非常有用。下面为你详细介绍 Postman 参数化设置的方法。 1. 准备数据文件 Postman 支持 CSV 和 JSON 格式的数据文件。 CSV 文件 CSV 文件由逗号分隔的…...
斯坦福大学李飞飞团队新突破!FlowMo 革新图像 Tokenizer
当我们悠然刷着手机,看到一张可爱猫咪的照片时,大脑会瞬间识别出「这是一只猫」,这一切不过是电光火石间的事儿。但在计算机的 “眼中”,情况却复杂得超乎想象。假设这是一张10001000像素的彩色照片,在计算机的世界里&…...
基于 Jackson 的 JSON 工具类实现解析与设计模式应用
一、项目背景与功能概览 在企业级开发中,JSON 序列化/反序列化是高频操作。本方案基于 Jackson 实现了一个双模式兼容(独立使用 Spring 整合)、安全可靠的 JSON 工具类,主要提供以下能力: ✅ 常用 JSON 转换方法✅ …...
87.在线程中优雅处理TryCatch返回 C#例子 WPF例子
在C#异步编程中,正确处理异常是确保程序稳定运行的关键。今天,我们通过一个实际的示例,展示如何在异步线程中使用try-catch块处理异常,并通过标志变量控制流程。同时,我们也会展示一个错误的示例,以便更好地…...
Vue + Axios + Mock.js 全链路实操:从封装到数据模拟的深度解析
一、项目架构深度设计 1.1 分层架构模式 采用经典的前端分层架构,实现高度可维护性: src/ ├─ api/ # 接口管理 │ └─ home.js # 模块化接口 ├─ mock/ # 模拟数据 │ ├─ index.js # Mock入口 │ └─ home.js # 首…...
博客文章:深入分析 PyMovie - 基于 Python和 MoviePy 的视频管理工具
这是一个使用 wxPython 构建界面、moviepy 处理视频的自定义 GUI 应用程序。该工具提供了视频播放、元数据提取、格式转换、视频裁剪和截图等功能。通过分析其设计和实现,我们将了解其工作原理、优点和潜在的改进空间。 C:\pythoncode\new\output\pymovieSample.py …...
Go基础一(Maps Functions 可变参数 闭包 递归 Range 指针 字符串和符文 结构体)
Maps 1.创建map make(map[键类型]值类型) 2.设置键值对 name[key]value; 3. name[key]获取键值 3.1 key不存在 则返回 0 4.len()方法 返回 map 上 键值对数量 len(name) 5.delete()方法 从map中删除 键值对 delete(name,key) 6.clear()方法 map中删除所有键值对 clear(name) 7…...
2025年渗透测试面试题总结-某 携程旅游-基础安全工程师(题目+回答)
网络安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 携程旅游-基础安全工程师 反序列化原理 核心原理 扩展分析 SQL注入本质 核心原理 扩展分析 SQL注…...
wireshak抓手机包 wifi手机抓包工具
移动端接口测试抓包工具指南 在做手机或移动端APP的接口测试时,获取完整的接口文档是关键。但如果没有文档,我们就需要使用专业的抓包工具来分析网络请求。本文将介绍两种常用的抓包方案:Fiddler和Sniff Master(抓包大师…...
niuhe插件, 在 go 中渲染网页内容
思路 niuhe 插件生成的 go 代码是基于 github.com/ma-guo/niuhe 库进行组织管理的, niuhe 库 是对 go gin 库的一个封装,因此要显示网页, 可通过给 gin.Engine 指定 HTMLRender 来实现。 实现 HTMLRender 我们使用 gitee.com/cnmade/pongo2gin 实现 1. main.go …...

