Linux学习记录——삼십이 协议、序列化和反序列化


UDP是面向数据报的,所以不用担心数据。TCP是面向字节流的,如何确定读的数据就是对的?

1、序列化、反序列化

实际生活中,很多时候都是结构化数据的网络发送,聊天时,头像,昵称,发送时间,内容等等都会发送,之前的代码,发送时都是发送一个字符串,而像这种结构化的数据如何传输?网络会把这些数据变成一条长字符串,发送给对方,对方再转回原来的结构,结构解释成字符串就是序列化,字符串解释成结构就是反序列化。序列化和反序列化是为了方便网络通信。

但对方怎么知道如何转化字符串为结构体?怎么知道这是自己要反序列化的字符串,怎么知道都有哪些成员,以什么顺序来反序列化?协议就是解决这个的,根据协议来进行通信,协议本质是约定好的某种格式的数据,常见的就是用结构体或者类来表达,意思就是客户端定义的数据的结构体,服务端也用一样的结构体对象,这样两方就都知道所有的成员了,也知道如何解释了。之前写的代码是用来的处理新套接字,没有定制协议,序列化和反序列化。

结构体数据其实也可以直接传,但对于两个平台适配性要求太高,没法保证通信稳定。

2、网络计算器

通过网络计算器代码的实现,来理解协议的自定义和序列反序列化。

本篇gitee

创建文件,TcpClient.hpp,TcpServer.hpp,CalculatorClient.cc,CalculatorServer.cc。两个cc文件对应两个hpp文件,以及Sock.hpp套接字头文件。makefile:

.PHONY:all
all:calserver calclient

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

.PHONY:clean
clean:
	rm -f calclient calserver

1、套接字

sock.hpp直接写

#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include "err.hpp"
#include "log.hpp"

static const int gbacklog = 32;
static const int defaultfd = -1;

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

    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);
        }
    }

    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)) < 0)
        {
            logMessage(Fatal, "bind error, code: %d, errstring: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
    }

    void Listen()
    {
        if(listen(_sock, gbacklog) < 0)//第二个参数维护了一个队列,发送了连接请求但是服务端没有处理的客户端,服务端开始accept后,就会出现另一个队列,就是服务端接受了请求但还没被accept的客户端
        {
            logMessage(Fatal, "listen error, code: %d, errstring: %s", errno, strerror(errno));
            exit(LISTEN_ERR);
        }
    }

    int Accept(std::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);//这个函数就可以从结构体中拿出ip地址,转换好后返回
            *clientport = ntohs(temp.sin_port);
        }
        return sock;
    }

    int Connect(const std::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;
    }

    ~Sock()
    {
        if(_sock != defaultfd) close(_sock);
    }
private:
    int _sock;
};

2、服务端和序列、反序列化部分

CalculatorServer.cc

#include "TcpServer.hpp"
#include <memory>

int main()
{
    uint16_t port = 8888;
    std::unique_ptr<TcpServer> tsvr(new TcpServer(port));
    tsvr->InitServer();
    tsvr->Start();
    return 0;
}

TcpServer.hpp

#pragma once

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

class ThreadData
{
public:
    ThreadData(int s, TcpServer* tsvrp): sock(s), tsvrp(p)
    {}
    ~ThreadData(){}
private:
    int sock;
    TcpServer* tsvrp;
};

class TcpServer
{
public:
    TcpServer(uint16_t port): _port(port)
    {}

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

    static void* ThreadRoutine(void* args)
    {
        pthread_detach(pthread_self());
        ThreadData* td = static_cast<ThreadData*>(args);
        //强转成ThreadData类型就可以使用类里的函数来完成任务了
        delete td;
        return nullptr;
    }

    void Start()
    {
        for( ; ; )
        {
            std::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类 
            ThreadData* td = new ThreadData(sock, this);
            pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
        }
    }

    ~TcpServer()
    {}
public:
    uint16_t _port;
    Sock _listensock;  
};

现在可以make一下看看能否正常运行,不过没写客户端,就用telnet这个接口,写法和之前的客户端一样,会向当前服务端发送连接请求,退出时Ctrl + ],然后按quit就可以了。

