自定义协议 ,序列化和反序列化

目录

​编辑

一,问题引入:

二,协议

三,自定义协议

1,协议

[2, 序列化和反序列化](#2, 序列化和反序列化)

四,网络版本的计算器

1,协议的定制

2,计算逻辑

3,服务端

4,客户端

5,main函数


一,问题引入:

当我们使用TCP协议来进行网络通信时,因为TCP通信的特点:

面向字节流,没有数据边界。

所以,当我们在读取数据时就会遇到一个问题:在使用TCP通信时到底怎样才算将一段数据读取完成呢?

二,协议

为了解决上面的问题,在网络中便引入了协议。比较著名的协议就有HTTP协议,TCP协议等等。

这些协议通俗的讲其实就是一个约定,收发数据两方的约定。两方约定好如何发数据,如何收数据。

三,自定义协议

虽然在平时使用网络通信时并不需要我们来写协议。但是,我们还是有自己定制协议的能力的。 所以,为了更好的了解协议这个东西。我们可以自己尝试来写一个协议,然后让双方互相通信。

1,协议

现在,我们就来实现一个网络版的计算器。但是我们要做如下约定:数据要以如下格式发送:

len: 代表内容content的长度

\n:代表一个分割符

content:代表数据的正文内容

\n:代表一个分割符

2, 序列化和反序列化

在QQ接收消息时,我们不仅仅会收到信息,我们还会收到头像和昵称。这些消息就是一个结构化的消息 。这些消息经过打包后会形成一段报文(一个整体),打包的过程就是序列化的过程 。在将这段报文解开的过程就是一个反序列化的过程。序列化和反序列化的过程中要使用的就是协议。

四,网络版本的计算器

1,协议的定制

在写这个计算器时首先确定的便是协议的定制。协议定制如下:

cpp 复制代码
#include<iostream>
#include"log.hpp"

const std::string blank_sep = " ";
const std::string protocol_sep = "\n";

std::string Encode(std::string& content)//加密:"len\ncontent\n"
{
    std::string package = std::to_string(content.size());
    package += protocol_sep;
    package+=content;
    package += protocol_sep;

    return package;//返回打包后的数据
}

bool Decode(std::string &package,std::string* content) // 解密一段打包后数据:"len\ncontent\n"
{
   
   //先找第一个protocol_sep
   int pos = package.find(protocol_sep);
   if(pos == std::string::npos)
   {
     // lg(Debug,"Decode err1");
      return false;
   }

   //找到了第一个protocol_sep,找到这段数据的长度
   std::string len_str = package.substr(0, pos);
   //std::cout << len_str << std::endl;
   int len = std::atoi(len_str.c_str());

   int total_len = len_str.size()+2+len;
   if(package.size()<total_len)
   {
      // lg(Debug, "Decode err2");
       return false;
   }

    *content = package.substr(pos+1,len);
    package.erase(0,total_len);//解码后将package消掉
    return true;
}

//定制协议
class Request//
{
public:
Request()
:x(0),y(0),op('+')
{}

Request(int data1,int data2,char oper)
:x(data1),y(data2),op(oper)
{}

//序列化:将结构化的数据变成字符串(x op y),并带出到外面
void  Serialize(std::string* content)
{
    std::string str = std::to_string(x);
    str += blank_sep;
    str += op;
    str+= blank_sep;
    str += std::to_string(y);
    *content = str;
}

bool Deserialize(std::string &content) // 将字符串转化为结构化的数据
{
   int pos = content.find(blank_sep);
   if(pos == std::string :: npos)
   {
      //lg(Debug,"Request Deserilization err1");
      return false;
   }

   x = std::stoi(content.substr(0, pos));
   op = content[pos + 1];

   pos = content.rfind(blank_sep);
   if (pos == std::string ::npos)
   {
      // lg(Debug, "Request Deserilization err2");
       return false;
   }

   y = std::stoi(content.substr(pos+1));
   return true;
}

public:
   int x;
   int y;
   char op;
};

class Response
{

 public:
    Response()
    :result(0),code(0)
    {}

    Response(int res,int c)
        : result(res), code(c)
    {}

    void Serialize(std::string* content)//序列化:str:"result code"
    {
        std::string str;
        str += std::to_string(result);
        str += blank_sep;
        str+=std::to_string(code);
        *content = str;
    }

    bool Deserialize(std::string &content)
    {
        int pos = content.find(blank_sep);
        if(pos == std::string::npos)
        {
            //lg(Debug, "Response Deserilization err");
            return false;
        }

        result = std::stoi(content.substr(0, pos));
        code = std::stoi(content.substr(pos + 1));
        return false;
    }

    void Debugprint()
    {
        std::cout <<"result: "<< result << " "<<"code: " << code << std::endl;
    }

  public:
  int result;//结果
  int code;//0表示结果正确,!0表示结果错误
};

在这段代码中,我定义了两个类:

Request:代表一个请求。这个类里面有两个方法,代表着序列化和反序列化方法。 类里面有三个成员:x y op,代表着左右操作数和操作符。
Response:代表一个响应。这个类里面有两个方法,代表着序列化和反序列化方法。 类里面有三个成员:result code,代表着结果和结果码(显示结果可不可信)。

在这段代码中还有两个公共的方法:

Encode:对序列化后的内容进行加码,变成如下形式:


Decode:对加码后的内容进行解码,变成一个简单的反序列化的代码。

2,计算逻辑

在制定好协议以后,便可以开始根据这些协议来对客户发来的数据进行处理了。但是如何处理呢?因为我们这里的处理逻辑就是一个计算。所以,写出计算逻辑如下:

cpp 复制代码
#pragma once
#include "Protocol.hpp"

class Calculator // 计算逻辑
{
public:
     //开始计算
    Response CalHelper(Request& req)//开始计算
     {
         Response resp(0, 0);
         switch (req.op)
         {

         case '+':
             resp.result = req.x + req.y;
             break;
         case '-':
             resp.result = req.x - req.y;
             break;
         case '*':
             resp.result = req.x * req.y;
             break;
         case '/':
             if(req.y!=0)
             resp.result = req.x / req.y;
             else
                 resp.code = 3;
             break;
         case '%':
             if (req.y != 0)
                 resp.result = req.x % req.y;
             else
                 resp.code = 3;
             break;
        default:
            resp.code = 4;
            break;
          }

          return resp;
     }

      
    // 对加包的数据进行计算,并且要返回序列化并打包的结果
    std::string Calculate(std::string &package)//要保证我的数据是一个package
    {
        std::string content;
        bool r = Decode(package, &content);//将package解包并且带出内容
        if(!r)
            return "";

        Request req;
        r = req.Deserialize(content);//反序列化后得到了一个结果,然后去计算
        if(!r)
            return "";

        Response resp;
        resp = CalHelper(req);//对req进行计算

        content = "";//序列化并打包
        resp.Serialize(&content);

        //std::string package;
        content = Encode(content);

        return content;//返回打包后的结果
    }
};

 

Calculate:对服务端接收到的消息进行处理。处理过程便是先对数据进行解包,然后再对数据进行反序列化。
CalHelper:对解包并且反序列化后的数据进行计算。并将结果存于Response对象中返回。

3,服务端

在写完如上代码后,我们的服务端便可以开始处理数据了。现在就让我们来搭建一个基于TCP协议的服务端。代码如下:

cpp 复制代码
#pragma once
#include "log.hpp"
#include"Protocol.hpp"
#include"Socket.hpp"
#include<signal.h>
#include"Calculator.hpp"

Calculator Cal;//定义一个计算器

class CalServer
{
public:
    CalServer(uint16_t port) : port_(port)
    {
    }
    bool Init()
    {
        listensock_.Sock();
        listensock_.Bind(port_);
        listensock_.Listen();
        lg(Info, "init server .... done");
        return true;
    }
    void Start()
    {
        signal(SIGCHLD, SIG_IGN);//父进程忽略子进程的信号
        while (true)
        {
            std::string clientip;
            int 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)//为什么是两个循环?因为我要保证读取到的数据拼接到inbuffer_stream中时是一个完整的报文。
                {
                    char buffer[1280];
                    ssize_t n = read(sockfd, buffer, sizeof(buffer));
                    if (n > 0)
                    {
                        buffer[n] = 0;
                        inbuffer_stream += buffer;

                        lg(Debug, "debug:\n%s", inbuffer_stream.c_str());

                        while (true)
                        {
                            std::string info = Cal.Calculate(inbuffer_stream);
                            if (info.empty())
                                break;
                            lg(Debug, "debug, response:\n%s", info.c_str());
                            lg(Debug, "debug:\n%s", inbuffer_stream.c_str());
                            write(sockfd, info.c_str(), info.size());
                        }
                    }
                    else if (n == 0)
                        break;
                    else
                        break;
                }

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

private:
    uint16_t port_;
    Socket listensock_;
};

这里的listensock_ 对象是一个Socket类对象。这个Socket类定义如下:

cpp 复制代码
#pragma once
#include<iostream>
#include <sys/types.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include<cstring>
#include<arpa/inet.h>
#include<unistd.h>


//定义一些变量
#define blog 10
#define defaultport 8080

class Socket
{
    public:
    //构造函数
        Socket()
            : sockfd_(0)
        {}

  public:
  //创建套接字
  bool Sock()
  {
      sockfd_ = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
      if (sockfd_ < 0)
      {
          std::cerr <<  "创建套接字失败" << std::endl;
          return false;
      }

      return true; // 将创建好的套接字返回
  }

  //bind,服务端只要绑定端口号

  bool Bind(int16_t port = defaultport)
  {
       sockaddr_in server_addr;
       memset(&server_addr, 0, sizeof (server_addr));//清空数据
       server_addr.sin_family = AF_INET;
       server_addr.sin_port = htons(port);
       server_addr.sin_addr.s_addr = INADDR_ANY;

       int r1 = bind(sockfd_,(sockaddr*)&server_addr,sizeof server_addr);
       if(r1<0)
       {
           std::cerr << "bind err" << std::endl;
           return false;
       }

       return true;
  }

  //监听
  bool Listen()
  {
     int r2 =  listen(sockfd_, blog);
     if(r2<0)
     {
         std::cerr << "listen err" << std::endl;
         return 0;
     }

     return true;
  }

  //接收
  int Accept(std::string* ip,int* port)
  {
      sockaddr_in cli_addr;
      socklen_t len = sizeof(cli_addr);
      int sockfd = accept(sockfd_, (sockaddr *)&cli_addr, &len);
      if(sockfd<0)
      {
          std::cerr << "accept err" << std::endl;
          return -1;
      }

      char buff[64]={0};
      inet_ntop(AF_INET, &cli_addr, buff, sizeof(buff));
      *ip = buff;
      *port = ntohs(cli_addr.sin_port);
      return sockfd;
  }

  //连接
  bool Connect(std::string& ip,int16_t port)
  {
    sockaddr_in addr_;
    addr_.sin_family = AF_INET;
    addr_.sin_port = htons(port);
    inet_pton(AF_INET, ip.c_str(), &(addr_.sin_addr));
    int r = connect(sockfd_, (sockaddr *)&addr_, sizeof (addr_));
    if(r<0)
    {
        std::cerr << "connect err" << std::endl;
        return false;
    }

    return true;
  }

  //关闭

  void Close()
  {
      close(sockfd_);
  }

    public:
     //成员
     int sockfd_;
};

CalServer内的函数作用:

**Init:**创建套接字,bind套接字,监听套接字。

**Start:**循环接收客户端套接字,并且创建子进程对客户端的Request进行服务。在服务过程中父进程要对子进程的信号进行忽略,所以在创建子进程之前加上 signal(SIGCHLD, SIG_IGN)对子进程信号进行忽略。

4,客户端

客户端的创建代码比较简单,代码如下:

cpp 复制代码
#include "Socket.hpp"
#include "Protocol.hpp"
#include "log.hpp"

class CalClient
{
public:
    void Init(std::string ip, int port)
    {
        Sock.Sock(); // 创建套接字
        Sock.Connect(ip, port);
        srand(time(0));
        lg(Info, "Connect sucess");
    }

    void Start()
    {
        // 创建100以内的数据

        const std::string opers = "+-*/%";
        const int len = opers.size();
        int cnt = 10;
        while (cnt--)
        {
            std::cout <<"------------"<< "第" << cnt << "次测试"
                      << "------------" << std::endl;
            int data1 = rand() % 100;
            int data2 = rand() % 100;
            char op = opers[rand() % len];

            // 建立需求
            Request req(data1, data2, op);

            // 序列化
            std::string content;
            req.Serialize(&content);

            // encode
            content = Encode(content);

            // 送数据到Server

            std::cout << "请求构建完成:" << req.x << req.op << req.y << "="
                      << "?" << std::endl;
            write(Sock.sockfd_, content.c_str(), content.size());

            // 接收数据
            char buff[1280];
            read(Sock.sockfd_, buff, sizeof(buff));
            // 解码
            std::string package = buff;
            content = "";
            Decode(package, &content);
            // 反序列化
            Response resp;
            resp.Deserialize(content);

            resp.Debugprint();
            std::cout << "结果相应完成"
                      << "-----------------" << std::endl;

            sleep(1);
        }
    }

private:
    Socket Sock;
};

**Init:**创建套接字 向客户端建立连接,生成随机数种子。

**Start:**随机数的方式构建请求,将请求序列化和加码后使用write发送给服务端,然后再使用read将服务端发送回来的结果读取显示。

5,main函数

在是实现完如上代码后,我们可以来实现两个main函数来调用一下如上代码。

Server.cc:

cpp 复制代码
#include "CalServer.hpp"
 

void usage(std::string proc)
{
    std::cout << proc << "port[1024+]" << std::endl;
}

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

    if(argc!=2)
    {
        usage(argv[0]);
    }

    int port = std::stoi(argv[1]);

    CalServer *sev = new CalServer(port);

    sev->Init();
    sev->Start();

    return 0;
}

Client.cc:

cpp 复制代码
#include"CalClient.hpp"
void usage(std::string proc)
{
    std::cout << proc << "port[1024+]" << std::endl;
}


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

    if(argc!=3)
    {
        usage(argv[0]);
    }

    std::string serip = argv[1];
    int serport = std::stoi(argv[2]);
    CalClient *cli = new CalClient;

    cli->Init(serip, serport);
    cli->Start();

}

调用以后结果如下:

相关推荐
律宏阔1 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔1 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴1 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千1 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei371 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei371 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei371 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW1 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW2 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux
yunwei372 小时前
eBPF 开发者教程: 简单的 XDP 负载均衡器
linux·后端·性能优化