前言:
上文我们讲到了基于TCP的Socket编程:实现了一个远程命令执行的功能【Linux 网络】TCP Socket 编程实战:手把手实现远程命令执行(附实操要点 + 完整代码)-CSDN博客
本文我们再来进阶一下,同时定制协议,来实现更复杂的功能!
为了让我们的代码结构划分更加清晰,我们先对部分功能进行封装
Socket封装
对Socket中经常需要使用到的接口进行封装。
采用模板方法模式 进行设计,通过子类继承并重写父类方法来实现。
Socket.hpp
cpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <memory>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Comment.hpp"
using namespace LogModule;
const int BACK = 16;
namespace SocketModule
{
// 模板方法模式
// 基类大部分函数都是虚函数
class Socket
{
public:
virtual void SocketOrDie() = 0;
virtual void BindOrDie(uint16_t prot) = 0;
virtual void ListenOrDie(int backlog) = 0;
virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
virtual void ConnectOrDie(InetAddr &addr) = 0;
virtual ssize_t Recv(string &buffer) = 0;
virtual int Send(const std::string &message) = 0;
virtual void Close() = 0;
void BuildTcpSocket(uint16_t prot, int backlog = BACK)
{
SocketOrDie();
BindOrDie(prot);
ListenOrDie(backlog);
}
};
class TcpSocket : public Socket
{
public:
TcpSocket() {}
TcpSocket(int fd)
: _sockfd(fd)
{
}
void SocketOrDie() override
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::ERROR) << "socket error";
exit(SOCK_ERR);
}
}
void BindOrDie(uint16_t prot) override
{
InetAddr addr(prot);
int ret = bind(_sockfd, addr.Getaddr(), addr.Len());
if (ret < 0)
{
LOG(LogLevel::ERROR) << "bind error";
exit(BIND_ERR);
}
}
void ListenOrDie(int backlog) override
{
int ret = listen(_sockfd, backlog);
if (ret < 0)
{
LOG(LogLevel::ERROR) << "listen error";
exit(LISTEN_ERR);
}
}
// 客户端
std::shared_ptr<Socket> Accept(InetAddr *client) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(_sockfd, (struct sockaddr *)&peer, &len);
if (fd < 0)
{
LOG(LogLevel::ERROR) << "accept error";
exit(ACCP_ERR);
}
// 将客户端信息带出
InetAddr addr(peer);
*client = addr;
// accept成功后返回一个新的套接字,用于服务
// 可以直接返回fd,也可以返回新对象(的指针)
return std::make_shared<TcpSocket>(fd);
}
void ConnectOrDie(InetAddr &addr) override
{
int ret = connect(_sockfd, addr.Getaddr(), addr.Len());
if (ret < 0)
{
LOG(LogLevel::ERROR) << "connect error";
exit(CONN_ERR);
}
}
ssize_t Recv(string &buffer) override
{
char str[1024];
ssize_t n = recv(_sockfd, str, sizeof(str), 0);
if (n)
{
// 首先,str并不是空白的,其空间可以被其他函数使用过,保存有垃圾数据
// 其次,revc函数读取后,并不会主动的为我们读取到的信息末尾加上'\0'
// 所以,我们必须自己显示的加上!
// 不然当buffer+=的时候,str作为C风格的字符串,遇到'\0'才会停止
// 导致程序就会不知道读到哪停,把后面的内存垃圾也一并收进去了!
// 最终导致json串解析的时候失败!
str[n] = 0;
buffer += str; // 一定是+=,缓冲区信息是累加,而不是覆盖
}
return n;
}
int Send(const std::string &message) override
{
return send(_sockfd, message.c_str(), message.size(), 0);
}
void Close() override
{
close(_sockfd);
}
int Sockfd()
{
return _sockfd;
}
private:
int _sockfd;
};
}
协议定制
协议定制主要分为:序列化与反序列化、编码与解码
序列化与反序列化
在之前的文章中,我们曾讲过:协议在实现上,其本质是结构体!大家都遵守协议,那么涉及网络通信的数据都必须以结构体的形式存放!
而序列化,就是将"结构体"变成"字节流"!虽然协议的本质是结构体,网络通信的数据都是用结构体存放的,但是想要把数据通过网络发送给对方,其数据就必须是**"字节流"**。
将结构体转化为"字节流"有两种主流流派:
流派一:
直接发送结构体本身!既发送二进程数据。
很高效,很简单,速度快!但是!这就意味着发送方和接收方都必须是C++环境,并且结构体的内布局必须一模一样!
不推荐!
流派二:
结构化数据文本化!
直接将结构体的数据按照一定规则,进行文本化。仅发送文本、接收文本。接收的文件再按照规则进行反序列化!再次得到结构化数据。
推荐!
我们这里采用结构化数据文本化!那么具体改如何做呢?通过Json库实现!
认识JsonCpp库
JsonCpp库是C++中最通用的JSON处理库之一。主要功能就是进行序列化、反序列化。
序列化 (Serialization):将结构化数据,打包成一个标准的JSON字符串
反序列化 (Deserialization):把收到是JSON字符串,解析回结构化数据
使用样例:
由于JsonCpp库的第三方库,编译时一定要带路径:g++ -o @ ^ -l jsoncpp
cpp
#include <iostream>
#include <unistd.h>
#include <string>
#include <jsoncpp/json/json.h> //头文件注意!!
using namespace std;
// 序列化
int main()
{
// Json::Value 对象
Json::Value root;
root["one"] = "yi";
root["two"] = "er";
root["three"] = "san";
string str = root.toStyledString();
cout << str << endl;
// Json::FastWriter 对象,不添加额外空格、换行符
Json::FastWriter fast;
str = fast.write(root);
cout << str << endl;
// Json::StreamWriterBuilder
Json::StreamWriterBuilder stream;
std::unique_ptr<Json::StreamWriter> writer(stream.newStreamWriter());
stringstream ss;
writer->write(root, &ss);
cout << ss.str() << endl;
}
bash
hyc@hyc-alicloud:~/linux/序列与反序列/json使用样例$ ./a
{
"one" : "yi",
"three" : "san",
"two" : "er"
}
{"one":"yi","three":"san","two":"er"}
{
"one" : "yi",
"three" : "san",
"two" : "er"
}
cpp
// 反序列化
int main()
{
// string str = {"\" one ":\" yi ",\" three ":3,\" two ":" er \""};
string str = "{\"name\":\"张三\", \"age\":30, \"city\":\"北京\"}";
Json::Value root;
Json::Reader read;
read.parse(str, root);
cout << root["name"].asString() << endl;
cout << root["city"].asString() << endl;
cout << root["age"].asInt() << endl;
}
bash
hyc@hyc-alicloud:~/linux/序列与反序列/json使用样例$ ./a
张三
北京
30
通过使用JsonCpp库,我们就可以实现序列化与反序列化了!
编码与解码
为什么要编码与解码
封包与解包是为了解决 TCP 的"粘包"和"半包"问题,明确消息的边界。
如果说序列化 是为了让数据能"顺着网线爬过去",那么编码就是为了让接收方知道"这一坨数据爬到哪里才算是个头"。
根本原因:TCP是面向字节流的,因此TCP发送数据,是不能像UDP一样保证其数据是完整的数据报。它的传输方式像流水 一样,而不是像快递包裹一样。因为 TCP 这种"流水"特性,在接收端会发生两种灾难性的情况:
粘包:当发送方快速的发送了多条消息时,在底层这多条数据会被全部交给发送缓冲区中,由OS负责进行发送!
那么就有可能出现OS将发送缓冲区中的内容全部发送过去!而如果我们没有特定的规则,就无法判断数据的边界在哪里!无法正确的读取数据!

