文章目录
- 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的三次握手和四次挥手。那么什么是三次握手和四次挥手呢?
在介绍之前我们首先看一个图,通过这个图来了解,接下来我们讲解这张图:
在最开始的时候客户端和服务器都是处于关闭状态的。
1. 开始前的准备
- 服务端和客户端在任意时刻在应用层调用
socket
函数分配一个文件描述符 - 服务端显示bind指定端口和任意IP地址
- 服务端调用
listen
使对应的文件描述符成为一个监听描述符 - 服务端调用
accept
阻塞等待客户端的连接(至此,服务端在通信钱的准备已经完成)
2. 三次握手
-
客户端调用
connect
函数向服务器发起连接请求,然后阻塞自己等待完成 -
服务端收到客户端的连接请求之后由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\t
std::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的长度不为1
std::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 Client
Server:calServer.cc
$(cc) -o $@ $^ -lpthread -ljsoncpp -std=c++11 # 这里加上-ljsoncpp
Client: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后面,处理整个inbuffer
auto 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;
}
else
return 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\t
if(!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;
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;
}
}
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\t
if(!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;
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;
}
}
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\t
std::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 MYSELF
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; // 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); // 转换后的字符串就是序列化后的结果
#endif
return true;
}
// "x op y"
bool deserialize(std::string &in) // 反序列化
{
#ifdef MYSELF
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的长度不为1
std::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];
#else
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();
#endif
return 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;
#else
Json::Value root;
root["first"] = exitcode;
root["second"] = result;
Json::FastWriter writer;
*out = writer.write(root);
#endif
return true;
}
bool deserialize(std::string &in) // 反序列化 "exitcode result"
{
#ifdef MYSELF
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);
#else
Json::Value root;
Json::Reader reader;
reader.parse(in, root);
exitcode = root["first"].asInt();
result = root["second"].asInt();
#endif
return 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后面,处理整个inbuffer
auto 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;
}
else
return 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 Client
Server:calServer.cc
$(cc) -o $@ $^ -lpthread -ljsoncpp -std=c++11
Client:calClient.cc
$(cc) -o $@ $^ -ljsoncpp -std=c++11
.PHONY:clean
clean:
rm -f Server Client
.PHONY:cleanlog
cleanlog:
rm -f log.error log.normal
本节完…