《Linux网络编程》3.应用层自定义协议与序列化

💡Yupureki:个人主页

✨个人专栏:《C++》 《算法》《Linux系统编程》《高并发内存池》《MySQL数据库》

《个人在线OJ平台》《Linux网络编程》《CMake自动化构建工具》


🌸Yupureki🌸的简介:


目录

[1. 应用层](#1. 应用层)

[1.1 再谈协议](#1.1 再谈协议)

[1.2 网络版计算器](#1.2 网络版计算器)

[1.3 序列化与反序列化](#1.3 序列化与反序列化)

[2. 网络计算器实现](#2. 网络计算器实现)

[2.1 Socket封装](#2.1 Socket封装)

[2.2 定制协议](#2.2 定制协议)

[2.3 序列化和反序列化的开源库](#2.3 序列化和反序列化的开源库)

[2.3.1 安装](#2.3.1 安装)

[2.3.2 序列化](#2.3.2 序列化)

[2.3.3 反序列化](#2.3.3 反序列化)

[2.3.4 Request和Response实现](#2.3.4 Request和Response实现)

[2.4 计算器设计](#2.4 计算器设计)

[2.5 服务器设计](#2.5 服务器设计)

[2.6 上层使用](#2.6 上层使用)

[2.7 总结](#2.7 总结)


1. 应用层

1.1 再谈协议

在Linux网络(以及所有计算机网络)中,协议 可以通俗地理解为一种事先约定好的规则和标准

TCP/IP模型协议分4层:

层级 作用 Linux中的常见协议 生活类比
应用层 为应用程序提供特定网络服务 HTTP (网页)、SSH (远程登录)、DNS (域名解析)、NFS (文件共享) 信的内容(用中文写的信)
传输层 负责端到端的连接和数据可靠性 TCP (可靠、有连接,如下载文件)、UDP (快速、无连接,如视频通话) 怎么把信安全送到(挂号信 vs 平信)
网络层 负责寻址和路由,找到目标设备 IP (IPv4/IPv6,定义源和目标IP地址)、ICMP (ping命令用的协议) 信封上的地址(国家-城市-街道)
网络接口层 处理物理硬件(网卡、网线) ARP (通过IP地址找MAC地址)、以太网协议 实际送信的交通工具(货车、飞机)

我们之前谈到的TCP/UDP是传输层的协议,负责传输快递,但快递里面的内容是什么,需要我们自定义,这属于应用层协议

1.2 网络版计算器

我们期望做一个网络版的计算器:

  1. 客户端发送计算式,如1+1,发送给服务器
  2. 服务器接受到计算式,在后台进行计算,然后发送给客户端

那我们怎么处理发送的计算式,客户端直接把"1+1"这个字符串发送给服务器吗?

但是互联网上的信息可能传输不完全,我假设要发送"1+1+1"这个式子给服务器。但由于传输的问题,没有传输完整,只发送了1+1,那么服务器拿到后就会直接开始计算,根本不知道没有拿完数据。

因此我们需要对数据进行一层封装(如同快递的包装盒),再交给服务端(拆解快递盒),如果发现封装后的数据不完整(快递盒子只有一半),就不进行处理,继续接受直到数据完整,这就是序列化与反序列化

1.3 序列化与反序列化

在网络传输中,我们在传输前会对数据以一种特定的格式进行封装,也就是序列化 ;当对端接收到数据后,会再次按照该格式进行拆分,也就是反序列化

如上述的struct message结构体,其中的变量:year,month,day和name我们依次排列 ,再在外面加上花括号,这就是序列化

对端接受的数据可能是以下情况:

  • {year,month,day,name} (完整)
  • {year,month,da (不完整)
  • {year,month,day,name}{year,mon (多出了不完整的部分)

如何判断是否完整?->外面的花括号是否完整

如果只有前花括号,那么就不完整;如果前后花括号都有,那么这中间的数据就是完整的,直接拿走,然后继续处理下一个数据

2. 网络计算器实现

2.1 Socket封装

为了方便,我们专门把socket相关函数用C++封装成类

  • Socket虚基类:保留socket基本接口,如bind,listen,recv等,设计InitTcpServer和InitTcpClient接口,后续派生类可调用这两个接口自由创建服务器端和客户端
  • TcpSocket派生类:根据虚基类实现具体的TCP的接口
cpp 复制代码
#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>
#include "com.hpp"
#include <memory>

class Socket
{
public:
    Socket()
    {}
    ~Socket()
    {}
    virtual void create_socket() = 0;
    virtual void Bind(uint16_t) = 0;
    virtual void Listen(int) = 0;
    virtual std::shared_ptr<Socket> Accept(InetAddr&) = 0;
    virtual bool Connect(const std::string&,const uint16_t&) = 0;
    virtual int Recv(std::string&) = 0;
    virtual void Send(const std::string&) = 0;
    virtual int get_sockfd() = 0;
    void InitTcpServer(uint16_t port = DEFAULT_PORT,int backlog = DEFAULT_BACKLOG)
    {
        create_socket();
        Bind(port);
        Listen(backlog);
    }
    void InitTcpClient(std::string ip = DEFAULT_IP,uint16_t port = DEFAULT_PORT)
    {
        create_socket();
        Connect(ip,port);
    }
};

class TcpSocket : public Socket
{
private:
    using func_t = std::function<void()>;
public:
    TcpSocket(int sockfd = DEFAULT_SOCKFD)
    :_sockfd(sockfd)
    {}
    void create_socket() override
    {
        _sockfd = socket(AF_INET,SOCK_STREAM,0);
        
        if(_sockfd < 0)
            exit(ExitCode::SOCKET);
    }
    void Bind(uint16_t port)override
    {
        InetAddr addr(port);
        int n = bind(_sockfd,CONV(addr.get_addr()),sizeof(addr.get_addr()));
        if(n < 0)
            exit(ExitCode::BIND);
    }
    void Listen(int backlog)override
    {
        int n = listen(_sockfd,backlog);
        if(n < 0)
            exit(ExitCode::LISTEN);
    }
    std::shared_ptr<Socket> Accept(InetAddr& addr)override
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int sockfd = accept(_sockfd,CONV(peer),&len);
        if(sockfd < 0)
            return nullptr;
        InetAddr tmp(peer);
        addr = tmp;
        std::shared_ptr<Socket> p = std::make_shared<TcpSocket>(sockfd);
        return p;
    }
    bool Connect(const std::string& ip,const uint16_t& port)override
    {
        InetAddr addr(ip,port);
        int n = connect(_sockfd,CONV(addr.get_addr()),sizeof(addr.get_addr()));
        if(n < 0)
            return false;
        return true;
    }
    int Recv(std::string& out)override
    {
        char buffer[MAXNUM];
        ssize_t n = recv(_sockfd,buffer,sizeof(buffer) - 1,0);
        if(n > 0)
        {
            buffer[n] = '\0';
            out += buffer;
            return n;
        }
        else if(n == 0)
            return 0;
        else
            return -1;
    }
    void Send(const std::string& out)override
    {
        send(_sockfd,out.c_str(),sizeof(out),0);
    }
    int get_sockfd()override
    {
        return _sockfd;
    }
private:
    int _sockfd;
};

2.2 定制协议

一个完整的协议至少需要这两个类:

  • Request:发送端发送的请求数据
  • Response:接收端根据请求,进行处理后返回给发送端的数据

具体流程:

发送端->request序列化,发送给接收端->接收端接受request,反序列化->对request处理,制作response->response序列化,返回给发送端->发送端接受response,反序列化得到结果

代码构造:

cpp 复制代码
class Request
{
public:
    Request(int x = 0,char oper = ' ',int y = 0)
    :_x(x),_oper(oper),_y(y)
    {}

    void Serialize(std::string& out)//序列化:成员变量序列化成字符串
    {
    }

    bool Dserialize(std::string& out)//反序列化:字符串反序列化,提取成类中的成员变量
    }
    int get_x(){return _x;}
    char get_oper(){return _oper;}
    int get_y(){return _y;}
    std::string get_request_string()
    {
        return std::to_string(_x) + " " + _oper + " " + std::to_string(_y);
    }
private:
    int _x;
    char _oper;
    int _y;
};
cpp 复制代码
class Reponse
{
public:
    Reponse(double ret = 0,int code = 0)
    :_result(ret),_code(code)
    {}
    Reponse(Reponse& rep)
    {}
    Reponse(Reponse&& rep)
    {}
    Reponse& operator=(Reponse&& rep)
    {}
    Reponse& operator=(Reponse& rep)
    {}
    void Serialize(std::string& out)//序列化
    {}

    bool Dserialize(std::string& out)//反序列化
    {}
    double get_ret()
    {
        return _result;
    }
    int get_code()
    {
        return _code;
    }
    std::string get_result_string()
    {
        return std::to_string(_result) + "[" +std::to_string(_code) + "]";
    }
private:
    double _result;//计算结果
    int _code;//计算状态码
};

问题:

TCP 是一个面向字节流 的协议,它本身不维护消息边界。当发送方连续发送多个小数据包,或接收方一次读取到多个包的数据时,就可能出现"粘包"------即多个应用层消息粘在一起,无法区分哪里是结束、哪里是开始。解决粘包问题的核心是在应用层定义消息边界

如:

  1. request对1+1序列化得:{1 + 1}
  2. 后续再对{1+1}定义消息边界,如7\r\n{1 + 1}\r\n
    1. \r\n表示每个消息的开头和结尾
    2. 7表示正文"{1 + 1}"(包括空格和花括号)的长度,相当于字节个数
  3. 接受端接收到7\r\n{1 + 1}\r\n,会先:
    1. 找到第一个"\r\n",如果没找到直接返回
    2. 找到数据长度7,计算完整报文的长度:1('7'的长度) + 2*2(两个"\r\n"的长度) + 7(正文的长度)
    3. 如果实际接受到的数据长度小于计算的长度,说明不完全,直接返回;如果大于或等于,则截断计算的长度的字符串,进行处理后再处理下一个报文

这就是打包和解包的过程

我们再构建Protocol类,这个类用来:

  1. 对数据的打包和解包
  2. 接发送数据
  3. 处理数据(具体业务,需要上层指定)
cpp 复制代码
class Protocol
{
private:
    const std::string proto_sep = " ";
    const std::string line_sep = "\r\n";//分隔符
    using func_t = std::function<Reponse(Request&)>;//业务处理函数,上层指定
public:
    Protocol()
    {}
    Protocol(func_t func)
    :_func(func)
    {}
    void Encode(std::string& message)//打包
    {
        message = std::to_string(message.size()) + line_sep + message + line_sep; 
    }
    
    bool Decode(std::string& package,std::string& message)//解包
    {
        int pos = package.find(line_sep);
        if(pos == std::string::npos)
            return false;
        std::string num = package.substr(0,pos);
        int total = std::stoi(num) + num.size() + 2 * line_sep.size();
        if(package.size() < total)
            return false;
        message = package.substr(pos + line_sep.size(),std::stoi(num));
        package.erase(0,total);
        return true;
    }

    bool get_request(std::shared_ptr<Socket>& sock,Request& req)//接受request,进行1.解包2.反序列化
    {
        std::string package;
        while(1)
        {
            int n = sock->Recv(package);
            if(n > 0)
            {
                std::string message;
                if(!Decode(package,message))
                    continue;
                req.Dserialize(message);
                return true;
            }
            else if(n == 0)
                exit(ExitCode::NORMAL);
            else
                return false;
        }
    }
    bool get_reponse(std::shared_ptr<Socket>& sock,Reponse& rep)//接受response,进行1.解包2.反序列化
    {
        std::string package;
        while(true)
        {
            int n = sock->Recv(package);
            if(n > 0)
            {
                std::string message;
                if(!Decode(package,message))
                    continue;
                rep.Dserialize(message);
                return true;
            }
            else if(n == 0)
                return false;
            else
                return false;
        }
    }
    Reponse handle(Request& req)//数据处理(接受端的任务)
    {
        return _func(req);
    }
private:
    func_t _func;
};

2.3 序列化和反序列化的开源库

Jsoncpp是一个用于处理JSON数据的C++库 。它提供了将JSON数据序列化 为字符串以及从字符串
反序列化为C++数据结构的功能。Jsoncpp是开源的,广泛用于各种需要处理JSON数据的C++项目中。

2.3.1 安装

ubuntu:sudo apt-get install libjsoncpp-dev
Centos:sudo yum installjsoncpp-devel

2.3.2 序列化

使用Json::ValueJson::FastWriter快速上手

cpp 复制代码
#include <jsoncpp/json/json.h>
#include <iostream>
#include <jsoncpp/json/value.h>

int main() 
{
    Json::Value json;
    json["x"] = 1;
    json["oper"] = '+';
    json["y"] = 2;
    Json::FastWriter w;
    std::string out = w.write(json);
    std::cout<<out;
    return 0;
}

2.3.3 反序列化

使用Json::ValueJson::Reader快速上手

cpp 复制代码
#include <jsoncpp/json/json.h>
#include <iostream>
#include <jsoncpp/json/reader.h>
#include <jsoncpp/json/value.h>

int main() 
{
    Json::Value json;
    json["x"] = 1;
    json["oper"] = '+';
    json["y"] = 2;
    Json::FastWriter w;
    std::string out = w.write(json);
    std::cout<<out;
    Json::Reader r;
    bool res = r.parse(out,json);
    if(res)
    {
        int x = json["x"].asInt();
        char oper = json["oper"].asInt();
        int y = json["y"].asInt();
        std::cout<<x<<" "<<oper<<" "<<y<<std::endl;
    }
    return 0;
}

2.3.4 Request和Response实现

我们使用jsoncpp来进行request和response的序列化和反序列化

cpp 复制代码
class Request
{
public:
    Request(int x = 0,char oper = ' ',int y = 0)
    :_x(x),_oper(oper),_y(y)
    {}

    void Serialize(std::string& out)
    {
        Json::Value json;
        json["x"] = _x;
        json["oper"] = _oper;
        json["y"] = _y;
        Json::FastWriter w;
        out = w.write(json);
    }

    bool Dserialize(std::string& out)
    {
        Json::Value json;
        Json::Reader r;
        bool res = r.parse(out,json);
        if(res)
        {
            _x = json["x"].asInt();
            _oper = json["oper"].asInt();
            _y = json["y"].asInt();
        }
        return res;
    }
    int get_x(){return _x;}
    char get_oper(){return _oper;}
    int get_y(){return _y;}
    std::string get_request_string()
    {
        return std::to_string(_x) + " " + _oper + " " + std::to_string(_y);
    }
private:
    int _x;
    char _oper;
    int _y;
};

class Reponse
{
public:
    Reponse(double ret = 0,int code = 0)
    :_result(ret),_code(code)
    {}
    Reponse(Reponse& rep)
    {
        _result = rep.get_ret();
        _code = rep.get_code();
    }
    Reponse(Reponse&& rep)
    {
        _result = rep.get_ret();
        _code = rep.get_code();
    }
    Reponse& operator=(Reponse&& rep)
    {
        _result = rep.get_ret();
        _code = rep.get_code();
        return *this;
    }
    Reponse& operator=(Reponse& rep)
    {
        _result = rep.get_ret();
        _code = rep.get_code();
        return *this;

    }
    void Serialize(std::string& out)
    {
        Json::Value json;
        json["result"] = _result;
        json["code"] = _code;
        Json::FastWriter w;
        out = w.write(json);
    }

    bool Dserialize(std::string& out)
    {
        Json::Value json;
        Json::Reader r;
        bool res = r.parse(out,json);
        if(res)
        {
            _result = json["result"].asDouble();
            _code = json["code"].asInt();
        }
        return res;
    }
    double get_ret()
    {
        return _result;
    }
    int get_code()
    {
        return _code;
    }
    std::string get_result_string()
    {
        return std::to_string(_result) + "[" +std::to_string(_code) + "]";
    }
private:
    double _result;
    int _code;
};

2.4 计算器设计

自定义协议能够让我们成功接收到"{1 + 1}"这个字符串,并对这个字符串反序列化成了Request,但这个Request怎么处理?我们需要上层自定义。而在这里,我们要对这个Request中的数据进行普通的加减乘除的运算,因此我们需要设计计算器

这个计算器需要:

  1. 接受Request参数
  2. 对Request中的数据进行处理
  3. 对处理后数据做成Response
cpp 复制代码
class cal
{
public:
    Reponse func(Request &req)
    {
        auto x = req.get_x();
        auto y = req.get_y();
        char oper = req.get_oper();
        switch (oper)
        {
        case '+':
            return Reponse(x + y, 0);
        case '-':
            return Reponse(x - y, 0);
        case '*':
            return Reponse(x * y, 0);
        case '/':
            if (y == 0)
                return Reponse(0, 1);
            else
                return Reponse(x / y, 0);
        case '%':
            return Reponse(x % y, 0);
        default:
            return Reponse(0, 2);
        }
    }
};

2.5 服务器设计

我们作为开发者,期望设计一个TcpServer,这个TcpServer内部包含TcpSocket和业务处理函数;TcpSocket由我们自己提供,而业务处理函数需要上层进行提供

cpp 复制代码
#pragma once

#include "socket.hpp"

class TcpServer : public nocopy
{
private:
    using func_t = std::function<void(std::shared_ptr<Socket>&,InetAddr&)>;//自定义处理函数
public:
    TcpServer(uint16_t port,func_t func)
    :_port(port),_ioserver(func),_listensock(std::make_shared<TcpSocket>())
    {}
    void init(int backlog = DEFAULT_BACKLOG)
    {
        _listensock->InitTcpServer(_port,backlog);
        _listensockfd = _listensock->get_sockfd();
    }
    void run()
    {
        _isrunning = true;
        while(_isrunning)
        {
            InetAddr addr;
            std::shared_ptr<Socket> sock = _listensock->Accept(addr);
            if(sock == nullptr)
                continue;
            pid_t pid = fork();
            if(pid > 0)
            {
                close(sock->get_sockfd());
                waitpid(pid,nullptr,0);
            }
            else if(pid == 0)
            {
                if(fork() > 0)
                    exit(ExitCode::NORMAL);
                close(_listensock->get_sockfd());
                _ioserver(sock,addr);//把数据全盘交给用户的业务处理函数
            }
            else
                exit(ExitCode::FORK);
        }
    }

private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning = false;
    std::shared_ptr<Socket> _listensock;
    func_t _ioserver;
};

2.6 上层使用

  • 底层:提供TcpServer,用来接受数据。但这个数据接受后,我该如何处理?看上层的选择
  • 上层:提供自定义协议,在TcpServer接受到数据后进行个性化处理
cpp 复制代码
#include "NetCal.hpp"
#include "protocol.hpp"
#include "TcpServer.hpp"
#include "com.hpp"

int main(int argv, char *argc[])
{
    if (argv != 2)
        exit(ExitCode::FORMAT);
    std::shared_ptr<cal> netcal = std::make_shared<cal>();
    std::shared_ptr<Protocol> prot = std::make_shared<Protocol>([&netcal](Request &req) -> Reponse
                                                                { return netcal->func(req); });
    TcpServer server(atoi(argc[1]), [&prot](std::shared_ptr<Socket> &server, InetAddr &addr)
    {//上层自定义处理函数
        while(1)
        {
            Request req;
            prot->get_request(server,req);
            std::cout<<"request : "<<req.get_request_string()<<std::endl;
            Reponse rep = prot->handle(req);
            std::cout<<"reponse : "<<rep.get_result_string()<<std::endl;
            std::string message;
            rep.Serialize(message);
            prot->Encode(message);
            server->Send(message);
        } 
    }
    );
    server.init();
    server.run();
}

2.7 总结

一图理清流程:

相关推荐
李日灐1 小时前
<3>Linux 基础指令:从时间、查找、文本过滤到 .zip/.tgz 压缩解压与常用热键
linux·运维·服务器·开发语言·后端·面试·指令
木雷坞2 小时前
2026年了,NAS拉个Docker镜像还要3小时?技术方案PK与实测对比 🚀
运维·docker·容器
hughnz2 小时前
自动化控压钻井系统的挑战与风险
linux·服务器·网络
wanhengidc2 小时前
云服务器和物理服务器的不同之处
运维·服务器·网络·网络协议·智能手机
色空大师2 小时前
【linux开放端口-以8848为例】
linux·运维·服务器·防火墙
咋吃都不胖lyh2 小时前
在 Linux 环境下,查看、编辑并使环境变量生效
linux·运维·服务器
曼岛_2 小时前
[网络安全] Linux权限维持-隐藏篇
linux·安全·web安全·安全威胁分析
修心光2 小时前
kuboard升级过程
运维
czxyvX2 小时前
主从Reactor模型实现并发服务器
linux·网络·epoll
keyipatience2 小时前
Linux 基本指令
linux·运维·服务器