【Linux网络编程七】网络序列化和反序列化(网络版本计算器)

一.网络读取问题

在网络通信时,通信双方是无法保证读取时,能够正确的读取到想要的内容的。
这是什么意思呢?
在这里插入图片描述

【解决方案】

在这里插入图片描述

1.定制协议

什么叫定制协议呢?就是让双方都要能知道,约定好的一些字段,然后以结构化的形式发送和接收。
在这里插入图片描述
定制的协议双方都要能认识才可以通信。不然一方认识,另一方不认识就无法通信了。
在这里插入图片描述

2.序列化和反序列化

定制好结构体后,然后构建一个对象,是不是就可以直接发送过去了呢?
当然不可以!为什么呢?

1.我们不能直接将结构体对象直接发送给对端机器,因为双方可能机器不同,对于结构体的解读会不一样,最终就会解析错误。所以通常我们不直接发送结构化数据到网络里,而是发送字符串形式的数据给对端。
2.也就是我们需要在构建完结构化数据后,在发送到网络里之前还需要将它转换成字符串形式,才能发送。这个过程就叫做序列化
3.而对端机器从网络里接收到字符串后,它并不认识这个字符串是什么意思,只有将这个字符串转成结构化化数据,它才能知道对方发送的是什么信息。而将字符串数据再转成结构化数据,就叫做反序列化。
在这里插入图片描述

在这里插入图片描述

这里是引用

3.添加报头

那么问题又回来了,对端是如何正确获取到想要的完整报文的呢?
在这里插入图片描述

我们可以利用一些特殊字符,来区别报文与报文。而如果想要对方准确的接收到一个完整的报文,那么就可以通过在报文前面添加一个长度单位,标识这个报文总长度有多少,一旦对端读取到这个长度,就可以直接从后面截取报文的长度,就可以直接完整的获取到一个报文。
而在报文前面添加一个长度单位这个行为,我们可以称为添加一个报头。

添加报头的目的就是为了让对端在读取的时候,能够根据报头,来完整的获取一个报文。所以添加报头也是属于定制协议的部分。通信的双方都要能识别。

①封包

所以构建完结构化数据后,在发送到网络之前,需要将之转换成字符串形式,也就是序列化。然后为了让对端能够准确的获取到一个完整的报文,我们还需要对这个字符串添加报头。

这里是引用

②解包

对端获取到从网络里发送来的数据后,并不理解是什么意思,所以需要反序列化,将字符串数据转换成结构化数据,这样它就可以理解是什么意思了。
但是这里存在的问题就是:它并不能确定对方发送的是一个报文还是多个报文,还是半个报文。因为从网络里发送来的就是一个字符串形式的数据。它分析不出来。而反序列化,是以一个完整的报文进行反序列化的,因为当时序列化的时候就是一个完整的结构体数据进行序列化的。

所以定制协议的人,考虑到这点,就在报文的前面添加了一个报头:表明报文的长度的字段。这样只要对端接收到报文,然后依据报头,就能分析出对方发送的数据是否是完整的了。如果是完整的,那么直接获取有效的报文。如果不是完整的,那么重新去读取,如果是多个报文,那么我们只要一个完整的报文即可。剩下的等下次再处理。

这里是引用

4.框架总结

这里是引用

在这里插入图片描述
在这里插入图片描述

网络部分套接字:

Socket.hpp
#pragma once
//将网络套接字编程部分直接封装打包,因为服务器和客户端都需要使用创建套接字等操作。
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include <cstdlib>
#include <cstring>
#include <unistd.h>
enum
{
  SocketErr=1,
  BindErr,
  ListenErr,
};
const int backlog=10;
class Sock
{

public:
    Sock()
    {}
    ~Sock()
    {}

