Custom protocols, serialization and deserialization

When writing TCP and UDP programs, we naturally use the read function to obtain data. For UDP, it provides connectionless transmission in the form of datagrams, and for TCP, it is data-oriented. Streaming, in the previous program we only performed the reading operation, but did not analyze the read content. If we want to transmit some structured data, then we need to introduce the concept of “protocol”.

Online Calculator

In this article, a server version of the adder will be implemented. The client needs to send the two addends to be calculated, and then the server will perform the calculation, and finally return the result to the client.

Provisions of the agreement

We agree on information here so that the agreement can be better implemented:

  • The client sends a string in the form “1 + 1”;
  • There are two operands in this string, both of which are integers;
  • There will be a character between the two numbers that is an operator, and the operator can only be “±*/%”;
  • There are no spaces between numbers and operators;

Serialization and Deserialization

  • Define a structure to represent the information we need to interact with;
  • When sending data, convert this structure into a string according to a rule, and when receiving data, convert the string back into a structure according to the same rule;
  • At the same time, serialization and deserialization of the protocol are performed by using the Jsoncpp library.

TcpServer.hpp

namespace tcpserver_ns{<!-- -->
    using namespace protocol_ns; // Workspace using custom protocol
    class TcpServer;
    using func_t = std::function<Response (const Request & amp;)>;
    class ThreadData{<!-- -->
    public:
        ThreadData(int sock, std::string ip, uint16_t port, TcpServer *tsvrp)
            : _sock(sock) , _ip(ip) ,_port(port), _tsvrp(tsvrp)
        {<!-- -->}
        ~ThreadData()
        {<!-- -->}
    public:
        int _sock;
        std::string _ip;
        uint16_t _port;
        TcpServer* _tsvrp;
    };
    class TcpServer{<!-- -->
    public:
        TcpServer(func_t func, uint16_t port) : _func(func), _port(port){<!-- -->}
        void InitServer(){<!-- --> // Initialize related socket information
            _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());
            logMessage(Debug, "thread running ...");
            ThreadData* td = static_cast<ThreadData*>(args);
            td->_tsvrp->ServiceIO(td->_sock, td->_ip, td->_port); // Call ServiceIO to read and write data
            logMessage(Debug, "thread quit, client quit ... ");
            delete td;
            return nullptr;
        }
        void Start(){<!-- --> // Accept to establish connection
            for(;;){<!-- -->
                std::string clientip;
                uint16_t clientport;
                int sock = _listensock.Accept( & amp;clientip, & amp;clientport);
                if (sock < 0) continue;
                logMessage(Debug, "get a new client, client info : [%s:%d]", clientip.c_str(), clientport);
                pthread_t tid;
                ThreadData *td = new ThreadData(sock, clientip, clientport, this); //Build thread data information
                pthread_create( & amp;tid, nullptr, ThreadRoutine, td); // Create a thread to run ThreadRoutine
            }

        }
        // This function is called by multiple threads
        // Here, if we directly use the read function used in the previous article to read, there is no guarantee that the data obtained is the calculation-related information we want.
        // Therefore, we need to process the data ourselves according to the custom protocol at this time--here we stipulate that the complete message we need each time is"7"\r\\
"10 + 20" \r\\
 In this form, the preceding number indicates the length of the valid message, and the message length and payload are separated by "\r\\
"
        void ServiceIO(int sock, const std::string & amp;ip, const uint16_t & amp;port){<!-- -->
            std::string inbuffer; //Put it outside to prevent it from being released every time the loop
            while(true){<!-- -->
                // 0. How to ensure that what is read is a complete string message? "7"\r\\
""10 + 20"\r\\

                std::string package;
                int n = ReadPackage(sock, inbuffer, & amp;package); // Process the obtained data stream and separate the messages required by the protocol
                if (n == -1)
                    break;
                else if (n == 0)
                    continue;
                else{<!-- -->
                    // Must have gotten a "7"\r\\
""10 + 20"\r\\

                    // 1. All you need is the payload "10 + 20"
                    package = RemoveHeader(package, n); // Separate the payload of the message and obtain the payload
                    // decode
                    // 2. Assume that a complete string "10 + 20" has been read
                    Request req;
                    req.Deserialize(package); // Deserialize the read request string
                    // 3. Directly extract the user's request data
                    Response resp = _func(req); //Business logic! Processing of response computing operations
                    // 4. Return response to user - serialization
                    std::string send_string;
                    resp.Serialize( & amp;send_string); // Serialize the calculated response structure to form a sendable string
                    // 5. Add header
                    send_string = AddHeader(send_string); // Add a header to the result that needs to be returned
                    //encode
                    // 6. Send to network -- weakened
                    send(sock, send_string.c_str(), send_string.size(), 0); // Simple version of sending
                }
            }
            close(sock);
        }
        ~TcpServer(){<!-- -->
            _listensock.Close();
        }
    private:
        uint16_t _port;
        Sock _listensock;
        func_t _func;
    };
}

