【Linux网络与网络编程】05.应用层自定义协议序列化和反序列化

news2025/4/7 22:11:11

前言 

本篇博客通过网络计算器的实现来帮助各位理解应用层自定义协议以及序列化和反序列化。

一、认识自定义协议&&序列化和反序列化

我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序都是在应用层。前面我们说到:协议是一种 "约定"。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 erase
    package.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;
}

其实编写实现之后我们发现其逻辑不过如下图:

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

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

相关文章

Flutter之页面布局二

目录&#xff1a; 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简介与危害&#xff1a;包括远程代码执行和远程命令执行漏洞。 在很多web应用中&#xff0c;开发人员会使用一些函数&#xff0c;这些函数以一些字符串作为输入&#xff0c;功能是将输入的字符串当作代码或者命令来进行执行。当用户可以控制这些函数的输入时&#xff0c;就…

单片机实现多线程的方法汇总

在单片机上实现“多线程”的方法有几种&#xff0c;下面按照从简单到复杂、从轻量到系统性来列出常见的方案&#xff1a; &#x1f9f5; 一、伪多线程&#xff08;最轻量&#xff09; 方法&#xff1a;主循环 状态机 / 定时器轮询 主循环中轮流调用各个任务的处理函数&#x…

Java八股文-List集合

集合的底层是否加锁也就代表是否线程安全 (一)List集合 一、数组 array[1]是如何通过索引找到堆内存中对应的这块数据的呢? (1)数组如何获取其他元素的地址值 (2)为什么数组的索引是从0开始的&#xff0c;不可以从1开始吗 (3)操作数组的时间复杂度 ①查找 根据索引查询 未…

从零构建大语言模型全栈开发指南:第四部分:工程实践与部署-4.2.3行业案例:智能客服中的图文交互系统

👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 从零构建大语言模型全栈开发指南-第四部分:工程实践与部署4.2.3 行业案例:智能客服中的图文交互系统1. 图文交互系统的核心挑战与价值2. 系统架构设计2.1 分层架构2.2 Adapter技术应用3. 行业应用案例…

华为IP(4)

VRRP&#xff08;虚拟路由冗余协议&#xff09; 前言&#xff1a; 局域网中的用户终端通常采用配置一个默认网关的形式访问外部网络&#xff0c;如果默认网关设备发生故障&#xff0c;那么所有用户终端访问外部网络的流量将会中断。可以通过部署多个网关的方式来解决单点故障…

计算机网络中科大 - 第1章 结构化笔记(详细解析)

博主主页 目录 **1. 计算机网络概述****1.1 计算机网络的定义****1.2 计算机网络的发展** **2. 计算机网络的组成与分类****2.1 计算机网络的组成****2.2 计算机网络的分类****按地理范围****按拓扑结构****按交换方式** **3. 计算机网络的性能指标****4. 计算机网络体系结构**…

【神经网络】python实现神经网络(三)——正向学习的模拟演练

有了之前的经验(【神经网络】python实现神经网络(二)——正向推理的模拟演练),我们继续来介绍如何正向训练神经网络中的超参(包含权重以及偏置),本章大致的流程图如下: 一.损失函数 神经网络以某个指标为基准寻求最优权重参数,而这个指标即可称之为 “损失函数” 。(…

PPTAgent:一款开源免费生成和评估幻灯片的项目

这篇文章介绍一下PPTAgent&#xff0c;一个从文档自动生成演示文稿的创新系统。该系统从人类的展示创作方法中汲取灵感&#xff0c;采用两步流程来确保卓越的整体质量。此外&#xff0c;本文还介绍了PPTEval&#xff0c;这是一个综合评估框架&#xff0c;可以跨多个维度评估演示…

Java 大视界 -- Java 大数据在智能供应链库存优化与成本控制中的应用策略(172)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

斯坦福大学李飞飞团队新突破!FlowMo 革新图像 Tokenizer

当我们悠然刷着手机&#xff0c;看到一张可爱猫咪的照片时&#xff0c;大脑会瞬间识别出「这是一只猫」&#xff0c;这一切不过是电光火石间的事儿。但在计算机的 “眼中”&#xff0c;情况却复杂得超乎想象。假设这是一张10001000像素的彩色照片&#xff0c;在计算机的世界里&…

博客文章:深入分析 PyMovie - 基于 Python和 MoviePy 的视频管理工具

这是一个使用 wxPython 构建界面、moviepy 处理视频的自定义 GUI 应用程序。该工具提供了视频播放、元数据提取、格式转换、视频裁剪和截图等功能。通过分析其设计和实现&#xff0c;我们将了解其工作原理、优点和潜在的改进空间。 C:\pythoncode\new\output\pymovieSample.py …

2025年渗透测试面试题总结-某 携程旅游-基础安全工程师(题目+回答)

网络安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 携程旅游-基础安全工程师 反序列化原理 核心原理 扩展分析 SQL注入本质 核心原理 扩展分析 SQL注…

niuhe插件, 在 go 中渲染网页内容

思路 niuhe 插件生成的 go 代码是基于 github.com/ma-guo/niuhe 库进行组织管理的, niuhe 库 是对 go gin 库的一个封装&#xff0c;因此要显示网页, 可通过给 gin.Engine 指定 HTMLRender 来实现。 实现 HTMLRender 我们使用 gitee.com/cnmade/pongo2gin 实现 1. main.go …

使用MySQL时出现 Ignoring query to other database 错误

Ignoring query to other database 错误 当在远程连接软件中输入MySQL命令出现该错误 导致错误原因是&#xff1a;登录mysql时账户名没有加上u 如果出现该错误&#xff0c;退出mysql&#xff0c;重新输入正确格式进入即可&#xff01;

java后端开发day34--脑子空空如何无痛想起所有知识点--概念拟人化

1.上半部学习思考 1.1反思–浮躁–二倍速 刚开始算半个小白吧&#xff0c;从最基础的知识点开始学习&#xff0c;到后面学习整个项目的布局和功能。可能是后面慢慢懂得多了&#xff0c;每次打代码搞项目啊什么的&#xff0c;就能明显感觉到自己很浮躁&#xff1a;脑子里已经明…

fastGPT—前端开发获取api密钥调用机器人对话接口(HTML实现)

官网文档链接&#xff1a;OpenAPI 介绍 | FastGPT 首先按照文档说明创建api密钥 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-sca…

解决 PDF 难题:批量处理、文档清理与自由拆分合并

软件介绍 在日常办公与学习中&#xff0c;处理 PDF 文件常常让人头疼不已&#xff0c;不过别担心&#xff0c;今天有一款堪称神器的国产老牌 PDF 工具要分享给大家。它就是 PDF 补丁丁&#xff0c;凭借其强大功能&#xff0c;为大家排忧解难。 界面体验 初次打开 PDF 补丁丁&…

使用pycharm社区版调试DIFY后端python代码

目录 背景 前置条件 DIFY使用的框架 API服务调试配置步骤&#xff08;基于tag为0.15.3的版本&#xff09; 1.配置.env文件 2.关闭docker里面的docker-api-1服务 3.使用DOCKER启动本地环境需要用到的中间件&#xff0c;并暴露端口 注意事项一&#xff1a; 注意事项二&#xff1a…

量子计算与人工智能的结合:未来科技的双重革命

引言 在过去几十年里&#xff0c;人工智能&#xff08;AI&#xff09;和计算能力的提升一直是推动科技进步的重要力量。然而&#xff0c;随着深度学习和大规模数据处理的发展&#xff0c;传统计算架构的算力瓶颈逐渐显现&#xff0c;人工智能的训练和推理效率受到了限制。在此背…