半包:当发送方发送了一条很长的数据,由于 TCP 的 MSS (最大报文段长度) 限制,或者网络拥塞窗口限制,这数据可能被拆成了两个包发送!
而这就可能导致其中有数据报是不完整 的!如果我们没有规定数据的边界,就无法判断数据报到底是否完整!
所以,编码与解码的作用:人为制造边界!保证读取数据的完整性!
如何进行编码与解码
我们这里规定编码 规则为:报文长度 + \r\n + 报文 + \r\n
解码就是反过来:去掉报文长度、\r\n,获取报文
协议具体实现
结合我们要设计的:网络计算器。具体实现思路如下:
1.要有一个序列与反序列化的模块,负责处理运算式(运算符与操作树)。
2.要有一个序列与反序列化的模块,负责处理运算式的结果(结果与状态)。
3.要有一个编码与解码模块,负责对要发送的序列化报文进程编码,对接收的报文进行解码。
Protocol.hpp
cpp
#pragma once
#include <string>
#include <cstring>
#include <functional>
#include "Socket.hpp"
#include <jsoncpp/json/json.h> //注意头文件
using namespace SocketModule;
// 协议定制:网络计算器
// 协议(基于TCP的)需要解决两个问题:
// 1. request和response必须得有序列化和反序列化功能
// 2. 你必须保证,读取的时候,读到完整的请求(TCP, UDP不用考虑)
//
// 处理运算式 -> 序列化与反序列化
class Request
{
public:
Request() {}
Request(int x, string op, int y)
: _x(x), _y(y), _op(op)
{
}
// 序列化
std::string Serialize()
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["op"] = _op;
Json::FastWriter fast;
return fast.write(root);
}
// 反序列化
bool Deserialize(string &str)
{
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(str, root);
if (ok == true)
{
_x = root["x"].asInt();
_y = root["y"].asInt();
_op = root["op"].asString();
}
return ok;
}
int X() { return _x; }
int Y() { return _y; }
string Oper() { return _op; }
private:
int _x;
int _y;
std::string _op; // 运算符(+ - * /)
};
// 处理运算式的结果 -> 序列与反序列化
class Respond
{
public:
Respond() {}
void Set(int result, int code)
{
_result = result;
_code = code;
}
// 序列化
std::string Serialize()
{
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter fast;
return fast.write(root);
}
// 反序列化
bool Deserialize(string &str)
{
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(str, root);
if (ok)
{
_result = root["result"].asInt();
_code = root["code"].asInt();
}
return ok;
}
bool Ret()
{
if (!_code)
return true;
return false;
}
int Result() { return _result; }
int Code() { return _code; }
private:
int _result; // 运算结果,无法区分清楚应答是计算结果,还是异常值
int _code; // 0:sucess, 1,2,3,4->不同的运算异常的情况, 约定!
};
// 协议(基于TCP的)需要解决两个问题:
// 1. request和response必须得有序列化和反序列化功能
// 2. 你必须保证,读取的时候,读到完整的请求(TCP, UDP不用考虑)
// 保证读取完整性:采用添加报头的形式
// json串长度 + \r\n + json串 + \r\n
const string tep = "\r\n";
using Func_t = function<void(Respond &, int, int, string)>;
class Protocol
{
public:
Protocol() {}
Protocol(Func_t func)
: _func(func)
{
}
// 编码
// 50\r\n{"x":10,"y":20,"op":"+"}\r\n
string Encode(const std::string &jsonstr)
{
int json_len_int = jsonstr.size();
string json_len = to_string(json_len_int);
return json_len + tep + jsonstr + tep;
}
// 从缓冲区读取的数据,可能如下:
//
// 50\r\
// 50\r\n{"x":10,"y":20,"op
// 50\r\n{"x":10,"y":20,"op":"+"}\r\n
// 50\r\n{"x":10,"y":20,"op":"+"}\r\n50\r\n{"x":10,"y":20,"op":"+"}\r\n50\r\n{"x":10,"
// 解码:必须保证读取上来的json串是完整的!!!
bool Decode(std::string &buffer, std::string *package)
{
// 先找分隔符
size_t pos = buffer.find(tep);
if (pos == string::npos)
{
// 不可能是完整的json串
return false;
}
int json_len = stoi(buffer.substr(0, pos));
int tep_len = tep.size();
// 错误点:sizeof(json_len)结果为4,因为是整数
// 应该用pos表示数字的长度!
int message_len = pos + tep_len + json_len + tep_len;
if (message_len > buffer.size())
{
// 一个完整的信息长度 > 当前buffer的长度
// 不可能是完整的json串
return false;
}
// 走到这里,一定有一个完整的报文!
// 读取一个完整的报文出来
*package = buffer.substr(pos + tep_len, message_len);
// 读取后,移除已经读取了的报文
buffer.erase(0, message_len);
return true;
}
// 获取客户端信息,处理后再发送回客户端
void GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client)
{
// 接收缓冲区
string buffer;
while (true)
{
ssize_t n = sock->Recv(buffer);
if (n > 0)
{
// 将buffer中的完整Json串,一次性处理完
string package;
while (Decode(buffer, &package))
{
// 读取成功,得到一个完整的json串!
// 反序列化
Request request;
request.Deserialize(package);
// 调用运算方法
Respond respond;
_func(respond, request.X(), request.Y(), request.Oper());
// 得到结果,序列化后发送给客户端
string ret = respond.Serialize();
// 修复:发送前需要编码
string final_ret = Encode(ret);
sock->Send(final_ret);
}
}
else if (n == 0)
{
// 读取到文件末尾!
LOG(LogLevel::INFO) << "读取到了文件末尾~";
break;
}
else
{
// 读取失败!
LOG(LogLevel::INFO) << "读取失败!";
break;
}
}
}
private:
Func_t _func;
};
网络计算器计算模块
协议我们也以及定制好了,还差最后一个板块!
协议中我们拿到了完整报文,也就是得到了计算式,接下来就是如何进行计算了,那其实就很简单了
NetCal.hpp
cpp
#pragma
#include <string>
#include <iostream>
#include "Protocol.hpp"
class NetCal
{
public:
void cal(Respond &respond, int x, int y, const char *op)
{
switch (*op)
{
case '+':
respond.Set(x + y, 0);
break;
case '-':
respond.Set(x - y, 0);
break;
case '*':
respond.Set(x * y, 0);
break;
case '/':
if (y == 0)
{
respond.Set(INT32_MIN, 1); // 除零错误!
}
else
{
respond.Set(x / y, 0);
}
break;
case '%':
if (y == 0)
{
respond.Set(INT32_MIN, 2); // mod错误!
}
else
{
respond.Set(x % y, 0);
}
break;
default:
// 设置错误code
respond.Set(INT32_MIN, 3); // 非法运算符
LOG(LogLevel::ERROR) << "非法运算符!";
break;
}
}
};
核心模块我们都实现了,接下来只需要在服务器端中的代码进行合理调用既可实现对应功能!
主要观察TcpServer.hpp TcpServer.cc TcpServer.hpp文件
完整代码实现
TcpServer.cc
cpp
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "NetCal.hpp"
void Usage(string str)
{
cout << "Please use: " << str << " prot!" << endl;
}
int main(int args, char *argv[])
{
if (args != 2)
{
Usage(argv[0]);
return 1;
}
// 顶层
NetCal netcal;
// 协议层
Protocol protocol([&netcal](Respond &respond, int x, int y, string op)
{ return netcal.cal(respond, x, y, op.c_str()); });
// 服务器层
uint16_t prot = stoi(argv[1]);
TcpServer ts(prot, [&protocol](shared_ptr<Socket> sock, InetAddr &client)
{ protocol.GetRequest(sock, client); });
ts.Start();
}
TcpServer.hpp
cpp
#pragma once
#include <sys/types.h>
#include <sys/wait.h>
#include <functional>
#include "Socket.hpp"
using namespace SocketModule;
class TcpServer
{
using Func_t = function<void(shared_ptr<Socket>, InetAddr &)>;
public:
TcpServer(uint16_t prot, Func_t func)
: _prot(prot),
_isrunning(false),
_func(func)
{
// 完成tcp套接字的创建、绑定、监听
ts.BuildTcpSocket(_prot);
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
auto sock = ts.Accept(&_client);
if (sock == nullptr)
continue;
pid_t pid = fork();
if (pid == 0)
{
// 关闭不需要的描述符
ts.Close();
// 子进程
if (fork() > 0)
return;
// 孙子进程执行任务
_func(sock, _client);
return;
}
else if (pid > 0)
{
// 父进程
// 关闭不需要的描述符
sock->Close();
// 子进程会立刻退出,不会阻塞等待
wait(&pid);
}
else
{
ts.Close();
LOG(LogLevel::ERROR) << "fork error";
return;
}
}
_isrunning = false;
}
private:
uint16_t _prot;
TcpSocket ts;
bool _isrunning;
InetAddr _client;
Func_t _func;
};
TcpClient.cc
cpp
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"
using namespace SocketModule;
void Usage(string str)
{
cout << "Please use: " << str << " prot!" << endl;
}
int main(int args, char *argv[])
{
if (args != 3)
{
Usage(argv[0]);
return 1;
}
string ip = argv[1];
uint16_t prot = stoi(argv[2]);
TcpSocket ts;
ts.SocketOrDie();
InetAddr addr(ip, prot);
ts.ConnectOrDie(addr);
while (true)
{
int a, b;
string op;
cout << "请输入参数一# ";
cin >> a;
cout << "请输入运算符# ";
cin >> op;
cout << "请输入参数二# ";
cin >> b;
getchar();
Request request(a, op, b);
Protocol protocol;
// 序列化 && 编码
string jsonstr = request.Serialize();
string finalstr = protocol.Encode(jsonstr);
// 发送编码后的信息
ts.Send(finalstr);
string buffer;
// 接收信息,并序列化
while (true)
{
int n = ts.Recv(buffer);
if (n)
{
string package;
// 2. 尝试解码(去除报头)
// 必须 Decode,因为服务器发来的是 Encode 过的数据
if (protocol.Decode(buffer, &package))
{
Respond respond;
if (respond.Deserialize(package))
{
if (respond.Ret())
cout << "结果: " << respond.Result() << endl;
else
cout << "运算错误: " << respond.Code() << endl;
}
break; // 成功得到一个结果,跳出接收循环,进行下一次输入
}
else
{
// 数据不完整,继续 Recv
continue;
}
}
else
{
cout << "服务器连接断开" << endl;
return 0;
}
}
}
}
NetCal.hpp
cpp
#pragma
#include <string>
#include <iostream>
#include "Protocol.hpp"
class NetCal
{
public:
void cal(Respond &respond, int x, int y, const char *op)
{
switch (*op)
{
case '+':
respond.Set(x + y, 0);
break;
case '-':
respond.Set(x - y, 0);
break;
case '*':
respond.Set(x * y, 0);
break;
case '/':
if (y == 0)
{
respond.Set(INT32_MIN, 1); // 除零错误!
}
else
{
respond.Set(x / y, 0);
}
break;
case '%':
if (y == 0)
{
respond.Set(INT32_MIN, 2); // mod错误!
}
else
{
respond.Set(x % y, 0);
}
break;
default:
// 设置错误code
respond.Set(INT32_MIN, 3); // 非法运算符
LOG(LogLevel::ERROR) << "非法运算符!";
break;
}
}
};
Comment.hpp
cpp
#pragma once
// 禁止拷贝类
class NoCopy
{
public:
NoCopy() {};
// 拷贝构造
NoCopy(const NoCopy &nocopy) = delete;
// 赋值重载
const NoCopy &operator=(const NoCopy &nocopy) = delete;
};
enum EXIT
{
OK = 0,
SOCK_ERR,
BIND_ERR,
LISTEN_ERR,
ACCP_ERR,
TCPSERVER_ERR,
TCPCLIENT_ERR,
CONN_ERR
};
Socket.hpp
cpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <memory>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Comment.hpp"
using namespace LogModule;
const int BACK = 16;
namespace SocketModule
{
// 模板方法模式
// 基类大部分函数都是虚函数
class Socket
{
public:
virtual void SocketOrDie() = 0;
virtual void BindOrDie(uint16_t prot) = 0;
virtual void ListenOrDie(int backlog) = 0;
virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
virtual void ConnectOrDie(InetAddr &addr) = 0;
virtual ssize_t Recv(string &buffer) = 0;
virtual int Send(const std::string &message) = 0;
virtual void Close() = 0;
void BuildTcpSocket(uint16_t prot, int backlog = BACK)
{
SocketOrDie();
BindOrDie(prot);
ListenOrDie(backlog);
}
};
class TcpSocket : public Socket
{
public:
TcpSocket() {}
TcpSocket(int fd)
: _sockfd(fd)
{
}
void SocketOrDie() override
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::ERROR) << "socket error";
exit(SOCK_ERR);
}
}
void BindOrDie(uint16_t prot) override
{
InetAddr addr(prot);
int ret = bind(_sockfd, addr.Getaddr(), addr.Len());
if (ret < 0)
{
LOG(LogLevel::ERROR) << "bind error";
exit(BIND_ERR);
}
}
void ListenOrDie(int backlog) override
{
int ret = listen(_sockfd, backlog);
if (ret < 0)
{
LOG(LogLevel::ERROR) << "listen error";
exit(LISTEN_ERR);
}
}
// 客户端
std::shared_ptr<Socket> Accept(InetAddr *client) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(_sockfd, (struct sockaddr *)&peer, &len);
if (fd < 0)
{
LOG(LogLevel::ERROR) << "accept error";
exit(ACCP_ERR);
}
// 将客户端信息带出
InetAddr addr(peer);
client = &addr;
// accept成功后返回一个新的套接字,用于服务
// 可以直接返回fd,也可以返回新对象(的指针)
return std::make_shared<TcpSocket>(fd);
}
void ConnectOrDie(InetAddr &addr) override
{
int ret = connect(_sockfd, addr.Getaddr(), addr.Len());
if (ret < 0)
{
LOG(LogLevel::ERROR) << "connect error";
exit(CONN_ERR);
}
}
ssize_t Recv(string &buffer) override
{
char str[1024];
ssize_t n = recv(_sockfd, str, sizeof(str), 0);
if (n)
{
// 首先,str并不是空白的,其空间可以被其他函数使用过,保存有垃圾数据
// 其次,revc函数读取后,并不会主动的为我们读取到的信息末尾加上'\0'
// 所以,我们必须自己显示的加上!
// 不然当buffer+=的时候,str作为C风格的字符串,遇到'\0'才会停止
// 导致程序就会不知道读到哪停,把后面的内存垃圾也一并收进去了!
// 最终导致json串解析的时候失败!
str[n] = 0;
buffer += str; // 一定是+=,缓冲区信息是累加,而不是覆盖
}
return n;
}
int Send(const std::string &message) override
{
return send(_sockfd, message.c_str(), message.size(), 0);
}
void Close() override
{
close(_sockfd);
}
int Sockfd()
{
return _sockfd;
}
private:
int _sockfd;
};
}
Protocol.hpp
cpp
#pragma once
#include <string>
#include <cstring>
#include <functional>
#include "Socket.hpp"
#include <jsoncpp/json/json.h> //注意头文件
using namespace SocketModule;
// 协议定制:网络计算器
// 协议(基于TCP的)需要解决两个问题:
// 1. request和response必须得有序列化和反序列化功能
// 2. 你必须保证,读取的时候,读到完整的请求(TCP, UDP不用考虑)
//
// 接收信息:处理模块 -> 客户端用
class Request
{
public:
Request() {}
Request(int x, string op, int y)
: _x(x), _y(y), _op(op)
{
}
// 序列化
std::string Serialize()
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["op"] = _op;
Json::FastWriter fast;
return fast.write(root);
}
// 反序列化
bool Deserialize(string &str)
{
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(str, root);
if (ok == true)
{
_x = root["x"].asInt();
_y = root["y"].asInt();
_op = root["op"].asString();
}
return ok;
}
int X() { return _x; }
int Y() { return _y; }
string Oper() { return _op; }
private:
int _x;
int _y;
std::string _op; // 运算符(+ - * /)
};
// 发送信息:发送模块 -> 服务器用
class Respond
{
public:
Respond() {}
void Set(int result, int code)
{
_result = result;
_code = code;
}
// 序列化
std::string Serialize()
{
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter fast;
return fast.write(root);
}
// 反序列化
bool Deserialize(string &str)
{
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(str, root);
if (ok)
{
_result = root["result"].asInt();
_code = root["code"].asInt();
}
return ok;
}
bool Ret()
{
if (!_code)
return true;
return false;
}
int Result() { return _result; }
int Code() { return _code; }
private:
int _result; // 运算结果,无法区分清楚应答是计算结果,还是异常值
int _code; // 0:sucess, 1,2,3,4->不同的运算异常的情况, 约定!
};
// 协议(基于TCP的)需要解决两个问题:
// 1. request和response必须得有序列化和反序列化功能
// 2. 你必须保证,读取的时候,读到完整的请求(TCP, UDP不用考虑)
// 保证读取完整性:采用添加报头的形式
// json串长度 + \r\n + json串 + \r\n
const string tep = "\r\n";
using Func_t = function<void(Respond &, int, int, string)>;
class Protocol
{
public:
Protocol() {}
Protocol(Func_t func)
: _func(func)
{
}
// 编码
// 50\r\n{"x":10,"y":20,"op":"+"}\r\n
string Encode(const std::string &jsonstr)
{
int json_len_int = jsonstr.size();
string json_len = to_string(json_len_int);
return json_len + tep + jsonstr + tep;
}
// 从缓冲区读取的数据,可能如下:
//
// 50\r\
// 50\r\n{"x":10,"y":20,"op
// 50\r\n{"x":10,"y":20,"op":"+"}\r\n
// 50\r\n{"x":10,"y":20,"op":"+"}\r\n50\r\n{"x":10,"y":20,"op":"+"}\r\n50\r\n{"x":10,"
// 解码:必须保证读取上来的json串是完整的!!!
bool Decode(std::string &buffer, std::string *package)
{
// 先找分隔符
size_t pos = buffer.find(tep);
if (pos == string::npos)
{
// 不可能是完整的json串
return false;
}
int json_len = stoi(buffer.substr(0, pos));
int tep_len = tep.size();
// 错误点:sizeof(json_len)结果为4,因为是整数
// 应该用pos表示数字的长度!
int message_len = pos + tep_len + json_len + tep_len;
if (message_len > buffer.size())
{
// 一个完整的信息长度 > 当前buffer的长度
// 不可能是完整的json串
return false;
}
// 走到这里,一定有一个完整的报文!
// 读取一个完整的报文出来
*package = buffer.substr(pos + tep_len, message_len);
// 读取后,移除已经读取了的报文
buffer.erase(0, message_len);
return true;
}
// 获取客户端信息,处理后再发送回客户端
void GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client)
{
// 接收缓冲区
string buffer;
while (true)
{
ssize_t n = sock->Recv(buffer);
if (n > 0)
{
// 将buffer中的完整Json串,一次性处理完
string package;
while (Decode(buffer, &package))
{
// 读取成功,得到一个完整的json串!
// 反序列化
Request request;
request.Deserialize(package);
// 调用运算方法
Respond respond;
_func(respond, request.X(), request.Y(), request.Oper());
// 得到结果,序列化后发送给客户端
string ret = respond.Serialize();
// 修复:发送前需要编码
string final_ret = Encode(ret);
sock->Send(final_ret);
}
}
else if (n == 0)
{
// 读取到文件末尾!
LOG(LogLevel::INFO) << "读取到了文件末尾~";
break;
}
else
{
// 读取失败!
LOG(LogLevel::INFO) << "读取失败!";
break;
}
}
}
private:
Func_t _func;
};
InetAddr.hpp
cpp
#pragma once
#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <cstring>
// 实现网络地址与主机地址的转换
class InetAddr
{
public:
InetAddr() {}
// 网络转主机
InetAddr(struct sockaddr_in &addr)
: _addr(addr)
{
_prot = ntohs(_addr.sin_port); // 网络地址转主机地址
char buff[1024];
inet_ntop(AF_INET, &addr.sin_addr, buff, sizeof(buff)); // 将4字节网络风格的IP -> 点分十进制的字符串风格的IP
_ip = std::string(buff);
}
// 主机转网络(客户端)
InetAddr(std::string ip, uint16_t prot)
: _ip(ip),
_prot(prot)
{
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
//&addr.sin_addr 是一个指向 struct in_addr 的指针,其内存地址等价于 &(addr.sin_addr.s_addr)(因为结构体的起始地址就是第一个成员的起始地址)
inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
_addr.sin_port = htons(_prot);
}
// 主机转网络(服务端)
InetAddr(uint16_t prot)
: _prot(prot)
{
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(prot);
_addr.sin_addr.s_addr = INADDR_ANY; // 任意ip地址
}
// 直接获取sockaddr_in
struct sockaddr *Getaddr()
{
return (struct sockaddr *)&_addr;
}
socklen_t Len()
{
socklen_t len = sizeof(_addr);
return len;
}
uint16_t prot()
{
return _prot;
}
std::string ip()
{
return _ip;
}
// 运算符重载
bool operator==(InetAddr &addr)
{
return _prot == addr._prot && _ip == addr._ip;
}
std::string Getname()
{
return _ip + ':' + std::to_string(_prot);
}
private:
struct sockaddr_in _addr;
uint16_t _prot;
std::string _ip;
};
Mutex.hpp
cpp
// 封装锁接口
#pragma once
#include <pthread.h>
namespace MutexModule
{
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&mutex, nullptr);
}
~Mutex()
{
pthread_mutex_destroy(&mutex);
}
void Lock()
{
pthread_mutex_lock(&mutex);
}
void Unlock()
{
pthread_mutex_unlock(&mutex);
}
pthread_mutex_t *Get()
{
return &mutex;
}
private:
pthread_mutex_t mutex;
};
class LockGuard
{
public:
LockGuard(Mutex &mutex)
: _Mutex(mutex)
{
_Mutex.Lock();
}
~LockGuard()
{
_Mutex.Unlock();
}
private:
// 为了保证锁的底层逻辑,锁是不能够拷贝的,并且也是没有拷贝构造函数的
// 避免拷贝,应该引用
Mutex &_Mutex;
};
}
Log.hpp
cpp
// 实现日志模块
#pragma once
#include <iostream>
#include <sstream> // 包含stringstream类
#include <filesystem> //C++17文件操作接口库
#include <fstream>
#include <sys/types.h>
#include <unistd.h>
#include "Mutex.hpp"
using namespace std;
using namespace MutexModule;
// 补充:外部类只能通过内部类的实例化对象,来访问内部类中的方法与成员,且受修饰符限制
// 内部类可以直接访问外部类的方法以及成员,没有限制
namespace LogModule
{
const string end = "\r\n";
// 实现刷新策略:a.向显示器刷新 b.向指定文件刷新
// 利用多态机制实现
// 包含至少一个纯虚函数的类称为抽象类,不能实例化,只能被继承
class LogStrategy // 基类
{
public:
//"=0"声明为纯虚函数。纯虚函数强制派生类必须重写该函数
virtual void SyncLog(const string &message) = 0;
};
// 向显示器刷新:子类
class ConsoleLogStrategy : public LogStrategy
{
public:
void SyncLog(const string &message) override
{
// 加锁,访问显示器,显示器也是临界资源
LockGuard lockguard(_mutex);
cout << message << end;
}
private:
Mutex _mutex;
};
// 向指定文件刷新:子类
const string defaultpath = "./log";
const string defaultfile = "my.log";
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const string &path = defaultpath, const string &file = defaultfile)
: _path(path), _file(file)
{
LockGuard lockguard(_mutex);
// 判断路径是否存在,如果不存在就创建对应的路径
if (!(filesystem::exists(_path)))
filesystem::create_directories(_path);
}
void SyncLog(const string &message) override
{
// 合成最后路径
string Path = _path + (_path.back() == '/' ? "" : "/") + _file;
// 打开文件
ofstream out(Path, ios::app);
out << message << end;
}
private:
string _path;
string _file;
Mutex _mutex;
};
//
// 日志等级
// enum class:强类型枚举。1.必须通过域名访问枚举值 2.枚举值不能隐式类型转化为整型
enum class LogLevel
{
DEBUG, // 调试级
INFO, // 信息级
WARNING, // 警告级
ERROR, // 错误级
FATAL // 致命级
};
//
// 将等级转化为字符串
string LevelToStr(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "DEBUG";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
return "UNKOWN";
}
}
// 获取时间
string GetTime()
{
// time函数:获取当前系统的时间戳
// localtime_r函数:将时间戳转化为本地时间(可重入函数,localtime则是不可重入函数)
// struct tm结构体,会将转化之后的本地时间存储在结构体中
time_t curr = time(nullptr);
struct tm curr_time;
localtime_r(&curr, &curr_time);
char buffer[128];
snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
curr_time.tm_year + 1900, // 年份是从1900开始计算的,需要加上1900才能得到正确的年份
curr_time.tm_mon + 1, // 月份了0~11,需要加上1才能得到正确的月份
curr_time.tm_mday, // 日
curr_time.tm_hour, // 时
curr_time.tm_min, // 分
curr_time.tm_sec); // 秒
return buffer;
}
//
// 实现日志信息,并选择刷新策略
class Logger
{
public:
Logger()
{
// 默认选择显示器刷新
Strategy = make_unique<ConsoleLogStrategy>();
}
void EnableConsoleLogStrategy()
{
Strategy = make_unique<ConsoleLogStrategy>();
}
void EnableFileLogStrategy()
{
Strategy = make_unique<FileLogStrategy>();
}
// 日志信息
class LogMessage
{
public:
LogMessage(const LogLevel &level, const string &name, const int &line, Logger &logger)
: _level(level),
_name(name),
_logger(logger),
_line_member(line)
{
_pid = getpid();
_time = GetTime();
// 合并:日志信息的左半部分
stringstream ss; // 创建输出流对象,stringstream可以将输入的所有数据全部转为为字符串
ss << "[" << _time << "] "
<< "[" << LevelToStr(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _name << "] "
<< "[" << _line_member << "] "
<< " - ";
// 返回ss中的字符串
_loginfo = ss.str();
}
// 日志文件的右半部分:可变参数,重载运算符<<
// e.g. <<"huang"<<123<<"dasd"<<24
template <class T>
LogMessage &operator<<(const T &message) // 引用返回可以让后续内容不断追加
{
stringstream ss;
ss << message;
_loginfo += ss.str();
// 返回对象!
return *this;
}
// 销毁时,将信息刷新
~LogMessage()
{
// 日志文件
_logger.Strategy->SyncLog(_loginfo);
}
private:
string _time;
LogLevel _level;
pid_t _pid;
string _name;
int _line_member;
string _loginfo; // 合并之后的一条完整信息
// 日志对象
Logger &_logger;
};
// 重载运算符(),便于创建LogMessage对象
// 这里返回临时对象:当临时对象销毁时,调用对应的析构函数,自动对象中创建好的日志信息进行刷新!
// 其次局部对象也不能传引用返回!
LogMessage operator()(const LogLevel &level, const string &name, const int &line)
{
return LogMessage(level, name, line, *this);
}
private:
unique_ptr<LogStrategy> Strategy;
};
// 为了用户使用更方便,我们使用宏封装一下
Logger logger;
// 切换刷新策略
#define Enable_Console_LogStrategy() logger.EnableConsoleLogStrategy();
#define Enable_File_LogStrategy() logger.EnableFileLogStrategy();
// 创建日志,并刷新
//__FILE__ 和 __LINE__ 是编译器预定义的宏,作用是获取当前代码所在的文件名、行号
#define LOG(level) logger(level, __FILE__, __LINE__) // 细节:不加;
};