    void Socket()//创建套接字
    {
     _socket=socket(AF_INET,SOCK_STREAM,0);
     if(_socket<0)
     {
        lg(Fatal,"socket err:%s :%d",strerror(errno),errno);
        exit(SocketErr);
     }
    }
    void Bind(uint16_t &port)//绑定套接字
    {
      struct sockaddr_in local;
      memset(&local,0,sizeof(local));
      local.sin_addr.s_addr=INADDR_ANY;//将ip地址初始化成0
      local.sin_family=AF_INET;
      local.sin_port=htons(port);
      if(bind(_socket,(struct sockaddr*)&local,sizeof(local))<0)
      {
        lg(Fatal,"bind err :%s :%d",strerror(errno),errno);
        exit(BindErr);
      }
    }
    void Listen()//将套接字设置成监听状态
    {
       if(listen(_socket,backlog)<0)
       {
        lg(Fatal,"listen err :%s :%d",strerror(errno),errno);
        exit(ListenErr);
       }
    }
    int Accept(std::string *clientip,std::uint16_t* clientport)//服务器获取连接,并获取对方的网络信息,将获取的到的新连接交给服务函数操作
    {
        struct sockaddr_in client;
        socklen_t len=sizeof(client);
        int newsock=accept(_socket,(struct sockaddr*)&client,&len);
        if(newsock<0)
        {
            lg(Warning,"accept err :%s :%d",strerror(errno),errno);
            return -1;
        }
        *clientport=ntohs(client.sin_port);
        char Clientip[32];
        inet_ntop(AF_INET,&client.sin_addr,Clientip,sizeof(Clientip));
        *clientip=Clientip;
        return newsock;
    }
    bool Connect(const std::string &serverip,const uint16_t &serverport)//需要知道要连接的对方的网络信息
    {
        struct sockaddr_in local;
        socklen_t len=sizeof(local);
        memset(&local,0,sizeof(local));
        local.sin_family=AF_INET;
        local.sin_port=htons(serverport);
        inet_pton(AF_INET,serverip.c_str(),&local.sin_addr);
     
        int n=connect(_socket,(struct sockaddr*)&local,len);
        if(n==-1)
        {
            std::cerr<<"connect to"<<serverip<<":"<<serverport<<"error"<<std::endl;
            return false;
        }
        return true;
    }
    void Close()
    {
        close(_socket);
    }
    int Fd()
    {
      return _socket;
    }
private: 
  int _socket;
};

协议部分

Protocol.hpp
#pragma once
//#define MySelf 1
// 在网络通信之前,我们服务器端和客户端都需要知道协议。我们也可以自己定制协议,这个协议要被双方都能识别
// 比如我们可以定制一个计数器协议。协议就是一种约定,除了数据本身还有其他的字段。
// 1.我们要求将数据以结构化的形式保存这样双方都可以识别这个结构体对象,但传入网络里时,需要转换成字符类型。这个过程就是序列化.序列化的过程就是在构建有效载荷
// 2.对方接收到字符串类型的数据时,想要用服务操作时,发现是不能操作的,是因为它不认识,这时还需要将字符类型转成结构体类型,这个过程叫做反序列化。
// 3.为了能让对方接收时,能接收读取到对方想要的完整报文时,我们采取添加报头的形式来解决。
// 4.所以在将报文传入到网络里时,还需要添加报文,当对端接收到报文时,想要对它进行处理之前,还需要将报文的报头解包才可以正确处理。
#include <iostream>
#include <jsoncpp/json/json.h>
#include <string>
const std::string blank_space = " ";
const std::string protocol_space="\n";
// 封包:报文在发送到网络之前需要添加一些报头,来达到一些要求
std::string Encode(const std::string &content)//content就是有效载荷
{
  //"x + y"------>"len"\n"x + y"\n"   添加了一个报文长度和两个\n
  std::string packpage=std::to_string(content.size());
  packpage+=protocol_space;
  packpage+=content;
  packpage+=protocol_space;
  return packpage;
}

// 解包:对端读取到报文(可能读取到的不是想要的,根据原先添加上去的报头来获取准确想要的报文),想要处理它,需要先解除报头才能处理
bool Decode(std::string &packpage, std::string *content)
{ 
  //"len"\n"x + y"\n"---->"x + y"
  std::size_t pos=packpage.find(protocol_space);
  if(pos==std::string::npos)
  return false;

  std::string len_str=packpage.substr(0,pos);
  //判断一下是否读取的内容是全部的
  std::size_t len =std::stoi(len_str);
  std::size_t total_len=len_str.size()+len+2;
  if(packpage.size()<total_len)//说明不是一个完整的报文
  return false;
  *content=packpage.substr(pos+1,len);
  //为了真正的拿走报文,还需要将响应inbuffer里的报文移除erase,这样才是真正的拿走报文
  
  packpage.erase(0,total_len);
  return true;
}

