计算机网络 -- 序列化与反序列化

一 协议的重要性

  我们都知道,在进行网络通信的过程中,通信的双方可以是不同的设备,不同的平台,不同的平台,比如说,手机用户和电脑用户进行通信,ios系统和安卓系统进行通信。

  自己的数据,如何保证对方端能安全接收到呢,假设linux为服务端,Windows为客户端,如何确保数据能被正确接收呢?

  就像我们中国人用中文进行交流一样,假设我们和某个外国人进行交流,母语的差异导致双方无法正常交流,信息也无法传达,于是我们只能打开手机上的 同声传译功能,可以将信息转换为对方能听懂的语言,最终实现交流。 

  同声传译 这个功能可以看做一种 协议(可以确保对端能理解自己传达的信息),协议 的出现解决了主机间的交流问题。

  也就是说,我们通过协议,规定了网络通信的双方,必须按照某种规则来对传输的内容进行解析或者是打包。

  对于网络来说,协议是双方通信的基石,如果没有协议,那么即使数据传输的再完美也无法使用,比如下面这个就是一个简单的 两正整数运算协议。

  • 协议要求:发送的数据必须由两个操作数(正整数)和一个运算符组成,并且必须遵循 x op y 这样的运算顺序。
int x;
int y;
char op; // 运算符

  主机A在发送消息时需要将 操作数x、操作数y和运算符op 进行传递,只要主机A和主机B都遵循这个 协议,那么主机B在收到消息后一定清楚这是两个操作数和一个运算符

  现在的问题是如何传递?

方案一:将两个操作数和一个运算符拼接在一起直接传递
方案二:将两个操作数和一个运算符打包成一个结构体传递

方案一:直接拼接 xopy

方案二:封装成结构体
struct Mssage{
	int x;
	int y;
	char op;
};

  无论是方案一还是方案二都存在问题,前者是对端接收到消息后无法解析,后者则是存在平台兼容问题(不同平台的结构体内存规则可能不同,会导致读取数据出错)

  要想确保双方都能正确理解 协议,还需要进行 序列化与反序列化 处理。

二 .什么是序列化与反序列化?

  序列化是指 将一个或多个需要传递的数据,按照协议的格式,拼接为一条字节流数据,反序列化则是 将收到的数据按照格式解析。 

  可见,反序列化和序列化就是协议的一部分。

  比如主机A想通过 两正整数运算协议 给主机B发送这样的消息:

//1+1
int x = 1;
int y = 1;
char op = '+';

可以根据格式(这里使用 (空格))进行 序列化,序列化后的数据长这样:

// 经过序列化后得到
string msg = "1 + 1";

在经过网络传输后,主机B收到了消息,并根据 (空格)进行 反序列化,成功获取了主机A发送的信息。

string msg = "1 + 1";

// 经过反序列化后得到
int x = 1;
int y = 1;
char op = '+';

   这里可以将需要传递的数据存储在结构体中,传递/接收 时将数据填充至类中,类中提供 序列化与反序列化 的相关接口即可。

  

class Request
{
public:
	void Serialization(string* str)
	{}

	void Deserialization(const sting& str)
	{}
	
public:
	int _x;
	int _y;
	char _op;
};

  以上就是一个简单的 序列化和反序列化 流程,简单来说就是 协议 定制后不能直接使用,需要配合 序列化与反序列化 这样的工具理解,接下来我们就基于 两正整数运算协议 编写一个简易版的网络计算器,重点在于 理解协议、序列化和反序列化。

三 相关程序的实现框架

我们接下来要编写的程序从实现功能来看是十分简单的:

  客户端给出两个正整数和一个运算符,服务器计算出结果后返回

整体框架为:

客户端获取正整数与运算符 -> 将这些数据构建出 Request 对象 -> 序列化 -> 将结果(数据包)传递给服务器 ->

服务器进行反序列化 -> 获取数据 -> 根据数据进行运算 -> 将运算结果构建出 Response 对象(回响对象) -> 序列化 -> 将结果(数据包)传递给客户端 -> 客户端反序列后获取最终结果。

 既然这是一个基于网络的简易版计算器,必然离不开网络相关接口,在编写 服务器 与 客户端 的逻辑之前,需要先将 socket 接口进行封装,方面后续的使用。

四 程序实现

4.1 封装socket相关操作

注:当前实现的程序是基于 TCP 协议的

   简单回顾下,服务器需要 创建套接字、绑定IP地址和端口号、进入监听连接状态、等待客户端连接,至于客户端需要 创建套接字、由操作系统绑定IP地址和端口号、连接服务器,等客户端成功连上服务器后,双方就可以正常进行网络通信了。

    为了让客户端和服务器都能使用同一个头文件,我们可以把客户端和服务器需要的所有操作都进行实现,各自调用即可。

Sock.hpp 套接字相关接口头文件

#pragma once

#include "Log.hpp"
#include "Err.hpp"

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

class Sock{

    const static int default_sock = -1;
    const static int default_backlog = 32;
public:
    Sock()
        :_sock(default_sock)
    {}

    // 创建套接字
    void Socket(){
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if(_sock == -1){
            logMessage(Fatal, "Creater Socket Fail! [%d]->%s", errno, strerror(errno));
            exit(SOCKET_ERR);
        }
        logMessage(Debug, "Creater Socket Success");
    }

