【网络】自定义协议——序列化和反序列化

> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:了解什么是序列化和分序列,并且自己能手撕网络版的计算器。

> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!

> 专栏选自:网络

> 望小伙伴们点赞👍收藏✨加关注哟💕💕

​​

 一、前言

前面我们已经学习了网络的基础知识,对网络的基本框架已有认识,算是初步认识到网络了,如果上期我们的学习网络是步入基础知识,那么这次学习的板块就是基础知识的实践,我们今天的板块是学习网络重要之一,学习完这个板块对虚幻的网络就不再迷茫!!!

 二主体

学习【网络】自定义协议——序列化和反序列化咱们按照下面的图解:

2.1 自定义协议(应用层)

概念:

网络协议:是通信计算机双方必须共同遵从的一组约定,为了使数据在网络上能够从源地址到目的地址,网络通信的参与方必须遵循相同的规则,因此我们将这套规则称为协议,协议最终需要通过计算机语言的方式表示出来。

应用层和传输层的概述:

  • 应用层:位于网络协议的最高层,直接为用户提供服务。这一层的协议如HTTP、FTP、SMTP等,负责处理特定应用程序的数据传输和通信。
  • 传输层:位于应用层之下,网络层之上。它提供端到端的通信服务,确保数据在网络中的可靠传输。传输层协议如TCP和UDP,管理数据包的顺序和流量控制。

应用层和传输层的区别:

  • 目的:应用层的目的是为了支持各种应用程序的特定需求,而传输层则致力于提供通用的通信服务。
  • 可靠性:应用层协议可以根据应用程序的需求提供可靠或不可靠的服务。而传输层协议,如TCP,保证数据包的顺序和完整性,提供可靠的端到端通信。
  • 数据传输方式:应用层协议如HTTP、FTP等,通常基于TCP或UDP传输数据。而传输层协议则负责管理这些数据包的发送和接收。
  • 服务类型:应用层协议服务于各种类型的应用,如网页浏览、电子邮件、文件传输等。而传输层协议主要为应用程序提供通用的数据传输服务。

应用层和传输层的关系:

应用层和传输层在网络通信中是紧密相关的。传输层提供了一种可靠的数据传输机制,确保数据在网络中的正确传输,而应用层协议定义了特定应用程序的数据格式和通信规则。两者之间的协同工作使得应用程序能够有效地进行数据交换和通信。

例如,当我们使用浏览器访问一个网页时,HTTP(应用层协议)定义了请求和响应的格式,而TCP(传输层协议)确保了数据的可靠传输。两者共同作用,使得我们能够顺利地浏览网页。

2.2 序列化和反序列化


2.2.1 结构化数据

概念说明:

如果需要传输的数据是字符串,那么我们可以直接将这个字符串发送到网络当中。但是如果我们传输的是一些结构化的数据,此时就不能直接将这些数据发送到网络当中。

如果服务器将这些结构化的数据单独一个一个发送到网络当中,那么服务器从网络当中获取这些数据时也只能一个一个获取,并且结构化的数据往往具有特定的格式和规范,例如数据库表格或者特定的数据模型。如果直接将这些数据发送到网络,服务端可能需要处理大量复杂的格式转换和数据清洗工作,而且还有可能会影响数据的正确性。所以客户端最好把这些结构化的数据打包后统一发送到网络当中,此时服务端每次从网络当中获取到的就是一个完整的请求数据。

常见的打包方式:

  •  将结构化的数据组合成一个字符串

 客户端可以按照某种方式将这些结构化的数据组合成一个字符串,然后将该字符串发送到网络当中,当服务器接收到这个字符串时,以相同的方式将这个字符串进行解析。就可以从这个字符串中提取出这些结构化的数据。 

  • 定制结构体 + 序列化与反序列化

 客户端可以定制一个结构体,将需要交互的信息定义到这个结构体当中。客户端发送数据时先对数据进行序列化,服务端接收到数据后在对其进行反序列化,此时服务端就能得到客户端发送过来的结构体了。  

2.2.2 序列化和反序列化

概念:

  • 序列化: 将对象的状态信息转换为可以存储或传输的形式的过程。
  • 反序列化: 将字节序列恢复为的过程。

补充说明:

OSI七层模型中表示层的作用就是,实现设备固有数据格式和网络标准数据格式的转换。其中设备固有的数据格式指的是数据在应用层上的格式,而网络标准数据格式则指的是序列化之后可以进行网络传输的数据格式。

序列化和反序列化的目的:

  • 序列化的目的是方便网络数据的发送和接收,无论何种类型的数据,经过序列化后都变成了二进制序列,此时底层在进行网络传输时看到的都是二进制序列。
  • 序列化后的二进制序列只有在网络传输时能够被底层识别,上层应用是无法识别序列化后的二进制数据的,因此需要将从网络中获取到的数据进行反序列化,将二进制序列的数据转换成应用层能够识别的数据格式。

  

2.3 网络版计算器


2.3.1 协议定制

请求数据和响应数据:(实现一个请求结构体和一个响应结构体)

  • 请求数据结构体:包含两个操作数和一个操作符,操作数表示需要进行计算的两个数,操作符表示是要进行 +-*/ 中的哪一个操作。
  • 响应数据结构体:需要包含一个计算结果和一个状态码,表示本次计算的结果和计算状态。

态码所表示的含义:

  • 0 表示计算成功
  • 1 表示出现除0错误
  • 2 表示模0错误
  • 3 表示非法计算

因为协议定制好以后要被客户端和服务端同时遵守,所以需要将它写到一个头文件中,同时被客户端和服务端包含该头文件:

// 请求
class Request
{
public:
	Request()
	{}
	Request(int x, int y, char op):_x(x), _y(y), _op(op)
	{}
public:	
	int _x;
	int _y;
	char _op;
};

// 响应
class Response
{
public:
	Response()
	{}
	Response(int result, int code):_result(result), _code(code)
	{}
public:
	int _result;
	int _code;
};