class Request
{
public:
  Request()
  {}
  Request(int data1, int data2, char op) : _x(data1), _y(data2), _op(op) // 最初形成结构化数据
  {
  }
  bool Serialize(std::string *out) // 序列化,单纯的就是将结构体转换成字符串
  {
#ifdef MySelf    // 构建报文的有效载荷
    // struct==》"x + y"
    std::string s = std::to_string(_x);
    s += blank_space;
    s += _op;
    s += blank_space;
    s += std::to_string(_y);
    *out = s;
    return true;
 #else
     Json::Value root;//定义一个万能对象,可以存储数据,k-v形式的结构体
     root["x"]=_x;
     root["y"]=_y;
     root["op"]=_op;
     //Json::FastWriter w;
     Json::StyledWriter w;
     *out=w.write(root);//序列化成字符串
     return true;   
    
 #endif

  }
  bool Deserialize(std::string &in) // 反序列化,就单纯的将字符串类型转成结构体
  {
#ifdef MySelf   
    //"x + y"==>struct
    //获取左操作数x
    std::size_t left=in.find(blank_space);
    if(left==std::string::npos)
    return false;
    std::string part_x=in.substr(0,left);
    //获取右操作数y
    std::size_t right=in.rfind(blank_space);
    if(right==std::string::npos)
    return false;
    std::string part_y=in.substr(right+1);
    //获取操作码op
    if(left+2!=right)
    return false;

    _op=in[left+1];
    _x=std::stoi(part_x);
    _y=std::stoi(part_y);
    return true;
#else
    Json::Value root;//定义一个万能对象,将序列化的数据存储在里面
    Json::Reader r;
    r.parse(in,root);
    //将数据存到万能对象里后,我们就可以根据key值找到
    _x=root["x"].asInt();
    _y=root["y"].asInt();
    _op=root["op"].asInt();
    return true;
#endif    
  }
    void DebugPrint()
    {
        std::cout<<"新请求构建完毕:"<<_x<<_op<<_y<<"=???"<<std::endl;
    }
public: // x + y
  int _x;
  int _y;
  char _op;
};
class Response
{
public:
  Response(int reslut, int code) : _reslut(reslut), _code(code)
  {
  }
  Response()
  {}
  bool Serialize(std::string *out) // 序列化,单纯的就是将结构体转换成字符串
  {
#ifdef MySelf
    //"reslut code"
    //构建报文的有效载荷
    std::string s=std::to_string(_reslut);
    s+=blank_space;
    s+=std::to_string(_code);
    *out=s;
    return true;
#else
     Json::Value root;
     root["reslut"]=_reslut;
     root["code"]=_code;
     //Json::FastWriter w;
     Json::StyledWriter w;
     *out=w.write(root);
     return true;
#endif    
  }
   bool Deserialize(std::string &in)
   {
#ifdef MySelf
    //"reslut code"-->结构体类型
    std::size_t pos=in.find(blank_space);
    if(pos==std::string::npos)
    return false;

    std::string part_left=in.substr(0,pos);
    std::string part_right=in.substr(pos+1);

    _reslut=std::stoi(part_left);
    _code=std::stoi(part_right);
    return true;
#else
    Json::Value root;
    Json::Reader r;
    r.parse(in,root);//将字符串数据存到万能对象里

    _reslut=root["reslut"].asInt();
    _code=root["code"].asInt();
    return true;
#endif
   }

   void DebugPrint()
   {
    std::cout<<"结果响应完成,reslut: "<<_reslut<<",code: "<<_code<<std::endl;
   }

public:
  int _reslut;
  int _code;
};

服务器服务部分:

