当前位置: 首页 > news >正文

【计算机网络_应用层】协议定制序列化反序列化

文章目录

  • 1. TCP协议的通信流程
  • 2. 应用层协议定制
  • 3. 通过“网络计算器”的实现来实现应用层协议定制和序列化
    • 3.1 protocol
    • 3.2 序列化和反序列化
      • 3.2.1 手写序列化和反序列化
      • 3.2.2 使用Json库
    • 3.3 数据包读取
    • 3.4 服务端设计
    • 3.5 最后的源代码和运行结果

1. TCP协议的通信流程

在之前的代码中,相信大家对TCP的通信过程的代码已经有了一定了了解。在很早之前就了解到过一些网络通信的相关描述,比如TCP的三次握手和四次挥手。那么什么是三次握手和四次挥手呢?

在介绍之前我们首先看一个图,通过这个图来了解,接下来我们讲解这张图:

ca04d7ca00e56d5855fd5d0bc694bc6d

在最开始的时候客户端和服务器都是处于关闭状态的。

1. 开始前的准备

  1. 服务端和客户端在任意时刻在应用层调用socket函数分配一个文件描述符
  2. 服务端显示bind指定端口和任意IP地址
  3. 服务端调用listen使对应的文件描述符成为一个监听描述符
  4. 服务端调用accept阻塞等待客户端的连接(至此,服务端在通信钱的准备已经完成

2. 三次握手

  1. 客户端调用connect函数向服务器发起连接请求,然后阻塞自己等待完成

  2. 服务端收到客户端的连接请求之后由OS完成连接然后accept调用完成

    这里connect是三次握手的开始,accept调用完成时三次握手一定已经结束了,三次握手是OS内部自己完成的在TCP层我们感知不到

3. 四次挥手

四次挥手的工作都是由双方的OS完成,而我们决定什么时候挥手,一旦调用系统调用close,应用层就不用管了

2. 应用层协议定制

我们在第一次谈到协议的时候就说协议其实就是一种约定。在此之前,我们也写过一些UDP和TCP的通信代码,使用过一些socket API,我们可以发现socket API在发送数据的时候都是按照“字符串”的形式来发送和接收的,那如果我们要传输一些结构化的数据该怎么办呢?

比如在发送一条QQ消息的时候,需要带上发消息的人的昵称、QQ号、消息本身等等,这些消息必须要一次性绑定的发送,那么我们在发送的时候就需要把这些内容打包成一个“字符串”来发送

为什么不直接发送一个结构体对象?

网络通信涉及到不同的机器,可能出现大小段问题和内存对齐问题等等,所以不能直接发送结构体

这个打包成一个字符串的过程就是序列化,将收到的一个字符串转化为多个信息的过程就是反序列化

那么最终我们发送的消息就可以看作是一个完整的Content,但是TCP通信是面向字节流的,所以在通信的过程中,我们也没有办法知道一次发送过来的数据里面有几个完整的Content,这就需要在应用层定制一些“协议”来保证能区分每个数据包,一般来说我们有以下几种方法

1. 确保每个数据包是定长的; 2. 用特殊符号来表示结尾; 3. 自描述

注意:这里序列化反序列化和协议定制是两码事。序列化反序列化的作用是将要发送的信息变成一整条消息;协议定制的作用是保证每次读取一整个数据包,这个数据包里面会包含包头和有效载荷,这个有效载荷就是我们所说的“一整条消息”

3. 通过“网络计算器”的实现来实现应用层协议定制和序列化

3.1 protocol

设计思想:实现两个类:request用于存储对应的运算请求,存放算式,包括两个操作数和一个操作符。response表示对应请求的响应,也就是运算的结果状态和运算结果。最终经过系列化和反序列化之后形成一个字符串形式的有效载荷,我们在这个有效载荷前面加上报头信息,这里我们**约定:报头的内容是一个字符串格式的数据,存放的是有效载荷的长度,有效载荷和报头之间存在一个分隔符**

这里的约定就是我们的协议

既然有了应用层的通信协议,那么我们就要实现对应的为有效载荷添加报头和去除报头

std::string enLength(const std::string &text) // 在text上加报头
{// "content_len"\r\t"text"\r\tstd::string send_string = std::to_string(text.size());send_string += LINE_SEP;send_string += text;send_string += LINE_SEP;return send_string;
}
bool deLength(const std::string &package, std::string *text) // 从package上去报头
{auto pos = package.find(LINE_SEP);if (pos == std::string::npos)return false;std::string text_len_string = package.substr(0, pos);int text_len = std::stoi(text_len_string);*text = package.substr(pos + LINE_SEP_LEN, text_len);return true;
}

3.2 序列化和反序列化

3.2.1 手写序列化和反序列化

按照我们的约定,我们希望发送的结构化的数据就是Request和Response,里面有一些特定的字段

enum // 协议定义的相关错误枚举
{OK = 0,DIV_ZERO,MOD_ZERO,OP_ERROR
};
class Request // 客户端请求数据
{
public:int x;int y;char op;
};
class Response // 服务器响应数据
{
public:int exitcode;int result;
};

那么对于结构化的数据,我们要首先将其序列化,才能够作为有效载荷去添加报头,然后发送。接收到发送的数据去除报头之后的有效载荷,同样需要进行反序列化才能拿到结构化的数据,进行操作

#define SEP " "                       // 分隔符
#define SEP_LEN strlen(SEP)           // 分隔符长度
#define LINE_SEP "\r\n"               // 行分隔符(分隔报头和有效载荷)
#define LINE_SEP_LEN strlen(LINE_SEP) // 行分隔符长度
// class Request // 客户端请求数据
bool serialize(std::string *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;return true;
}
// "x op y"
bool deserialize(std::string &in) // 反序列化
{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 - SEP_LEN - left != 1)return false; // op的长度不为1std::string left_str = in.substr(0, left);std::string right_str = in.substr(right + SEP_LEN);if (left_str.empty() || right_str.empty())return false;x = std::stoi(left_str);y = std::stoi(right_str);op = in[left + SEP_LEN];return true;
}
// class Response // 服务器响应数据
bool serialize(std::string *out) // 序列化
{// "exitcode result"*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;return true;
}
bool deserialize(std::string &in) // 反序列化 "exitcode result"
{auto pos = in.find(SEP);if (pos == std::string::npos)return false;std::string ec_string = in.substr(0, pos);std::string res_string = in.substr(pos + SEP_LEN);if (ec_string.empty() || res_string.empty())return false;exitcode = std::stoi(ec_string);result = std::stoi(res_string);return true;
}