Protocol.hpp

Custom protocol class and Jsoncpp library use:

#include <jsoncpp/json/json.h>
#include "Util.hpp"
#defineMYSELF 1
namespace protocol_ns{<!-- -->
#define SEP " "
#define SEP_LEN strlen(SEP)
#define HEADER_SEP "\r\\
"
#define HEADER_SEP_LEN strlen(HEADER_SEP)

    // "length"\r\\
""_x _op _y"\r\\

    // "10 + 20" => "7"\r\\
""10 + 20"\r\\
 => Header + Payload
    // Request/Response = Headers\r\\
Payload\r\\

    std::string AddHeader(const std::string & amp;str){<!-- --> // Add header
        std::cout << "AddHeader before:\\
" << str << std::endl;
        std::string s = std::to_string(str.size());
        s + = HEADER_SEP;
        s + = str;
        s + = HEADER_SEP;
        std::cout << "After AddHeader:\\
" << s << std::endl;
        return s;
    }

    // "7"\r\\
""10 + 20"\r\\
 => "10 + 20"
    std::string RemoveHeader(const std::string & amp;str, const int & amp;len){<!-- --> // Remove header
        std::cout << "RemoveHeader before:\\
" << str << std::endl;
        std::string res = str.substr(str.size() - HEADER_SEP_LEN - len, len);
        std::cout << "AfterRemoveHeader:\\
" << res << std::endl;
        return res;
    }
    int ReadPackage(int sock, std::string & amp;inbuffer, std::string *package){<!-- --> // Correctly read the required packets
        std::cout << "ReadPackage inbuffer before:\\
" << inbuffer << std::endl;
        // Read while reading
        char buffer[1024];
        ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if (s <= 0)
            return -1; // Reading error
        buffer[s] = 0;
        inbuffer + = buffer;
        std::cout << "ReadPackage inbuffer among:\\
" << inbuffer << std::endl;
        // side analysis
        auto pos = inbuffer.find(HEADER_SEP);
        if (pos == std::string::npos)
            return 0; // Incomplete reading
        std::string lenStr = inbuffer.substr(0, pos); // Obtained the header string
        int len = Util::toInt(lenStr); // "123" -> 123
        int targetpackageLen = lenStr.size() + len + 2 * HEADER_SEP_LEN; // Target string length with header added
        if (inbuffer.size() < targetpackageLen)
            return 0; // Incomplete reading
        *package = inbuffer.substr(0, targetpackageLen); // Extract the message
        // So far, nothing has been touched in the inbuffer.
        inbuffer.erase(0, targetpackageLen); // Remove the entire packet directly from the inbuffer
        std::cout << "ReadPackage inbuffer after:\\
" << inbuffer << std::endl;
        return len; // read successfully
    }
    // Request & amp; & amp; Response must provide serialization and deserialization functions
    class Request{<!-- -->
    public:
        Request()
        {<!-- -->}
        Request(int x, int y, char op) : _x(x), _y(y), _op(op)
        {<!-- -->}
        // Currently "_x _op _y"
        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;
            std::cout << "Request Serialize:\\
" << *outStr << std::endl;
#else
            Json::Value root; // Value: a universal object that accepts any kv object
            root["x"] = _x;
            root["y"] = _y;
            root["op"] = _op; // Fill in the fields
            // Json::FastWriter writer; // Writer: used for serialization struct -> string
            Json::StyledWriter writer; // Convert Json into a better-looking string
            // {<!-- -->
            // "op" : 43,
            // "x" : 1,
            // "y" : 1
            // }
            *outStr = writer.write(root);
#endif
            return true;
        }
        bool Deserialize(const std::string & amp;inStr){<!-- -->
#ifdef MYSELF
            // inStr : 10 + 20 => [0]=>10, [1]=> + , [2]=>20
            // string -> vector
            std::vector<std::string> result;
            Util::StringSplit(inStr, SEP, & amp;result);
            if (result.size() != 3)
                return false;
            if (result[1].size() != 1)
                return false;
            _x = Util::toInt(result[0]);
            _y = Util::toInt(result[2]);
            _op = result[1][0];
#else
            Json::Value root;
            Json::Reader reader; // Reader: used for deserialization
            reader.parse(inStr, root);
            _x = root["x"].asInt();
            _y = root["y"].asInt();
            _op = root["op"].asInt();
#endif
            return true;
        }
        ~Request()
        {<!-- -->}
    public:
        // _x + _op + _y
        int _x;
        int _y;
        char_op;
    };

    class Response{<!-- -->
    public:
        Response()
        {<!-- -->}
        Response(int result, int code) : _result(result), _code(code)
        {<!-- -->}
        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;
            std::cout << "Response Serialize:\\
" << *outStr << std::endl;
#else
            Json::Value root;
            root["result"] = _result;
            root["code"] = _code;
            // Json::FastWriter writer;
            Json::StyledWriter writer;
            *outStr = writer.write(root);
#endif
            return true;
        }

        bool Deserialize(const std::string & amp;inStr){<!-- -->
#ifdef MYSELF
            std::vector<std::string> result;
            Util::StringSplit(inStr, SEP, & amp;result);
            if (result.size() != 2)
                return false;
            _result = Util::toInt(result[0]);
            _code = Util::toInt(result[1]);
#else
            Json::Value root;
            Json::Reader reader; // Reader: used for deserialization
            reader.parse(inStr, root);
            _result = root["result"].asInt();
            _code = root["code"].asInt();
#endif
            return true;
        }
        ~Response()
        {<!-- -->}
    public:
        int _result;
        int _code; // 0->success 1,2,3,4 represent different errors
    };
}