1、制定协议,完成序列化、反序列化

这部分写完后,就开始写service函数,服务端提供服务。读取数据,处理数据,需要协议,所以再创建一个Protocol.hpp头文件。先建立一个大概框架。

#pragma once

#include <iostream>
#include <string>

//给网络版本计算机定制协议
namespace protocol_ns
{
    class Request
    {
    public:
        Request() {}
        ~Request() {}
    public:
        int _x;
        int _y;
        char op;
    };

    class Response
    {
    public:
        Response() {}
        ~Response() {}
    public:
        int _result;
        int _code;//0表示计算成功,剩余的数字就是各种非法操作的错误码
    };
}

服务端和客户端都需要序列化和反序列化,服务端发送服务,客户端得到后处理,然后再给出响应,服务端收到,所以两方都会发结构体和得到字符串。

#pragma once

#include <iostream>
#include <string>

//给网络版本计算机定制协议
namespace protocol_ns
{
    class Request
    {
    public:
        Request() {}//为无参构造而准备的,这样就是一个无参一个有参
        Request(int x, int y, char op) 
        : _x(x), _y(y), _op(op)
        {}

        bool Serialize(std::string* outstr)//序列化:结构体转字符串
        {
            
        }

        bool Deserialize(const std::string* instr)//反序列化:字符串转结构体
        {

        }
        ~Request() {}
    public:
        int _x;
        int _y;
        char _op;
    };

    class Response
    {
    public:
        Response() {}
        Response(int result, int code)
        : _result(result), _code(code)
        {}
        ~Response() {}
    public:
        int _result;
        int _code;//0表示计算成功,剩余的数字就是各种非法操作的错误码
    };
}

成员变量对应这个计算器要用的变量。写一下服务端使用这个头文件的逻辑。

    void ServiceIO(int sock, const std::string& ip, const uint16_t& port)
    {
        //1. 读取数据
        std::string str;
        //2. 假设已经拿到一个完整的报文
        Request req;
        req.Deserialize(str);//反序列化
        //3. 直接提取用户的请求数据
        x = req._x;
    }

逻辑就是这样,再看序列化和反序列化。我们定义字符串应当是这样的:_x op _y,两个操作数,一个计算符,中间有两个空格。不过分隔符可以不是空格。

        bool Serialize(std::string* outstr)//序列化:结构体转字符串
        {
            *outstr = "";
            std::string x_string = std::to_string(_x);
            std::string y_string = std::to_string(_y);
            //手动序列化
            *outstr = x_string + SEP + _op + SEP + y_string;
            return true;
        }

        bool Deserialize(const std::string* instr)//反序列化:字符串转结构体
        {
            std::vector<std::string> result;
            StringSplit(instr, SEP, &result);
            _x = toInt(str[0]);
            op = str[1][0];//因为是字符,所以只要一个符号即可
            _y = toInt(str[2]);
            return true;
        }

创建一个Util.hpp写自定义的函数

Util.hpp

#pragma once

#include <iostream>
#include <string>
#include <vector>
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());
    }
};

在Protocol.hpp中

        bool Deserialize(const std::string& instr)//反序列化:字符串转结构体
        {
            std::vector<std::string> result;
            Util::StringSplit(instr, SEP, &result);
            if(result.size() != 3) return false;
            _x = Util::toInt(result[0]);
            _y = Util::toInt(result[2]);
            if(result[1].size() == 1) return false;//协议规定
            _op = result[1][0];//因为是字符,所以只要一个符号即可
            return true;
        }

这样的Request类里序列化和反序列都做完了,再写Response的。

        bool Serialize(std::string* outstr)
        {
            *outstr = "";
            std::string res_string = std::to_string(_result);
            std::string code_string = std::to_string(_code);
            //手动序列化
            *outstr = res_string + SEP + code_string;
            return true;
        }

        bool Deserialize(const std::string& instr)
        {
            std::vector<std::string> result;
            Util::StringSplit(instr, SEP, &result);
            if(result.size() != 2) return false;
            _result = Util::toInt(result[0]);
            _code = Util::toInt(result[1]);
            return true;
        }

