• 模拟实现应用层协议


    模拟实现应用层协议

    应用层

    image-20230829191153866

    应用层(Application layer)是OSI模型的第七层。应用层直接和应用程序接口并提供常见的网络应用服务。应用层也向表示层发出请求。应用层是开放系统的最高层,是直接为应用进程提供服务的。其作用是在实现多个系统应用进程相互通信的同时,完成一系列业务处理所需的服务。我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层。

    再谈协议

    运输层为应用进程提供了端到端的通信服务,但不同的网络应用的应用进程之间,还需要有不同的通信规则,因此在运输层协议之上,还需要有应用层协议。协议作为一种”约定“,那么必须遵守一些准则。对应应用层协议一般要遵守:

    1. 应用进程交换的报文类型,如请求报文和响应报文。
    2. 各种报文类型的语法,如报文中的各个字段及其详细描述。
    3. 字段的语义,即包含在字段中的信息的含义。
    4. 进程何时,如何发送报文,以及对报文进行响应的规则

    序列化和反序列化

    官方定义:序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

    序列化有两个用途:

    • 把对象的字节序列永久保存到硬盘上,通常存放在一个文件中(序列化对象)
    • 在网络上传送对象的字节序列(网络传输对象)

    实际上就是将数据持久化,防止一直存储在内存当中,消耗内存资源。而且序列化后也能更好的便于网络运输何传播。

    image-20230829223851219

    例如在微信里你給对方发送一条消息,实际上会将头像、昵称、消息内容、发送时间等构建一个结构,然后将该结构进行序列化,即将该结构形成一个字节流报文,通过网络将该报文发送給对方,对方进行反序列化,将该报文转化为结构,然后重新拆解为头像、昵称、消息内容、发送时间等。

    而序列化还解决了网络传输结构体由于大小端、内存对齐导致数据出错等问题

    网络版计算器

    现通过指定简单的协议,实现一个服务器版的计算器。我们需要在客户端把要计算的两个数和运算符发过去, 然后由服务器进行计算, 最 后再把结果返回给客户端。

    对应网络计算器约定协议:

    • 客户端发送一个形如"1+1"的字符串
    • 这个字符串中有两个操作数都是整形
    • 两个操作数只间有一个运算符,操作数和运算符之间不能有空格

    对应网络的序列化和反序列化:

    • 定义一个结构体来表示我们需要交互的信息
    • 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体。这个过程就称为序列化和反序列化

    在TCP协议中,如何保证接收方收到的是完整的报文呢?

    image-20230829231938344

    • 我们调用的发送或接收函数,本质上是拷贝函数。

    • 应用层调用的发送或接收函数,并不是直接从网络中读取或发送数据。例如客户端在应用层调用发送函数发送数据时,是将数据从应用层的发送缓冲区拷贝一份到传输层的发送缓冲区。然后由传输层自主决定何时将数据发送至网络中,服务器的传输层再通过网络将数据读取到接收缓冲区,然后将数据拷贝一份到应用层的接收缓冲区。因此TCP协议是一种传输控制协议

    • 由于使用在TCP协议的双方都有发送缓冲区和接收缓冲区,即读取数据和发送数据不会互相干扰,可以同时双向传输数据,因此TCP协议是一种全双工的通信协议。

    • 由于TCP协议是一种全双工的通信协议,因此也会产生客户端发送数据的速度远远大于服务器读取数据的速度,此时会造成服务器的接收缓冲区内积攒大量的报文,这些报文是线性连接在一起的,那么如何将一条条报文完整的读取上来呢?**通过指定的协议,按照协议规定的方式读取上来。**协议定制的方式有:

      • 定长:规定该报文的长度
      • 间隔符号:规定报文之间存在间隔符号
      • 自描述方式:自定义协议

    该网络版计算器对应的协议如下:

    image-20230829235118910

    • 由于UDP协议发送和接收数据都是以数据报的形式,在传输过程中数据是完整的,因此并不需要通过序列和反序列化、对数据添加特殊内容的方式去界定报文边界;而TCP协议发送和接收数据是以字节流的方式,就必须使用相关协议手段去标识、保护报文。

    自定义协议

    protocol.hpp

    #pragma once
    #include
    #include
    #include 
    #include 
    #include 
    using namespace std;
    
    #define SEP " "
    #define SEP_LEN strlen(SEP)//strlen统计'\0'之前的字符个数,而sizeof统计的是所占内存的空间大小,使用sizeof会越界出问题
    #define LINE_SEP "\r\n"
    #define LINE_SEP_LEN strlen(LINE_SEP)
    
    enum {
        NONE=0,
        DIV_ZERO,
        MOD_ZERO,
        OP_ERR
    };
    //"x op y"->"text_len"\r\n"x op y"\r\n---給内容加上报头
    std::string enLength(const std::string& text)//协议定制
    {
        std::string send_str=to_string(text.size());
        send_str+=LINE_SEP;
        send_str+=text;
        send_str+=LINE_SEP;
    
        return send_str;
    }
    //"text_len"\r\n"x op y"\r\n -> "x op y"---去掉报头,取出里面的内容
    bool deLength(const std::string& str,string* ret)//协议定制
    {
        auto it=str.find(LINE_SEP);//找到报头
        if(it==std::string::npos) return false;//如果没找到则直接返回
    
        int len=stoi(str.substr(0,it));//取出字符串的长度
        *ret=str.substr(it+LINE_SEP_LEN,len);//取出数据
        return true;
    }
    
    class Request
    {
    public:
    Request():_x(0),_y(0),_op(0){}
    Request(int x,int y,int op):_x(x),_y(y),_op(op){}
    
    bool Serialize(std::string* out)//序列化,将传入的x op y转化为字符串"x op y"
    {
        *out="";
        *out+=to_string(_x);
        *out+=SEP;
        *out+=to_string(_op);
        *out+=SEP;
        *out+=to_string(_y);
    
        return true;
    }
    
    bool Deserialize( const string& origin)//反序列化,将传过来的字符串拆出来传参給_x _op _y
    {//"_xSEP_opSEP_y"-> _x,_op,_y
    
        auto leftit=origin.find(SEP);
        cout<<"Deserialize找到了leftSEP: "<<leftit<<endl;
        auto rightit=origin.rfind(SEP);
        cout<<"Deserialize找到了rightSEP: "<<rightit<<endl;
        if(leftit==string::npos|| rightit==string::npos) return false;
    
        if(leftit==rightit) return false;
        int opsize=rightit-leftit-1;
        cout<<"opsize: "<<opsize<<endl;
    //1 43 1--leftit=1,rightit=4,opsize=rightit-leftit-1=4-1-1=2;
    //1 3 1--leftit=1,right=3,opsize=rightit-leftit-1=3-1-1=1
       // if(rightit-(leftit+SEP_LEN)!=1) return false;
        if(rightit-(leftit+SEP_LEN)!=opsize) return false;
        //+号ASCII码是43,从char转int被解析成43即stringlen为两位,这里的运算rightit-(leftit+SEP_LEN)!=1就出问题
    //4-(1+1)==2;3-(1+1)=1
        std::string origin_x=origin.substr(0,leftit);
        std::string origin_y=origin.substr(rightit+SEP_LEN);
        if(origin_x.empty()) return false;
    
        if(origin_y.empty()) return false;
    
        cout<<"origin_x: "<<origin_x<<" origin_y: "<<origin_y<<endl;
        _x=stoi(origin_x);
        int opf=stoi(origin.substr(leftit,rightit));
        _op=opf;
        cout<<"opf: "<<opf<<"_op: "<<_op<<endl;
        _y=stoi(origin_y);
    
        return true;
    
    }
        public:
        int _x;
        int _y;
        char _op;
    };
    
    class Response
    {
    public:
    Response():_exitcode(0),_result(0){}
    Response(int exitcode,int result):_exitcode(exitcode),_result(result){}
    bool Serialize(string*out)//序列化
    {//_exitcode _result ->"_exitcodeSEP_result"
    *out="";
    *out+=to_string(_exitcode);
    *out+=SEP;
    *out+=to_string(_result);
    
    return true;
    }
    
    bool Deserialize(const string& in)//反序列化
    {//_exitcodeSEP_result"->_exitcode _result
    
    auto pos=in.find(SEP);
    if(pos==string::npos) return false;
    
    string excstr=in.substr(0,pos);
    string resstr=in.substr(pos+SEP_LEN);
    if(excstr.empty()||resstr.empty()) return false;
    
    _exitcode=stoi(excstr);
    _result=stoi(resstr);
    
    return true;
    
    }
    
    public:
    int _exitcode;//退出码
    int _result;//结果
    };
    
    //"text_len"\r\n"x op y"\r\n
    bool recvPackage(int sock,string& inbuffer,string*out)
    {
    char buffer[1024];
    
    while(true)
    {
    ssize_t  s=recv(sock,buffer,sizeof(buffer)-1,0);
    if(s>0)
    {
        buffer[s]=0;
        inbuffer+=buffer;
        auto pos=inbuffer.find(LINE_SEP);
        if(pos==string::npos)continue;//没找到报头和有效载荷之间的分隔符---如果字节流式的报文没读全就继续读
        string text_len=inbuffer.substr(0,pos);//报头是有效载荷的长度
        int len=stoi(text_len);
        
        int totallen=text_len.size()+LINE_SEP_LEN*2+len;//整个报文的长度
        if(inbuffer.size()<totallen) 
        {
            cout<<"输入的消息不完整,请继续输入.continue..."<<endl;
            continue;//报文没读完继续读
        }
        
        cout<<"处理前的inbuffer: \n"<<inbuffer<<endl;
    
        *out=inbuffer.substr(0,totallen);
        inbuffer.erase(0,totallen);
    
        cout<<"处理后的inbuffer: \n"<<inbuffer<<endl;
    
        break;
    }
    else return false;
    }
    
    return true;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174

    介绍一下:

    • 在Request类内:
    1. 外部传参两个操作数和一个运算符进来构造Request对象,调用Serialize序列化函数构造字符串"_xSEP_opSEP_y",其中SEP是空格, _x是左操作数, _op是运算符, _y是右操作数
    2. 外部传参数字符串"_xSEP_opSEP_y"进来,调用Deserialize反序列化通过该字符串构造内嵌参数 _x, _op, _y。注意的是,运算符 _op在传入时会将符号转化成其对应的ASCII码。例如"+“会被转化成"43”,此时需要将字符串里的"43"取出来再转化成整形43,方便后续转化回"+"
    • 在Response类内:
    1. 外部传字符串"_exitcodeSEP_result"进来,调用Deserialize反序列化函数构造内嵌参数 _exitcode和 _result。其中 exitcode是退出码,当计算结果正确时退出码为NONE。计算时出现除零错误退出码为DIV_ZERO。计算时出现商零错误退出码为MOD_ZERO。传入参数时运算符传入错误,退出码为OP_ERR; _result是计算结果。
    2. 调用Serialize序列化函数利用内嵌参数 _exitcode和 _result构造字符串" _exitcodeSEP_result"
    • enLength协议定制函数,給传入的字符串加上"报头",传入字符串"xSEPopSEPy",加上"报头后字符串为"text_len"\r\n"xSEPopSEPy"\r\n。其中"xSEPopSEPy"为有效载荷,text_len为有效载荷的长度,SEP是空格。

    • deLength协议定制函数。函数作用是給传入的字符串str去"报头",并把有效载荷通过ret传出去。传入的字符串是"text_len"\r\n"xSEPopSEPy"\r\n,去掉"报头"后,取出字符串 “xSEPopSEPy”。其中SEP是空格

    • recvPackage函数是供服务器调用接收数据包的函数。服务器接收客户端发送来的报文,若报文不符合定义的协议形式或者没读上来的报文不是完整的则阻塞式读取,直到读上来完整的报文。然后通过输出型参数out将报文传出去。

    calserver.cc

    
    #include"calserver.hpp"
    #include"log.hpp"
    #include
    #include
    #include
    using namespace Server;
    using namespace std;
    
    static void Usage(string proc)
    {
        cout<<"\nUsage:\n\t"<<proc<<" local_port\n\n"<<endl;
    }
    //req是一个已经处理的完整的对象
    bool cal(const Request& req,Response& rep)
    //根据req填充rep 
    {
    switch(req._op)
    {
        case '+':
        rep._result=req._x+req._y;
        break;
        case '-':
            rep._result=req._x-req._y;
        break;    
        case '*':
            rep._result=req._x*req._y;
        break;    
        case '/':
        {
            if(req._y==0) rep._exitcode=DIV_ZERO;
            else
            rep._result=req._x/req._y;
        }   
        break;    
        case '%':
        {
            if(req._y==0) rep._exitcode=MOD_ZERO;
            else
            rep._result=req._x%req._y;
        }
        break;
        default:
        rep._exitcode=OP_ERR;
        break;
    }
    return true;
    
    }
    
    int main(int argc,char* argv[])
    {
    if(argc!=2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    
    uint16_t port=atoi(argv[1]);//将字符串转化为整数
    
    unique_ptr<calserver> ts(new calserver(port));
    ts->initserver();
    ts->start(cal);
    
    
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • cal是计算函数,传入Request对象和Response对象。Request对象中参数有两个操作数 _ x、_ y 和一个运算符 _ op,计算出结果放到Response对象的参数_result中,退出码放到 _exitcode。

    calserver.hpp

    #pragma once
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include
    #include"log.hpp"
    #include"protocol.hpp"
    #define NUM 1024
    
    using namespace std;
    static const int gbacklog = 5;
    namespace Server
    {
        enum
        {
            USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR
        };
    
    typedef function<bool(const Request&req,Response& res)> func_t;
    
    void handlerentry(int sock,func_t func)
    {
         string inbuffer;
        while(true)
        {
        //1. 获取客户端发送来的数据报,确定数据报是带报头的数据报
        //"text_len"\r\n"x op y"\r\n 
       string req_text,req_str;
        if(!recvPackage(sock,inbuffer,&req_text)) return;
        cout<<"带报头的请求(数据报): "<<req_text<<endl;
        //2.对数据报进行反序列化
        //"text_len"\r\n"x op y"\r\n -> "x op y"---去掉报头,取出里面的内容
        if(!deLength(req_text,&req_str)) return;
        cout<<"去掉报头的请求(数据报):"<<req_str<<endl;
        //走到这里再往下就卡主了,只打印到上面那条日志后面都没打印到!!!
        //3.获得一个结构化的请求对象
        Request req;
       if(!req.Deserialize(req_str)) return;//如果反序列化失败直接返回
        //4.对对象进行操作---进行服务器业务
        //4.1.获得一个结构化响应
         Response rep;
        func(req,rep);
        //5.对对象进行序列化
        //_exitcode _result ->"_exitcodeSEP_result"
        string rep_str;
        rep.Serialize(&rep_str);
        cout<<"计算完成后的响应: "<<rep_str<<endl;
        //6.給有效载荷加上报头
         //"exitcode result" -> "content_len"\r\n"exitcode result"\r\n
        string rep_text=enLength(rep_str);
        cout<<"加上报头的完整响应(报文): "<<rep_text<<endl;
    
        //7.把报文发送回給客户端
        send(sock,rep_text.c_str(),rep_text.size(),0);
        
        }
        
    }
    typedef function<bool(const Request&req,Response& res)> func_t;
    class calserver
    {
     
    public:
    calserver(const uint16_t& port):_port(port),_listensock(-1){}
    
    
    void initserver()
    {
    //1.创建套接字
    _listensock=socket(AF_INET,SOCK_STREAM,0);
    if(_listensock<0)
    {
        logMessage(FATAL,"create listensocket error");
        exit(SOCK_ERR);
    }
     logMessage(NORMAL, "create socket success: %d", _listensock);
    //2.bind ip和port
    struct sockaddr_in local;
    local.sin_family=AF_INET;
    local.sin_port=htons(_port);
    local.sin_addr.s_addr=INADDR_ANY;
    if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败
    {
        logMessage(FATAL,"bind error");
        exit(BIND_ERR);
    }
     logMessage(NORMAL,"bind success");
    //3.将套接字设置为监听模式
    if(listen(_listensock,gbacklog)<0)
    {
        logMessage(FATAL,"listen error");
        exit(LISTEN_ERR);
    }
    logMessage(NORMAL,"listen success");
    }
    void start(func_t fun)
    {
        while(true)
        {
            struct sockaddr_in cli;
            socklen_t len=sizeof(cli);
            bzero(&cli,len);
    
            int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
            if(sock<0)
            {
                logMessage(FATAL,"accept client error");
                continue;
            }
            logMessage(NORMAL,"accept client success");
    
            cout<<"accept sock: "<<sock<<endl;
    
            //多进程版---
            //一个客户端占用一个文件描述符,原因在于孙子进程执行IO任务需要占用独立的文件描述符,而文件描述符是继承父进程的,而每次客户端进来都要占用新的文件描述符
            //因此若接收多个客户端不退出的话文件描述符会越来越少。
             pid_t id=fork();//创建子进程
             if(id==0)//子进程进入
             {
                 close(_listensock);//子进程不需要用于监听因此关闭该文件描述符
                 handlerentry(sock,fun);
                 close(sock);
                 exit(0);
            }
            //父进程
            close(sock);
            pid_t ret=waitpid(id,nullptr,0);
            if(ret<0)
            {
                cout << "waitsuccess: " << ret << endl;
            }
        }
    }
    ~calserver(){}
    private:
    int _listensock;//用于监听服务器的sock文件描述符
    uint16_t _port;//端口号
    };
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • handlerentry函数是供服务器调用的接收发送函数。传入参数为用于通信的文件描述符sock,调用的计算函数cal(cal在calserver.cc文件中)。服务器调用handlerentry函数,接收到客户端发送来的报文,形式形如"text_len"\r\n"x op y"\r\n;调用deLength函数去"报头",转化后的字符串形如"x op y";调用Request对象的反序列函数Deserialize,通过字符串"x op y"填充Request对象的 _ x、_ op、 _ y参数;调用func即cal函数利用Request对象内的参数计算得出结果并构造Response对象;调用Response对象的序列化函数将 参数_exitcode _result 转化为字符串" _exitcodeSEP_result";调用enLength函数加上报头,转化后的字符串形如"content_len"\r\n"exitcode result"\r\n;然后通过send函数将字符串发送給客户端。
    • 注意的是服务器是多进程版,即能够与多个客户端进行并行通信。

    calclient.cc

    #include
    #include
    #include
    #include"calclient.hpp"
    using namespace std;
    using namespace client;
    static void Usage(string proc)
    {
        cout<<"\nUsage :\n\t"<<proc<<" serverip serverport\n"<<endl;
    }
    int main(int argc, char* argv[])
    {
        if(argc!=3)
        {
            Usage(argv[0]);
            exit(1);
        }
    
    string serverip=argv[1];
    uint16_t serverport=atoi(argv[2]);
    
    unique_ptr<calclient> tc(new calclient(serverip,serverport));
    
    tc->initclient();
    tc->start();
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    calclient.hpp

    #pragma once
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include"protocol.hpp"
    using namespace std;
    #define NUM 1024
    namespace client
    {
    
        class calclient
    {
    
    public:
    calclient(const string& ip,const uint16_t& port)
    :_sock(-1)
    ,_port(port)
    ,_ip(ip)
    {}
    
    void initclient()
    {
    //1.创建sockfd
    _sock=socket(AF_INET,SOCK_STREAM,0);
    if(_sock<0)
    {
       cerr<<"socket create error"<<endl;
       exit(2);
    }
    //2.绑定 ip port,不显示绑定,OS自动绑定
    }
    
    void start()
    {
    struct sockaddr_in ser;
    bzero(&ser,sizeof(ser));
    socklen_t len=sizeof(ser);
    ser.sin_family=AF_INET;
    ser.sin_port=htons(_port);
    ser.sin_addr.s_addr=inet_addr(_ip.c_str());
    if(connect(_sock,(struct sockaddr *)&ser,len)!=0)
    {
        cerr<<"connect error"<<endl;
    }else
    {
        string line;
        string inbuffer;
        while(true)
        {
            cout<<"mycal>>: ";//输入"xopy"
            getline(cin,line);
            Request req=ParseLine(line);//用"xopy"取出x op y构造Request对象
            string context;
            req.Serialize(&context);//序列化,用x op y构造字符串"xSEPopSEPy"
            string send_str=enLength(context);//定制协议---"x op y"->"text_len"\r\n"x op y"\r\n---給内容加上报头
            cout<<"calclient send str: "<<send_str<<endl;
            send(_sock,send_str.c_str(),send_str.size(),0);//客户端把报文发送給服务器
    
            string package;
            if(!recvPackage(_sock,inbuffer,&package)) continue;//服务器处理完数据,客户端接收服务器发送来的报文
            //  "content_len"\r\n"exitcode result"\r\n
            string reser_len;
            if(!deLength(package,&reser_len)) continue;//去报头
             //  "content_len"\r\n"exitcode result"\r\n -> "exitcode result"
            Response rep;
            rep.Deserialize(reser_len);//反序列化://_exitcodeSEP_result"->_exitcode _result
    
            cout<<"_exitcode: "<<rep._exitcode<<endl;
            cout<<"_result: "<<rep._result<<endl;
        }
    }
    }
    
    ~calclient()
    {
        if(_sock>=0) close(_sock);
    }
    
    Request ParseLine(const string& line)
    {//"xopy"->取出来到x op y 上
    int i=0;
    int status=0;
    int num=line.size();
    string left,right;
    char op;
    
    while(i<num)
    {
    switch(status)
    {
        case 0:
        {
            if(!isdigit(line[i]))
        {
            op=line[i];//取出运算符**
            status=1;
        }else
        left.push_back(line[i++]);//取出左操作数
        }
        break;
        case 1:
        i++;
        status=2;
        break;
    
        case 2:
        right.push_back(line[i++]);
        break;
    }
    }
    cout<<"left: "<<stoi(left)<<" op: "<<op<<" right: "<<stoi(right)<<endl;
    return Request(stoi(left),stoi(right),op);//返回Request对象
    
    }
    
    private:
    int _sock;
    uint16_t _port;
    string _ip;
    
    };
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • ParseLine函数接收形如"1+1"的字符串,解析字符串后构造Request对象
    • 客户端调用start函数,接收命令行发送来的形如"1+1"的字符串,然后调用ParseLine函数构造Request对象。调用Request对象的序列化函数Serialize构造字符串"xSEPopSEPy"。调用协议定制函数enLength給字符串加上"报头",转化后的字符串为"text_len"\r\n"x op y"\r\n;调用send函数将字符串发送給服务器;服务器计算完成后将结果发送回来,调用recvPackage函数接收服务器发送回来的字符串,字符串形如 “content_len”\r\n"exitcode result"\r\n;调用deLength函数去报头,转化后的字符串形如"exitcode result";调用Response对象的反序列化函数Deserialize通过字符串获取参数_exitcode _result,并进行打印。

    makefile

    .PHONY:all
    all:calclient calserver
    
    calclient:calclient.cc
    	g++ -o $@ $^ -std=c++11
    
    calserver:calserver.cc
    	g++ -o $@ $^ -std=c++11 
    
    .PHONY:clean
    clean:
    	rm -rf calserver calclient
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    image-20230830121135167

    利用Json进行序列化和反序列化

    JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,它以易于阅读和写作的文本形式表示结构化数据。JSON由两种结构构成:键值对(键值对集合)和值的有序列表。在这里以键值对的方式使用。

    json库的安装

    输入以下指令安装

    sudo yum install -y jsoncpp-devel
    
    • 1

    安装完后可通过ls查询

    image-20230830151558485

    makefile

    cc=g++
    LD=-DMYPRO
    .PHONY:all
    all:calclient calserver
    
    calclient:calclient.cc
    	$(cc) -o $@ $^ -std=c++11 -ljsoncpp #${LD}
    
    calserver:calserver.cc
    	$(cc) -o $@ $^ -std=c++11 -ljsoncpp #${LD}
    
    .PHONY:clean
    clean:
    	rm -rf calserver calclient
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 如果不使用Json序列化就注释掉第二行LD=-DMYPRO

    protocol.hpp

    #pragma once
    #include
    #include
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    
    #define SEP " "
    #define SEP_LEN strlen(SEP)//strlen统计'\0'之前的字符个数,而sizeof统计的是所占内存的空间大小,使用sizeof会越界出问题
    #define LINE_SEP "\r\n"
    #define LINE_SEP_LEN strlen(LINE_SEP)
    
    enum {
        NONE=0,
        DIV_ZERO,
        MOD_ZERO,
        OP_ERR
    };
    //"x op y"->"text_len"\r\n"x op y"\r\n---給内容加上报头
    std::string enLength(const std::string& text)//协议定制
    {
        std::string send_str=to_string(text.size());
        send_str+=LINE_SEP;
        send_str+=text;
        send_str+=LINE_SEP;
    
        return send_str;
    }
    //"text_len"\r\n"x op y"\r\n -> "x op y"---去掉报头,取出里面的内容
    bool deLength(const std::string& str,string* ret)//协议定制
    {
        auto it=str.find(LINE_SEP);//找到报头
        if(it==std::string::npos) return false;//如果没找到则直接返回
    
        int len=stoi(str.substr(0,it));//取出字符串的长度
        *ret=str.substr(it+LINE_SEP_LEN,len);//取出数据
        return true;
    }
    
    class Request
    {
    public:
    Request():_x(0),_y(0),_op(0){}
    Request(int x,int y,int op):_x(x),_y(y),_op(op){}
    
    bool Serialize(std::string* out)//序列化,将传入的x op y转化为字符串"x op y"
    {
    #ifdef MYPRO
        *out="";
        *out+=to_string(_x);
        *out+=SEP;
        *out+=to_string(_op);
        *out+=SEP;
        *out+=to_string(_y);
    #else
    Json::Value root;//json的对象是键值对[key,value]
    root["first"]=_x;//int类型被设置进json的键值对时自动转换为string类型
    root["second"]=_y;
    root["oper"]=_op;
    
    Json::FastWriter writer;
    
    *out=writer.write(root);//调用接口序列化返回值为字符串
    
    #endif
    
        return true;
    }
    
    bool Deserialize( const string& origin)//反序列化,将传过来的字符串拆出来传参給_x _op _y
    {//"_xSEP_opSEP_y"-> _x,_op,_y
    
    #ifdef MYPRO
        auto leftit=origin.find(SEP);
        cout<<"Deserialize找到了leftSEP: "<<leftit<<endl;
        auto rightit=origin.rfind(SEP);
        cout<<"Deserialize找到了rightSEP: "<<rightit<<endl;
        if(leftit==string::npos|| rightit==string::npos) return false;
    
        if(leftit==rightit) return false;
        int opsize=rightit-leftit-1;
        cout<<"opsize: "<<opsize<<endl;
    //1 43 1--leftit=1,rightit=4,opsize=rightit-leftit-1=4-1-1=2;
    //1 3 1--leftit=1,right=3,opsize=rightit-leftit-1=3-1-1=1
       // if(rightit-(leftit+SEP_LEN)!=1) return false;
        if(rightit-(leftit+SEP_LEN)!=opsize) return false;
        //+号ASCII码是43,从char转int被解析成43即stringlen为两位,这里的运算rightit-(leftit+SEP_LEN)!=1就出问题
    //4-(1+1)==2;3-(1+1)=1
        std::string origin_x=origin.substr(0,leftit);
        std::string origin_y=origin.substr(rightit+SEP_LEN);
        if(origin_x.empty()) return false;
    
        if(origin_y.empty()) return false;
    
        cout<<"origin_x: "<<origin_x<<" origin_y: "<<origin_y<<endl;
        _x=stoi(origin_x);
        int opf=stoi(origin.substr(leftit,rightit));
        _op=opf;
        cout<<"opf: "<<opf<<"_op: "<<_op<<endl;
        _y=stoi(origin_y);
    #else
    Json::Value root;
    Json::Reader reader;
    reader.parse(origin,root);//反序列化,将字符串中的协议字符串填进对象对应的元素中
    
    _x=root["first"].asInt();
    _y=root["second"].asInt();
    _op=root["oper"].asInt();
    #endif
        return true;
    
    }
        public:
        int _x;
        int _y;
        char _op;
    };
    
    class Response
    {
    public:
    Response():_exitcode(0),_result(0){}
    Response(int exitcode,int result):_exitcode(exitcode),_result(result){}
    bool Serialize(string*out)//序列化
    {//_exitcode _result ->"_exitcodeSEP_result"
    #ifdef MYPRO
    *out="";
    *out+=to_string(_exitcode);
    *out+=SEP;
    *out+=to_string(_result);
    #else
    Json::Value root;
    root["exitcode"]=_exitcode;
    root["result"]=_result;
    
    Json::FastWriter writer;
    *out= writer.write(root);
    #endif
    
    return true;
    }
    
    bool Deserialize(const string& in)//反序列化
    {//_exitcodeSEP_result"->_exitcode _result
    
    #ifdef MYPRO
    auto pos=in.find(SEP);
    if(pos==string::npos) return false;
    
    string excstr=in.substr(0,pos);
    string resstr=in.substr(pos+SEP_LEN);
    if(excstr.empty()||resstr.empty()) return false;
    
    _exitcode=stoi(excstr);
    _result=stoi(resstr);
    
    #else
    Json::Value root;
    Json::Reader reader;
    reader.parse(in,root);
    _exitcode=root["exitcode"].asInt();
    _result=root["result"].asInt();
    #endif
    return true;
    
    }
    
    public:
    int _exitcode;//退出码
    int _result;//结果
    };
    
    //"text_len"\r\n"x op y"\r\n
    bool recvPackage(int sock,string& inbuffer,string*out)
    {
    char buffer[1024];
    
    while(true)
    {
    ssize_t  s=recv(sock,buffer,sizeof(buffer)-1,0);
    if(s>0)
    {
        buffer[s]=0;
        inbuffer+=buffer;
        auto pos=inbuffer.find(LINE_SEP);
        if(pos==string::npos)continue;//没找到报头和有效载荷之间的分隔符---如果字节流式的报文没读全就继续读
        string text_len=inbuffer.substr(0,pos);//报头是有效载荷的长度
        int len=stoi(text_len);
        
        int totallen=text_len.size()+LINE_SEP_LEN*2+len;//整个报文的长度
        if(inbuffer.size()<totallen) 
        {
            cout<<"输入的消息不完整,请继续输入.continue..."<<endl;
            continue;//报文没读完继续读
        }
        
        cout<<"处理前的inbuffer: \n"<<inbuffer<<endl;
    
        *out=inbuffer.substr(0,totallen);
        inbuffer.erase(0,totallen);
    
        cout<<"处理后的inbuffer: \n"<<inbuffer<<endl;
    
        break;
    }
    else return false;
    }
    
    return true;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • Json序列化在Request类和Response类中使用。
    条件编译
    • #ifdef指令说明,如果预处理已经定义了后面的标识符(DEBUG),即DEBUG为真,则执行 #ifdef 与 #else 之间的所有所有代码,不执行#else之后的代码。若DEBUFG为未定义,即DEBUG为假,则执行#else与#endif之间的代码。#endif 用于结束该条件编译指令。#ifdef和#endif搭配使用。

    格式

    #ifdef DEBUG
    //......
    #else
    //......
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    //Request类内的Json序列化片段-Serialize
    Json::Value root;//json的对象是键值对[key,value]
    root["first"]=_x;//int类型被设置进json的键值对时自动转换为string类型
    root["second"]=_y;
    root["oper"]=_op;
    
    Json::FastWriter writer;
    
    *out=writer.write(root);//调用接口序列化返回值为字符串
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 创建一个Json的Value对象,键为"first"对应的值为_x,将操作数和运算符设置进Value对象里,然后通过Json的FastWriter对象调用write进行序列化。
    Request类内的Json反序列化片段-Deserialize
    Json::Value root;
    Json::Reader reader;
    reader.parse(origin,root);//反序列化,将字符串中的协议字符串填进对象对应的元素中
    
    _x=root["first"].asInt();
    _y=root["second"].asInt();
    _op=root["oper"].asInt();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 创建一个Json的Value对象,然后再创建一个Json的Reader对象,调用Reader对象的parse把携带协议的字符串填进Value对象对应的元素里。然后再通过键值对的方式把元素取出。

    image-20230830150840323

  • 相关阅读:
    C语言代码的编译过程及命令
    对C语言函数的再认识
    JAVA之单元测试:Junit框架
    C++精通之路:设计模式(特殊类设计)
    Python学习笔记--初识 Python 正则表达式
    【面试题】说一下浏览器垃圾回收机制?
    这份数据安全自查checklist请拿好,帮你补齐安全短板的妙招全在里面!
    源码分析基础
    音视频学习 - Qt6.3.1版本下实现屏幕截图功能
    为啥不建议使用Select *
  • 原文地址:https://blog.csdn.net/m0_71841506/article/details/132583387