ServerCal.hpp
#pragma once
#include "Protocol.hpp"
#include <iostream>
#include <string>
// 服务器端,从网络里读取到数据后,就要进行处理服务。
// 1.首先需要对报文进行解包,2.解包后还需要将报文转成结构体类型对方才能识别
enum
{
  Div_Zero = 1,
  Mod_Zero,
  Other_Oper
} ;
class ServerCal
{
public:
  Response Calculatorhelpor(const Request &req)
  {
    Response resp(0, 0);
    switch (req._op)
    {
    case '+':
      resp._reslut = req._x + req._y;
      break;
    case '-':
      resp._reslut = req._x - req._y;
      break;
    case '*':
      resp._reslut = req._x * req._y;
      break;
    case '/':
    {
      if (req._y == 0)
        resp._code = Div_Zero;
      else
        resp._reslut = req._x / req._y;
    }
    break;
    case '%':
    {
      if (req._y == 0)
        resp._code = Mod_Zero;
      else
        resp._reslut = req._x % req._y;
    }
    break;

    default:
     resp._code=Other_Oper;
      break;
    }
    return resp;
  }

  std::string Calculator(std::string &package)
  {
    std::string content;                //"len""\n""20 + 10""\n"
    bool r = Decode(package, &content); //"20 + 10"
    if (!r)
      return "";
    Request req; // 反序列化
    r = req.Deserialize(content);
    if (!r)
      return "";

    // 服务器端解包获取到报文后,就可以进行计算,再将计算结果返回回到网络里,网络里需要序列化的数据

    Response res = Calculatorhelpor(req); // reslut=30  code=0
    content = "";
    res.Serialize(&content);   //"30 0"
    content = Encode(content); //"len""\n""30 0""\n"

    return content;
  }
};

二.自定义协议:网络计算器协议

Ⅰ.客户端发送请求,服务器端接收请求

ClientCal.cc
#include <iostream>
#include <time.h>
#include <unistd.h>
#include <assert.h>
#include "Socket.hpp"
#include "Protocol.hpp"
void Usage(std::string proc)
{
    std::cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<std::endl;
}
//./tcpclient ip port
int main(int args,char* argv[])
{
    if(args!=3)
    {
     Usage(argv[0]);
     exit(1);
    }
    std::string serverip=argv[1];
    uint16_t serverport=std::stoi(argv[2]);
    
    Sock sockfd;
    sockfd.Socket();//创建套接字
    bool r=sockfd.Connect(serverip,serverport);//发起连接
    if(!r)return 1;

    
    srand(time(nullptr)^getpid());
    int cnt=1;
    std::string oper="+-*/%=$";
    std::string inbuffer_stream;
    while(cnt<=10)
    {
        std::cout<<"========第"<<cnt<<"次测试"<<"============"<<std::endl;
        //1.开始构建请求
        int x=rand()%100+1;
        usleep(1234);
        int y=rand()%100;
        usleep(4321);
        char op=oper[rand()%oper.size()];
        Request req(x,y,op);
        //2.请求构建完毕
        req.DebugPrint();
        //3.数据序列化形成报文
        std::string content;
        req.Serialize(&content);
        //4.添加报头
        std::string packpage=Encode(content);
        //5.发送到网络里
        write(sockfd.Fd(),packpage.c_str(),packpage.size());

        //6.接收服务器端发送来的响应
        char buffer[128];
        ssize_t n=read(sockfd.Fd(),buffer,sizeof(buffer));
       //6.1处理读取
        if(n>0)
        {
            buffer[n]=0;
            inbuffer_stream+=buffer;//接收到的是一个协议报文"len"\n"reslut code"\n
            std::cout<<std::endl;
            std::cout<<"获取到的网络答应:"<<std::endl;
            std::cout<<inbuffer_stream<<std::endl;//将从网络里获取到的报文打印出来

            //7.首先需要解包检测
            std::string content;
            bool r =Decode(inbuffer_stream,&content);
            assert(r);
            //8.反序列化,将答应变成客户端可认识的形式
            Response resp;
            r=resp.Deserialize(content);
            assert(r);
            //9.结果响应完成
            resp.DebugPrint();
        }
        std::cout<<"============================="<<std::endl;
        sleep(1);
        cnt++;

    }

    sockfd.Close();
}