同时,最好还是把协议中的分隔符给定义出来,方便后续统一使用或更改:

#define CRLF "\t"   //分隔符
#define CRLF_LEN strlen(CRLF) //分隔符长度
#define SPACE " "   //空格
#define SPACE_LEN strlen(SPACE) //空格长度
 
#define OPS "+-*/%" //运算符

2.3.2 编码解码

编码的是往字符串的开头添加上长度和分隔符:

长度\t序列化字符串\t

解码就是将长度和分隔符去掉,只解析出序列化字符串:

序列化字符串

编码解码的整个过程在注释里面都写明了 为了方便请求和回应去使用,直接放到外头,不做类内封装:

//参数len为in的长度,是一个输出型参数。如果为0代表err
std::string decode(std::string& in,size_t*len)
{
    assert(len);//如果长度为0是错误的
    
    // 1.确认in的序列化字符串完整(分隔符)
    *len=0;
 
    size_t pos = in.find(CRLF);//查找\t第一次出现时的下标
    //查找不到,err
    if(pos == std::string::npos){
        return "";//返回空串
    }
   
    // 2.有分隔符,判断长度是否达标
    // 此时pos下标正好就是标识大小的字符长度
    std::string inLenStr = in.substr(0,pos);//从下标0开始一直截取到第一个\t之前
 
    //到这里我们要明白,我们这上面截取的是最开头的长度,也就是说,我们截取到的一定是个数字,这个是我们序列化字符的长度
    
    size_t inLen = atoi(inLenStr.c_str());//把截取的这个字符串转int,inLen就是序列化字符的长度
 
    //传入的字符串的长度 - 第一个\t前面的字符数 - 2个\t
    size_t left = in.size() - inLenStr.size()- 2*CRLF_LEN;//原本预计的序列化字符串长度
    if(left<inLen){//真实的序列化字符串长度和预计的字符串长度进行比较
        return ""; //剩下的长度(序列化字符串的长度)没有达到标明的长度
    }
 
    // 3.走到此处,字符串完整,开始提取序列化字符串
    std::string ret = in.substr(pos+CRLF_LEN,inLen);//从pos+CRLF_LEN下标开始读取inLen个长度的字符串——即序列化字符串
 
    *len = inLen;
 
    // 4.因为in中可能还有其他的报文(下一条)
    // 所以需要把当前的报文从in中删除,方便下次decode,避免二次读取
    size_t rmLen = inLenStr.size() + ret.size() + 2*CRLF_LEN;//长度+2个\t+序列字符串的长度
    in.erase(0,rmLen);//移除从索引0开始长度为rmLen的字符串
 
    // 5.返回
    return ret;
}
 
//编码不需要修改源字符串,所以const。参数len为in的长度
std::string encode(const std::string& in,size_t len)
{
    std::string ret = std::to_string(len);//将长度转为字符串添加在最前面,作为标识
    ret+=CRLF;
    ret+=in;
    ret+=CRLF;
    return ret;
}

2.3.3 request(请求)


2.3.3.1 构造

功能说明:

用户可能输入x+y,x+ y,x +y,x + y等等格式

这里还需要注意,用户的输入不一定是标准的X+Y,里面可能在不同位置里面会有空格。为了统一方便处理,在解析之前,最好先把用户输入内的空格给去掉!

代码呈现:

class Request
{
public:
    // 将用户的输入转成内部成员
    // 用户可能输入x+y,x+ y,x +y,x + y等等格式
    // 提前修改用户输入(主要还是去掉空格),提取出成员
    Request(std::string in,bool* status)
        :_x(0),_y(0),_ops(' ')
    {
        rmSpace(in);//删除空格
 
        // 这里使用c的字符串,因为有strtok
        char buf[1024];
 
        // 打印n个字符,多的会被截断
        snprintf(buf,sizeof(buf),"%s",in.c_str());//将报文存到buf里面去,方便使用strtok
 
        char* left = strtok(buf,OPS);//left变成从字符串in的最开始到第一个运算符的这段字符串——即左操作数
        if(!left){//找不到
            *status = false;
            return;
        }
        
        //right变成从第一个运算符开始,到第二个运算符中间的这段字符串——即右操作数
        char*right = strtok(nullptr,OPS);//在上次寻找的基础上继续寻找
        if(!right){//找不到
            *status = false;
            return;
        }
 
        // x+y, strtok会将+设置为\0
        char mid = in[strlen(left)];//截取出操作符
        //这是在原字符串里面取出来,buf里面的这个位置被改成\0了
 
        _x = atoi(left);
        _y = atoi(right);
        _ops = mid;
        *status=true;
    }
 
public:
    int _x;
    int _y;
    char _ops;
};
2.3.3.2 序列化

功能说明:

解析出成员以后,我们要做的就是对成员进行序列化,将其按指定的位置摆成一个字符串。这里采用了输出型参数的方式来序列化字符串,也可以改成用返回值的方式来操作。

代码呈现:

// 序列化 (入参应该是空的)
void serialize(std::string& out)
{
    // x + y
    out.clear(); // 序列化的入参是空的
    out+= std::to_string(_x);
    out+= SPACE;
    out+= _ops;//操作符不能用tostring,会被转成ascii
    out+= SPACE;
    out+= std::to_string(_y);
    // 不用添加分隔符(这是encode要干的事情)
}
2.3.3.3 反序列化

说明:

在客户端和服务端都需要使用request,客户端进行序列化,服务端对接收到的结果利用request进行反序列化。request只关注于对请求的处理,而不处理服务器的返回值。

代码呈现:

// 反序列化
bool deserialize(const std::string &in)
{
    // x + y 需要取出x,y和操作符
    size_t space1 = in.find(SPACE); //第一个空格
    if(space1 == std::string::npos) //没找到
    {
        return false;
    }
 
    size_t space2 = in.rfind(SPACE); //第二个空格
    if(space2 == std::string::npos)  //没找到
    {
        return false;
    }
 
    // 两个空格都存在,开始取数据
    std::string dataX = in.substr(0,space1);
    std::string dataY = in.substr(space2+SPACE_LEN);//默认取到结尾
    
    std::string op = in.substr(space1+SPACE_LEN,space2 -(space1+SPACE_LEN));
    if(op.size()!=1)
    {
        return false;//操作符长度有问题
    }
 
    //没问题了,转内部成员
    _x = atoi(dataX.c_str());
    _y = atoi(dataY.c_str());
    _ops = op[0];
    return true;
}

2.3.4 response(响应)


2.3.4.1 构造

说明:

返回值的构造比较简单,因为是服务器处理结果之后的操作;这些成员变量都设置为了公有,方便后续修改。

代码呈现:

class Response//服务端必须回应
{
    Response(int code=0,int result=0)
        :_exitCode(code),_result(result)
    {}
 
public:
	int _exitCode; //计算服务的退出码
	int _result;  // 结果
};
2.3.4.2 序列化

功能说明:

解析出成员以后,我们要做的就是对成员进行序列化,将其按指定的位置摆成一个字符串。这里采用了输出型参数的方式来序列化字符串,也可以改成用返回值的方式来操作。

代码呈现:

class Response//服务端必须回应
{
    Response(int code=0,int result=0)
        :_exitCode(code),_result(result)
    {}
 
    // 入参是空的
    void serialize(std::string& out)
    {
    // code ret
        out.clear();
        out+= std::to_string(_exitCode);
        out+= SPACE;
        out+= std::to_string(_result);
        out+= CRLF;
    }
 
 
public:
	int _exitCode; //计算服务的退出码
	int _result;  // 结果
};
2.3.4.3 反序列化

说明:

响应的反序列化只需要处理一个空格,相对来说较为简单。

代码呈现:

 
class Response//服务端必须回应
{
    Response(int code=0,int result=0)
        :_exitCode(code),_result(result)
    {}
 
    //序列化
    void serialize(std::string& out)
    {
    // code ret
        out.clear();
        out+= std::to_string(_exitCode);
        out+= SPACE;
        out+= std::to_string(_result);
        out+= CRLF;
    }
    
    // 反序列化
    bool deserialize(const std::string &in)
    {
        // 只有一个空格
        size_t space = in.find(SPACE);//寻找第一个空格的下标
        if(space == std::string::npos)//没找到
        {
            return false;
        }
 
        std::string dataCode = in.substr(0,space);
        std::string dataRes = in.substr(space+SPACE_LEN);
 
        _exitCode = atoi(dataCode.c_str());
        _result = atoi(dataRes.c_str());
        return true;
    }
 
 
public:
	int _exitCode; //计算服务的退出码
	int _result;  // 结果
};

2.3.5 完整的自定义协议代码

使用说明:

 客户端发送的消息是使用Request来进行序列化和反序列化的: 

  • 客户端发消息时:客户端先序列化,再编码
  • 服务端收消息时,服务端先解码,再反序列化

服务端发送的消息是使用Response来进行序列化和反序列化的:

  • 服务端发消息时:客户端先序列化,再编码
  • 客户端收消息时,服务端先解码,再反序列化

代码呈现:Serialization.hpp

#pragma
#define CRLF "\t"               // 分隔符
#define CRLF_LEN strlen(CRLF)   // 分隔符长度
#define SPACE " "               // 空格
#define SPACE_LEN strlen(SPACE) // 空格长度
#define OPS "+-*/%"             // 运算符
 
#include <iostream>
#include <string>
#include <cstring>
#include<assert.h>
 
//参数len为in的长度,是一个输出型参数。如果为0代表err
std::string decode(std::string& in,size_t*len)
{
    assert(len);//如果长度为0是错误的
    
    // 1.确认in的序列化字符串完整(分隔符)
    *len=0;
 
    size_t pos = in.find(CRLF);//查找\t第一次出现时的下标
    //查找不到,err
    if(pos == std::string::npos){
        return "";//返回空串
    }
   
    // 2.有分隔符,判断长度是否达标
    // 此时pos下标正好就是标识大小的字符长度
    std::string inLenStr = in.substr(0,pos);//从下标0开始一直截取到第一个\t之前
 
    //到这里我们要明白,我们这上面截取的是最开头的长度,也就是说,我们截取到的一定是个数字,这个是我们序列化字符的长度
    
    size_t inLen = atoi(inLenStr.c_str());//把截取的这个字符串转int,inLen就是序列化字符的长度
 
    //传入的字符串的长度 - 第一个\t前面的字符数 - 2个\t
    size_t left = in.size() - inLenStr.size()- 2*CRLF_LEN;//原本预计的序列化字符串长度
    if(left<inLen){//真实的序列化字符串长度和预计的字符串长度进行比较
        return ""; //剩下的长度(序列化字符串的长度)没有达到标明的长度
    }
 
    // 3.走到此处,字符串完整,开始提取序列化字符串
    std::string ret = in.substr(pos+CRLF_LEN,inLen);//从pos+CRLF_LEN下标开始读取inLen个长度的字符串——即序列化字符串
 
    *len = inLen;
 
    // 4.因为in中可能还有其他的报文(下一条)
    // 所以需要把当前的报文从in中删除,方便下次decode,避免二次读取
    size_t rmLen = inLenStr.size() + ret.size() + 2*CRLF_LEN;//长度+2个\t+序列字符串的长度
    in.erase(0,rmLen);//移除从索引0开始长度为rmLen的字符串
 
    // 5.返回
    return ret;
}
 
//编码不需要修改源字符串,所以const。参数len为in的长度
std::string encode(const std::string& in,size_t len)
{
    std::string ret = std::to_string(len);//将长度转为字符串添加在最前面,作为标识
    ret+=CRLF;
    ret+=in;
    ret+=CRLF;
    return ret;
}
 