回到TcpServer.hpp部分,我们更处理反序列化后的数据,用一个回调函数。

#pragma once

#include <iostream>
#include <pthread.h>
#include <functional>//头文件
#include "Sock.hpp"
#include "Protocol.hpp"

namespace tcpserver_ns
{
    using namespace protocol_ns;
    class TcpServer;
    using func_t = std::function<Response(const Request&)>;//所有类的前面加上一个函数的初始化,返回值是Response这个函数的结果,参数类型是const Request&
    //......
    TcpServer(func_t func, uint16_t port) : _port(port), _func(func)
    {}
    //......
private:
    uint16_t _port;
    Sock _listensock;
    func_t _func;

处理数据的过程放在外面的文件,也就是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 = 8888;
    std::unique_ptr<TcpServer> tsvr(new TcpServer(calculate, port));
    tsvr->InitServer();
    tsvr->Start();
    return 0;
} 

回到服务端文件TcpServer.cc中

        void ServiceIO(int sock, const std::string &ip, const uint16_t &port)
        {
            // 1. 读取数据
            std::string str;

            // 2. 假设已经拿到一个完整的报文
            Request req;
            req.Deserialize(str); //对读到的request字符串反序列化

            // 3. 直接提取用户的请求数据
            Response resp = _func(req);//业务逻辑
            //_func调用了外部函数,既然是回调,就会把结果给返回回来,给到resp

            //4. 给客户端返回响应,对计算完成的response结构序列化
            std::string send_string;
            resp.Serialize(&send_string);
            
            //5. 发送到网络
        }

还有其它部分需要改动

    class ThreadData
    {
    public:
        ThreadData(int sock, std::string ip, uint16_t port, TcpServer *tsvrp)
        : _sock(sock), _tsvrp(tsvrp), _ip(ip), _port(port)
        {}
        ~ThreadData() {}

    public:
        int _sock;
        std::string _ip;
        uint16_t _port;
        TcpServer* _tsvrp;
    };
    //...
    static void *ThreadRoutine(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        // 强转成ThreadData类型就可以使用类里的函数来完成任务了
        td->_tsvrp->ServiceIO(td->_sock, td->_ip, td->_port);
        delete td;
        return nullptr;
    }

现在还有两个问题,一个是如何读取数据,一个是如何发送到网络。

2、从网络中读取数据以及发送到网络中

我们当然可以用read系统调用,放到一个buffer数组中,然后接下来进行反序列化,业务处理,序列化,发送到网络中,但前提得是这个数据能够做反序列化。

用户层,也就是最上层应用层,发送一个数据,也就是用write时,是把数据放到了网络层TCP的发送缓冲区里,而一端read则是从TCP的接收缓冲区里读数据,数据write到发送缓冲区后,如何发送,何时发送,由TCP决定,所以TCP是传输控制协议。read和write本质是拷贝到对应缓冲区,TCP发送数据是从自己的发送缓冲区到对方的接收缓冲区,本质也是数据的拷贝。一端可以同时接收数据和发送数据,所以TCP协议是全双工的。TCP的工作很多,缓冲区有很多请求和数据,如果不读,那么就会一直积压在那里,那么某一次去读时,就不能保证读到的就是用户所需要的,所以光靠这个是无法读到正确数据的,所以保证这个工作放在了协议那里,由协议来管理数据,让用户能拿到正确的数据。

所以Protocol.hpp需要改。 假设报文是这样的: “7”\r\n"10 + 20"\r\n,这就相当于报头 + 有效载荷,所以请求/响应 = 报头\r\n有效载荷\r\n,只是请求和响应的有效载荷不同。

//给网络版本计算机定制协议
namespace protocol_ns
{
    #define SEP " "
    #define SEP_LEN strlen(SEP)//不能用sizeof
    #define HEADER_SEP "\r\n"
    #define HEADER_SEP_LEN strlen("\r\n")
    //假设报文是这样的: "7"\r\n"10 + 20"\r\n,这就相当于报头 + 有效载荷
    //请求/响应 = 报头\r\n有效载荷\r\n,只是请求和响应的有效载荷不同

