模拟实现应用层协议

news2024/10/6 2:22:52

模拟实现应用层协议

文章目录

  • 模拟实现应用层协议
      • 应用层
        • 再谈协议
      • 序列化和反序列化
    • 网络版计算器
      • 自定义协议
      • 利用Json进行序列化和反序列化
        • json库的安装
        • 条件编译

应用层

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<iostream>
#include<string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
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;

}

介绍一下:

  • 在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<iostream>
#include<stdlib.h>
#include<memory>
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;
}
  • cal是计算函数,传入Request对象和Response对象。Request对象中参数有两个操作数 _ x、_ y 和一个运算符 _ op,计算出结果放到Response对象的参数_result中,退出码放到 _exitcode。

calserver.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include<functional>
#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;//端口号
};

}
  • 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<iostream>
#include<string>
#include<memory>
#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;
}

calclient.hpp

#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 <ctype.h>
#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;

};
}

  • 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

image-20230830121135167

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

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

json库的安装

输入以下指令安装

sudo yum install -y jsoncpp-devel

安装完后可通过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
  • 如果不使用Json序列化就注释掉第二行LD=-DMYPRO

protocol.hpp

#pragma once
#include<iostream>
#include<string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include <jsoncpp/json/json.h>
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;

}
  • Json序列化在Request类和Response类中使用。

条件编译

  • #ifdef指令说明,如果预处理已经定义了后面的标识符(DEBUG),即DEBUG为真,则执行 #ifdef 与 #else 之间的所有所有代码,不执行#else之后的代码。若DEBUFG为未定义,即DEBUG为假,则执行#else与#endif之间的代码。#endif 用于结束该条件编译指令。#ifdef和#endif搭配使用。

格式

#ifdef DEBUG
//......
#else
//......
#endif
//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);//调用接口序列化返回值为字符串
  • 创建一个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();
  • 创建一个Json的Value对象,然后再创建一个Json的Reader对象,调用Reader对象的parse把携带协议的字符串填进Value对象对应的元素里。然后再通过键值对的方式把元素取出。

image-20230830150840323

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

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

相关文章

CountDownLatch详解——深入探究CountDownLatch源码

这篇文章将会详细介绍CountDownLatch这个并发类&#xff0c;通过深入底层源代码讲解其具体实现。 /*** A synchronization aid that allows one or more threads to wait until* a set of operations being performed in other threads completes.*/ 上面是CountDownLatch这个…

对SAE的测评报告

对SAE的测评报告&#xff1a; 很高兴在这里跟大家分享我对SAE产品的测评体验&#xff0c;以下我将通过实验以及本期话题的问题这两个方面带领大家走进SAE&#xff1a; 产品介绍&#xff1a; Serverless应用引擎SAE是一款极简易用、自适应弹性的容器化应用平台。它提供全托管…

C++ 动态多态(虚函数)

所谓动态多态即&#xff0c;在运行过程中&#xff0c;会随参数的变化而展现不同的功能&#xff0c;其关键原理便是虚函数&#xff0c;与之对应的静态多态便是函数重载。 虚函数&#xff0c;即当存在与虚函数一样的函数&#xff0c;即返回值&#xff0c;函数名&#xff0c;参数等…

【毕业设计】基于SSM的电子图书分享系统

前言 &#x1f525;本系统可以选作为毕业设计&#xff0c;运用了现在主流的SSM框架&#xff0c;采用Maven来帮助我们管理依赖&#xff0c;所选结构非常合适大学生所学的技术&#xff0c;本系统结构简单&#xff0c;容易理解&#xff01;本系统功能结构完整&#xff0c;非常高适…

回归预测 | MATLAB实现IBES-ELM改进的秃鹰搜索优化算法优化极限学习机多输入单输出回归预测(多指标,多图)

回归预测 | MATLAB实现IBES-ELM改进的秃鹰搜索优化算法优化极限学习机多输入单输出回归预测&#xff08;多指标&#xff0c;多图&#xff09; 目录 回归预测 | MATLAB实现IBES-ELM改进的秃鹰搜索优化算法优化极限学习机多输入单输出回归预测&#xff08;多指标&#xff0c;多图…

基于STM32的甲醛浓度检测报警仿真设计(仿真+程序+讲解)

仿真图proteus 8.9 程序编译器&#xff1a;keil 5 编程语言&#xff1a;C语言 设计编号&#xff1a;C0083 甲醛浓度检测报警仿真 1.主要功能2.仿真3. 程序4. 资料清单&下载链接 1.主要功能 功能说明&#xff1a; 1、以STM32单片机和控制核心设计甲醛浓度检测报警设计&…

Kubernetes技术--使用kubeadm快速部署一个K8s集群

这里我们配置一个单master集群。(一个Master节点,多个Node节点) 1.硬件环境准备 一台或多台机器,操作系统 CentOS7.x-86_x64。这里我们使用安装了CentOS7的三台虚拟机 硬件配置:2GB或更多RAM,2个CPU或更多CPU,硬盘30GB或更多 2.主机名称和IP地址规划 3. 初始化准备工作…

15.MyCat数据库分片