class Request//客户端使用的
{
public:
    // 将用户的输入转成内部成员
    // 用户可能输入x+y,x+ y,x +y,x + y等等格式
    // 提前修改用户输入(主要还是去掉空格),提取出成员
    Request(std::string in, bool *status)
        : _x(0), _y(0), _ops(' ')
    {
        rmSpace(in); // 删除空格
 
        // 这里使用c的字符串,因为有strtok
        char buf[1024];
 
        // 打印n个字符,多的会被截断
        snprintf(buf, sizeof(buf), "%s", in.c_str()); // 将报文存到buf里面去,方便使用strtok
 
        char *left = strtok(buf, OPS); // left变成从字符串in的最开始到第一个运算符的这段字符串——即左操作数
        if (!left)
        { // 找不到
            *status = false;
            return;
        }
 
        // right变成从第一个运算符开始,到第二个运算符中间的这段字符串——即右操作数
        char *right = strtok(nullptr, OPS); // 在上次寻找的基础上继续寻找
        if (!right)
        { // 找不到
            *status = false;
            return;
        }
 
        // x+y, strtok会将+设置为\0
        char mid = in[strlen(left)]; // 截取出操作符
        // 这是在原字符串里面取出来,buf里面的这个位置被改成\0了
 
        _x = atoi(left);
        _y = atoi(right);
        _ops = mid;
        *status = true;
    }
 
    // 删除输入中的空格
    void rmSpace(std::string &in)
    {
        std::string tmp;
        for (auto e : in)
        {
            if (e != ' ')
            {
                tmp += e;
            }
        }
        in = tmp;
    }
 
    // 序列化 (入参应该是空的,会返回一个序列化字符串)
    void serialize(std::string &out)//这个是客户端在发送消息给服务端时使用的,在这之后要先编码,才能发送出去
    {
        // x + y
        out.clear(); // 序列化的入参是空的
        out += std::to_string(_x);
        out += SPACE;
        out += _ops; // 操作符不能用tostring,会被转成ascii
        out += SPACE;
        out += std::to_string(_y);
        // 不用添加分隔符(这是encode要干的事情)
    }
    //序列化之后应该要编码,去加个长度
 
    // 反序列化(解开
    bool deserialize(const std::string &in)//这个是服务端接收到客户端发来的消息后使用的,在这之前要先解码
    {
        // x + y 需要取出x,y和操作符
        size_t space1 = in.find(SPACE);  // 第一个空格
        if (space1 == std::string::npos) // 没找到
        {
            return false;
        }
 
        size_t space2 = in.rfind(SPACE); // 第二个空格
        if (space2 == std::string::npos) // 没找到
        {
            return false;
        }
 
        // 两个空格都存在,开始取数据
        std::string dataX = in.substr(0, space1);
        std::string dataY = in.substr(space2 + SPACE_LEN); // 默认取到结尾
 
        std::string op = in.substr(space1 + SPACE_LEN, space2 - (space1 + SPACE_LEN));
        if (op.size() != 1)
        {
            return false; // 操作符长度有问题
        }
 
        // 没问题了,转内部成员
        _x = atoi(dataX.c_str());
        _y = atoi(dataY.c_str());
        _ops = op[0];
        return true;
    }
 
public:
    int _x;
    int _y;
    char _ops;
};
 
class Response // 服务端必须回应
{
    Response(int code = 0, int result = 0)
        : _exitCode(code), _result(result)
    {
    }
 
    // 序列化
    void serialize(std::string &out)//这个是服务端发送消息给客户端使用的,使用之后要编码
    {
        // code ret
        out.clear();
        out += std::to_string(_exitCode);
        out += SPACE;
        out += std::to_string(_result);
        out += CRLF;
    }
 
    // 反序列化
    bool deserialize(const std::string &in)//这个是客户端接收服务端消息后使用的,使用之前要先解码
    {
        // 只有一个空格
        size_t space = in.find(SPACE);  // 寻找第一个空格的下标
        if (space == std::string::npos) // 没找到
        {
            return false;
        }
 
        std::string dataCode = in.substr(0, space);
        std::string dataRes = in.substr(space + SPACE_LEN);
 
        _exitCode = atoi(dataCode.c_str());
        _result = atoi(dataRes.c_str());
        return true;
    }
 
public:
    int _exitCode; // 计算服务的退出码
    int _result;   // 结果
};

2.3.6 客户端/服务端完整代码

CalculatorClient.cc:

#include "TcpClient.hpp"

static void usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
              << std::endl;
}

enum
{
    LEFT,
    OPER,
    RIGHT
};


Request ParseLine(const std::string &line)
{
    std::string left, right;
    char op;
    int status = LEFT;

    int i = 0;
    while(i < line.size())
    {
        switch (status)
        {
        case LEFT:
            if (isdigit(line[i]))
                left.push_back(line[i++]);
            else
                status = OPER;
            break;
        case OPER:
            op = line[i++];
            status = RIGHT;
            break;
        case RIGHT:
            if (isdigit(line[i]))
                right.push_back(line[i++]);
            break;
        }
    }

    Request req;
    req._x = std::stoi(left);
    req._y = std::stoi(right);
    req._op = op;

    return req;
}

int main(int argc, char* argv[])
{
	if(argc != 3)
	{
		usage(argv[0]);
		exit(USAGE_ERR);
	}

	string serverip = argv[1];
	uint16_t serverport = atoi(argv[2]);

	Sock sock;
	sock.Socket();

	int n = sock.Connect(serverip, serverport);
	if(n != 0) return 1;

	string buffer;
	while(true)
	{
		cout << "Enter# ";
		string line;
		getline(cin, line);

		Request req = ParseLine(line);
		cout << "test:" << req._x << req._op << req._y << endl;

		// 序列化
		string sendString;
		req.Serialize(&sendString);

		// 添加报头
		sendString = AddHeader(sendString);

		// send
		send(sock.Fd(), sendString.c_str(), sendString.size(), 0);

		// 获取响应
		string package;
		int n = 0;
	START:
		n = ReadPackage(sock.Fd(), buffer, &package);
		if(n == 0)
			goto START;
		else if(n < 0)
			break;
		else
		{}

		// 去掉报头
		package = RemoveHeader(package, n);

		// 反序列化
		Response resp;
		resp.Deserialize(package);

		cout << "result: " << resp._result << "[code: " << resp._code << "]" << endl;
	}

	sock.Close();
	return 0;
}