    // 绑定IP与端口号
    void Bind(const uint16_t& port){
        
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));

        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) == -1){
            logMessage(Fatal, "Bind Socket Fail! [%d]->%s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        logMessage(Debug, "Bind Socket Success");
    }

    // 进入监听状态
    void Listen(){
        if(listen(_sock, default_backlog) == -1) {
            logMessage(Fatal, "Listen Socket Fail! [%d]->%s", errno, strerror(errno));
            exit(LISTEN_ERR);
        }
    }

    // 尝试处理连接请求
    int Accept(std::string* ip, uint16_t* port){
        struct sockaddr_in client;
        socklen_t len = sizeof(client);

        int retSock = accept(_sock, (struct sockaddr*)&client, &len)
;
        if(retSock < 0)
            logMessage(Warning, "Accept Fail! [%d]->%s", errno, strerror(errno));
        else{
            *ip = inet_ntoa(client.sin_addr);
            *port = ntohs(client.sin_port);
            logMessage(Debug, "Accept [%d -> %s:%d] Success", retSock, ip->c_str(), *port);
        }

        return retSock;
    }

    // 尝试进行连接
    int Connect(const std::string& ip, const uint16_t& port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));

        server.sin_family = AF_INET;
        server.sin_port = htons(port);
        server.sin_addr.s_addr = inet_addr(ip.c_str());

        return connect(_sock, (struct sockaddr*)&server, sizeof(server));
    }

    // 获取sock
    int GetSock(){
        return _sock;
    }

    // 关闭sock
    void Close(){
        if(_sock != default_sock){
            close(_sock);
        }
        logMessage(Debug, "Close Sock Success");
    }

    ~Sock()
    {}
private:
    int _sock; // 既可以是监听套接字,也可以是连接成功后返回的套接字
};

Err.hpp 错误码头文件

#pragma once

enum{

    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    FORK_ERR,
    SETSID_ERR,
    CHDIR_ERR,
    OPEN_ERR,
    READ_ERR,
    
};

Log.hpp 日志输出头文件 

#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdarg.h>

using namespace std;

enum{
    Debug = 0,
    Info,
    Warning,
    Error,
    Fatal
};

static const string file_name = "./tcp_log";

string getLevel(int level){

    vector<string> vs = {"<Debug>", "<Info>", "<Warning>", "<Error>", "<Fatal>", "<Unknown>"};
    
    //避免非法情况
    if(level < 0 || level >= vs.size() - 1){
        return vs[vs.size() - 1];
    }
    
    return vs[level];
}

string getTime(){

    time_t t = time(nullptr);   //获取时间戳
    struct tm *st = localtime(&t);    //获取时间相关的结构体

    char buff[128];
    snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d", st->tm_year + 1900, st->tm_mon + 1, st->tm_mday, st->tm_hour, st->tm_min, st->tm_sec);

    return buff;
}

//处理信息
void logMessage(int level, const char* format, ...)
{
    //日志格式:<日志等级> [时间] [PID] {消息体}
    string logmsg = getLevel(level);    //获取日志等级
    logmsg += " " + getTime();  //获取时间
    logmsg += " [" + to_string(getpid()) + "]";    //获取进程PID

    //截获主体消息
    char msgbuff[1024];
    va_list p;
    va_start(p, format);    //将 p 定位至 format 的起始位置
    vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自动根据格式进行读取
    va_end(p);

    logmsg += " {" + string(msgbuff) + "}";    //获取主体消息

    // 直接输出至屏幕上 方便进行测试
    cout << logmsg << endl;

    // //持久化。写入文件中
    // FILE* fp = fopen(file_name.c_str(), "a");   //以追加的方式写入
    // if(fp == nullptr) return;   //不太可能出错

    // fprintf(fp, "%s\n", logmsg.c_str());
    // fflush(fp); //手动刷新一下
    // fclose(fp);
    // fp = nullptr;
} 

有了 Sock.hpp 头文件后,服务器/客户端就可以专注于逻辑编写了.

4.2 服务器

  首先准备好 TcpServer.hpp 头文件,其中实现了服务器初始化、服务器启动、序列化与反序列化等功能。

server.hpp 服务器头文件

#pragma once

#include "Sock.hpp"
#include <iostream>
#include <string>
#include <pthread.h>

namespace My_Server{

    class server;
    // 线程所需要的信息类
    class ThreadDate{
    public:

        ThreadDate(int& sock, std::string& ip, uint16_t& port, TcpServer* ptsvr)
            :_sock(sock)
            ,_ip(ip)
            ,_port(port)
            ,_ptsvr(ptsvr)
        {}

        ~ThreadDate()
        {}

        int _sock;
        std::string _ip;
        uint16_t _port;
        TcpServer* _ptsvr; // 回指指针
    };

    class server{
        const static uint16_t default_port = 8888;
    private:
        // 线程的执行函数
        static void* threadRoutine(void* args){

            // 线程剥离
            pthread_detach(pthread_self());

            ThreadDate* td = static_cast<ThreadDate*>(args);

            td->_ptsvr->ServiceIO(td->_sock, td->_ip, td->_port);
            delete td;
            return nullptr;
        }

        // 进行IO服务的函数
        void ServiceIO(const int& sock, const std::string ip, const uint16_t& port){
            // TODO
        }

    public:

        server(const uint16_t port = default_port)
            :_port(port)
        {}

        // 初始化服务器
        void Init(){
            _listen_sock.Socket();
            _listen_sock.Bind(_port);
            _listen_sock.Listen();
        }


        // 启动服务器
        void Start(){

            while(true){
                std::string ip;
                uint16_t port;

                int sock = _listen_sock.Accept(&ip, &port);
                if(sock == -1){
                   continue;
                }
                // 创建子线程,执行业务处理
                pthread_t tid;
                ThreadDate* td = new ThreadDate(sock, ip, port, this);
                pthread_create(&tid, nullptr, threadRoutine, td);
            }
        }

        ~server(){
            _listen_sock.Close();
        }

    private:
        Sock _listen_sock; // 监听套接字
        uint16_t _port;    // 服务器端口号
    };
}

server.cc 简易计算器服务器源文件

#include <iostream>
#include <memory>
#include "server.hpp"

