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

目录

​编辑

一,问题引入:

 二,协议

三,自定义协议

1,协议

2, 序列化和反序列化

四,网络版本的计算器

1,协议的定制

2,计算逻辑

3,服务端

4,客户端

5,main函数


 

一,问题引入:

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

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

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

 二,协议

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

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

三,自定义协议

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

1,协议

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

len: 代表内容content的长度

\n:代表一个分割符

content:代表数据的正文内容

\n:代表一个分割符

2, 序列化和反序列化

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

四,网络版本的计算器

1,协议的定制

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

#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,计算逻辑

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

#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协议的服务端。代码如下:

#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类定义如下:

#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,客户端

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

#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:

#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:

#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. Linux:协议定制以及序列序列

    2024-03-30 17:50:02       17 阅读
  2. 【Kotlin】定义Json序列

    2024-03-30 17:50:02       32 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-03-30 17:50:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-30 17:50:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-30 17:50:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-30 17:50:02       20 阅读

热门阅读

  1. Python中的文件读取与保存

    2024-03-30 17:50:02       18 阅读
  2. 动态规划 Leetcode 674 最长连续递增序列

    2024-03-30 17:50:02       22 阅读
  3. 解决Nginx+ThinkPHP+VUE的跨域问题

    2024-03-30 17:50:02       14 阅读
  4. 算法——运动模型

    2024-03-30 17:50:02       21 阅读
  5. (67)动态口令 (68)解码异或后的数组

    2024-03-30 17:50:02       18 阅读
  6. 详解索引及优化

    2024-03-30 17:50:02       18 阅读
  7. SublimeText3多次保存自动弹出窗口

    2024-03-30 17:50:02       18 阅读
  8. 【Go】Context

    2024-03-30 17:50:02       16 阅读
  9. IO流主要有哪些?

    2024-03-30 17:50:02       18 阅读
  10. 实现文件下载

    2024-03-30 17:50:02       18 阅读
  11. Nginx专栏分享

    2024-03-30 17:50:02       20 阅读
  12. DNS 域名解析流程

    2024-03-30 17:50:02       21 阅读
  13. vue3路由跳转

    2024-03-30 17:50:02       15 阅读