一、再谈协议
在前面博客谈网络的时候,我们认为协议就是一种约定,在用socket api接口的时候,都是按照“字符串”的方式来进行发送和接受的,但是如果我们要传输一些结构化的数据,又该怎么办?
这就要我们将数据序列化进行传输或者接收。
在网络通信中,传输结构化数据的常见方法有以下几种:
- 序列化: 将结构化数据转换为字符串或字节流进行传输。在发送端,可以使用一种序列化格式将数据转换为字符串或字节流;在接收端,再将接收到的字符串或字节流反序列化为相应的数据结构。常见的序列化格式包括 JSON(JavaScript Object Notation)、XML(eXtensible Markup Language)、Protocol Buffers(protobuf)、MessagePack 等。选择序列化格式时,需要考虑数据的大小、可读性、解析效率等因素。
- 数据格式协议: 使用一种规定的数据格式协议进行数据传输。这种方法通常需要双方预先约定好数据格式协议,并按照协议的规定进行数据的编码和解码。常见的数据格式协议包括 HTTP、FTP、SMTP 等。在 HTTP 协议中,可以使用 Content-Type 来指定数据的格式,如 application/json 表示 JSON 格式数据,application/xml 表示 XML 格式数据。
- 自定义协议: 自定义通信协议,定义数据的传输格式和规则。在自定义协议中,可以根据实际需求灵活地定义数据的结构和编码方式,以及通信过程中的规则和约定。自定义协议通常需要双方进行协商和实现,但可以更好地满足特定场景下的需求。
二、序列化和反序化
为了更好的理解,下面我们将通过自己定制tcp协议,将数据进行序列化和反序列化。为了方便叙述,我们简单的制定网络版本的计算 ,来理解协议中序列化和反序列化。
1、网络版本计算器的场景搭建
我们的构想是,客户端通过将计算请求数据序列化,发送到网络中,服务端接收后将数据进行反序列化进行计算处理。
处理过程大致如上图:
- c->s调用函数发送数据的本质就是将一个缓冲区的拷贝到另外一个缓冲区。
- s->c服务器回显数据的本质也是将s中缓冲区的数据拷贝到c中的缓冲区
- 所以所tcp是全双工的
2、服务器构建
这里构建的服务器和上篇在套接字中的是没有本质区别的
#pragma once #include <iostream> #include <string> #include <cstring> #include <cstdlib> #include <functional> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/wait.h> #include <signal.h> #include "log.hpp" #include "protocol.hpp" using namespace std; namespace server { enum { USAGE_ERR = 1, SOCKET_ERR, BIND_ERR, LISTEN_ERR }; static const uint16_t gport = 8080; static const int gbacklog = 5; // const Request &req: 输入型 // Response &resp: 输出型 typedef std::function<bool(const Request &req, Response &resp)> func_t; // 保证解耦 void handlerEntery(int sock, func_t func) { std::string inbuffer; while (true) { // 1. 读取:"content_len"\r\n"x op y"\r\n // 1.1 你怎么保证你读到的消息是 【一个】完整的请求 std::string req_text, req_str; // 1.2 我们保证,我们req_text里面一定是一个完整的请求:"content_len"\r\n"x op y"\r\n if (!recPackage(sock, inbuffer, &req_text)) return; std::cout << "带报头的请求:\n" << req_text << std::endl; if (!deLength(req_text, &req_str)) return; std::cout << "去掉报头的正文:\n" << req_str << std::endl; // 2. 对请求Request,反序列化 // 2.1 得到一个结构化的请求对象 Request req; if (!req.deserialize(req_str)) return; // 3. 计算机处理,req.x, req.op, req.y --- 业务逻辑 // 3.1 得到一个结构化的响应 Response resp; func(req, resp); // req的处理结果,全部放入到了resp, 回调是不是不回来了?不是! // 4.对响应Response,进行序列化 // 4.1 得到了一个"字符串" std::string resp_str; resp.serialize(&resp_str); std::cout << "计算完成, 序列化响应: " << resp_str << std::endl; // 5. 然后我们在发送响应 // 5.1 构建成为一个完整的报文 std::string send_string = enLength(resp_str); send(sock, send_string.c_str(), send_string.size(), 0); // 其实这里的发送也是有问题的,不过后面再说 } } class CalServer { public: CalServer(const uint16_t &port = gport) : _listensock(-1), _port(port) { } void initServer() { // 1. 创建socket文件套接字对象 _listensock = socket(AF_INET, SOCK_STREAM, 0); if (_listensock < 0) { logMessage(FATAL, "create socket error"); exit(SOCKET_ERR); } logMessage(NORMAL, "create socket success: %d", _listensock); // 2. bind绑定自己的网络信息 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(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0) { logMessage(FATAL, "bind socket error"); exit(BIND_ERR); } logMessage(NORMAL, "bind socket success"); // 3. 设置socket 为监听状态 if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑 { logMessage(FATAL, "listen socket error"); exit(LISTEN_ERR); } logMessage(NORMAL, "listen socket success"); } void start(func_t func) { for (;;) { // 4. server 获取新链接 // sock, 和client进行通信的fd struct sockaddr_in peer; socklen_t len = sizeof(peer); int sock = accept(_listensock, (struct sockaddr *)&peer, &len); if (sock < 0) { logMessage(ERROR, "accept error, next"); continue; } logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // ? // version 2 多进程版(2) pid_t id = fork(); if (id == 0) // child { close(_listensock); // if(fork()>0) exit(0); // serviceIO(sock); handlerEntery(sock, func); close(sock); exit(0); } close(sock); // father pid_t ret = waitpid(id, nullptr, 0); if (ret > 0) { logMessage(NORMAL, "wait child success"); // ? } } } ~CalServer() {} private: int _listensock; // 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的! uint16_t _port; }; }
对于 handlerEntery这个操作我们要分析,在这函数中首先我们要获取到由客户端发送过来的报文,而客户端发送过来的报文肯定是经过序列化的,所以我们要进行反序列化,在通过func回调函数得到一个结构化的响应,在将响应通过处理发送给客户端。
// 保证解耦 void handlerEntery(int sock, func_t func) { std::string inbuffer; while (true) { // 1. 读取:"content_len"\r\n"x op y"\r\n // 1.1 你怎么保证你读到的消息是 【一个】完整的请求 std::string req_text, req_str; // 1.2 我们保证,我们req_text里面一定是一个完整的请求:"content_len"\r\n"x op y"\r\n if (!recPackage(sock, inbuffer, &req_text)) return; std::cout << "带报头的请求:\n" << req_text << std::endl; if (!deLength(req_text, &req_str)) return; std::cout << "去掉报头的正文:\n" << req_str << std::endl; // 2. 对请求Request,反序列化 // 2.1 得到一个结构化的请求对象 Request req; if (!req.deserialize(req_str)) return; // 3. 计算机处理,req.x, req.op, req.y --- 业务逻辑 // 3.1 得到一个结构化的响应 Response resp; func(req, resp); // req的处理结果,全部放入到了resp, 回调是不是不回来了?不是! // 4.对响应Response,进行序列化 // 4.1 得到了一个"字符串" std::string resp_str; resp.serialize(&resp_str); std::cout << "计算完成, 序列化响应: " << resp_str << std::endl; // 5. 然后我们在发送响应 // 5.1 构建成为一个完整的报文 std::string send_string = enLength(resp_str); send(sock, send_string.c_str(), send_string.size(), 0); // 其实这里的发送也是有问题的,不过后面再说 } }
对于calServer.cc的主程序,就是简单进行cal计算业务
#include "calServer.hpp" #include <memory> using namespace server; using namespace std; static void Usage(string proc) { cout << "\nUsage:\n\t" << proc << " local_port\n\n"; } // req: 里面一定是我们的处理好的一个完整的请求对象 // resp: 根据req,进行业务处理,填充resp,不用管理任何读取和写入,序列化和反序列化等任何细节 bool cal(const Request &req, Response &resp) { // req已经有结构化完成的数据啦,你可以直接使用 resp._exitcode = OK; resp._result = OK; 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._exitcode = DIV_ZERO; else resp._result = req._x / req._y; } break; case '%': { if (req._y == 0) resp._exitcode = MOD_ZERO; else resp._result = req._x % req._y; } break; default: resp._exitcode = OP_ERROR; break; } return true; } // tcp服务器,启动上和udp server一模一样 // ./tcpserver local_port int main(int argc, char *argv[]) { if (argc != 2) { Usage(argv[0]); exit(USAGE_ERR); } uint16_t port = atoi(argv[1]); unique_ptr<CalServer> tsvr(new CalServer(port)); tsvr->initServer(); tsvr->start(cal); return 0; }
3、客户端构建
对于客户端calClinet.hpp完成的主要逻辑是进行连网,输入计算,对服务器进行计算请求,在进行计算信息的构建。
#pragma once #include <iostream> #include <string> #include <cstring> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include "protocol.hpp" #define NUM 1024 using namespace std; class CalClient { public: CalClient(const std::string &serverip, const uint16_t &serverport) : _sock(-1), _serverip(serverip), _serverport(serverport) { } void initClient() { // 1. 创建socket _sock = socket(AF_INET, SOCK_STREAM, 0); if (_sock < 0) { std::cerr << "socket create error" << std::endl; exit(2); } // 2. tcp的客户端要不要bind?要的! 要不要显示的bind?不要!这里尤其是client port要让OS自定随机指定! // 3. 要不要listen?不要! // 4. 要不要accept? 不要! // 5. 要什么呢??要发起链接! } void start() { 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()); if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0) { std::cerr << "socket connect error" << std::endl; } else { std::string line; std::string inbuffer; while (true) { std::cout << "mycal>>> "; // 输入计算 getline(cin, line); // 进行请求 Request req = ParseLine(line); string content; // 存放计算信息 req.serialize(&content); string send_string = enLength(content); // 添加报头 send(_sock, send_string.c_str(), send_string.size(), 0); string package, text; if (!recPackage(_sock, inbuffer, &package)) continue; if (!deLength(package, &text)) continue; // 响应 Response resp; resp.deserialize(text); std::cout << "exitCode: " << resp._exitcode << std::endl; std::cout << "result: " << resp._result << std::endl; } } } // 从文本中提取计算格式信息,然后用这些信息构建请求 Request ParseLine(const std::string &line) { //"1+1" "123*456" "12/0" int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后 int i = 0; int cnt = line.size(); string left, right; char op; while (i < cnt) { switch (status) { case 0: { if (!isdigit(line[i])) // isdigit检查字符是否是十进制 { op = line[i]; status = 1; } else left.push_back(line[i++]); } break; case 1: { i++; status = 2; } break; case 2: { right.push_back(line[i++]); } break; } } std::cout << std::stoi(left) << " " << std::stoi(right) << " " << op << std::endl; return Request(std::stoi(left), std::stoi(right), op); } ~CalClient() { if (_sock >= 0) close(_sock); } private: int _sock; std::string _serverip; uint16_t _serverport; };
calClient.cc
#include "calClient.hpp" #include <memory> using namespace std; static void Usage(string proc) { cout << "\nUsage:\n\t" << proc << " serverip serverport\n\n"; } // ./tcpclient serverip serverport int main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); exit(1); } string serverip = argv[1]; uint16_t serverport = atoi(argv[2]); unique_ptr<CalClient> tcli(new CalClient(serverip, serverport)); tcli->initClient(); tcli->start(); return 0; }
4、序列化和反序列化
4.1自定义序列化和反序列化
前面说了一大堆要对数据进行序列化和反序列化 ,那到底什么是序列化和反序列,其实本质就是将一堆字符串,整和成一个字符串。为了完成我们网络计算器,这里我们写了二个类,一个是 Request对数据进行请求,另外一个是Response,每个类中都要对序化和反序列进行设计
class Request { public: Request() : _x(0), _y(0), _op(0) { } Request(int x, int y, char op) : _x(x), _y(y), _op(op) { } // 1. 自己写 // 2. 用现成的 bool serialize(std::string *out) { } // "x op yyyy"; bool deserialize(const std::string &in) { } public: // "x op y" int _x; int _y; char _op; };
// 响应 class Response { public: Response() : _exitcode(0), _result(0) { } Response(int exitcode, int result) : _exitcode(exitcode), _result(result) { } bool serialize(std::string *out) { } bool deserialize(const std::string &in) { } public: int _exitcode; // 0:计算成功,!0表示计算失败,具体是多少,定好标准 int _result; // 计算结果 };
其中序列化和反序列化可以自己进行编写,也可以通过库进行完成。
在大部分场景中我们都是用json进行序列结构的,对于自定义序列化,每个实现方式可能不同,所以不重点分析了,下面会有完整代码,大家可以参考实现
4.2json实现序列化和反序列化
在Linux上使用json我们要进行库的安装
sudo yum install -y jsoncpp-devel
序列化
使用了 JsonCpp 库来创建一个 JSON 对象 root
,然后将一些值 _x
、_y
和 _op
分别存储到 JSON 对象的键 "first"
、"second"
和 "oper"
中。接下来,使用 Json::FastWriter
对象 writer
来将 JSON 对象 root
转换为字符串,并将结果写入到 out
指向的位置。
Json::Value root; root["first"] = _x; root["second"] = _y; root["oper"] = _op; Json::FastWriter writer; // Json::StyledWriter writer; *out = writer.write(root);
反序列化
使用了 JsonCpp 库来解析输入的 JSON 字符串 in
,并将解析得到的值存储到 Json::Value
类型的对象 root
中。然后,通过访问 root
对象的键 "first"
、"second"
和 "oper"
来获取相应的值,并将这些值转换为整数类型,并分别存储到 _x
、_y
和 _op
变量中。
Json::Value root; Json::Reader reader; reader.parse(in, root); _x = root["first"].asInt(); _y = root["second"].asInt(); _op = root["oper"].asInt();
注意:asInt()
函数用于将 JSON 值转换为整数类型
protocol.hpp 完整代码实现:添加报文,去报头,请求,响应获取数据包
#pragma once #include <iostream> #include <cstring> #include <string> #include <sys/types.h> #include <sys/socket.h> #include <jsoncpp/json/json.h> #define SEP " " #define SEP_LEN strlen(SEP) #define LINE_SEP "/r/n" // /r回车 /n换行 /r/n表示换行的同时将光标移动到行首 #define LINE_SEP_LEN strlen(LINE_SEP) using namespace std; // 错误枚举 enum { OK = 0, DIV_ZERO, MOD_ZERO, OP_ERROR }; // "x op y" -> "content_len"\r\n"x op y"\r\n // "exitcode result" -> "content_len"\r\n"exitcode result"\r\n // 添加报头 string enLength(string &text) { string send_string = to_string(text.size()); send_string += LINE_SEP; send_string += text; send_string += LINE_SEP; return send_string; } // 去报头 // "content_len"\r\n"exitcode result"\r\n bool deLength(const std::string &package, std::string *text) { auto pos = package.find(LINE_SEP); if (pos == string::npos) return false; string text_len_string = package.substr(0, pos); int text_len = stoi(text_len_string); *text = package.substr(pos + LINE_SEP_LEN, text_len); return true; } // 请求 class Request { public: Request() : _x(0), _y(0), _op(0) { } Request(int x, int y, char op) : _x(x), _y(y), _op(op) { } // 1. 自己写 // 2. 用现成的 bool serialize(std::string *out) { #ifdef MYSELF *out = ""; // 结构化 -> "x op y"; std::string x_string = std::to_string(_x); std::string y_string = std::to_string(_y); *out = x_string; *out += SEP; *out += _op; *out += SEP; *out += y_string; #else Json::Value root; root["first"] = _x; root["second"] = _y; root["oper"] = _op; Json::FastWriter writer; // Json::StyledWriter writer; *out = writer.write(root); #endif return true; } // "x op yyyy"; bool deserialize(const std::string &in) { #ifdef MYSELF // "x op y" -> 结构化 auto left = in.find(SEP); auto right = in.rfind(SEP); if (left == std::string::npos || right == std::string::npos) return false; if (left == right) return false; if (right - (left + SEP_LEN) != 1) return false; std::string x_string = in.substr(0, left); // [0, 2) [start, end) , start, end - start std::string y_string = in.substr(right + SEP_LEN); if (x_string.empty()) return false; if (y_string.empty()) return false; _x = std::stoi(x_string); _y = std::stoi(y_string); _op = in[left + SEP_LEN]; #else Json::Value root; Json::Reader reader; reader.parse(in, root); _x = root["first"].asInt(); _y = root["second"].asInt(); _op = root["oper"].asInt(); #endif return true; } public: // "x op y" int _x; int _y; char _op; }; // 响应 class Response { public: Response() : _exitcode(0), _result(0) { } Response(int exitcode, int result) : _exitcode(exitcode), _result(result) { } bool serialize(std::string *out) { #ifdef MYSELF *out = ""; std::string ec_string = std::to_string(_exitcode); std::string res_string = std::to_string(_result); *out = ec_string; *out += SEP; *out += res_string; #else Json::Value root; root["exitcode"] = _exitcode; root["result"] = _result; Json::FastWriter writer; *out = writer.write(root); #endif return true; } bool deserialize(const std::string &in) { #ifdef MYSELF // "exitcode result" auto mid = in.find(SEP); if (mid == std::string::npos) return false; std::string ec_string = in.substr(0, mid); std::string res_string = in.substr(mid + SEP_LEN); if (ec_string.empty() || res_string.empty()) return false; _exitcode = std::stoi(ec_string); _result = std::stoi(res_string); #else Json::Value root; Json::Reader reader; reader.parse(in, root); _exitcode = root["exitcode"].asInt(); _result = root["result"].asInt(); #endif return true; } public: int _exitcode; // 0:计算成功,!0表示计算失败,具体是多少,定好标准 int _result; // 计算结果 }; // 获取数据包 // "content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\n"content_len"\r\n"x op bool recPackage(int sock, string &inbuffer, string *text) { char buffer[1024]; while (true) { // 从网络中接受资源 ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); if (n > 0) { buffer[n] = 0; inbuffer += buffer; // 分析 auto pos = inbuffer.find(LINE_SEP); if (pos == string::npos) continue; string text_len_string = inbuffer.substr(0, pos); int text_len = stoi(text_len_string); // 报文的总长度 int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len; // text_len_string + "\r\n" + text + "\r\n" <= inbuffer.size(); std::cout << "处理前#inbuffer: \n" << inbuffer << std::endl; if (inbuffer.size() < total_len) { std::cout << "你输入的消息,没有严格遵守我们的协议,正在等待后续的内容, continue" << std::endl; continue; } // 最少一个完整报文 *text = inbuffer.substr(0, total_len); inbuffer.erase(0, total_len); std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl; break; } else return false; } return true; }
5、测试
下面我们进行几组简单的测试:
左边是服务器,又边上是客户端
Linux小命令
killall
是一个在 Unix 和类 Unix 操作系统上用于终止进程的命令。与 kill
命令不同,killall
不是根据进程 ID(PID)来终止进程,而是根据进程的名称来匹配并终止相应的进程。
killall [选项] 进程名
其中,选项可以用来指定不同的操作行为,而进程名则是要终止的进程的名称。
一些常用的选项包括:
-e
:显示详细的错误信息。-i
:交互模式,在终止进程之前询问用户。-q
:安静模式,不显示任何输出。-u
:指定用户,仅终止指定用户的进程。
注意:killall
命令会终止所有匹配名称的进程,因此需要谨慎使用,以免意外终止系统中重要的进程