CalculatorServer.cc:

#include "TcpServer.hpp"
#include <memory>
using namespace tcpserver_ns;

Response calculate(const Request &req)
{
    // 走到这里,一定保证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._code = 1;
        else
            resp._result = req._x / req._y;
        break;
    case '%':
        if (req._y == 0)
            resp._code = 2;
        else
            resp._result = req._x % req._y;
        break;
    default:
        resp._code = 3;
        break;
    }

    return resp;
}

int main()
{
	uint16_t port = 8080;
	unique_ptr<TcpServer> tsvr(new TcpServer(calculate, port));
	tsvr->InitServer();
	tsvr->Start();
	return 0;
}

Err.hpp:

#pragma once

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    SETSID_ERR,
    OPEN_ERR
};

Log.hpp:

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdio>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include <stdarg.h>
using namespace std;

const string filename = "log/tcpserver.log";

// 枚举日志等级
enum
{
    Debug = 0, // 调试信息
    Info,      // 正常运行
    Warning,   // 报警
    Error,     // 正常错误
    Fatal,     // 严重错误
    Uknown
};

// 字符串形式获取日志等级
static string toLevelString(int level)
{
    switch (level)
    {
    case Debug:
        return "Debug";
    case Info:
        return "Info";
    case Warning:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";
    default:
        return "Uknown";
    }
}