using namespace std;

int main(){
    unique_ptr<My_Server::server> tsvr(new My_Server::server());

    tsvr->Init();
    tsvr->Start();

    return 0;
}

Makefile 自动编译脚本

.PHONY:all
all:server 
# //client

server:server.cc
	g++ -o $@ $^ -std=c++11 -lpthread

# client:client.cc
# 	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -rf server 
# client

编译并运行程序,同时查看网络使用情况:
 

netstat -nltp

此时就证明前面写的代码已经没有问题了,接下来是填充 ServiceIO() 函数

4.3 序列化和反序列化

ServiceIO() 函数需要做这几件事

  • 读取数据
  • 反序列化
  • 业务处理
  • 序列化
  • 发送数据

除了 序列化和反序列化 外,其他步骤之前都已经见过了,所以我们先来看看如何实现 序列化与反序列化。

ServiceIO() 函数 — 位于 server.hpp 头文件中的 server 类中

// 进行IO服务的函数
void ServiceIO(const int& sock, const std::string ip, const uint16_t& port){
    // 1.读取数据

    // 2.反序列化

    // 3.业务处理

    // 4.序列化

    // 5.发送数据
}

  需要明白我们当前的 协议 为 两正整数运算,分隔符为 (空格),客户端传给服务器两个操作数和一个运算符,服务器在计算完成后将结果返回,为了方便数据的读写,可以创建两个类:Request (客户端发送的一串待求的运算字符串)和 Response(服务器发送给客户端的结果),类中的成员需要遵循协议要求,并在其中支持 序列化与反序列化。

  但这两个函数,明显重复率有点高,我们的分隔符同样为一个空格,需要进行提取的,也都是数字,因此我们可以写个工具类,从而方便序列化和反序列化。

Util.hpp 工具类

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

class Util{
public:

    //数字转化为字符串
    static std::string IntToStr(int val){
         // 特殊处理
        if(val == 0)
            return "0";
        
        std::string str;
        while(val){
            str += (val % 10) + '0';
            val /= 10;
        }

        int left = 0;
        int right = str.size() - 1;
        while(left < right){
             std::swap(str[left++], str[right--]);
        }
        return str;
    }
   

   //字符串转化为数字
    static int StrToInt(const std::string& str) {
        int ret = 0;
        for(auto e : str){
            ret = (ret * 10) + (e - '0');
        }
        return ret;
    }

    // 将给定的字符串用分隔符进行分割
    static void StringSplit(const std::string& str, const std::string& sep, std::vector<std::string>* result){
        size_t left = 0;
        size_t right = 0;
        while(right < str.size()){
            // 每次right都查找到下一个分隔符
            right = str.find(sep, left); 
            if(right == std::string::npos){
                  break;
            }
            //left到right之间即为要提取的数字
            result->push_back(str.substr(left, right - left));
            //left指向分割符下一个数字
            left = right + sep.size();
        }
        
        //会漏掉最后一个数字
        if(left < str.size()){
            result->push_back(str.substr(left));
        }
    }
};

  Protocol.hpp 协议处理相关头文件。

#pragma once
#include <string>
#include"Util.hpp"
#include<iostream>
namespace My_protocol{

// 协议的分隔符 这里我们自己设定为" "
const char* SEP= " ";
//分隔符长度
const int SEP_LEN = strlen(SEP);

//对运算进行序列和反序列化
class Request{
public:
    Request(int x = 0, int y = 0, char op = '+')
        : _x(x), _y(y), _op(op)
    {}

    // 序列化
    bool Serialization(std::string *outStr){
        *outStr = ""; // 清空

        std::string left = Util::IntToStr(_x);
        std::string right = Util::IntToStr(_y);
        *outStr = left + SEP + _op + SEP + right;

        return true;
    }

    // 反序列化
    bool Deserialization(const std::string &inStr){

        std::vector<std::string> result;
        Util::StringSplit(inStr, SEP, &result);

        // 协议规定:只允许存在两个操作数和一个运算符
        if(result.size() != 3){
           return false;
        }
        
        // 规定:运算符只能为一个字符
        if(result[1].size() != 1){
           return false;
        }

        _x = Util::StrToInt(result[0]);
        _y = Util::StrToInt(result[2]);
        _op = result[1][0];

        return true;
    }

    ~Request()
    {}

public:
    int _x;
    int _y;
    char _op;
};

   //业务处理函数
    class Response
   {
    public:
    Response(int result = 0, int code = 0)
        :_result(result), _code(code)
    {}

    // 序列化
    bool Serialization(std::string *outStr) {
        
        *outStr = ""; // 清空
        std::string left = Util::IntToStr(_result);
        std::string right = Util::IntToStr(_code);
        *outStr = left + SEP + right;

        return true;
    }

    // 提取结果和错误码
    bool Deserialization(const std::string &inStr){

        std::vector<std::string> result;
        Util::StringSplit(inStr, SEP, &result);

        if(result.size() != 2)
            return false;

        _result = Util::StrToInt(result[0]);
        _code = Util::StrToInt(result[1]);

        return true;
    }

    ~Response()
    {}

   public:
    int _result; // 结果
    int _code;   // 错误码
   };

}

4.4 业务的实际处理

  server 中的业务处理函数由 CalcServer.cc 传递,规定业务处理函数的类型为 void(Request&, Response*)

Calculate() 函数 — 位于server.cc

#include "server.hpp"
#include "Protocol.hpp"

#include <iostream>
#include <memory>
#include <functional>
#include <unordered_map>

using namespace std;