1.构建请求(结构化数据)

2.请求序列化

3.添加报头,发送到网络

4.服务器读取请求

5.解除报头

6.请求反序列化

Ⅱ.服务器端发送响应,客户端接收响应

Main.cc
#include "Tcpserver.hpp"
#include "ServerCal.hpp"
#include <memory>
void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " port[1024+]\n"
              << std::endl;
}

int main(int args, char *argv[])
{

    if (args != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);
    ServerCal cal;
    Tcpserver *tcpsvr = new Tcpserver(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));
    tcpsvr->Init();
    tcpsvr->Start();
    return 0;
}
Tcpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include "Log.hpp"
#include <signal.h>
#include "Socket.hpp"
#include "ServerCal.hpp"
Sock sock;
using func_t =std::function<std::string(std::string &package)>;

class Tcpserver
{

public:
    Tcpserver(uint16_t port,func_t callback) : _port(port),_callback(callback)
    {
    }
    bool Init()
    {
        _listensock.Socket();    // 创建套接字
        _listensock.Bind(_port); // 绑定套接字
        _listensock.Listen();    // 将套接字设置成监听状态
        lg(Info, "init server...done");
        return true;
    }
    void Start() // 启动服务器
    {            // 启动之前需要先忽略一些信号
        signal(SIGPIPE, SIG_IGN);
        signal(SIGCHLD, SIG_IGN);
        // 获取连接
        while (true)
        {
            std::string clientip;
            uint16_t clientport;
            int sockfd = _listensock.Accept(&clientip, &clientport);
            if (sockfd < 0)
                continue;
            lg(Info, "accept a new link, sockfd:%d, clientip:%s ,clientport: %d",sockfd,clientip.c_str(),clientport);
            
            // 提供服务-->让子进程提供服务
            if (fork() == 0)
            {
                _listensock.Close();
                std::string inbuffer_stream;
                while (true)
                {
                    // 1.读取网络中的数据流
                    char buffer[128];
                    size_t n = read(sockfd, buffer, sizeof(buffer));
                    if (n > 0)
                    {
                        buffer[n]=0;
                        //将读取的报文进行处理
                       inbuffer_stream+=buffer;//注意读取的内容必须是一个完整的内容,不然调用回调时,就回调用失败
                       lg(Debug,"获取的网络请求:\n %s",inbuffer_stream.c_str());
                       std::string info=_callback(inbuffer_stream);
                       
                       if(info.empty())continue;//如果进行计算时,发现报文有问题,就重新回来读取。

                       // 2.将处理的结果发送回网络中
                       write(sockfd,info.c_str(),info.size());
                    }
                    else if(n==0)break;
                    else break;

                    
                }
                exit(0);
            }
            close(sockfd);
        }
    }
    ~Tcpserver()
    {
    }

private:
    uint16_t _port;
    Sock _listensock;
    func_t _callback;
};

1.构建响应(结构化数据)

2.响应序列化

3.添加报头,发送到网络

4.客户端读取响应

5.解除报头

6.响应反序列化

三.自动序列化和反序列化Json

在这里插入图片描述

 bool Serialize(std::string *out) // 序列化,单纯的就是将结构体转换成字符串
  {
     Json::Value root;//定义一个万能对象,可以存储数据,k-v形式的结构体
     root["x"]=_x;
     root["y"]=_y;
     root["op"]=_op;
     //Json::FastWriter w;
     Json::StyledWriter w;
     *out=w.write(root);//序列化成字符串
     return true;  

  }
  bool Deserialize(std::string &in) // 反序列化,就单纯的将字符串类型转成结构体
  {
    Json::Value root;//定义一个万能对象,将序列化的数据存储在里面
    Json::Reader r;
    r.parse(in,root);
    //将数据存到万能对象里后,我们就可以根据key值找到
    _x=root["x"].asInt();
    _y=root["y"].asInt();
    _op=root["op"].asInt();
    return true;
  }

四.理解OSI七层协议

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小陶来咯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值