// Implementation of Util.hpp tool class
class Util
{<!-- -->
public:
    // Input: const & amp;
    // Output: *
    // Input and output: & amp;
    static bool StringSplit(const string & amp;str, const string & amp;sep, vector<string> *result){<!-- -->
        size_t start = 0;
        // + 20
        // "abcd efg" -> for(int i = 0; i < 10; i + + ) != for(int i = 0; i <= 9; i + + )
        while (start < str.size()){<!-- -->
            auto pos = str.find(sep, start);
            if (pos == string::npos) break;
            result->push_back(str.substr(start, pos-start));
            //Reload of position
            start = pos + sep.size();
        }
        if(start < str.size()) result->push_back(str.substr(start));
        return true;
    }
    static int toInt(const std::string & amp;s){<!-- -->
        return atoi(s.c_str());
    }

CalculatorServer.cc

Response calculate(const Request & amp;req){<!-- --> // Main calculation processing module
    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 = 8081;
    std::unique_ptr<TcpServer> tsvr(new TcpServer(calculate, port)); // TODO
    tsvr->InitServer();
    tsvr->Start();
    return 0;
}

CalculatorClient.cc

using namespace protocol_ns;
static void usage(std::string proc){<!-- -->
    std::cout << "Usage:\\
\t" << proc << " serverip serverport\\
"
              << std::endl;
}
enum{<!-- -->
    LEFT,
    OPER,
    RIGHT
};
// 10 + 20
Request ParseLine(const std::string & amp;line){<!-- --> // Split the input data through the ParseLine function and add it to the variables required by the request
    std::string left, right;
    char op;
    int status = LEFT;
    int i = 0;
    while(i < line.size()){<!-- -->
        // if(isdigit(e)) left.push_back;

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

// ./tcpclient serverip serverport
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# "; // 1 + 1,2*9 // The goal here is to construct the input data into the previous form
        std::string line;
        std::getline(std::cin, line);
        Request req = ParseLine(line);
        std::cout << "test: " << req._x << req._op << req._y << std::endl;
        // 1. Serialization
        std::string sendString;
        req.Serialize( & amp;sendString);
        // 2. Add header
        sendString = AddHeader(sendString);
        // 3. send
        send(sock.Fd(), sendString.c_str(), sendString.size(), 0);
        // 4. Get response
        std::string package;
        int n = 0;
    START:
        n = ReadPackage(sock.Fd(), buffer, & amp;package);
        if(n==0)
            goto START;
        else if (n < 0)
            break;
        else
        {<!-- -->}
        // 5. Remove the header
        package = RemoveHeader(package, n);
        // 6. Deserialization
        Response resp;
        resp.Deserialize(package);
        std::cout << "result: " << resp._result << "[code: " << resp._code << "]" << std::endl;
    }
    sock.Close();
    return 0;
}