void Calculate(My_protocol::Request& req, My_protocol::Response* resp){

    // 这里只是简单的计算而已
    int x = req._x;
    int y = req._y;
    char op = req._op;
    unordered_map<char, function<int()>> hash = 
    {
        {'+', [&](){ return x + y; }},
        {'-', [&](){ return x - y; }},
        {'*', [&](){ return x * y; }},
        {'/', [&]()
            {
                if(y == 0)
                {
                    resp->_code = 1;
                    return 0;
                } 
                return x / y; 
            }
        },
        {'%', [&]()
            { 
                if(y == 0)
                {
                    resp->_code = 2;
                    return 0;
                }
                return x % y;
            }
        }
    };

    if(hash.count(op) == 0)
        resp->_code = 3;
    else
        resp->_result = hash[op]();
}

int main(){

    unique_ptr<My_Server::server> tsvr(new My_Server::server(Calculate));

    tsvr->Init();
    tsvr->Start();

    return 0;
}

既然 CalcServer 中传入了 Calculate() 函数对象,server 类中就得接收并使用,也就是业务处理.

server.hpp 头文件

#pragma once

#include "Sock.hpp"
#include <iostream>
#include <string>
#include <pthread.h>
#include "Protocol.hpp"
#include<functional>

namespace My_Server
{

    class server;
    // 线程所需要的信息类
    class ThreadDate
    {
    public:
        ThreadDate(int &sock, std::string &ip, uint16_t &port, server *ptsvr)
            : _sock(sock), _ip(ip), _port(port), _ptsvr(ptsvr)
        {
        }

        ~ThreadDate()
        {
        }

        int _sock;
        std::string _ip;
        uint16_t _port;
        server *_ptsvr; // 回指指针
    };
    
     using func_t = std::function<void(My_protocol::Request&, My_protocol::Response*)>;

    class server
    {
        const static uint16_t default_port = 8088;

    private:
        // 线程的执行函数
        static void *threadRoutine(void *args)
        {

            // 线程剥离
            pthread_detach(pthread_self());

            ThreadDate *td = static_cast<ThreadDate *>(args);

            td->_ptsvr->ServiceIO(td->_sock, td->_ip, td->_port);
            delete td;
            return nullptr;
        }

        // 进行IO服务的函数
        void ServiceIO(const int &sock, const std::string ip, const uint16_t &port){
            while (true){
                // 1.读取数据
                std::string package; // 假设这是已经读取到的数据包,格式为 "1 + 1"

                // 2.反序列化
                My_protocol::Request req;
                if (req.Deserialization(package) == false){
                    logMessage(Warning, "Deserialization fail!");
                    continue;
                }

                // 3.业务处理
                // TODO
                My_protocol::Response resp; // 业务处理完成后得到的响应对象
                _func(req, &resp);

                // 4.序列化
                std::string sendMsg;
                resp.Serialization(&sendMsg);
                std::cout<<sendMsg<<std::endl;
                // 5.发送数据
            }
        }

    public:
        server(func_t fun,const uint16_t port = default_port)
            : _port(port)
            ,_func(fun)
        {}

        // 初始化服务器
        void Init(){
            _listen_sock.Socket();
            _listen_sock.Bind(_port);
            _listen_sock.Listen();
        }

        // 启动服务器
        void Start(){

            while (true){
                std::string ip;
                uint16_t port;

                int sock = _listen_sock.Accept(&ip, &port);
                if (sock == -1)
                {
                    continue;
                }
                // 创建子线程,执行业务处理
                pthread_t tid;
                ThreadDate *td = new ThreadDate(sock, ip, port, this);
                pthread_create(&tid, nullptr, threadRoutine, td);
            }
        }

        ~server(){
            _listen_sock.Close();
        }

    private:
        Sock _listen_sock; // 监听套接字
        uint16_t _port;    // 服务器端口号
        func_t _func;      // 上层传入的业务处理函数

    };
}

  这就做好业务处理了,ServiceIO() 函数已经完成了 50% 的工作,接下来的重点是如何读取和发送数据?

  TCP 协议是面向字节流的,这也就意味着数据在传输过程中可能会因为网络问题,分为多次传输,这也就意味着我们可能无法将其一次性读取完毕,需要制定一个策略,来确保数据全部递达.

4.5 报头处理

如何确认自己已经读取完了所以数据?答案是提前知道目标数据的长度,边读取边判断

数据在发送时,是需要在前面添加 长度 这个信息的,通常将其称为 报头,而待读取的数据称为 有效载荷,报头 和 有效载荷 的关系类似于快递单与包裹的关系,前者是后者成功递达的保障

最简单的 报头 内容就是 有效载荷 的长度

问题来了,如何区分 报头 与 有效载荷 呢?

  • 当前可以确定的是,我们的报头中只包含了长度这个信息
  • 可以通过添加特殊字符,如 \r\n 的方式进行区分
  • 后续无论有效载荷变成什么内容,都不影响我们通过报头进行读取

报头处理属于协议的一部分

所以在正式读写数据前,需要解决 报头 的问题(收到数据后移除报头,发送数据前添加报头)

ReadPackage() 读取函数 — 位于 Protocol.hpp 头文件

在 Protocol.hpp 中完成报头的添加和移除

#define HEAD_SEP "\r\n"
#define HEAD_SEP_LEN strlen(HEAD_SEP)

// 添加报头
void AddHeader(std::string& str){
    // 先计算出长度
    size_t len = str.size();
    std::string strLen = Util::IntToStr(len);

    // 再进行拼接
    str = strLen + HEAD_SEP + str;
}

// 移除报头
void RemoveHeader(std::string& str, size_t len){
    // len 表示有效载荷的长度
    str = str.substr(str.size() - len);
}

报头+有效载荷需要通过 read() 或者 recv() 函数从网络中读取,并且需要边读取边判断。

ReadPackage() 读取函数 — 位于 Protocol.hpp 头文件

