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

文章目录

  • 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


本节完…

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/716341.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深入分析Android运行时环境ART:原理、特点与优化策略

摘要 随着移动互联网的快速发展&#xff0c;智能手机的性能和功能日益强大&#xff0c;其中Android操作系统因其开放性和灵活性而占据主导地位。Android运行时环境&#xff08;ART&#xff09;作为执行应用程序代码的关键组件&#xff0c;在系统性能和用户体验方面起着至关重要…

Vue+SpringBoot打造高校学生管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 学生管理模块2.2 学院课程模块2.3 学生选课模块2.4 成绩管理模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 学生表3.2.2 学院课程表3.2.3 学生选课表3.2.4 学生成绩表 四、系统展示五、核心代码5.1 查询课程5.2 新…

超详细红黑树的模拟实现

前言 有人说设计出AVL树的的人是个大牛&#xff0c;那写红黑树&#xff08;RBTree&#xff09;的人就是天才&#xff01; 上一篇文章&#xff0c;我们已经学习了AVL树&#xff0c;牛牛个人认为AVL树已经够优秀了&#xff0c;那让我们一起探究一下&#xff0c;为什么红黑树比AV…

链表类型题目

文章目录 简介链表的常用技巧两数相加原理代码代码|| 两两交换链表中的节点代码原理 重排链表(重要)原理代码 合并 K 个升序链表代码递归代码 K 个一组翻转链表原理代码 简介 大家好,这里是jiantaoyab,这篇文章给大家带来的是链表相关的题目练习和解析,希望大家能相互讨论进步 …

[线代]自用大纲

部分内容整理自张宇和网络 序 题型分布&#xff1a; 题型单题分值题目数量总分值选择题5315填空题515解答题12112 *一道大题可能用到六部分所有知识 矩阵 性质 k k k倍和乘积行列式 ∣ k A ∣ k n ∣ A ∣ |kA|k^n|A| ∣kA∣kn∣A∣ ∣ A B ∣ ≠ ∣ A ∣ ∣ B ∣ |AB|≠…

如何解决微服务的数据一致性分发问题?

介绍 系统架构微服务化以后,根据微服务独立数据源的思想,每个微服务一般具有各自独立的数据源,但是不同微服务之间难免需要通过数据分发来共享一些数据,这个就是微服务的数据分发问题。Netflix/Airbnb等一线互联网公司的实践[参考附录1/2/3]表明,数据一致性分发能力,是构…

【 10X summary report】怎么看?详细解读笔记

报告内容 在开始正式的分析之前&#xff0c;需要查看在对齐和计数过程中生成的任何总结统计信息。下图是由Cell Ranger工具创建的10X总结报告&#xff0c;在从10X scRNA-seq实验生成计数矩阵时会生成。 The left half of the report describes sequencing and mapping statist…

C++之stack

1、stack简介 stack是实现的一个先进后出&#xff0c;后进先出的容器。它只有一个出口&#xff0c;只能操作最顶端元素。 2、stack库函数 &#xff08;1&#xff09;push() //向栈压入一个元素 &#xff08;2&#xff09;pop() //移除栈顶元素 &#xff08;3…

基于springboot+vue的中国陕西民俗网

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

nginx,php-fpm

一&#xff0c;Nginx是异步非阻塞多进程&#xff0c;io多路复用 1、master进程&#xff1a;管理进程 master进程主要用来管理worker进程&#xff0c;具体包括如下4个主要功能&#xff1a; &#xff08;1&#xff09;接收来自外界的信号。 &#xff08;2&#xff09;向各worker进…

SAP PP学习笔记04 - BOM2 -通过Serial来做简单的BOM变式配置,副明细,BOM状态,BOM明细状态,项目种类,递归BOM

本章继续讲BOM。 本章讲通过Serial来做简单的BOM变式配置。还讲了BOM的相关概念&#xff1a;副明细&#xff0c;BOM状态&#xff0c;BOM明细状态&#xff0c;项目种类&#xff0c;递归BOM 等。 1&#xff0c;通过Serial&#xff08;序列号&#xff09;来做简单的 VC&#xff0…