3.2.2 使用Json库

我们会发现手写序列化好麻烦 ,那么实际上有人已经帮我们做过这件事情了,提供了一些可以使用的组件,我们只需要按照规则使用即可。常用的序列化和反序列化工具有1. Json; 2. protobuf; 3. xml。这里我们为了使用的方便,采用Json来写。(protobuf在之后的博文会更新使用方式)

// class Request // 客户端请求数据
bool serialize(std::string *out) // 序列化
{Json::Value root; // Json::Value 是一个KV结构。首先定义出这个结构root["first"] = x; // 按照KV结构的模式,为每个字段添加一个Key,给这个字段赋值root["second"] = y;root["oper"] = op;Json::FastWriter writer; // FastWriter是一个序列化的类,里面提供了write方法,这个方法可以将Value的对象转成std::string*out = writer.write(root); // 转换后的字符串就是序列化后的结果return true;
}
bool deserialize(std::string &in) // 反序列化
{Json::Value root; // 序列化后的结果需要被存放Json::Reader reader; // Reader类是用作读取的,里面提供了parse(解析)方法,可以将对应的序列化结果string转化成Value对象reader.parse(in, root);x = root["first"].asInt();// 按照KV结构的模式将存放的内容提取出来,提取出来的结果的类型是Json内部的,要使用的时候需要指定类型y = root["second"].asInt();op = root["oper"].asInt();return true;
}// class Response // 服务器响应数据
bool serialize(std::string *out) // 序列化
{Json::Value root;root["first"] = exitcode;root["second"] = result;Json::FastWriter writer;*out = writer.write(root);return true;
}
bool deserialize(std::string &in) // 反序列化 "exitcode result"
{Json::Value root;Json::Reader reader;reader.parse(in, root);exitcode = root["first"].asInt();result = root["second"].asInt();return true;
}

Json库不是标准库的内容,所以在使用之前需要安装,在cent OS下的安装命令

sudo yum install -y jsoncpp-devel # 安装json

安装之后编译我们的代码会报错么?当然会!因为我们没有链接

cc=g++.PHONY:all
all:Server ClientServer:calServer.cc$(cc) -o $@ $^ -lpthread -ljsoncpp -std=c++11 # 这里加上-ljsoncppClient:calClient.cc$(cc) -o $@ $^ -ljsoncpp -std=c++11 # 这里加上-ljsoncpp.PHONY:clean
clean:rm -f Server Client

3.3 数据包读取

首先明确一点:TCP协议是面向字节流的,不能确定是否当前收到的就是一个完整的报文,所以需要进行判断与读取

这里我们采用的方法是:如果读取到一个完整的报文就进行后续处理,如果没有读取到一个完整的报文,那就继续读取,直到遇到完整报文再处理