#define BUFF_SIZE 1024
// 读取数据
int ReadPackage(int sock, std::string& inBuff, std::string* package){
   
    // 也可以使用 read 函数
    char buff[BUFF_SIZE];
    int n = recv(sock, buff, sizeof(buff) - 1, 0);
    if(n < 0)
        return -1; // 表示读取失败
    else if(n == 0)
        return 0; // 需要继续读取
    
    buff[n] = '\0';
    inBuff += buff;

    // 判断 inBuff 中是否存在完整的数据包(报头\r\n有效载荷)
    int pos = inBuff.find(HEAD_SEP);
    if(pos == std::string::npos)
        return -1;
    
    std::string strLen = inBuff.substr(0, pos); // 有效载荷的长度
    int packLen = strLen.size() + HEAD_SEP_LEN + Util::StrToInt(strLen); // 这是 报头+分隔符+有效载荷 的总长度
    if(inBuff.size() < packLen)
        return -1;
    
    *package = inBuff.substr(0, packLen); // 获取 报头+分隔符+有效载荷 ,也就是数据包
    inBuff.erase(0, packLen); // 从缓冲区中取走字符串
    return Util::StrToInt(strLen);
}

完整代码:
 

#pragma once
#include <string>
#include"Util.hpp"
#include<iostream>



namespace My_protocol{

#define HEAD_SEP "\r\n"
#define HEAD_SEP_LEN strlen(HEAD_SEP)

// 添加报头
void AddHeader(std::string& str){
    // 先计算出长度
    size_t len = str.size();
    std::string strLen = Util::IntToStr(len);

    // 再进行拼接
    str = strLen + HEAD_SEP + str;
}

// 移除报头
void RemoveHeader(std::string& str, size_t len){
    // len 表示有效载荷的长度
    str = str.substr(str.size() - len);
}

#define BUFF_SIZE 1024
// 读取数据
int ReadPackage(int sock, std::string& inBuff, std::string* package){
   
    // 也可以使用 read 函数
    char buff[BUFF_SIZE];
    int n = recv(sock, buff, sizeof(buff) - 1, 0);
    if(n < 0)
        return -1; // 表示读取失败
    else if(n == 0)
        return 0; // 需要继续读取
    
    buff[n] = '\0';
    inBuff += buff;

    // 判断 inBuff 中是否存在完整的数据包(报头\r\n有效载荷)
    int pos = inBuff.find(HEAD_SEP);
    if(pos == std::string::npos)
        return -1;
    
    std::string strLen = inBuff.substr(0, pos); // 有效载荷的长度
    int packLen = strLen.size() + HEAD_SEP_LEN + Util::StrToInt(strLen); // 这是 报头+分隔符+有效载荷 的总长度
    if(inBuff.size() < packLen)
        return -1;
    
    *package = inBuff.substr(0, packLen); // 获取 报头+分隔符+有效载荷 ,也就是数据包
    inBuff.erase(0, packLen); // 从缓冲区中取走字符串
    return Util::StrToInt(strLen);
}



// 协议的分隔符 这里我们自己设定为" "
const char* SEP= " ";
//分隔符长度
const int SEP_LEN = strlen(SEP);

//对运算进行序列和反序列化
class Request{
public:
    Request(int x = 0, int y = 0, char op = '+')
        : _x(x), _y(y), _op(op)
    {}

    // 序列化
    bool Serialization(std::string *outStr){
        *outStr = ""; // 清空

        std::string left = Util::IntToStr(_x);
        std::string right = Util::IntToStr(_y);
        *outStr = left + SEP + _op + SEP + right;

        return true;
    }

    // 反序列化
    bool Deserialization(const std::string &inStr){

        std::vector<std::string> result;
        Util::StringSplit(inStr, SEP, &result);

        // 协议规定:只允许存在两个操作数和一个运算符
        if(result.size() != 3){
           return false;
        }
        
        // 规定:运算符只能为一个字符
        if(result[1].size() != 1){
           return false;
        }

        _x = Util::StrToInt(result[0]);
        _y = Util::StrToInt(result[2]);
        _op = result[1][0];

        return true;
    }

    ~Request()
    {}

public:
    int _x;
    int _y;
    char _op;
};

   //业务处理函数
    class Response
   {
    public:
    Response(int result = 0, int code = 0)
        :_result(result), _code(code)
    {}

    // 序列化
    bool Serialization(std::string *outStr) {

        *outStr = ""; // 清空
        std::string left = Util::IntToStr(_result);
        std::string right = Util::IntToStr(_code);
        *outStr = left + SEP + right;

        return true;
    }

    // 提取结果和错误码
    bool Deserialization(const std::string &inStr){

        std::vector<std::string> result;
        Util::StringSplit(inStr, SEP, &result);

        if(result.size() != 2)
            return false;

        _result = Util::StrToInt(result[0]);
        _code = Util::StrToInt(result[1]);

        return true;
    }

    ~Response()
    {}

   public:
    int _result; // 结果
    int _code;   // 错误码
   };

}

此时对于 ServiceIO() 函数来说,核心函数都已经准备好了,只差拼装了。

ServiceIO() 函数 — 位于 server.hpp 头文件中的server 类中

 // 进行IO服务的函数
        void ServiceIO(const int &sock, const std::string ip, const uint16_t &port)
        {
            std::string inBuff;
            while (true)
            {
                // 1.读取数据
                std::string package; // 假设这是已经读取到的数据包,格式为 "5\r\n1 + 1"
                int len = My_protocol::ReadPackage(sock, inBuff, &package);
                if (len < 0)
                    break;
                else if (len == 0)
                    continue;

                // 2.移除报头
                My_protocol::RemoveHeader(package, len);

                // 3.反序列化
                My_protocol::Request req;
                if (req.Deserialization(package) == false)
                {
                    logMessage(Warning, "Deserialization fail!");
                    continue;
                }

                // 4.业务处理
                My_protocol::Response resp; // 业务处理完成后得到的响应对象
                _func(req, &resp);

                // 5.序列化
                std::string sendMsg;
                resp.Serialization(&sendMsg);
                cout << sendMsg << endl;

                // 6.添加报头
                My_protocol::AddHeader(sendMsg);

                // 7.发送数据
                send(sock, sendMsg.c_str(), sendMsg.size(), 0);
            }
        }