Some collections -- 2024.3

一、TensorFlow Android (dataset: Mnist) We used TensorFlow to define and train our machine learning model, which can recognize handwritten numbers, called a number classifier model in machine learning terminology. We transform the trained TensorFlow mod…

2024.03.01作业

1. 基于UDP的TFTP文件传输 #include "test.h"#define SER_IP "192.168.1.104" #define SER_PORT 69 #define IP "192.168.191.128" #define PORT 9999enum mode {TFTP_READ 1,TFTP_WRITE 2,TFTP_DATA 3,TFTP_ACK 4,TFTP_ERR 5 };void get_…

高维中介数据:基于交替方向乘子法(ADMM)的高维度单模态中介模型的参数估计(入门+实操)

全文摘要 用于高维度单模态中介模型的参数估计&#xff0c;采用交替方向乘子法&#xff08;ADMM&#xff09;进行计算。该包提供了确切独立筛选&#xff08;SIS&#xff09;功能来提高中介效应的敏感性和特异性&#xff0c;并支持Lasso、弹性网络、路径Lasso和网络约束惩罚等不…

npm 镜像源切换与设置

项目背景 依赖安装中断或响应特别慢。 可以看到当前所用的镜像是 https://registry.npmjs.org 。 切换淘宝镜像之后总算能够安装下来 命令行模式 查看当前镜像源 # 查看当前镜像源 npm config get registry 可以看到默认情况下是官方默认全局镜像 https://registry.npmjs.o…

竞争加剧下,登顶后的瑞幸该做什么?

瑞幸咖啡仅用短短18个月时间从品牌创立到纳斯达克上市&#xff0c;刷新全球最快上市记录。2020年因交易造假事件被勒令退市股价暴跌80%&#xff0c;有人说这个创造了赴美IPO奇迹的“巨婴”将是下一个倒下的ofo。2023年瑞幸咖啡以逆势超速增长领跑咖啡赛道有力回应了市场的质疑&…

Java多线程实现发布和订阅

目录 简介 步骤 1: 定义消息类 步骤 2: 创建发布者 步骤 3: 创建订阅者 步骤 4: 实现发布-订阅模型 前言-与正文无关 生活远不止眼前的苦劳与奔波&#xff0c;它还充满了无数值得我们去体验和珍惜的美好事物。在这个快节奏的世界中&#xff0c;我们往往容易陷入工作的漩涡…

棋牌室计时计费管理系统的灯控器连接教程

棋牌室计时计费管理系统的灯控器连接教程 一、前言 以下教程以 佳易王棋牌室计时计费管理系统软件V18.0为例说明 软件文件下载可以点击最下方官网卡片——软件下载——试用版软件下载 如上图&#xff0c;计时计费软件在开始计时的时候&#xff0c;点击 开始计时 如果连接了…

YOLOv9独家改进|动态蛇形卷积Dynamic Snake Convolution与空间和通道重建卷积SCConv与RepNCSPELAN4融合

专栏介绍&#xff1a;YOLOv9改进系列 | 包含深度学习最新创新&#xff0c;主力高效涨点&#xff01;&#xff01;&#xff01; 一、改进点介绍 Dynamic Snake Convolution是一种针对细长微弱的局部结构特征与复杂多变的全局形态特征设计的卷积模块。 SCConv是一种即插即用的空间…

华为OD机试真题C卷-篇6

100分值题 宽度最小的子矩阵部门人力分配电脑病毒感染会议室占用时间段 宽度最小的子矩阵 给定一个n行 * m列的矩阵&#xff1b;给定一个k个整数的数组k_list&#xff1b;在n*m的矩阵中找一个宽度最小的子矩阵&#xff0c;该子矩阵包含k_list中所有的整数&#xff1b; 输入描述…