/*** sock:读取对应套接字的报文* inbuffer:接收缓冲区,这里存放接收到的所有数据* req_text:输出型参数,如果读到完整报文就将报文内容存放到req_text中* 返回值:读取成功返回true,失败返回false
*/
bool recvPackage(int sock, std::string &inbuffer, std::string *req_text)
{char buffer[1024];while (true){ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 接收数据if (n > 0){buffer[n] = 0;      // 当前本次接收的数据inbuffer += buffer; // 放在inbuffer后面,处理整个inbufferauto pos = inbuffer.find(LINE_SEP);if (pos == std::string::npos)continue; // 还没有接收完一个完整的报头// 走到当前位置确定能接收到一个完整的报头std::string text_len_string = inbuffer.substr(0, pos);                // 报头拿完了,报头就是这个有效载荷的长度int text_len = std::stoi(text_len_string);                            // 有效载荷的长度int total_len = text_len + 2 * LINE_SEP_LEN + text_len_string.size(); // 报文总长度if (inbuffer.size() < total_len){// 收到的信息不是一个完整的报文continue;}// 到这里就拿到了一个完整的报文*req_text = inbuffer.substr(0, total_len);inbuffer.erase(0, total_len); // 在缓冲区中删除拿到的报文return true;}elsereturn false;}
}

3.4 服务端设计

按照我们在上一篇博文的多进程版本设计,这里服务端将会让一个孙子进程来执行相关的操作,其中孙子进程需要执行的任务分为5个步骤:

1. 读取报文,读取到一个完整报文之后去掉报头; 2. 将有效载荷反序列化; 3. 进行业务处理(回调); 4. 将响应序列化; 5. 将徐姐话的响应数据构建成一个符合协议的报文发送回去

void handleEntery(int sock, func_t func) // 服务端调用
{std::string inbuffer;// 接收缓冲区while(true){// 1. 读取数据std::string req_text, req_str;// 1.1 读到一个完整的请求(带报头)req_text = "content_len"\r\t"x op y"\r\tif(!recvPackage(sock, inbuffer, &req_text)) return;// 1.2 将req_text解析成req_str(不带报头)"x op y"if(!deLength(req_text, &req_str)) return;// 2. 数据反序列化Request req;if(!req.deserialize(req_str)) return;// 3. 业务处理Response resp;func(req, resp);// 4. 数据序列化std::string send_str;if(!resp.serialize(&send_str)) return;// 5. 发送响应数据// 5.1 构建一个完整的报文std::string resp_str = enLength(send_str);// 5.2 发送send(sock, resp_str.c_str(), resp_str.size(), 0);}
}

对应需要执行的内容我们就在业务逻辑层来处理

bool cal(const Request &req, Response &resp)
{// 此时结构化的数据就在req中,可以直接使用resp.exitcode = 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;elseresp.result = req.x / req.y;}break;case '%':{if (req.y == 0)resp.exitcode = MOD_ZERO;elseresp.result = req.x % req.y;}break;default:resp.exitcode = OP_ERROR;break;}
}

3.5 最后的源代码和运行结果

/*calServer.hpp*/
#pragma once#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <pthread.h>#include <string>
#include <functional>#include "log.hpp"
#include "protocol.hpp"namespace Server
{enum{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR};static const uint16_t gport = 8080;static const int gbacklog = 5;typedef std::function<bool(const Request &req, Response &resp)> func_t;void handleEntery(int sock, func_t func) // 服务端调用{std::string inbuffer;// 接收缓冲区while(true){// 1. 读取数据std::string req_text, req_str;// 1.1 读到一个完整的请求(带报头)req_text = "content_len"\r\t"x op y"\r\tif(!recvPackage(sock, inbuffer, &req_text)) return;// 1.2 将req_text解析成req_str(不带报头)"x op y"if(!deLength(req_text, &req_str)) return;// 2. 数据反序列化Request req;if(!req.deserialize(req_str)) return;// 3. 业务处理Response resp;func(req, resp);// 4. 数据序列化std::string send_str;if(!resp.serialize(&send_str)) return;// 5. 发送响应数据// 5.1 构建一个完整的报文std::string resp_str = enLength(send_str);// 5.2 发送send(sock, resp_str.c_str(), resp_str.size(), 0);}}class tcpServer;class ThreadData // 封装线程数据,用于传递给父进程{public:ThreadData(tcpServer *self, int sock) : _self(self), _sock(sock) {}public:tcpServer *_self;int _sock;};class tcpServer{public:tcpServer(uint16_t &port) : _port(port){}void initServer(){// 1. 创建socket文件套接字对象_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock == -1){logMessage(FATAL, "create socket error");exit(SOCKET_ERR);}logMessage(NORMAL, "create socket success:%d", _listensock);// 2.bind自己的网络信息sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;int n = bind(_listensock, (struct sockaddr *)&local, sizeof local);if (n == -1){logMessage(FATAL, "bind socket error");exit(BIND_ERR);}logMessage(NORMAL, "bind socket success");// 3. 设置socket为监听状态if (listen(_listensock, gbacklog) != 0) // listen 函数{logMessage(FATAL, "listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL, "listen socket success");}void start(func_t func){while (true){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;}// version 2:多进程版本pid_t id = fork();if (id == 0){close(_listensock); // 子进程不会使用监听socket,但是创建子进程的时候写时拷贝会拷贝,这里先关掉// 子进程再创建子进程if (fork() > 0)exit(0); // 父进程退出// 走到当前位置的就是子进程handleEntery(sock, func); // 使用close(sock);     // 关闭对应的通信socket(这里也可以不关闭,因为此进程在下个语句就会退出)exit(0);         // 孙子进程退出}// 走到这里的是监听进程(爷爷进程)pid_t n = waitpid(id, nullptr, 0);if (n > 0){logMessage(NORMAL, "wait success pid:%d", n);}close(sock);}}~tcpServer() {}private:uint16_t _port;int _listensock;};} // namespace Server
/*calServer.cc*/
#include <iostream>
#include <memory>#include "calServer.hpp"
#include "protocol.hpp"using namespace Server;static void Usage(const char *proc)
{std::cout << "\n\tUsage:" << proc << " local_port\n";
}bool cal(const Request &req, Response &resp)
{// 此时结构化的数据就在req中,可以直接使用resp.exitcode = 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;elseresp.result = req.x / req.y;}break;case '%':{if (req.y == 0)resp.exitcode = MOD_ZERO;elseresp.result = req.x % req.y;}break;default:resp.exitcode = OP_ERROR;break;}
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::unique_ptr<tcpServer> tsvr(new tcpServer(port));tsvr->initServer();tsvr->start(cal);return 0;
}
/*protocol.hpp*/
#pragma once#include <cstring>
#include <string>
#include <jsoncpp/json/json.h>#define SEP " "                       // 分隔符
#define SEP_LEN strlen(SEP)           // 分隔符长度
#define LINE_SEP "\r\n"               // 行分隔符(分隔报头和有效载荷)
#define LINE_SEP_LEN strlen(LINE_SEP) // 行分隔符长度enum // 协议定义的相关错误枚举
{OK = 0,DIV_ZERO,MOD_ZERO,OP_ERROR
};std::string enLength(const std::string &text) // 在text上加报头
{// "content_len"\r\t"text"\r\tstd::string send_string = std::to_string(text.size());send_string += LINE_SEP;send_string += text;send_string += LINE_SEP;return send_string;
}
bool deLength(const std::string &package, std::string *text) // 从package上去报头
{auto pos = package.find(LINE_SEP);if (pos == std::string::npos)return false;std::string text_len_string = package.substr(0, pos);int text_len = std::stoi(text_len_string);*text = package.substr(pos + LINE_SEP_LEN, text_len);return true;
}class Request // 客户端请求数据
{
public:Request() {}Request(int x_, int y_, char op_) : x(x_), y(y_), op(op_) {}bool serialize(std::string *out) // 序列化 -> "x op y"{
#ifdef MYSELFstd::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;
#elseJson::Value root; // Json::Value 是一个KV结构。首先定义出这个结构root["first"] = x; // 按照KV结构的模式,为每个字段添加一个Key,给这个字段赋值root["second"] = y;root["oper"] = op;Json::FastWriter writer; // FastWriter是一个序列化的类,里面提供了write方法,这个方法可以将Value的对象转成std::string*out = writer.write(root); // 转换后的字符串就是序列化后的结果
#endifreturn true;}// "x op y"bool deserialize(std::string &in) // 反序列化{
#ifdef MYSELFauto 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 - SEP_LEN - left != 1)return false; // op的长度不为1std::string left_str = in.substr(0, left);std::string right_str = in.substr(right + SEP_LEN);if (left_str.empty() || right_str.empty())return false;x = std::stoi(left_str);y = std::stoi(right_str);op = in[left + SEP_LEN];
#elseJson::Value root; // 序列化后的结果需要被存放Json::Reader reader; // Reader类是用作读取的,里面提供了parse(解析)方法,可以将对应的序列化结果string转化成Value对象reader.parse(in, root);x = root["first"].asInt();// 按照KV结构的模式将存放的内容提取出来,提取出来的结果的类型是Json内部的,要使用的时候需要指定类型y = root["second"].asInt();op = root["oper"].asInt();
#endifreturn true;}public:int x;int y;char op;
};class Response // 服务器响应数据
{
public:bool serialize(std::string *out) // 序列化{
#ifdef MYSELF// "exitcode result"*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;
#elseJson::Value root;root["first"] = exitcode;root["second"] = result;Json::FastWriter writer;*out = writer.write(root);
#endifreturn true;}bool deserialize(std::string &in) // 反序列化 "exitcode result"{
#ifdef MYSELFauto pos = in.find(SEP);if (pos == std::string::npos)return false;std::string ec_string = in.substr(0, pos);std::string res_string = in.substr(pos + SEP_LEN);if (ec_string.empty() || res_string.empty())return false;exitcode = std::stoi(ec_string);result = std::stoi(res_string);
#elseJson::Value root;Json::Reader reader;reader.parse(in, root);exitcode = root["first"].asInt();result = root["second"].asInt();
#endifreturn true;}public:int exitcode;int result;
};/*** sock:读取对应套接字的报文* inbuffer:接收缓冲区,这里存放接收到的所有数据* req_text:输出型参数,如果读到完整报文就将报文内容存放到req_text中* 返回值:读取成功返回true,失败返回false
*/
bool recvPackage(int sock, std::string &inbuffer, std::string *req_text)
{char buffer[1024];while (true){ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 接收数据if (n > 0){buffer[n] = 0;      // 当前本次接收的数据inbuffer += buffer; // 放在inbuffer后面,处理整个inbufferauto pos = inbuffer.find(LINE_SEP);if (pos == std::string::npos)continue; // 还没有接收完一个完整的报头// 走到当前位置确定能接收到一个完整的报头std::string text_len_string = inbuffer.substr(0, pos);                // 报头拿完了,报头就是这个有效载荷的长度int text_len = std::stoi(text_len_string);                            // 有效载荷的长度int total_len = text_len + 2 * LINE_SEP_LEN + text_len_string.size(); // 报文总长度if (inbuffer.size() < total_len){// 收到的信息不是一个完整的报文continue;}// 到这里就拿到了一个完整的报文*req_text = inbuffer.substr(0, total_len);inbuffer.erase(0, total_len); // 在缓冲区中删除拿到的报文return true;}elsereturn false;}
}
/*calClient.hpp*/
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>#include <string>#include "log.hpp"
#include "protocol.hpp"namespace Client
{class tcpClient{public:tcpClient(uint16_t &port, std::string &IP) : _serverPort(port), _serverIP(IP), _sockfd(-1) {}void initClient(){// 1. 创建socket_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd == -1){std::cerr << "create socket error" << std::endl;exit(2);}}void run(){struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(_serverPort);server.sin_addr.s_addr = inet_addr(_serverIP.c_str());if (connect(_sockfd, (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>>> ";std::getline(std::cin, line);Request req = ParseLine(line);std::string content;req.serialize(&content); // 序列化结果存放的content中std::string send_string = enLength(content); // 添加报头send(_sockfd, send_string.c_str(), send_string.size(), 0);std::string package, text;if (!recvPackage(_sockfd, inbuffer, &package))continue;if (!deLength(package, &text))continue;// text中的结果就是 "exitcode result"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){int status = 0; // 0 操作符之前 1 操作符 2 操作符之后int i = 0, size = line.size();char op;std::string left, right;while (i < size){switch (status){case 0:if(!isdigit(line[i])){// 遇到字符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;}}return Request(std::stoi(left), std::stoi(right), op);}~tcpClient(){if (_sockfd >= 0)close(_sockfd); // 使用完关闭,防止文件描述符泄露(当然这里也可以不写,当进程结束之后一切资源都将被回收)}private:uint16_t _serverPort;std::string _serverIP;int _sockfd;};} // namespace Client
/*calClient.cc*/
#include <memory>
#include <string>#include "calClient.hpp"
using namespace Client;static void Usage(const char *proc)
{std::cout << "\n\tUsage:" << proc << " server_ip server_port\n";
}int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(1);}std::string IP = argv[1];uint16_t port = atoi(argv[2]);std::unique_ptr<tcpClient> tclt(new tcpClient(port, IP));tclt->initClient();tclt->run();return 0;
}
/*log.hpp*/
#include <unistd.h>
#include <iostream>
#include <cstdio>
#include <ctime>
#include <cstdarg>// 这里是日志等级对应的宏
#define DEBUG (1 << 0)
#define NORMAL (1 << 1)
#define WARNING (1 << 2)
#define ERROR (1 << 3)
#define FATAL (1 << 4)#define NUM 1024 // 日志行缓冲区大小
#define LOG_NORMAL "log.normal" // 日志存放的文件名
#define LOG_ERR    "log.error"const char *logLevel(int level) // 把日志等级转变为对应的字符串
{switch (level){case DEBUG:return "DEBUG";case NORMAL:return "NORMAL";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "UNKNOW";}
}
//[日志等级][时间][pid]日志内容
void logMessage(int level, const char *format, ...) // 核心调用
{char logprefix[NUM]; // 存放日志相关信息time_t now_ = time(nullptr);struct tm *now = localtime(&now_);snprintf(logprefix, sizeof(logprefix), "[%s][%d年%d月%d日%d时%d分%d秒][pid:%d]",logLevel(level), now->tm_year + 1900, now->tm_mon + 1, now->tm_mday, now->tm_hour, now->tm_min, now->tm_sec, getpid());char logcontent[NUM];va_list arg; // 声明一个变量arg指向可变参数列表的对象va_start(arg, format); // 使用va_start宏来初始化arg,将它指向可变参数列表的起始位置。// format是可变参数列表中的最后一个固定参数,用于确定可变参数列表从何处开始vsnprintf(logcontent, sizeof(logcontent), format, arg); // 将可变参数列表中的数据格式化为字符串,并将结果存储到logcontent中FILE *log =  fopen(LOG_NORMAL, "a");FILE *err = fopen(LOG_ERR, "a");if(log != nullptr && err != nullptr){FILE *curr = nullptr;if(level == DEBUG || level == NORMAL || level == WARNING) curr = log;if(level == ERROR || level == FATAL) curr = err;if(curr) fprintf(curr, "%s%s\n", logprefix, logcontent);fclose(log);fclose(err);}
}
cc=g++.PHONY:all
all:Server ClientServer:calServer.cc$(cc) -o $@ $^ -lpthread -ljsoncpp -std=c++11Client:calClient.cc$(cc) -o $@ $^ -ljsoncpp -std=c++11.PHONY:clean
clean:rm -f Server Client.PHONY:cleanlog
cleanlog:rm -f log.error log.normal

image-20240227195945695


本节完…

相关文章:

【计算机网络_应用层】协议定制序列化反序列化

文章目录 1. TCP协议的通信流程2. 应用层协议定制3. 通过“网络计算器”的实现来实现应用层协议定制和序列化3.1 protocol3.2 序列化和反序列化3.2.1 手写序列化和反序列化3.2.2 使用Json库 3.3 数据包读取3.4 服务端设计3.5 最后的源代码和运行结果 1. TCP协议的通信流程 在之…...

Doris实战——银联商务实时数仓构建

目录 前言 一、应用场景 二、OLAP选型 三、实时数仓构建 四、实时数仓体系的建设与实践 4.1 数仓分层的合理规划 4.2 分桶分区策略的合理设置 4.3 多源数据迁移方案 4.4 全量与增量数据的同步 4.5 离线数据加工任务迁移 五、金融级数仓稳定性最佳实践 5.1 多租户资…...

代码随想录算法训练营第七天

● 自己看到题目的第一想法 第454题.四数相加II 方法&#xff1a; 方法一&#xff1a; 暴力法 思路&#xff1a; 注意&#xff1a; 代码&#xff1a; class Solution { public:int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<i…...

文件操作和IO(2):Java中操作文件

目录 一、File的属性 二、File的构造方法 三、File的方法 四、代码示例 1、getName&#xff0c;getParent&#xff0c;getPath方法 2、getAbsolutePath&#xff0c;getCanonicalPath方法 3、exists&#xff0c;isDirectory&#xff0c;createNewFile方法 4、createNewF…...

人工智能-零基础

机缘 扩充下知识栈&#xff0c;准备零基础开始 人工智能零基础 日常 日常水一下博客… 憧憬 努力成为一个会人工智能的程序员...

网络爬虫部分应掌握的重要知识点

目录 一、预备知识1、Web基本工作原理2、网络爬虫的Robots协议 二、爬取网页1、请求服务器并获取网页2、查看服务器端响应的状态码3、输出网页内容 三、使用BeautifulSoup定位网页元素1、首先需要导入BeautifulSoup库2、使用find/find_all函数查找所需的标签元素 四、获取元素的…...

git命令笔记

文章目录 0、参考文档1、常用指令git checkout系列git pull系列git push系列git reset系列git branch系列git log系列git revert 撤销某次提交git diff其他 2、查看和修改用户名和邮箱&#xff0c;生成密钥other 0、参考文档 Git使用教程&#xff1a;最详细、最傻瓜、最浅显、…...

微服务day03-Nacos配置管理与Nacos集群搭建

一.Nacos配置管理 Nacos不仅可以作为注册中心&#xff0c;可以进行配置管理 1.1 统一配置管理 统一配置管理可以实现配置的热更新&#xff08;即不用重启当服务发生变更时也可以直接更新&#xff09; dataId格式&#xff1a;服务名-环境名.yaml&#xff0c;分组一般使用默认…...

DFS剪枝

剪枝 将搜索过程中一些不必要的部分剔除掉&#xff0c;因为搜索过程构成了一棵树&#xff0c;剔除不必要的部分&#xff0c;就像是在树上将树枝剪掉&#xff0c;故名剪枝。 剪枝是回溯法中的一种重要优化手段&#xff0c;方法往往先写一个暴力搜索&#xff0c;然后找到某些特…...

基于SpringBoot多模块项目引入其他模块时@Autowired无法注入

基于SpringBoot多模块项目引入其他模块时Autowired无法注入 一、问题描述1、解决方案 一、问题描述 启动Spring Boot项目时报 Could not autowire. No beans of ‘xxxxxxxx’ type found. 没有找到bean的实例&#xff0c;即spring没有实例化对象&#xff0c;也就无法根据配置文…...

每日一题——LeetCode1566.重复至少K次且长度为M的模式

方法一 暴力枚举 var containsPattern function(arr, m, k) {const n arr.length;for (let l 0; l < n - m * k; l) {let offset;for (offset 0; offset < m * k; offset) {if (arr[l offset] ! arr[l offset % m]) {break;}}if (offset m * k) {return true;}}r…...

代码随想录刷题笔记-Day27

1. 全排列 46. 全排列https://leetcode.cn/problems/permutations/ 给定一个不含重复数字的数组 nums &#xff0c;返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#xff1a;[[1,2,3],[1,3,2],[2,1,3],…...

【小沐学GIS】QGIS安装和入门使用

文章目录 1、简介2、下载和安装3、使用3.1 XYZ Tiles3.2 WMS / WMTS3.3 GeoJson文件加载 4、在线资源结语 1、简介 QGIS是一款开源地理信息系统。该项目于2002年5月诞生&#xff0c;同年6月作为SourceForge上的一个项目建立。QGIS目前运行在大多数Unix平台、Windows和macOS上。…...

黑马程序员——接口测试——day03——Postman断言、关联、参数化

目录&#xff1a; Potman断言 Postman断言简介Postman常用断言 断言响应状态码断言包含某字符串断言JSON数据Postman断言工作原理Postman关联 简介实现步骤核心代码创建环境案例1案例2Postman参数化 简介数据文件简介编写数据文件 CSV文件JSON文件导入数据文件到postman读取数…...

Unreal触屏和鼠标控制旋转冲突问题

Unreal触屏和鼠标控制旋转冲突问题 鼠标控制摄像机旋转添加Input轴计算旋转角度通过轴事件控制旋转 问题和原因问题原因 解决办法增加触摸控制旋转代码触屏操作下屏蔽鼠标轴响应事件 鼠标控制摄像机旋转 通过Mouse X和Mouse Y控制摄像机旋转。 添加Input轴 计算旋转角度 通过…...

Vins-Moon配准运行

Vins-Moon运行 求助&#xff01;&#xff01;&#xff01;源码地址电脑配置环境配置编译Kitti数据集制作IMU时间戳问题 适配Kitti数据集运行结果Euroc数据集kitti数据集 evo评估&#xff08;KITTI数据&#xff09;输出轨迹(tum格式)结果 求助&#xff01;&#xff01;&#xff…...

MSCKF3讲:后端理论推导(上)

MSCKF3讲&#xff1a;后端理论推导&#xff08;上&#xff09; 文章目录 MSCKF3讲&#xff1a;后端理论推导&#xff08;上&#xff09;1 MSCKF中的状态变量① IMU状态:② cam0状态&#xff1a;③ IMU和cam0间状态关系 2 微分方程递推&#xff08;数值解&#xff09;3 IMU状态预…...

群控代理IP搭建教程:打造一流的网络爬虫

目录 前言 一、什么是群控代理IP&#xff1f; 二、搭建群控代理IP的步骤 1. 获取代理IP资源 2. 配置代理IP池 3. 选择代理IP策略 4. 编写代理IP设置代码 5. 异常处理 三、总结 前言 群控代理IP是一种常用于网络爬虫的技术&#xff0c;通过使用多个代理IP实现并发请求…...

【IO流系列】字符流练习(拷贝、文件加密、修改文件数据)

字符流练习 练习1&#xff1a;文件夹拷贝1.1 需求1.2 代码实现1.3 输出结果 练习2&#xff1a;文件加密与解密2.1 需求2.2 代码实现2.3 输出结果 练习3&#xff1a;修改文件数据&#xff08;常规方法&#xff09;3.1 需求3.2 代码实现3.3 输出结果 练习4&#xff1a;修改文件数…...

华为云磁盘挂载

华为云磁盘挂载 磁盘挂载情况 fdisk -l 2. 查看当前分区情况 df -h 3.给新硬盘添加新分区 fdisk /dev/vdb 4.分区完成&#xff0c;查询所有设备的文件系统类型 blkid 发现新分区并没有文件系统类型&#xff08;type为文件系统具体类型&#xff0c;有ext3,ext4,xfs,iso9660等…...

STM32标准库-DMA直接存储器存取

文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA&#xff08;Direct Memory Access&#xff09;直接存储器存取 DMA可以提供外设…...

Pinocchio 库详解及其在足式机器人上的应用

Pinocchio 库详解及其在足式机器人上的应用 Pinocchio (Pinocchio is not only a nose) 是一个开源的 C 库&#xff0c;专门用于快速计算机器人模型的正向运动学、逆向运动学、雅可比矩阵、动力学和动力学导数。它主要关注效率和准确性&#xff0c;并提供了一个通用的框架&…...

CRMEB 中 PHP 短信扩展开发:涵盖一号通、阿里云、腾讯云、创蓝

目前已有一号通短信、阿里云短信、腾讯云短信扩展 扩展入口文件 文件目录 crmeb\services\sms\Sms.php 默认驱动类型为&#xff1a;一号通 namespace crmeb\services\sms;use crmeb\basic\BaseManager; use crmeb\services\AccessTokenServeService; use crmeb\services\sms\…...

LRU 缓存机制详解与实现(Java版) + 力扣解决

&#x1f4cc; LRU 缓存机制详解与实现&#xff08;Java版&#xff09; 一、&#x1f4d6; 问题背景 在日常开发中&#xff0c;我们经常会使用 缓存&#xff08;Cache&#xff09; 来提升性能。但由于内存有限&#xff0c;缓存不可能无限增长&#xff0c;于是需要策略决定&am…...

在 Spring Boot 项目里,MYSQL中json类型字段使用

前言&#xff1a; 因为程序特殊需求导致&#xff0c;需要mysql数据库存储json类型数据&#xff0c;因此记录一下使用流程 1.java实体中新增字段 private List<User> users 2.增加mybatis-plus注解 TableField(typeHandler FastjsonTypeHandler.class) private Lis…...

【Linux手册】探秘系统世界:从用户交互到硬件底层的全链路工作之旅

目录 前言 操作系统与驱动程序 是什么&#xff0c;为什么 怎么做 system call 用户操作接口 总结 前言 日常生活中&#xff0c;我们在使用电子设备时&#xff0c;我们所输入执行的每一条指令最终大多都会作用到硬件上&#xff0c;比如下载一款软件最终会下载到硬盘上&am…...

Python竞赛环境搭建全攻略

Python环境搭建竞赛技术文章大纲 竞赛背景与意义 竞赛的目的与价值Python在竞赛中的应用场景环境搭建对竞赛效率的影响 竞赛环境需求分析 常见竞赛类型&#xff08;算法、数据分析、机器学习等&#xff09;不同竞赛对Python版本及库的要求硬件与操作系统的兼容性问题 Pyth…...

Pydantic + Function Calling的结合

1、Pydantic Pydantic 是一个 Python 库&#xff0c;用于数据验证和设置管理&#xff0c;通过 Python 类型注解强制执行数据类型。它广泛用于 API 开发&#xff08;如 FastAPI&#xff09;、配置管理和数据解析&#xff0c;核心功能包括&#xff1a; 数据验证&#xff1a;通过…...

【2D与3D SLAM中的扫描匹配算法全面解析】

引言 扫描匹配(Scan Matching)是同步定位与地图构建(SLAM)系统中的核心组件&#xff0c;它通过对齐连续的传感器观测数据来估计机器人的运动。本文将深入探讨2D和3D SLAM中的各种扫描匹配算法&#xff0c;包括数学原理、实现细节以及实际应用中的性能对比&#xff0c;特别关注…...

LTR-381RGB-01RGB+环境光检测应用场景及客户类型主要有哪些?

RGB环境光检测 功能&#xff0c;在应用场景及客户类型&#xff1a; 1. 可应用的儿童玩具类型 (1) 智能互动玩具 功能&#xff1a;通过检测环境光或物体颜色触发互动&#xff08;如颜色识别积木、光感音乐盒&#xff09;。 客户参考&#xff1a; LEGO&#xff08;乐高&#x…...