💡Yupureki:个人主页

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

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


🌸Yupureki🌸的简介:


目录

1. 应用层

1.1 再谈协议

1.2 网络版计算器

1.3 序列化与反序列化

2. 网络计算器实现

2.1 Socket封装

2.2 定制协议

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

2.3.1 安装

2.3.2 序列化

2.3.3 反序列化

2.3.4 Request和Response实现

2.4 计算器设计

2.5 服务器设计

2.6 上层使用

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的接口
#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,反序列化得到结果

代码构造:

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;
};
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. 处理数据(具体业务,需要上层指定)
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快速上手

#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快速上手

#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的序列化和反序列化

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
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由我们自己提供,而业务处理函数需要上层进行提供

#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接受到数据后进行个性化处理
#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 总结

一图理清流程:

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