// 获取日志产生时间
static string getTime()
{
    time_t curr = time(nullptr);
    struct tm* tmp = localtime(&curr);

    // 缓冲区
    char buffer[128];
    snprintf(buffer, sizeof buffer, "%d-%d-%d %d:%d:%d", tmp->tm_year + 1900, tmp->tm_mon + 1, tmp->tm_mday,
            tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
    
    return buffer;
}

// 日志格式: 日志等级 时间 pid 消息体 —— 日志函数

void logMessage(int level, const char* format, ...)
{
    char logLeft[1024];
    string level_string = toLevelString(level);
    string curr_time = getTime();
    snprintf(logLeft, sizeof(logLeft), "[%s] [%s] [%d] ", level_string.c_str(), curr_time.c_str(), getpid());

    char logRight[1024];
    va_list p;
    va_start(p, format);
	vsnprintf(logRight, sizeof(logRight), format, p);
    va_end(p);

    // 打印日志
    printf("%s%s\n", logLeft, logRight);

    // 将日志保存到文件中
    // FILE* fp = fopen(filename.c_str(), "a");
    // if(!fp) return;
    // fprintf(fp, "%s%s\n", logLeft, logRight);
    // fflush(fp);
    // fclose(fp);
}

Protocol.hpp:

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include "Sock.hpp"
#include <jsoncpp/json/json.h>

#include "Util.hpp"
// 给网络版本计算器定制协议

// #define MYSELF 1

// 给网络版本计算器定制协议
namespace protocol_ns
{
	#define SEP " "
    #define SEP_LEN strlen(SEP) // 注意不能写成 sizeof
    #define HEADER_SEP "\r\n"
    #define HEADER_SEP_LEN strlen("\r\n")

	// 添加报头后 —— "长度"\r\n""_x _op _y"\r\n 
	string AddHeader(const string& str)
	{
		std::cout << "AddHeader 之前:\n"
                  << str << std::endl;

		string s = to_string(str.size());
		s += HEADER_SEP;
		s += str;
		s += HEADER_SEP;

		std::cout << "AddHeader 之后:\n"
                  << s << std::endl;

		return s;
	}

	// 去除报头 "长度"\r\n""_x _op _y"\r\n —— _x _op _y
	string RemoveHeader(const std::string &str, int len)
    {
		std::cout << "RemoveHeader 之前:\n"
                  << str << std::endl;

		string res = str.substr(str.size() - HEADER_SEP_LEN - len, len);
		return res;

		std::cout << "RemoveHeader 之后:\n"
                  << res << std::endl;
    }

	int ReadPackage(int sock, string& inbuffer, string*package)
	{
		std::cout << "ReadPackage inbuffer 之前:\n"
                  << inbuffer << std::endl;

		// 边读取
		char buffer[1024];
		ssize_t s = recv(sock, buffer, sizeof(buffer - 1), 0);

		if(s <= 0)
			return -1;
		buffer[s] = 0;
		inbuffer += buffer;

		std::cout << "ReadPackage inbuffer 之中:\n"
                  << inbuffer << std::endl;

		// 边读取边分析, "7"\r\n""10 + 20"\r\n
		auto pos = inbuffer.find(HEADER_SEP); // 查找 \r\n
		if(pos == string::npos)
			return 0;
		
		string lenStr = inbuffer.substr(0, pos); // 获取头部字符串
		int len = Util::toInt(lenStr);

		int targetPackageLen = lenStr.size() + len + 2 * HEADER_SEP_LEN; // 获取到的完整字符串的长度
		if(inbuffer.size() < targetPackageLen)
			return 0;
		*package = inbuffer.substr(0, targetPackageLen);
		inbuffer.erase(0, targetPackageLen);		
		
		std::cout << "ReadPackage inbuffer 之后:\n"
                  << inbuffer << std::endl;

		return len; // 返回有效载荷的长度
	}

	// Request && Response都要提供序列化和反序列化功能——自己手写

	// 请求
	class Request
    {
	public:
		Request()
		{}
		Request(int x, int y, char op):_x(x), _y(y), _op(op)
		{}
		
		// 协议的序列化 struct -> string
		bool Serialize(string* outStr)
		{
			*outStr = "";
#ifdef MYSELF
			string x_string = to_string(_x);
			string y_string = to_string(_y);

			// 手动序列化
			*outStr = x_string + SEP + _op + SEP + y_string;

			std::cout << "Request Serialize:\n"
                      << *outStr << std::endl;
#else
			Json::Value root; // Value 是一种万能对象, 可以接受任意的kv类型
			root["x"] = _x;
			root["y"] = _y;
			root["op"] = _op;

			// Json::FastWriter writer; // Writer 是用来序列化的, struct -> string
			Json::StyledWriter writer;
			*outStr = writer.write(root);
#endif
			return true;
		}

		// 协议的反序列化 string -> struct
		bool Deserialize(const string& inStr)
		{
#ifdef MYSELF
			// 将inStr分隔到数组里面 -> [0]=>10 [1]=>+ [2]=>20
			vector<string> result;
			Util::StringSplit(inStr, SEP, &result);
			if(result.size() != 3)
				return false;
			if(result[1].size() != 1)
				return false;
			
			_x = Util::toInt(result[0]);
			_y = Util::toInt(result[2]);
			_op = result[1][0];
#else
			Json::Value root;
			Json::Reader reader; // Reader是用来进行反序列化的
			reader.parse(inStr, root);

			_x  = root["x"].asInt();
			_y = root["y"].asInt();
			_op = root["op"].asInt();
#endif
			return true;
		}
		~Request()
		{}
	
	public:	
		int _x; // 操作数 _x
		int _y; // 操作数 _y
		char _op;// 操作符 _op
	};

	// 响应
	class Response
	{
	public:
		Response()
		{}
		Response(int result, int code):_result(result), _code(code)
		{}

		bool Serialize(string* outStr)
		{
			*outStr = "";
#ifdef MYSELF
			string res_string = to_string(_result);
			string code_string = to_string(_code);

			// 手动序列化
			*outStr = res_string + SEP + code_string;

			std::cout << "Response Serialize:\n"
                      << *outStr << std::endl;
#else
			Json::Value root;
			root["result"] = _result;
			root["code"] = _code;

			// Json::FastWriter writer;
			Json::StyledWriter writer;
			*outStr = writer.write(root);
#endif
			return true;
		}

		bool Deserialize(const string& inStr)
		{
#ifdef MYSELF
			// 将inStr分隔到数组里面 -> [0]=>10 [1]=>+ [2]=>20
			vector<string> result;
			Util::StringSplit(inStr, SEP, &result);
			if(result.size() != 2)
				return false;
			
			_result = Util::toInt(result[0]);
			_code = Util::toInt(result[1]);
#else
			Json::Value root;
			Json::Reader reader;
			reader.parse(inStr, root);
			_result = root["result"].asInt();
			_code = root["code"].asInt();
#endif
			Print();
			return true;
		}

		void Print()
        {
            std::cout << "_result: " << _result << std::endl;
            std::cout << "_code: " << _code << std::endl;
        }

	public:
		int _result;
		int _code;
	};
}

Sock.hpp:

#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Err.hpp"

using namespace std;
static const int gbacklog = 32;
static const int defaultfd = -1;

class Sock
{
public:
	Sock():_sock(defaultfd)
	{}

	Sock(int sock):_sock(sock)
	{}
	
	// 创建套接字
	void Socket()
	{
		_sock = socket(AF_INET, SOCK_STREAM, 0);
		if(_sock < 0)
		{
			logMessage(Fatal, "socket error, code: %d, errstring: %s", errno, strerror(errno));
			exit(SOCKET_ERR);
		}
	}

	// 绑定端口号 和 ip地址
	void Bind(const uint16_t& port)
	{
		struct sockaddr_in local;
		local.sin_addr.s_addr = INADDR_ANY; // 绑定任意IP地址
		local.sin_port = htons(port); // 绑定端口号
		local.sin_family = AF_INET;

		if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
		{
			logMessage(Fatal, "bind error, code: %d, errstring: %s", errno, strerror(errno));
			exit(BIND_ERR);
		}
	}

	// 监听
	void Listen()
	{
		if(listen(_sock, gbacklog) < 0)
		{
			logMessage(Fatal, "listen error, code: %d, errstring: %s", errno, strerror(errno));
			exit(LISTEN_ERR);
		}
	}

	// 获取连接
	int Accept(string* clientip, uint16_t* clientport)
	{
		struct sockaddr_in temp;
		socklen_t len = sizeof(temp);

		int sock = accept(_sock, (struct sockaddr*)&temp, &len);
		if(sock < 0)
		{
			logMessage(Warning, "accept error, code: %d, errstring: %s", errno, strerror(errno));
		}
		else
		{
			*clientip = inet_ntoa(temp.sin_addr);
			*clientport = ntohs(temp.sin_port);
		}
		return sock;
	}

	// 客户端和服务器建立连接
	int Connect(const string& serverip, const uint16_t& serverport)
	{
		struct sockaddr_in server;
		memset(&server, 0, sizeof(server));
		server.sin_family = AF_INET;
		server.sin_port = htons(serverport);
		server.sin_addr.s_addr = inet_addr(serverip.c_str());
		
		return connect(_sock, (struct sockaddr*)&server, sizeof(server));
	}

	int Fd()
	{
		return _sock;
	}

	void Close()
	{
		if(_sock != defaultfd)
			close(_sock);
	}
	~Sock()
    {}

private:
	int _sock;
};

TcpClient.hpp:

#pragma once

#include "TcpClient.hpp"
#include "Sock.hpp"
#include "Protocol.hpp"

#include <iostream>
#include <string>

using namespace std;
using namespace protocol_ns;

TcpServer.hpp:

#pragma once
#include <iostream>
#include <functional>
#include <cstring>
#include <pthread.h>
#include "Sock.hpp"
#include "Protocol.hpp"

using namespace std;

namespace tcpserver_ns
{
	using namespace protocol_ns;
	using func_t = function<Response(const Request&)>;
	
	class TcpServer;
	class ThreadData
	{
	public:
		ThreadData(int sock, string ip, uint16_t port, TcpServer* tsvrp)
			:_sock(sock), _ip(ip), _port(port), _tsvrp(tsvrp)
		{}
		~ThreadData()
		{}
	public:
		int _sock;
		string _ip;
		uint16_t _port;
		TcpServer *_tsvrp;
	};

	class TcpServer
	{
	public:
		TcpServer(func_t func, uint16_t port)
		:_func(func), _port(port)
		{}

		// 初始化服务器
		void InitServer()
		{
			_listensock.Socket();
			_listensock.Bind(_port);
			_listensock.Listen();
			logMessage(Info, "init server done, listensock: %d, errstring: %s", errno, strerror(errno));
		}

		// 运行服务器
		void Start()
		{
			while(true)
			{
				string clientip;
				uint16_t clientport;

				int sock = _listensock.Accept(&clientip, &clientport);
				if(sock < 0) continue;
				logMessage(Debug, "get a new client, client info : [%s:%d]", clientip.c_str(), clientport);

				pthread_t tid; // 创建多线程
				ThreadData *td = new ThreadData(sock, clientip, clientport, this);
				pthread_create(&tid, nullptr, ThreadRoutine, td);
			}
		}

		static void* ThreadRoutine(void* args)
		{
			pthread_detach(pthread_self());
			ThreadData *td = static_cast<ThreadData *>(args);

			td->_tsvrp->ServiceIO(td->_sock, td->_ip, td->_port);
			logMessage(Debug, "thread quit, client quit ...");
			delete td;
			return nullptr;
		}

		// 服务器对客户端的数据进行IO处理
		void ServiceIO(int sock, const std::string &ip, const uint16_t &port)
		{
			string inbuffer;
			while(true)
			{
				// 保证自己读到一个完整的字符串报文 "7"\r\n""10 + 20"\r\n
				string package;
				int n = ReadPackage(sock, inbuffer, &package);
				if(n == -1)
					break;
				else if(n == 0)
					continue;
				else
				{
					// 已经得到了一个"7"\r\n""10 + 20"\r\n
					// 1. 提取有效载荷
					package = RemoveHeader(package, n);

					// 2. 已经读到了一个完整的string
					Request req;
					req.Deserialize(package);

					// 3. 直接提取用户的请求数据
					Response resp = _func(req); // 业务逻辑
					
					// 4. 给用户返回响应——序列化
					string send_string;
					resp.Serialize(&send_string);

					// 5. 添加报头
					send_string = AddHeader(send_string);

					// 6. 发送
					send(sock, send_string.c_str(), send_string.size(), 0);
				}
			}
			close(sock);
		}

		~TcpServer()
		{}
	private:
		uint16_t _port; // 端口号
		Sock _listensock; // 监听套接字
		func_t _func;
	};
}

Util.hpp:

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>

using namespace std;

class Util
{
public:
	static bool StringSplit(const string&str, const string& sep, vector<string>* result)
	{
		size_t start = 0;
		while(start < str.size())
		{
			auto pos = str.find(sep, start);
			if(pos == string::npos) break;
			result->push_back(str.substr(start, pos - start));
			start = pos + sep.size();
		}

		if(start < str.size())
			result->push_back(str.substr(start));
		return true;
	}

	static int toInt(const string& s)
	{
		return atoi(s.c_str());
	}
};

makefile:

.PHONY:all
all:calserver calclient

calclient:CalculatorClient.cc
	g++ -o $@ $^ -std=c++11 -ljsoncpp

calserver:CalculatorServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp

.PHONY:clean
clean:
	rm -f calclient calserver

三、结束语 

       今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

​​ 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Abaqus随机骨料过渡区孔隙三维网格插件:Random Agg ITZ Pore 3D (Mesh)

插件介绍 Random Agg ITZ Pore 3D (Mesh) V1.0 - AbyssFish 插件可在Abaqus内参数化建立包含水泥浆基体、粗细骨料、界面过渡区&#xff08;ITZ&#xff09;、孔隙在内的多相材料混凝土细观背景网格模型。 模型说明 插件采用材料映射单元的方式&#xff0c;将不同相材料赋值…

【含开题报告+文档+源码】基于SpringBoot+Vue智能居民健康检测系统设计与实现

开题报告 随着社会发展和人民生活水平的提高&#xff0c;人们对健康生活的要求越来越高。而广大居民由于条件限制&#xff0c;存在着健康管理服务不足的问题。本文基于JavaWeb技术&#xff0c;设计并实现了一种居民健康检测系统。首先&#xff0c;本文对该平台的需求进行了分析…

基于Multisim8路抢答器电路仿真电路(含仿真和报告)

【全套资料.zip】8路抢答器电路仿真电路Multisim仿真设计数字电子技术 文章目录 功能一、Multisim仿真源文件二、原理文档报告资料下载【Multisim仿真报告讲解视频.zip】 功能 1.设计数字式抢答器&#xff0c;每组选手具有一个抢答按钮。 2.电路具有第一抢答信号的鉴别和锁存…

Java 网络编程(一)—— UDP数据报套接字编程

概念 在网络编程中主要的对象有两个&#xff1a;客户端和服务器。客户端是提供请求的&#xff0c;归用户使用&#xff0c;发送的请求会被服务器接收&#xff0c;服务器根据请求做出响应&#xff0c;然后再将响应的数据包返回给客户端。 作为程序员&#xff0c;我们主要关心应…

人工智能学习--归一化(Normalization)

概念 归一化是数据预处理中将不同量纲的特征数据缩放至同一尺度的过程&#xff0c;使特征值落在同一范围&#xff08;如[0, 1]或[-1, 1]&#xff09;。归一化有助于消除量纲影响&#xff0c;提升算法的收敛速度和模型稳定性&#xff0c;尤其在梯度下降和距离计算等算法中尤为重…

高校实验室安全巡检系统设计与实现(源码+定制+开发)高校实验室巡检系统、实验室安全管理平台、实验室安全监控系统、智能实验室巡查系统、高校实验室风险管理

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

解决程序因缺少xinput1_3.dll无法运行的有效方法,有效修复丢失xinput1_3.dll

如果你的电脑在运行某些应用程序或游戏时提示“xinput1_3.dll丢失”或“找不到xinput1_3.dll”的错误消息&#xff0c;那么很可能是因为你的系统中缺少这个重要的DLL文件而导致的问题。那么电脑出现xinput1_3.dll丢失的问题时有哪些方法进行修复呢&#xff1f; 如何确定电脑是否…

论文笔记(五十四)pi0: A Vision-Language-Action Flow Model for General Robot Control

π0: A Vision-Language-Action Flow Model for General Robot Control 文章概括摘要I. INTRODUCTIONII. RELATED WORKIII. OVERVIEWIV. π 0 \pi_0 π0​模型V. 数据收集和培训配方A. 预训练和后训练B. 语言和高级策略C. 机器人系统细节 VI. 实验评估A. 基础模型评估B. 遵循语…

Redis 基础数据改造

优质博文&#xff1a;IT-BLOG-CN 一、服务背景 基础数据查询服务&#xff1a;提供航司&#xff08;5000家&#xff09;、机场&#xff08;4000&#xff09;、票台&#xff08;40000&#xff09;、城市&#xff08;4000&#xff09;等基础数据信息。 痛点一&#xff1a;因为基…

C# String系列(3):StringBuilder有诸多优势,它能代替String吗?

前言 嗨&#xff0c;大家好&#xff01; 之前我们在文章《C# String 类型&#xff1a;那些你可能不知道的秘密》分享了 C# String 类型的一些小秘密和小技巧&#xff0c;其中提到一个性能提升的小贴士&#xff1a;在拼接字符串时&#xff0c;使用 StringBuilder 替代 String。…

6.1、实验一:静态路由

源文件获取&#xff1a;6.1_实验一&#xff1a;静态路由.pkt: https://url02.ctfile.com/f/61945102-1420248902-c5a99e?p2707 (访问密码: 2707) 一、目的 理解路由表的概念 会使用基础命令 根据需求正确配置静态路由 二、准备实验 1.实验要求 让PC0、PC1、PC2三台电脑…

嵌入式linux中设备树控制硬件的方法

大家好,今天主要给大家分享一下,如何使用linux系统下的设备树进行硬件控制方法。 第一:linux系统中设备树驱动LED原理 在linux系统中可以使用设备树向Linux内核传递相关的寄存器地址,linux驱动中使用OF函数从设备树中获取所需的属性值,然后使用获取到的属性值来初始化相关…

一文解秘Rust如何与Java互操作

本博客所有文章除特别声明外&#xff0c;均采用CC BY-NC-SA 4.0许可协议。转载请注明来自 唯你 使用场景 JAVA 与 Rust 互操作让 Rust 可以背靠 Java 大生态来做更多事情&#xff0c;而 Java 也可以享受 Rust 语言特性的内存安全&#xff0c;所有权机制&#xff0c;无畏并发。…

【贪心算法】No.1---贪心算法(1)

文章目录 前言一、贪心算法&#xff1a;二、贪心算法示例&#xff1a;1.1 柠檬⽔找零1.2 将数组和减半的最少操作次数1.3 最⼤数1.4 摆动序列1.5 最⻓递增⼦序列1.6 递增的三元⼦序列 前言 &#x1f467;个人主页&#xff1a;小沈YO. &#x1f61a;小编介绍&#xff1a;欢迎来到…

阿里云-防火墙设置不当导致ssh无法连接

今天学网络编程的时候&#xff0c;看见有陌生ip连接&#xff0c;所以打开了防火墙禁止除本机之外的其他ip连接&#xff1a; 但是当我再次用ssh的时候&#xff0c;连不上了才发现大事不妙。 折腾了半天&#xff0c;发现阿里云上可以在线向服务器发送命令&#xff0c;所以赶紧把2…

基于物联网设计的地下煤矿安全监测与预警

文章目录 一、前言1.1 项目介绍【1】项目开发背景【2】设计实现的功能【3】项目硬件模块组成 1.2 设计思路1.3 系统功能总结1.4 开发工具的选择【1】设备端开发【2】上位机开发 1.5 模块的技术详情介绍【1】NBIOT-BC26模块【2】MQ5传感器【4】DHT11传感器【5】红外热释电人体检…

揭秘全向轮运动学:机动艺术与上下位机通信的智慧桥梁

✨✨ Rqtz 个人主页 : 点击✨✨ &#x1f308;Qt系列专栏:点击 &#x1f388;Qt智能车上位机专栏: 点击&#x1f388; 本篇文章介绍的是有关于全向轮运动学分析&#xff0c;单片机与上位机通信C代码以及ROS里程计解算的内容。 目录 大纲 ROS&#xff08;机器人操作系统&…

《AI在企业战略中的关键地位:以微软和阿里为例》

内容概要 在当今商业环境中&#xff0c;人工智能&#xff08;AI&#xff09;的影响力如滔滔洪水&#xff0c;愈演愈烈。文章将揭示AI在企业战略中的崛起&#xff0c;尤其以微软和阿里巴巴为代表的企业&#xff0c;这两家科技巨头通过不同方式&#xff0c;将智能技术融入其核心…

Pandas | 理性判断数据是否存在缺失值的一种方法

理性判断 一般思路进一步思考df[B].explode() 一般思路 tcc.info()上述信息info显示没有缺失值 但是真实的情况还是要根据业务实际分析tcc.isnull().sum() # 和tcc.info()作用和tcc.info() 其实是一样的 进一步思考 在此过程中&#xff0c;我们需要检验是否存在采用别的值来表…

大数据新视界 -- 大数据大厂之经典案例解析:广告公司 Impala 优化的成功之道(下)(10/30)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…