至此服务器编写完毕,接下来就是进行客户端的编写了.

4.5 客户端

Client.hpp 客户端头文件

#pragma once

#include "Sock.hpp"
#include "Protocol.hpp"
#include "Log.hpp"
#include "Err.hpp"

#include <iostream>
#include <string>
#include <unistd.h>

namespace My_Client
{
    class client
    {
    public:
        client(const std::string& ip, const uint16_t& port)
            :_server_ip(ip)
            ,_server_port(port)
        {}

        void Init()
        {
            _sock.Socket();
        }

        void Start()
        {
            int i = 5;
            while(i > 0)
            {
                if(_sock.Connect(_server_ip, _server_port) != -1)
                    break;
                
                logMessage(Warning, "Connect Server Fail! %d", i--);
                sleep(1);
            }

            if(i == 0)
            {
                logMessage(Fatal, "Connect Server Fail!");
                exit(CONNECT_ERR);
            }

            // 执行读写函数
            ServiceIO();
        }

        void ServiceIO()
        {
            while(true)
            {
                std::string str;
                std::cout << "Please Enter:> ";
                std::getline(std::cin, str);

                // 1.判断是否需要退出
                if(str == "quit")
                    break;

                // 2.分割输入的字符串
                My_protocol::Request req;
                [&]()
                {
                    std::string ops = "+-*/%";
                    int pos = 0;
                    for(auto e : ops)
                    {
                        pos = str.find(e);
                        if(pos != std::string::npos)
                            break;
                    }

                    req._x = Util::StrToInt(str.substr(0, pos));
                    req._y = Util::StrToInt(str.substr(pos + 1));
                    req._op = str[pos];
                }();

                // 3.序列化
                std::string sendMsg;
                req.Serialization(&sendMsg);

                // 4.添加报头
                My_protocol::AddHeader(sendMsg);

                // 5.发送数据
                send(_sock.GetSock(), sendMsg.c_str(), sendMsg.size(), 0);

                // 6.获取数据
                std::string inBuff;
                std::string package;
                int len = 0;
                while(true)
                {
                    len = My_protocol::ReadPackage(_sock.GetSock(), inBuff, &package);
                    if(len < 0)
                        exit(READ_ERR);
                    else if(len > 0)
                        break;
                }

                // 7.移除报头
                My_protocol::RemoveHeader(package, len);

                // 8.反序列化
                My_protocol::Response resp;
                if(resp.Deserialization(package) == false)
                {
                    logMessage(Warning, "Deserialization fail!");
                    continue;
                }
                
                // 9.获取结果
                std::cout << "The Result: " << resp._result << " " << resp._code << endl;
            }
        }

        ~client()
        {
            _sock.Close();
        }

    private:
        Sock _sock;
        std::string _server_ip;
        uint16_t _server_port;
    };
}

client.cc 客户端源文件

#include "client.hpp"

#include <iostream>
#include <memory>

using namespace std;

int main()
{
    unique_ptr<My_Client::client> tclt(new My_Client::client("127.0.0.1", 8888));

    tclt->Init();
    tclt->Start();
    
    return 0;
}

五 测试

六 使用库

事实上,序列化与反序列化 这种工作轮不到我们来做,因为有更好更强的库,比如 JsonXMLProtobuf 等

比如我们就可以使用 Json 来修改程序

首先需要安装 json-cpp 库,如果是 CentOS7 操作系统的可以直接使用下面这条命令安装

yum install -y jsoncpp-devel

安装完成后,可以引入头文件 <jsoncpp/json/json.h>

然后就可以在 Protocol.hpp 头文件中进行修改了,如果想保留原来自己实现的 序列化与反序列化 代码,可以利用 条件编译 进行区分

Protocol.hpp 协议相关头文件

#pragma once
#include "Util.hpp"

#include <jsoncpp/json/json.h>
#include <string>
#include <vector>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>

namespace My_protocol
{
// 协议的分隔符
#define SEP " "
#define SEP_LEN strlen(SEP)
#define HEAD_SEP "\r\n"
#define HEAD_SEP_LEN strlen(HEAD_SEP)
#define BUFF_SIZE 1024
// #define USER 1

    // 添加报头
    void AddHeader(std::string& str)
    {
        // 先计算出长度
        size_t len = str.size();
        std::string strLen = Util::IntToStr(len);

        // 再进行拼接
        str = strLen + HEAD_SEP + str;
    }

    // 移除报头
    void RemoveHeader(std::string& str, size_t len)
    {
        // len 表示有效载荷的长度
        str = str.substr(str.size() - len);
    }

    // 读取数据
    int ReadPackage(int sock, std::string& inBuff, std::string* package)
    {
        // 也可以使用 read 函数
        char buff[BUFF_SIZE];
        int n = recv(sock, buff, sizeof(buff) - 1, 0);
        if(n < 0)
            return -1; // 表示什么都没有读到
        else if(n == 0)
            return 0; // 需要继续读取
        
        buff[n] = 0;
        inBuff += buff;

        // 判断 inBuff 中是否存在完整的数据包(报头\r\n有效载荷)
        int pos = inBuff.find(HEAD_SEP);
        if(pos == std::string::npos)
            return 0;
        
        std::string strLen = inBuff.substr(0, pos); // 有效载荷的长度
        int packLen = strLen.size() + HEAD_SEP_LEN + Util::StrToInt(strLen); // 这是 报头+分隔符+有效载荷 的总长度
        if(inBuff.size() < packLen)
            return 0;
        
        *package = inBuff.substr(0, packLen); // 获取 报头+分隔符+有效载荷 ,也就是数据包
        inBuff.erase(0, packLen); // 从缓冲区中取走字符串
        return Util::StrToInt(strLen);
    }

