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