MyCat 是一个开源的数据库中间件&#xff0c;主要用于将数据库操作请求路由和分发到后端的多个数据库节点。 1.Mycat环境搭建 在两个不同数据库中创建相同表 下载mycat https://github.com/MyCATApache/Mycat-Serverhttps://github.com/MyCATApache/Mycat-Server 将下…

【线程同步】AQS抽象排队同步器(AbstractQueuedSynchronizer)

AQS(AbstractQueuedSynchronizer)抽象排队同步器 AbstractQueuedSynchronizer AQS就是AbstractQueuedSynchronizer类 AQS其实就是JUC包下的一个基类&#xff0c;JUC下的很多内容都是基于AQS实现了部分功能&#xff0c;比如ReentrantLock&#xff0c;ThreadPoolExecutor&#…

十二、集合(1)

本章概要 泛型和类型安全的集合基本概念 如果一个程序只包含固定数量的对象且对象的生命周期都是已知的&#xff0c;那么这是一个非常简单的程序。 通常&#xff0c;程序总是根据运行时才知道的某些条件去创建新的对象。在此之前&#xff0c;无法知道所需对象的数量甚至确切类…

热烈祝贺蜀益表面处理成功入选航天系统采购供应商库

经过航天系统采购平台的严审&#xff0c;眉山市蜀益表面处理科技有限公司成功入选中国航天系统采购供应商库。航天系统采购平台是航天系统内企业采购专用平台&#xff0c;服务航天全球范围千亿采购需求&#xff0c;目前&#xff0c;已有华为、三一重工、格力电器、科大讯飞等企…

Simulink建模与仿真(3)-Simulink 简介

分享一个系列&#xff0c;关于Simulink建模与仿真&#xff0c;尽量整理成体系 1、Simulink特点 Simulink是一个用来对动态系统进行建模、仿真和分析的软件包。使用Simulink来建模、分析和仿真各种动态系统(包括连续系统、离散系统和混合系统)&#xff0c;将是一件非常轻松的事…

natApp内网穿透工作原理

如图所示&#xff0c;用户启动内网穿透工具会将token传入natapp服务器与我们自己的主机建立一个类似于websocket的长链接&#xff0c;当从外网访问我们主机的接口时&#xff0c;会进行一个本地接口地址的截取&#xff0c;然后进行拼接成我们主机应用的真实地址。然后将数据返回…

AntDesign 自定义图片上传前端压缩画质

为什么压缩图片&#xff1f; 应为现在公司没有使用云数据库 从而为了减少服务器的消耗需要将用户上传的图片压缩 前端压缩图片的技术选择&#xff01; 查阅资料发现当下两种压缩的方法&#xff01;&#xff01;第一种使用工具库实现 npm install image-conversion --save 第…

JAVA集合汇总

数组&#xff1a; 固定长度&#xff0c;从栈中分配空间&#xff0c;对于程序方便快速&#xff0c;但是自由度小。 ​优点&#xff1a;下标定位&#xff0c;随机访问性强&#xff0c;查找速度快&#xff1b; ​缺点&#xff1a;插入、删除效率低&#xff0c;内存利用率低&…

前端最能打的本地存储方案

产品的原话就是“要又大又全”。既然存储量大&#xff0c;也要覆盖全多种设备多种浏览器。 方案选择 既然要存储的数量大&#xff0c;得排除cookielocalStorage&#xff0c;虽然比cookie多&#xff0c;但是同样有上限&#xff08;5M&#xff09;左右&#xff0c;备选websql 使…

SPSS--s04典型相关分析

典型相关基本原理 典型相关分析是主成分分析和因子分析的进一步发展 ,是研究两组变量间的相互依赖关系 ,把两组变量之间的相互关系变为研究两个新的变量之间的相关,而且又不抛弃原来变量的信息 ,这两个新的变量分别由第一组变量和第二组变量的线性组合构成 ,并且两组变量的个数…

以太网POE供电浪涌静电防护推荐TVS二极管

POE是一种传输技术&#xff0c;可在以太网电缆上传输电力和数据。1000M千兆以太网POE供电端口广泛用于安防、视频监控以及智能电网等工业系统&#xff0c;以实现系统内的数据、视频传输、流量控制、以及通过总线实现供电。由于工业以太网工作环境非常严酷苛刻&#xff0c;对于以…

Java不重启服务 扩展新支付功能demo

应用场景 对于支付&#xff1a;经常有些需求&#xff0c;需要我们扩展新的支付方式&#xff0c;但又不希望重启服务对于IOT&#xff1a;经常需要新增一些解析数据的Handler 那我们如何能做到不重启服务&#xff0c;就能扩展新功能呢&#xff1f;本文给一个简单的demo示例&…

无涯教程-Android - AutoCompleteTextView函数

AutoCompleteTextView是一个类似于EditText的视图&#xff0c;只是它在用户键入时自动显示补充数据。 AutoCompleteTextView - 属性 以下是与AutoCompleteTextView控件相关的重要属性。您可以查看Android官方文档以获取属性的完整列表以及可以在运行时更改这些属性的相关方法。…