    class Request
    {
    public:
        Request(int x = 0, int y = 0, char op = '+')
            : _x(x), _y(y), _op(op)
        {}

        // 序列化
        bool Serialization(std::string *outStr)
        {
            *outStr = ""; // 清空
#ifdef USER
            std::string left = Util::IntToStr(_x);
            std::string right = Util::IntToStr(_y);
            *outStr = left + SEP + _op + SEP + right;
#else
            // 使用 Json
            Json::Value root;
            root["x"] = _x;
            root["op"] = _op;
            root["y"] = _y;

            Json::FastWriter writer;
            *outStr = writer.write(root);
#endif
            std::cout << "序列化完成: " << *outStr << std::endl << std::endl;
            return true;
        }

        // 反序列化
        bool Deserialization(const std::string &inStr)
        {
#ifdef USER
            std::vector<std::string> result;
            Util::StringSplit(inStr, SEP, &result);

            // 协议规定:只允许存在两个操作数和一个运算符
            if(result.size() != 3)
                return false;
            
            // 规定:运算符只能为一个字符
            if(result[1].size() != 1)
                return false;

            _x = Util::StrToInt(result[0]);
            _y = Util::StrToInt(result[2]);
            _op = result[1][0];
#else
            // 使用Json
            Json::Value root;
            Json::Reader reader;
            reader.parse(inStr, root);

            _x = root["x"].asInt();
            _op = root["op"].asInt();
            _y = root["y"].asInt();
#endif
            return true;
        }

        ~Request()
        {}

    public:
        int _x;
        int _y;
        char _op;
    };

    class Response
    {
    public:
        Response(int result = 0, int code = 0)
            :_result(result), _code(code)
        {}

        // 序列化
        bool Serialization(std::string *outStr)
        {
            *outStr = ""; // 清空
#ifdef USER
            std::string left = Util::IntToStr(_result);
            std::string right = Util::IntToStr(_code);
            *outStr = left + SEP + right;
#else
            // 使用 Json
            Json::Value root;
            root["_result"] = _result;
            root["_code"] = _code;

            Json::FastWriter writer;
            *outStr = writer.write(root);
#endif
            std::cout << "序列化完成: " << *outStr << std::endl << std::endl;
            return true;
        }