    std::string AddHeader(const std::string& str)
    {
        std::string s = std::to_string(str.size());
        s += HEADER_SEP;
        s += str;
        s += HEADER_SEP;
        return s;
    }

报头添加完成了,但读取方如何确定读取的是一个正确的,按照协议规定的数据?我们先写好一个逻辑,之后还会改

        void ServiceIO(int sock, const std::string &ip, const uint16_t &port)
        {
            // 1. 正确地读取字符串报文
            std::string inbuffer;
            std::string package;
            if(ReadPackage(sock, inbuffer, &package))
            {
                ;
            }
            
            // 2. 拿到有效载荷
            package = RemoveHeader(package);

            // 3. 拿到一个完整的报文
            Request req;
            req.Deserialize(package); //对读到的request字符串反序列化

            // 4. 直接提取用户的请求数据
            Response resp = _func(req);//业务逻辑
            //_func调用了外部函数,既然是回调,就会把结果给返回回来,给到resp

            // 5. 给客户端返回响应,对计算完成的response结构序列化
            std::string send_string;
            resp.Serialize(&send_string);

            // 6. 发送到网络
        }

写读取报文的函数

    int ReadPackage(int sock, std::string& inbuffer, std::string* package)
    {
        //读取 ———— 字符串"7"\r\n"10 + 20"\r\n
        char buffer[1024];
        ssize_t s = recv(sock, buffer, sizeof(buffer - 1), 0);
        if(s <= 0) return -1;
        buffer[s] = 0;
        inbuffer += buffer;//此时inbuffer里就有了这样的字符串: "7"\r\n"10 + 20"\r\n
        //分析
        auto pos = inbuffer.find(HEADER_SEP);
        if(pos == std::string::npos) return 0;//没找到\r\n那么就不是正确的字符串,不动inbuffer里的内容,退出
        std::string lenStr = inbuffer.substr(0, pos);//获取头部字符串
        int len = Util::toInt(lenStr);//得到了长度7,也就是有效载荷长度
        int targetPackagelen = lenStr.size() + len + 2 * HEADER_SEP_LEN;//接收到的有报文的字符串长度就是这个
        if(inbuffer.size() < targetPackagelen) return 0;
        *package = inbuffer.substr(0, targetPackagelen);//package保存了"7"\r\n"10 + 20"\r\n,去掉其它符号的工作交给RemoveHeader
        inbuffer.erase(0, targetPackagelen);//只有到这里才改变inbuffer里的内容
        return len;//len就是有效载荷的长度
    }

那么TcpServer.hpp那边

        void ServiceIO(int sock, const std::string &ip, const uint16_t &port)
        {
            std::string inbuffer;
            while (true)
            {
                // 1. 正确地读取字符串报文
                std::string package;
                int n = ReadPackage(sock, inbuffer, &package);
                if (n == -1) break;
                else if (n == 0) continue;
                else
                {
                    // 2. 已有字符串"7"\r\n"10 + 20"\r\n,希望得到有效载荷"10 + 20"
                    package = RemoveHeader(package, n);


                    // 3. 拿到一个完整的报文
                    Request req;
                    req.Deserialize(package); // 对读到的request字符串反序列化

                    // 4. 直接提取用户的请求数据
                    Response resp = _func(req); // 业务逻辑
                    //_func调用了外部函数,既然是回调,就会把结果给返回回来,给到resp

                    // 5. 给客户端返回响应,对计算完成的response结构序列化
                    std::string send_string;
                    resp.Serialize(&send_string);

                    // 6. 发送到网络
                }
            }
            close(sock);
        }

接下来写去除报头的函数

    std::string RemoveHeader(const std::string& str, int len)
    {
        std::string res = str.substr(str.size() - HEADER_SEP_LEN - len, len);
        return res;
    }

那么服务端那里package = RemoveHeader(package, n)就得到了有效载荷,然后进行反序列化,业务处理,序列化,再发送到网络。

