• 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    1、套接字

    sock.hpp直接写

    #pragma once
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93

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

    CalculatorServer.cc

    #include "TcpServer.hpp"
    #include 
    
    int main()
    {
        uint16_t port = 8888;
        std::unique_ptr tsvr(new TcpServer(port));
        tsvr->InitServer();
        tsvr->Start();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    TcpServer.hpp

    #pragma once
    
    #include 
    #include 
    #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(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;  
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

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

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

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

    #pragma once
    
    #include 
    #include 
    
    //给网络版本计算机定制协议
    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表示计算成功,剩余的数字就是各种非法操作的错误码
        };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

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

    #pragma once
    
    #include 
    #include 
    
    //给网络版本计算机定制协议
    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表示计算成功,剩余的数字就是各种非法操作的错误码
        };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

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

        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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    逻辑就是这样,再看序列化和反序列化。我们定义字符串应当是这样的:_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 result;
                StringSplit(instr, SEP, &result);
                _x = toInt(str[0]);
                op = str[1][0];//因为是字符,所以只要一个符号即可
                _y = toInt(str[2]);
                return true;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

    Util.hpp

    #pragma once
    
    #include 
    #include 
    #include 
    using namespace std;
    
    class Util
    {
    public:
        static bool StringSplit(const string& str, const string& sep, vector* 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());
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在Protocol.hpp中

            bool Deserialize(const std::string& instr)//反序列化:字符串转结构体
            {
                std::vector 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;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这样的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 result;
                Util::StringSplit(instr, SEP, &result);
                if(result.size() != 2) return false;
                _result = Util::toInt(result[0]);
                _code = Util::toInt(result[1]);
                return true;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

    #pragma once
    
    #include 
    #include 
    #include //头文件
    #include "Sock.hpp"
    #include "Protocol.hpp"
    
    namespace tcpserver_ns
    {
        using namespace protocol_ns;
        class TcpServer;
        using func_t = std::function(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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    处理数据的过程放在外面的文件,也就是CalculatorServer.cc中

    #include "TcpServer.hpp"
    #include 
    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 tsvr(new TcpServer(calculate, port));
        tsvr->InitServer();
        tsvr->Start();
        return 0;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    回到服务端文件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. 发送到网络
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    还有其它部分需要改动

        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*>(args);
            // 强转成ThreadData类型就可以使用类里的函数来完成任务了
            td->_tsvrp->ServiceIO(td->_sock, td->_ip, td->_port);
            delete td;
            return nullptr;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

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

    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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

            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. 发送到网络
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    写读取报文的函数

        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就是有效载荷的长度
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    那么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);
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    接下来写去除报头的函数

        std::string RemoveHeader(const std::string& str, int len)
        {
            std::string res = str.substr(str.size() - HEADER_SEP_LEN - len, len);
            return res;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    那么服务端那里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);
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    3、客户端

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

        void Close()
        {
            if(_sock != defaultfd) close(_sock);
        }
    
        ~Sock()
        {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
            ~TcpServer()
            {
                _listensock.Close();
            }
    
        private:
            uint16_t _port;
            Sock _listensock;
            func_t _func;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    CalculatorClient.cc这样写

    #include "TcpClient.hpp"
    #include "Sock.hpp"
    #include 
    #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;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

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

    #include "TcpClient.hpp"
    #include "Sock.hpp"
    #include 
    #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;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97

    这样整体就写完了

    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 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 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;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90

    因为用了第三方库,所以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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

    3、其它

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

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

    结束。

  • 相关阅读:
    工程(十四)——ubuntu20.04 PL-VINS
    tcp字节传输(java)-自定义包头和数据识别
    Day 7.UDP编程、不同主机之间用网络进行通信
    【云原生】设备云之FlexManager历史数据的运用
    计算机操作系统-第十一天
    传奇服务端如何添加地图
    背靠TON公链的Notcoin游戏项目,能否杀出GameFi的红海?
    git配置
    【OBS】stream-labs-desktop 编译运行采坑全攻略
    mysql表的增删改查(进阶)
  • 原文地址:https://blog.csdn.net/kongqizyd146/article/details/133558496