        // 反序列化
        bool Deserialization(const std::string &inStr)
        {
#ifdef USER
            std::vector<std::string> result;
            Util::StringSplit(inStr, SEP, &result);

            if(result.size() != 2)
                return false;

            _result = Util::StrToInt(result[0]);
            _code = Util::StrToInt(result[1]);
#else
            // 使用Json
            Json::Value root;
            Json::Reader reader;
            reader.parse(inStr, root);

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

        ~Response()
        {}

    public:
        int _result; // 结果
        int _code;   // 错误码
    };
}

注意: 因为现在使用了 Json 库,所以编译代码时需要指明其动态库

.PHONY:all
all:server client

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

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

.PHONY:clean
clean:
	rm -rf server  client

使用了 Json 库之后,序列化 后的数据会更加直观,当然也更易于使用

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

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

相关文章

Android实践:查看Activity信息

问题&#xff1a;本地Android SDK的monitor无法正常运行&#xff0c;看不了进程相关信息&#xff0c;确认当前显示Activity十分不便 解决办法&#xff1a;使用adb shell指令可以快速查看 命令&#xff1a; adb shell dumpsys activity activities 这个命令用于获取Android设…

MySQL 进阶使用【函数、索引、视图、存储过程、存储函数、触发器】

前言 做数仓开发离不开 SQL &#xff0c;写了很多 HQL 回头再看 MySQL 才发现&#xff0c;很多东西并不是 HQL 所独创的&#xff0c;而是几乎都来自于关系型数据库通用的 SQL&#xff1b;想到以后需要每天和数仓打交道&#xff0c;那么不管是 MySQL 还是 Oracle &#xff0c;都…

MS5173M-16bit、单通道、200kSPS、 SAR 型 ADC

MS5173M 是单通道、 16bit 、电荷再分配逐次 逼近型模数转换器&#xff0c;采用单电源供电。 MS5173M 包含一个低功耗、高速数据采样且 无失码的真 16 位 SAR ADC 和一个内部转换时钟。 MS5173M 使用通用的串口接口实现转换结果 的接收&#xff0c;还包含低噪声、宽…

AI助力内容创作:让效率与质量齐飞

简述&#xff1a; 本文介绍了AI如何帮助创作者在保持内容质量的同时&#xff0c;大幅度提升生产效率的一些方法&#xff0c;希想 对大家有帮助。 一、自动化内容生成 1. 文本内容生成 使用GPT等模型&#xff1a;利用如GPT-3或GPT-4等大型语言模型&#xff0c;可以直接输入关…

好烦啊,我真的不想写增删改查了!

大家好&#xff0c;我是程序员鱼皮。 很想吐槽&#xff1a;我真的不想写增删改查这种重复代码了&#xff01; 大学刚做项目的时候&#xff0c;就在写增删改查&#xff0c;万万没想到 7 年后&#xff0c;还在和增删改查打交道。因为增删改查是任何项目的基础功能&#xff0c;每…

创新指南 | 企业AI战略 实施方案探讨(上):如何构建基于AI的新商业模型和业务场景

2023年以ChatGPT为代表的生成式AI推出以来&#xff0c;从投资界到企业界都掀起了一股热潮。那么从企业角度来看&#xff0c;生成式AI到底能为业务带来哪些增量呢&#xff1f;企业如何构建基于AI的商业模式并进行落地实施呢&#xff1f; 企业AI战略 实施方案探讨分为上下两篇&am…

内网安全工具之ADExplorer的使用

ADExplorer是域内一款信息查询工具&#xff0c;它是独立的可执行文件&#xff0c;无需安装。它能够列出域组织架构、用户账号、计算机账号登&#xff0c;可以帮助寻找特权用户和数据库服务器等敏感目标。 下载地址&#xff1a;http://live.sysinternals.com/ 连接 下载了ADE…

Java项目:基于ssm框架实现的实验室耗材管理系统(B/S架构+源码+数据库+毕业论文+答辩PPT)

一、项目简介 本项目是一套基于ssm框架实现的实验室耗材管理系统 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 二、技术实现 jdk版本&#xff1a;1.8 …

vue+ts+vite+pinia+less+echarts 前端可视化 实战项目

1.初始化前端 输入 npm init vuelatest 命令 然后 选择需要的插件2.构建完成后 在终端切换到vue-project文件夹下 npm install 下载依赖 3.下载 less样式 npm install less less-loader -D 4.下载axios npm install axios 5.下载echarts npm install echarts -S 6.引入中国…

【Spring security】Note01-pig登录验证过程

&#x1f338;&#x1f338; pig 登录验证 &#x1f338;&#x1f338; 一、大概执行顺序&#xff0c;便于理解 pig spring-security 二、执行过程分析 请求拦截&#xff1a; 当客户端发送请求时&#xff0c;Spring Security 的过滤器链会首先拦截该请求。过滤器链中的每个…

Python-元组

元组()&#xff1a; 1、不可变序列&#xff0c;不能增加、修改、删除元组中的元素&#xff0c;但是是序列&#xff0c;可以有序列的相关操作 ttuple([10,20,30]) print("10在元组中是否存在&#xff1a;",(10 in t)) print("最大值:",max(t)) print(&quo…

熬了快两个月,终于拿到了淘天后端offer!

今年的暑期实习挺难找的&#xff0c;很多同学忙了几个月到现在还没有一个offer&#xff0c;真的很常见&#xff01;没找到暑期实习的同学千万不要太焦虑&#xff0c;可以留意留意日常实习&#xff0c;日常实习也找不到&#xff0c;那就去完善自己的项目经历&#xff0c;认真准备…

【上海大学计算机组成原理实验报告】五、机器语言程序实验

一、实验目的 理解计算机执行程序的实际过程。 学习编制机器语言简单程序的方法。 二、实验原理 根据实验指导书的相关内容&#xff0c;指令的形式化表示是指采用一种规范化的符号系统&#xff0c;以更清晰、精确地描述和表示指令的逻辑功能和操作步骤。 汇编是一种编程语言…

最新版rancher环境配置安装和集群搭建详细教程记录

&#x1f680; 作者 &#xff1a;“二当家-小D” &#x1f680; 博主简介&#xff1a;⭐前荔枝FM架构师、阿里资深工程师||曾任职于阿里巴巴担任多个项目负责人&#xff0c;8年开发架构经验&#xff0c;精通java,擅长分布式高并发架构,自动化压力测试&#xff0c;微服务容器化k…

基础之音视频2

01 前言 02 mp 03 mp实例 简易音乐播放器 04 音频 sound-pool 1.作用 播放多个音频&#xff0c;短促音频 2.过程 加载load- 3.示例 模拟手机选铃声 步骤&#xff1a; 创建SoundPool对象&#xff0c;设置相关属性 音频流存入hashmap 播放音频 05 videoview 3gp 体积小 mp4 …

Redis简单使用

认识Redis redis&#xff1a;字典型数据库&#xff0c;存储的是键值对&#xff0c;是NoSql数据库 关系型数据库和NoSql之间的区别&#xff1a; 结构化&#xff1a;NoSql非结构化数据库&#xff0c;松散结构&#xff08;键值对key-value&#xff08;可以任意类型&#xff09;&…

计算机Java项目|Springboot高校心理教育辅导设计与实现

作者主页&#xff1a;编程指南针 作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、CSDN内容合伙人、掘金特邀作者、阿里云博客专家、51CTO特邀作者、多年架构师设计经验、腾讯课堂常驻讲师 主要内容&#xff1a;Java项目、Python项目、前端项目、人工智能与大数据、简…

计算机寄存器是如何实现的

你好&#xff0c;我是 shengjk1&#xff0c;多年大厂经验&#xff0c;努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注&#xff01;你会有如下收益&#xff1a; 了解大厂经验拥有和大厂相匹配的技术等 希望看什么&#xff0c;评论或者私信告诉我&#xff01; 文章目录 一…

##22 深入理解Transformer模型

文章目录 前言1. Transformer模型概述1.1 关键特性 2. Transformer 架构详解2.1 编码器和解码器结构2.1.1 多头自注意力机制2.1.2 前馈神经网络 2.2 自注意力2.3 位置编码 3. 在PyTorch中实现Transformer3.1 准备环境3.2 构建模型3.3 训练模型 4. 总结与展望 前言 在当今深度学…

MySQL基础指南:从入门到精通

MySQL基础指南&#xff1a;从入门到精通 MySQL是一个流行的开源关系型数据库管理系统&#xff0c;被广泛用于Web应用程序和服务器端开发。本文将从MySQL的基本概念开始&#xff0c;逐步介绍MySQL的安装、常用操作、数据类型、查询语句等内容&#xff0c;帮助你快速入门MySQL数…