        void ServiceIO(int sock, const std::string &ip, const uint16_t &port)
        {
            std::string inbuffer;
            while (true)
            {
                // 1. 正确地读取字符串报文
                std::string package;
                int n = ReadPackage(sock, inbuffer, &package);
                if (n == -1) break;
                else if (n == 0) continue;
                else
                {
                    // 2. 已有字符串"7"\r\n"10 + 20"\r\n,希望得到有效载荷"10 + 20"
                    package = RemoveHeader(package, n);

                    // 3. 拿到一个完整的报文
                    Request req;
                    req.Deserialize(package); // 对读到的request字符串反序列化

                    // 4. 直接提取用户的请求数据
                    Response resp = _func(req); // 业务逻辑
                    //_func调用了外部函数,既然是回调,就会把结果给返回回来,给到resp

                    // 5. 给客户端返回响应,对计算完成的response结构序列化
                    std::string send_string;
                    resp.Serialize(&send_string);

                    // 6. 发送到网络
                    send_string = AddHeader(send_string);
                    send(sock, send_string.c_str(), send_string.size(), 0);
                }
            }
            close(sock);
        }

3、客户端

客户端有两个文件,TcpClient.hpp和CalculatorClient.cc两个文件。为了方便,Sock.hpp和TcpServer.hpp这样更改

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

    ~Sock()
    {}
        ~TcpServer()
        {
            _listensock.Close();
        }

    private:
        uint16_t _port;
        Sock _listensock;
        func_t _func;

CalculatorClient.cc这样写

#include "TcpClient.hpp"
#include "Sock.hpp"
#include <string>
#include "Protocol.hpp"
using namespace protocol_ns;

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

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    std::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;
    std::string buffer;
    while(true)
    {
        Request req;//用户输入,填充req的三个成员变量
        //1. 序列化
        std::string sendString;
        req.Serialize(&sendString);
        //2. 添加报头
        sendString = AddHeader(sendString);
        //3. 发送数据
        send(sock.Fd(), sendString.c_str(), sendString.size(), 0);
        //4. 获取响应
        std::string package;
        int n = 0;
START:
        n = ReadPackage(sock.Fd(), buffer, &package);
        if (n == 0) goto START;
        else if (n < 0) break;
        // 5. 去掉报头
        package = RemoveHeader(package, n);
        // 6. 反序列化
        Response resp;
        resp.Deserialize(package);
        std::cout << "result: " << resp._result << "[code: " << resp._code << "]" << std::endl;
    }
    sock.Close();
    return 0;
} 

现在就可以正常运行了,不过还剩一个输入内容的问题没解决,我们定义一个函数,返回结果给req,返回类型就是Request。

#include "TcpClient.hpp"
#include "Sock.hpp"
#include <string>
#include "Protocol.hpp"
using namespace protocol_ns;

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

enum
{
    Left,
    Oper,
    Right
};

Request ParseLine(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;
    std::cout << "left: " << left << std::endl;
    std::cout << "right: " << right << std::endl;
    std::cout << "op: " << op << std::endl;
    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);
    }
    std::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;
    std::string buffer;
    while(true)
    {
        std::cout << "Enter# " << std::endl;//希望用户这样输入就行:1+1, 1*1
        std::string line;
        std::getline(std::cin, line);

        Request req = ParseLine(line);//用户输入,填充req的三个成员变量
        std::cout << "test: " << req._x << req._op << req._y << std::endl;
        //1. 序列化
        std::string sendString;
        req.Serialize(&sendString);
        //2. 添加报头
        sendString = AddHeader(sendString);
        //3. 发送数据
        send(sock.Fd(), sendString.c_str(), sendString.size(), 0);
        //4. 获取响应
        std::string package;
        int n = 0;
START:
        n = ReadPackage(sock.Fd(), buffer, &package);
        if (n == 0) goto START;
        else if (n < 0) break;
        // 5. 去掉报头
        package = RemoveHeader(package, n);
        // 6. 反序列化
        Response resp;
        resp.Deserialize(package);
        std::cout << "result: " << resp._result << "[code: " << resp._code << "]" << std::endl;
    }
    sock.Close();
    return 0;
} 

这样整体就写完了

4、成熟的序列化反序列化方案

上面是我们在自己写序列化反序列化,是为了学习它的具体工作。实际应用中,有很多成熟的方案,通常都是用这些,而不是自己写。

protobuf,json,xml。这篇博客用json来写,json把结构化数据转化成kv的数据格式,这篇用一个比较简单的方式来使用json。

修改Protocol.hpp,用条件编译,如果定义了一个宏,那就用自己的方法,没有就用别的方法。不过在使用json之前,需要先安装

yum install -y jsoncpp-devel

安装好后,ls /usr/include/jsoncpp/命令就会查看到json这个目录。头文件是jsoncpp/json/json.h,这是一条路径,json目录我们只需要json.h头文件就可以。详细解析在代码中。

//#define MYSELF 1

//Request
        bool Serialize(std::string* outstr)//序列化:结构体转字符串
        {
            *outstr = "";
#ifdef MYSELF
            std::string x_string = std::to_string(_x);
            std::string y_string = std::to_string(_y);
            // 手动序列化
            *outstr = x_string + SEP + _op + SEP + y_string;
#else
            Json::Value root;//Value是一个万能对象,接受任何一个kv类型
            root["x"] = _x;
            root["y"] = _y;//所有放进去的会自动转为string类型
            root["op"] = _op;
            Json::FastWriter writer;//FastWriter用来序列化,把结构化的数据转为字符串类型
            *outstr = writer.write(root);
#endif
            return true;
        }

        bool Deserialize(const std::string& instr)//反序列化:字符串转结构体
        {
#ifdef MYSELF
            std::vector<std::string> result;
            Util::StringSplit(instr, SEP, &result);
            if (result.size() != 3)
                return false;
            _x = Util::toInt(result[0]);
            _y = Util::toInt(result[2]);
            if (result[1].size() == 1)
                return false;   // 协议规定
            _op = result[1][0]; // 因为是字符,所以只要一个符号即可
            std::cout << "_x: \n"
                      << _x << "_y: \n"
                      << _y << "_op: " << _op << std::endl;
#else
            Json::Value root;
            Json::Reader reader;//Reader用来反序列化
            reader.parse(instr, root);
            _x = root["x"].asInt();//拿到的是字符串,要转成int类型
            _y = root["y"].asInt();
            //_op虽然是char,但它在计算机里就是整数,序列化时放进root的就是整数类型,反序列化时转成int类型,然后编译器会根据char类型自动解释成char类型
            _op = root["op"].asInt();
#endif
            return true;
        }



//Response
        bool Serialize(std::string* outstr)
        {
            *outstr = "";
#ifdef MYSELF
            std::string res_string = std::to_string(_result);
            std::string code_string = std::to_string(_code);
            // 手动序列化
            *outstr = res_string + SEP + code_string;
#else
            Json::Value root;
            root["result"] = _result;
            root["code"] = _code;
            Json::FastWriter writer;
            *outstr = writer.write(root);
#endif
            return true;
        }

        bool Deserialize(const std::string& instr)
        {
#ifdef MYSELF
            std::vector<std::string> result;
            Util::StringSplit(instr, SEP, &result);
            if (result.size() != 2)
                return false;
            _result = Util::toInt(result[0]);
            _code = Util::toInt(result[1]);
            std::cout << "_result: \n"
                      << _result << "_code: " << _code << std::endl;
#else
            Json::Value root;
            Json::Reader reader;
            reader.parse(instr, root);
            _result = root["result"].asInt();
            _code = root["code"].asInt();
#endif
            return true;
        }

因为用了第三方库,所以makefile要加上一句

.PHONY:all
all:calserver calclient

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

.PHONY:clean
clean:
	rm -f calclient calserver

如果想用自己的方案,就把定义的宏解开注释,就可以用了。序列化中把FastWriter改成StyledWriter,打印出来的格式不一样,写的代码还是一样。

3、其它

这篇的重点就是协议,序列化和反序列化,协议就是Prorocol.hpp的Request和Response,里面定义了成员变量,如果不是按照要求的数据,就会退出返回,不处理;序列化和反序列化就是这两个类里的两个接口,根据协议规定来做这方面工作。

协议不只有一个,还可以定多个协议放在一起,符合哪个协议就去找哪个协议对应的方法,比如在报文前加上协议号,多创建几个类去实现方法。

结束。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值