项目第二弹:第三方工具选择、介绍与使用
一、序列化框架:protobuf
1.序列化框架
我们都知道,网络通信,必须要将结构化数据进行序列化转为字节流才能发送出去。
同样的,字节流必须在读取之后进行反序列化转为结构化数据才能拿到
因此我们的项目当中必须要有一个序列化、反序列化框架,我们自己写的话没必要(其实就是自定义协议嘛),我们是有现成的序列化,反序列化框架的
JSON、XML、protobuf
其中:
- JSON常用于web服务器开发,因为浏览器对JSON的支持非常好,有很多内建函数来支持
- XML序列化反序列化的效率太低,强调的是数据结构化的能力和可读性
- protobuf适合高性能,对数据传输能力有要求的场景
2.为何我们的消息队列服务器不使用http协议
我们的客户端需要跟我们服务器使用同样的我们自定义的应用层协议,而不是使用Http协议
因为http请求报文头和响应报文头是比较复杂的,包含了cookie、状态码、响应码等等字段,
对于消息队列而言,我们并不需要这么复杂,也没有这个必要性
它其实就是负责数据传递,存储,分发的,要追求的是高性能、尽量简洁,快速。
因此我们的这个项目的基础版本并不是web服务器端(尽管RabbitMQ的确有提供给用户的图形化管理界面)
所以我们不采用JSON和XML,而是采用protobuf
3.protobuf快速上手
当然,我们不可能在一篇博客当中直接把protobuf全部介绍完。
只需要介绍到我们能够很好的使用protobuf即可,至于它的更多详细内容,我们就不赘述了
1.介绍
protobuf如何使用?
其实就是编写proto文件,然后使用命令来编译对应proto文件
编译后会生成对应的.h和.cc文件,使用时包含对应的头文件即可使用
我们所定义的message消息体,它们都继承自同一个父类:message
而message继承自MessageLite这个类
消息序列和反序列化的函数都在MessageLite这个类进行的实现
因此我们的message消息体,它们都是通过使用父类的函数来进行的序列反序列化
2.结合使用继续介绍
1.编译指令介绍
2.查看一下.h与.cc文件
namespace ns_proto
{
enum Gender : int
{
MAN = 0,
WOMAN = 1,
};
bool Gender_IsValid(int value); // 检测某个常数值是否对应于某个枚举常量
const ::google::protobuf::EnumDescriptor *Gender_descriptor();
}
namespace ns_proto
{
class Student final : public ::google::protobuf::Message
{
public:
static const ::google::protobuf::Descriptor *descriptor()
{
return GetDescriptor();
}
static const ::google::protobuf::Descriptor *GetDescriptor()
{
return default_instance().GetMetadata().descriptor; // 返回该类的描述符
}
// string name = 1;
void clear_name();
const std::string &name() const; // get方法
template <typename ArgT0 = const std::string &, typename... ArgT>
void set_name(ArgT0 &&arg0, ArgT... args); // set方法
std::string *mutable_name(); // 开辟好空间,然后返回对象的非const指针,我们可以直接修改该对象
// int32 id = 2;
void clear_id();
int32_t id() const;
void set_id(int32_t value);
// map<string, string> args = 4;
int args_size() const;
void clear_args();
const ::google::protobuf::Map<std::string, std::string> &args() const;
::google::protobuf::Map<std::string, std::string> * mutable_args();
};
}
3.使用一下
enum类型当中还有一个可以用枚举常量值获取枚举常量名称的方法,是一个函数模板
template<typename T>
inline const std::string& Gender_Name(T enum_t_value);
这个函数名就是枚举类型后面加上_Name
T是枚举类型
二、网络通信:muduo库
1.介绍
muduo库由陈硕⼤佬开发,是⼀个基于⾮阻塞IO和事件驱动的⾼并发TCP⽹络编程库。
它是⼀款基于主从Reactor模型的⽹络库,其使⽤的线程模型是one loop per thread, 所谓one loop per thread
指的是:
⼀个线程只能有⼀个事件循环(EventLoop), ⽤于响应计时器和IO事件
⼀个⽂件描述符只能由⼀个线程进⾏读写,换句话说就是⼀个TCP连接必须归属于某个EventLoop管理
muduo库是基于reactor模式的
关于reactor模式,大家可以看我的这篇博客:
reactor模式的步步探索和实现
2.为何要用muduo库?
- muduo库是一个基于非阻塞和事件驱动的高并发TCP网络编程库,效率非常好,使用方便,使用起来代码清晰明了
- muduo库采用线程池和高效的事件注册和处理机制,能够有效处理大量并发连接和IO事件。
尤其是事件注册与处理机制正好是我们所需要的(最后介绍网络协议设计时我们会介绍) - muduo库支持protobuf,二者结合使用,功能更为强大
muduo库是一个很大的网络编程库,支持很多丰富的功能。
我们这里只考虑使用muduo库与protobuf搭建服务器和客户端即可,无需使用其他的功能
而且使用muduo库,我们就无需把过度精力放到服务器网络模块的搭建上,无需使用/封装原生套接字接口来完成TCP通信
从而让我们能够把更多的精力放到项目的实现上
3.纯muduo库搭建简单服务器和客户端
1.服务器
1.模块介绍
2.代码
我们就随便实现一个翻译服务器吧,先不用结构化字段,直接发字符串
也是先不考虑数据包粘包问题(自定义一个应用层协议就可以,不过跟muduo库的使用就无关了,而且也不麻烦,咱就把重心放到项目上吧,这个大家有兴趣就写一写吧)
#include "muduo/net/TcpServer.h"
#include "muduo/net/TcpConnection.h"
#include "muduo/net/EventLoop.h"
#include <functional>
#include <iostream>
#include <unordered_map>
using namespace std;
class Server
{
public:
Server(uint16_t port)
: _server(&_baseloop, muduo::net::InetAddress("0.0.0.0", port), "Server", muduo::net::TcpServer::Option::kReusePort)
{
// 注册事件响应回调
_server.setConnectionCallback(std::bind(&Server::OnConnectionCallback, this, std::placeholders::_1));
_server.setMessageCallback(std::bind(&Server::OnMessageCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
}
void start()
{
// 开始监听
_server.start();
// 开启事件死循环监控
_baseloop.loop();
}
private:
string translate(const string &word)
{
unordered_map<string, string> umap = {
{
"hi", "嗨"}, {
"how are you", "你怎么样"}, {
"i am fine,thanks", "很好,谢谢你"}};
if (umap.count(word))
{
return umap[word];
}
return "查无此词";
}
void OnConnectionCallback(const muduo::net::TcpConnectionPtr &conn)
{
// bool connected() const { return state_ == kConnected; }
// 返回当前是否处于连接状态
if (conn->connected())
{
cout << "服务器已经连接\n";
}
else
{
cout << "服务器断开连接\n";
}
}
void OnMessageCallback(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buff, muduo::Timestamp)
{
if (!conn->connected())
{
cout << "服务器尚未建立连接,该请求无法处理\n";
return;
}
string recv_str = buff->retrieveAllAsString(); // 把缓冲区当中的所有数据都当成string拿上来
string send_str = translate(recv_str);
conn->send(send_str);
}
muduo::net::EventLoop _baseloop;
muduo::net::TcpServer _server;
};
/*
TcpServer(muduo::net::EventLoop *loop, const muduo::net::InetAddress &listenAddr,
const std::string &nameArg, muduo::net::TcpServer::Option option = muduo::net::TcpServer::kNoReusePort);
1. 事件循环对象
2. 监听套接字绑定所需信息
3. 服务器的名字
4. 是否开启端口重用,解决TIME_WAIT状态引发的bind失败的情况
*/
int main()
{
Server svr(8888);
svr.start();
return 0;
}
2.客户端
1.介绍
客户端跟服务器大致相同,只不过客户端有几点要注意:
2.代码实现
#include "muduo/net/TcpClient.h"
#include "muduo/net/TcpConnection.h"
#include "muduo/base/CountDownLatch.h"
#include "muduo/net/EventLoopThread.h"
#include <iostream>
#include <functional>
using namespace std;
class Client
{
public:
Client(const string &server_ip, uint16_t server_port)
: _latch(1), _client(_loop_thread.startLoop(), muduo::net::InetAddress(server_ip, server_port), "Client")
{
_client.setConnectionCallback(std::bind(&Client::OnConnectionCallback, this, std::placeholders::_1));
_client.setMessageCallback(std::bind(&Client::OnMessageCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
}
void connect()
{
// 向服务器发起连接请求
_client.connect();
// 等待成功连接
_latch.wait();
}
void send(const string &word)
{
_conn->send(word);
}
private:
void OnMessageCallback(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buffer, muduo::Timestamp)
{
if (!conn->connected())
{
cout << "尚未建立连接\n";
return;
}
cout << buffer->retrieveAllAsString() << endl;
}
void OnConnectionCallback(const muduo::net::TcpConnectionPtr &conn)
{
if (conn->connected())
{
cout << "成功建立连接\n";
_conn = conn;
_latch.countDown();
}
else
{
_conn.reset();
cout << "成功断开连接\n";
}
}
muduo::CountDownLatch _latch;
muduo::net::EventLoopThread _loop_thread;
muduo::net::TcpClient _client;
muduo::net::TcpConnectionPtr _conn;
};
#include <thread>
int main()
{
Client clt("127.0.0.1", 8888);
clt.connect();
clt.send("hi");
this_thread::sleep_for(chrono::seconds(1));
clt.send("how are you");
this_thread::sleep_for(chrono::seconds(1));
clt.send("i am fine,thanks");
this_thread::sleep_for(chrono::seconds(1));
clt.send("how old are you?");
this_thread::sleep_for(chrono::seconds(10));
return 0;
}
三、使用muduo库和protobuf搭建简易服务器和客户端
1.分析陈硕大佬示例代码
1.ProtobufDispatcher
2.ProtobufCodec
陈硕大佬针对protobuf与TCP粘包问题自定义了一个应用层协议,采用LV方式(Length-Value)
而这个ProtobufCodec模块就是负责对Protobuf类型的数据进行序列化+封装报头然后发出、读取数据之后解包+反序列化的模块
有了它之后,我们就无需自行序列化,反序列化,添加报头,解包了
3.代码分析
Client也是类似的,下面我们直接上代码
2.代码实现
我们实现一个翻译+加法服务器
1.proto文件
syntax = "proto3";
package ns_proto;
message TranslateRequest
{
string word = 1;
}
message TranslateResponse
{
string word = 1;
}
message AddRequest
{
int32 num1 = 1;
int32 num2 = 2;
}
message AddResponse
{
int32 ret = 1;
}
2.Server.cc
#include "muduo/net/TcpServer.h"
#include "muduo/net/TcpConnection.h"
#include "muduo/net/EventLoop.h"
#include "proto/codec.h"
#include "proto/dispatcher.h"
#include "demo.pb.h"
#include <memory>
#include <iostream>
#include <functional>
using namespace std;
using namespace ns_proto;
using TranslateRequestPtr = shared_ptr<TranslateRequest>;
using AddRequestPtr = shared_ptr<AddRequest>;
class Server
{
public:
Server(uint16_t port)
: _server(&_baseloop, muduo::net::InetAddress("0.0.0.0", port), "Server", muduo::net::TcpServer::Option::kReusePort), _dispatcher(std::bind(&Server::OnUnknownCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)), _codec(std::bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))
{
_dispatcher.registerMessageCallback<TranslateRequest>(std::bind(&Server::OnTranslateCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
_dispatcher.registerMessageCallback<AddRequest>(std::bind(&Server::OnAddCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
_server.setConnectionCallback(std::bind(&Server::OnConnectionCallback, this, std::placeholders::_1));
_server.setMessageCallback(std::bind(&ProtobufCodec::onMessage, &_codec, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
}
void start()
{
// 开始监听
_server.start();
// 开始事件死循环监控
_baseloop.loop();
}
private:
string translate(const string &word)
{
unordered_map<string, string> umap = {
{
"hi", "嗨"}, {
"how are you", "你怎么样"}, {
"i am fine,thanks", "很好,谢谢你"}};
if (umap.count(word))
{
return umap[word];
}
return "查无此词";
}
void OnUnknownCallback(const muduo::net::TcpConnectionPtr &conn, const MessagePtr &message, muduo::Timestamp)
{
cout << "未知请求,不予处理,即将断开连接\n";
if (conn->connected())
conn->shutdown();
}
void OnTranslateCallback(const muduo::net::TcpConnectionPtr &conn, const TranslateRequestPtr &message, muduo::Timestamp)
{
if (!conn->connected())
{
cout << "尚未建立连接,无法进行翻译任务\n";
return;
}
// 1. 根据请求进行业务处理
string recv_str = message->word();
string send_str = translate(recv_str);
// 2. 构建响应
TranslateResponse resp;
resp.set_word(send_str);
// 3. 直接通过codec发送出去
_codec.send(conn, resp);
}
void OnAddCallback(const muduo::net::TcpConnectionPtr &conn, const AddRequestPtr &message, muduo::Timestamp)
{
if (!conn->connected())
{
cout << "尚未建立连接,无法进行加法任务\n";
return;
}
// 1. 根据请求进行业务处理
int num1 = message->num1(), num2 = message->num2();
int ret = num1 + num2;
// 2. 构建响应
AddResponse resp;
resp.set_ret(ret);
// 3. 直接通过codec发送出去
_codec.send(conn, resp);
}
void OnConnectionCallback(const muduo::net::TcpConnectionPtr &conn)
{
// bool connected() const { return state_ == kConnected; }
// 返回当前是否处于连接状态
if (conn->connected())
{
cout << "服务器已经连接\n";
}
else
{
cout << "服务器断开连接\n";
}
}
muduo::net::EventLoop _baseloop;
muduo::net::TcpServer _server;
// 注意:_dispatcher要在_codec之前声明,因为初始化列表走的是声明顺序,而不是出现顺序
ProtobufDispatcher _dispatcher;
ProtobufCodec _codec;
};
int main()
{
Server svr(8888);
svr.start();
return 0;
}
3.Client.cc
#include "muduo/net/TcpClient.h"
#include "muduo/net/TcpConnection.h"
#include "muduo/base/CountDownLatch.h"
#include "muduo/net/EventLoopThread.h"
#include "proto/dispatcher.h"
#include "proto/codec.h"
#include "demo.pb.h"
#include <functional>
#include <memory>
#include <iostream>
using namespace std;
using namespace ns_proto;
using TranslateResponsePtr = shared_ptr<TranslateResponse>;
using AddResponsePtr = shared_ptr<AddResponse>;
class Client
{
public:
Client(const string &server_ip, uint16_t server_port)
: _latch(1), _client(_loop_thread.startLoop(), muduo::net::InetAddress(server_ip, server_port), "Client"), _dispatcher(std::bind(&Client::OnUnknownCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)), _codec(std::bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))
{
_dispatcher.registerMessageCallback<TranslateResponse>(std::bind(&Client::OnTranslateCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
_dispatcher.registerMessageCallback<AddResponse>(std::bind(&Client::OnAddCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
_client.setConnectionCallback(std::bind(&Client::OnConnectionCallback, this, std::placeholders::_1));
_client.setMessageCallback(std::bind(&ProtobufCodec::onMessage, &_codec, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
}
void connect()
{
_client.connect();
_latch.wait();
}
void add(int num1,int num2)
{
// 1. 构建请求
// 2. 发送
AddRequest req;
req.set_num1(num1);
req.set_num2(num2);
_codec.send(_conn,req);
}
void translate(const string &word)
{
// 1. 构建请求
// 2. 发送
TranslateRequest req;
req.set_word(word);
_codec.send(_conn,req);
}
private:
void OnUnknownCallback(const muduo::net::TcpConnectionPtr &conn, const MessagePtr &message, muduo::Timestamp)
{
cout << "未知请求,不予处理,即将断开连接\n";
if (conn->connected())
conn->shutdown();
}
void OnTranslateCallback(const muduo::net::TcpConnectionPtr &conn, const TranslateResponsePtr &message, muduo::Timestamp)
{
if (!conn->connected())
{
cout << "尚未建立连接\n";
return;
}
cout << message->word() << endl;
}
void OnAddCallback(const muduo::net::TcpConnectionPtr &conn, const AddResponsePtr &message, muduo::Timestamp)
{
if (!conn->connected())
{
cout << "尚未建立连接\n";
return;
}
cout << message->ret() << endl;
}
void OnConnectionCallback(const muduo::net::TcpConnectionPtr &conn)
{
// bool connected() const { return state_ == kConnected; }
// 返回当前是否处于连接状态
if (conn->connected())
{
_conn = conn;
cout << "客户端已经连接\n";
_latch.countDown();
}
else
{
cout << "客户端断开连接\n";
_conn.reset();
}
}
muduo::CountDownLatch _latch;
muduo::net::EventLoopThread _loop_thread;
muduo::net::TcpClient _client;
muduo::net::TcpConnectionPtr _conn;
ProtobufDispatcher _dispatcher;
ProtobufCodec _codec;
};
#include <thread>
int main()
{
Client clt("127.0.0.1",8888);
clt.connect();
clt.add(1,2);
this_thread::sleep_for(chrono::seconds(1));
clt.translate("how are you");
this_thread::sleep_for(chrono::seconds(1));
this_thread::sleep_for(chrono::seconds(5));
return 0;
}
4.编译+验证
.PHONY:all
all:server client
server:Server.cc demo.pb.cc ../third/include/proto/codec.cc
g++ -o $@ $^ -std=c++11 -I ../third/include -L ../third/lib -lmuduo_net -lmuduo_base -pthread -lprotobuf -lz
client:Client.cc demo.pb.cc ../third/include/proto/codec.cc
g++ -o $@ $^ -std=c++11 -I ../third/include -L ../third/lib -lmuduo_net -lmuduo_base -pthread -lprotobuf -lz
.PHONY:clean
clean:
rm -f server client
不要忘了连接codec.cc和demo.pb.cc
因为codec.cc里面包含了z库的东西,所以我们要指名连接z库
四、数据库:SQLite3
SQLite是⼀个进程内的轻量级数据库,它实现了⾃给⾃⾜的、⽆服务器的、零配置的、事务性的 SQL数据库引擎
1.为何要用SQLite?
• 不需要⼀个单独的服务器进程或操作的系统(⽆服务器的)
• SQLite 不需要配置
• ⼀个完整的 SQLite 数据库是存储在⼀个单⼀的跨平台的磁盘⽂件
• SQLite 是⾮常⼩的,是轻量级的,完全配置时⼩于 400KiB,省略可选功能配置时⼩于250KiB
• SQLite 是⾃给⾃⾜的,这意味着不需要任何外部的依赖
• SQLite 事务是完全兼容 ACID 的,允许从多个进程或线程安全访问
• SQLite ⽀持 SQL92(SQL2)标准的⼤多数查询语⾔的功能
• SQLite 使⽤ ANSI-C 编写的,并提供了简单和易于使⽤的 API
• SQLite 可在 UNIX(Linux, Mac OS-X, Android, iOS)和 Windows(Win32, WinCE, WinRT)中运⾏
因此使用SQLite3,我们就无需用MySQL这么大的数据库了
2.常用接口介绍
3.SQLiteHelper编写
我们封装一下这个SQLite3数据库的接口,我们写项目的时候要用,封装一下更方便
下面直接上代码
#include <sqlite3.h>
#include <iostream>
using namespace std;
class SqliteHelper
{
public:
using SqliteCallback = int (*)(void *, int, char **, char **);
SqliteHelper(const string &dbfile)
: _dbfile(dbfile), _handler(nullptr) {
}
bool open()
{
if (sqlite3_open_v2(_dbfile.c_str(), &_handler, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK)
{
cout << "打开数据库失败 " << errmsg() << endl;
return false;
}
return true;
}
~SqliteHelper()
{
close();
}
void close()
{
if (_handler != nullptr)
{
sqlite3_close_v2(_handler);
_handler = nullptr;
}
}
bool exec(const string &sql, SqliteCallback cb, void *arg)
{
if (sqlite3_exec(_handler, sql.c_str(), cb, arg, nullptr) != SQLITE_OK)
{
cout << "执行sql语句:" << sql << " 失败," << errmsg() << endl;
return false;
}
return true;
}
string errmsg()
{
if (_handler != nullptr)
return sqlite3_errmsg(_handler);
else
return "sqlite3句柄为空";
}
private:
string _dbfile;
sqlite3 *_handler;
};
下面我们验证一下
4.测试验证
1.非查询功能验证
#include "sqlite.hpp"
int main()
{
SqliteHelper handler("test.db");
handler.open();
handler.exec(R"(create table if not exists student(
id int primary key,
name varchar(32),
score float
);)",nullptr,nullptr);
handler.exec(R"(insert into student values(1,'zs',65.0),(2,'lisi',70.9);)",nullptr,nullptr);
//R"(C:\Users\Public\Documents)"是原生字符串,里面的转义字符\不会生效
handler.close();
return 0;
}
下面我们在终端当中这么做:
sqlite3 test.db 相当于MySQL当中的 use test.db 使用该数据库
然后就能进入sqlite3的命令行界面
.tables查看所有表
然后sql语句跟MySQL当中的sql语句书写方式是一样的,末尾都要加;
.exit退出该界面
wzs@iZ2ze5xfmy1filkylv86zbZ:~/RabbitMQ/blog/sqlite3$ sqlite3 test.db
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
sqlite> .tables
student
sqlite> select * from student
...> ;
1|zs|65.0
2|lisi|70.9
sqlite> .exit
wzs@iZ2ze5xfmy1filkylv86zbZ:~/RabbitMQ/blog/sqlite3$
2.select验证
那个callback回调函数只有在select查询的时候才需要使用
每查询到一行数据就会调用该函数
也就是说查到zs,便会调用一次cb,查到lisi,也会调用一次cb
int (*callback)(void*,int,char**,char**)
void* : 是设置的在回调时传⼊的arg参数
int:⼀⾏中数据的列数
char**:存储⼀⾏数据的字符指针数组
char**:每⼀列的字段名称
这个回调函数有个int返回值,成功处理的情况下必须返回0,返回⾮0会触发ABORT退出程序
整体的使用方式就类似于我们非常熟悉的pthread库当中使用pthread_create创建线程时传入的回调函数和参数
因此我们就可以写出这样的代码
#include "sqlite.hpp"
#include <vector>
int SelectCallback(void *arg, int column, char **rows, char **fields)
{
vector<vector<string>> *total_elem = static_cast<vector<vector<string>> *>(arg);
vector<string> one_elem;
if (total_elem->empty())
{
for (int i = 0; i < column; i++)
{
one_elem.push_back(fields[i]);
}
total_elem->push_back(move(one_elem));
}
for (int i = 0; i < column; i++)
{
one_elem.push_back(rows[i]);
}
total_elem->push_back(move(one_elem));
return 0;
}
int main()
{
SqliteHelper handler("test.db");
handler.open();
handler.exec(R"(create table if not exists student(
id int primary key,
name varchar(32),
score float
);)",
nullptr, nullptr);
//handler.exec(R"(insert into student values(1,'zs',65.0),(2,'lisi',70.9);)", nullptr, nullptr);
vector<vector<string>> vvs;
handler.exec(R"(select * from student;)", SelectCallback, &vvs);
// R"(C:\Users\Public\Documents)"是原生字符串,里面的转义字符\不会生效
handler.close();
for (auto &vec : vvs)
{
for (auto &elem : vec)
{
printf("%-5s",elem.c_str());
}
cout << endl;
}
return 0;
}
5.C和C++回调函数的区别
因为SQLite3的这些接口都是基于C语言实现的,都是C风格的回调函数,所以用起来稍微麻烦一点
在思考一下C++的方式…
我们就能得出C和C++回调函数的区别
因此不得不说C++11的包装器(function和bind)和lambda对于网络和数据库编程的优化是非常香的
但是对于我们C/C++程序员来说,这两个我们都要掌握,因为C在效率上还是略胜一筹(更贴近硬件)的
五、单元测试框架:GTest
1.介绍
看到事件机制这里大家可能会有点懵,下面我们结合代码来体会事件机制的好处
2.全局事件机制
全局事件:针对整个测试程序,能够在测试之前配置测试环境数据,测试完毕后清理数据
步骤:
- 先定义环境类,通过继承testing::Environment的派⽣类来完成
- 重写的虚函数接⼝SetUp会在测试之前被调⽤; TearDown会在测试完毕后调⽤
#include <gtest/gtest.h>
#include <unordered_map>
using namespace std;
// 比如我们要测试哈希表的增删查改
unordered_map<string, string> umap;
class HashTest : public testing::Environment
{
public:
virtual void SetUp()
{
umap.insert(make_pair("monday", "周一"));
umap.insert(make_pair("tuesday", "周二"));
umap.insert(make_pair("wednesday", "周三"));
umap.insert(make_pair("thursday", "周四"));
umap.insert(make_pair("friday", "周五"));
umap.insert(make_pair("saturday", "周六"));
umap.insert(make_pair("sunday", "周日"));
}
virtual void TearDown()
{
umap.clear();
}
};
// january, february, march, april, may, june, july...
TEST(hash_test, insert_test)
{
ASSERT_EQ(umap.size(), 7);
umap.insert(make_pair("january", "一月"));
ASSERT_EQ(umap.size(), 8);
}
TEST(hash_test, delete_test)
{
ASSERT_EQ(umap.size(), 8);
umap.erase("monday");
ASSERT_EQ(umap.size(), 7);
}
TEST(hash_test, find_test)
{
// 注意:GTest这里的断言非常严格,const char* 不能转换为 string
ASSERT_EQ(umap["tuesday"], string("周二"));
ASSERT_EQ(umap["wednesday"], string("周三"));
ASSERT_EQ(umap["thursday"], string("周四"));
ASSERT_EQ(umap["friday"], string("周五"));
ASSERT_EQ(umap["saturday"], string("周六"));
ASSERT_EQ(umap["sunday"], string("周日"));
ASSERT_EQ(umap["january"], string("一月"));
ASSERT_EQ(umap.find("monday"), umap.end());
}
int main(int argc, char *argv[])
{
// 注册全局测试环境
testing::AddGlobalTestEnvironment(new HashTest);
// 初始化 GTest 框架
testing::InitGoogleTest(&argc, argv);
// 进行测试
return RUN_ALL_TESTS();
}
全局事件机制是一种各个测试用例串行化的方式来进行测试
好处是使用起来非常方便,思路很清晰
不足是:每个测试套件之间是相互依赖的,若前面出现错误,对后面的影响比较大,即测试套件的耦合度高
因此轮到我们的下一个事件机制出场了:
TestSuite单元测试套件(每个测试条件是独立的)
3.单元测试(TestSuite)机制
它跟全局事件机制不同的地方在于:
- 测试环境无需注册,即无需下面这行代码
testing::AddGlobalTestEnvironment(new HashTest); - 继承的类不是testing::Environment,而是testing::Test
- 测试套件不是TEST,而是TEST_F
- 只有当测试套件的第一个名称跟类名相同时,每个测试套件内部才能访问独属于自己的类的成员变量
- 实现的函数不是SetUp和TearDown这两个虚函数了,而是SetUpTestCase和TearDownTestCase这两个静态成员函数
talk is cheap,show me the code:
要解释东西还是代码好用,理论跟实践相结合嘛
#include <gtest/gtest.h>
#include <iostream>
#include <unordered_map>
using namespace std;
// 比如我们要测试哈希表的增删查改
class HashTest : public testing::Test
{
public:
static void SetUpTestCase()
{
cout << "总体测试套件的测试环境初始化\n";
}
static void TearDownTestCase()
{
cout << "总体测试套件的测试环境销毁\n";
}
unordered_map<string, string> umap;
};
void init(unordered_map<string, string> &umap)
{
std::cout << "init\n";
umap.insert(make_pair("monday", "周一"));
umap.insert(make_pair("tuesday", "周二"));
umap.insert(make_pair("wednesday", "周三"));
umap.insert(make_pair("thursday", "周四"));
umap.insert(make_pair("friday", "周五"));
umap.insert(make_pair("saturday", "周六"));
umap.insert(make_pair("sunday", "周日"));
}
// january, february, march, april, may, june, july...
TEST_F(HashTest, insert_test)
{
init(umap);
ASSERT_EQ(umap.size(), 7);
umap.insert(make_pair("january", "一月"));
ASSERT_EQ(umap.size(), 8);
}
TEST_F(HashTest, delete_test)
{
init(umap);
ASSERT_EQ(umap.size(), 7);
umap.erase("monday");
ASSERT_EQ(umap.size(), 6);
ASSERT_EQ(umap.find("monday"), umap.end());
}
TEST_F(HashTest, find_test)
{
init(umap);
// 注意:GTest这里的断言非常严格,const char* 不能转换为 string
ASSERT_EQ(umap["monday"], string("周一"));
ASSERT_EQ(umap["tuesday"], string("周二"));
ASSERT_EQ(umap["wednesday"], string("周三"));
ASSERT_EQ(umap["thursday"], string("周四"));
ASSERT_EQ(umap["friday"], string("周五"));
ASSERT_EQ(umap["saturday"], string("周六"));
ASSERT_EQ(umap["sunday"], string("周日"));
}
int main(int argc, char *argv[])
{
// 单元事件机制无需注册
// 直接初始化 GTest 框架即可
testing::InitGoogleTest(&argc, argv);
// 进行测试
return RUN_ALL_TESTS();
}
可以看出,这样做的好处是各个测试套件之间的耦合度为0,
不过不足是不太方便,有些时候我们还是想要让所有测试套件一开始的初始化操作比较“自动化”一些的
因此,TestCase测试用例事件机制应运而生
4.测试用例(TestCase)机制
TestCase跟TestSuite唯一的区别就是:
TestCase使用了SetUp跟TearDown完成每个独立测试套件环境的初始化和销毁
因此这两个虚函数依然要实现
直接上代码,看起来才清楚明了
#include <gtest/gtest.h>
#include <iostream>
#include <unordered_map>
using namespace std;
// 比如我们要测试哈希表的增删查改
class HashTest : public testing::Test
{
public:
static void SetUpTestCase()
{
cout << "总体测试套件的测试环境初始化\n";
}
static void TearDownTestCase()
{
cout << "总体测试套件的测试环境销毁\n";
}
virtual void SetUp()
{
cout<<"单个测试套件测试环境初始化\n";
umap.insert(make_pair("monday", "周一"));
umap.insert(make_pair("tuesday", "周二"));
umap.insert(make_pair("wednesday", "周三"));
umap.insert(make_pair("thursday", "周四"));
umap.insert(make_pair("friday", "周五"));
umap.insert(make_pair("saturday", "周六"));
umap.insert(make_pair("sunday", "周日"));
}
virtual void TearDown()
{
cout<<"单个测试套件测试环境销毁\n";
umap.clear();
}
unordered_map<string, string> umap;
};
// january, february, march, april, may, june, july...
TEST_F(HashTest, insert_test)
{
ASSERT_EQ(umap.size(), 7);
umap.insert(make_pair("january", "一月"));
ASSERT_EQ(umap.size(), 8);
}
TEST_F(HashTest, delete_test)
{
ASSERT_EQ(umap.size(), 7);
umap.erase("monday");
ASSERT_EQ(umap.size(), 6);
ASSERT_EQ(umap.find("monday"), umap.end());
}
TEST_F(HashTest, find_test)
{
// 注意:GTest这里的断言非常严格,const char* 不能转换为 string
ASSERT_EQ(umap["monday"], string("周一"));
ASSERT_EQ(umap["tuesday"], string("周二"));
ASSERT_EQ(umap["wednesday"], string("周三"));
ASSERT_EQ(umap["thursday"], string("周四"));
ASSERT_EQ(umap["friday"], string("周五"));
ASSERT_EQ(umap["saturday"], string("周六"));
ASSERT_EQ(umap["sunday"], string("周日"));
}
int main(int argc, char *argv[])
{
// 单元事件机制无需注册
// 直接初始化 GTest 框架即可
testing::InitGoogleTest(&argc, argv);
// 进行测试
return RUN_ALL_TESTS();
}
5.为何要用GTest单元测试框架?
因为使用GTest单元测试框架,我们在测试时就可以更加系统,全面且便捷的对我们的每个模块进行单独的功能测试
从而让我们能够把更多的精力放到项目的实现上
六、C++11 异步操作接口的介绍
1.为何要用异步工作线程池?
1.比喻
2.较为专业的术语
我们的服务器和客户端的服务线程在运行时,会遇到一些异步任务,此时我们就需要一个异步工作线程池来完成这些异步任务,因此我们需要写一个异步线程池
异步操作在C++11当中有对应的接口,我们可以使用它们来写一个异步线程池
Talk is cheap,Show me the code
光说理论,不写代码,理解的不够深刻
光写代码,不讲理论,那就是瞎写,根本没用
下面我们学习接口,写代码
2.future模板类的介绍
下面我们一一介绍future配合的三种使用场景:
async、promise、packaged_task
3.异步操作介绍
1.async
也可以不传入具体策略,默认采用launch::deffered | launch::async结合在一起来使用
理论谈完,上代码,然后我们再回看理论
1.async策略
我们可以看到在我们主执行流调用sleep_for休眠期间,async的确会创建一个工作线程来跑我们的add函数,并且我们可以通过future对象来获取对应的返回结果
这个工作线程的创建与管理被隐藏了,我们无法也不需要join或者detach,这个线程对我们而言是不可见的,根本不用我们操心
2.deffered策略
直接把代码那里的async策略改成deffered即可,其他的,啥也不用动
我们可以看到,只有当我们调用了get方法,才会调用对应的add函数来执行对应任务
那到底有没有创建新线程呢?我们打印一下线程ID就知道了
可以看出:没有创建新线程
那这个deferred有什么用呢?不都还是我在做一次吗?
非也,效率上讲,没毛病:的确没啥实质性的提升
但是写代码的时候,有些返回结果我现在不需要用,但是我以后要用,而且我现在还不想执行那个函数得到我的结果(因为我要做其他事情),既然我已经明确了,我以后一定会用的着这个返回值,那么我就可以用deffed预订一下,到时候,直接使用future对象就可以拿到对应的返回值了
方便我们写代码,在代码编写和效率上面实现了一箭双雕
一句话概括:延迟执行,结果预订
3.自动化策略
我这里面是采用了async的方法
4.promise的引入
有了async之后,有了一个问题:
如果我要的返回结果在add函数中间状态下就能拿到,我还要等到add函数执行完,那不就是浪费吗?
结合我们这一年多的学习,大家也就能够很好的想到解决方法:输出型参数不就OK了吗?
正是基于这个想法,诞生了promise
2.promise
1.介绍与使用
代码走起:
#include <iostream>
#include <future>
#include <thread>
using namespace std;
void add(int a, int b, promise<int> &pro)
{
cout << "---------- 学徒 " << this_thread::get_id() << " 执行1+1=? ---------------" << endl;
pro.set_value(a + b);
this_thread::sleep_for(chrono::seconds(3));
}
int main()
{
int a = 1, b = 1;
cout << "师傅遇到了一些比较耗时且低耦合的任务(1 + 1 = ?), 将它们交给学徒去完成\n";
promise<int> pro;
future<int> fut = pro.get_future(); // 将promise对象跟future对象相绑定
thread t(add, a, b, ref(pro)); // 自己创建线程对象来执行add,修改promise对象的值
// 这里可以进行其他任务,无需任何等待
cout << "师傅 " << this_thread::get_id() << " 正在做核心工作\n";
// 此时师傅需要拿对应任务的结果
cout << "---------- 1 ---------------" << endl;
cout << fut.get() << endl;
t.join(); // 别忘了Join我们自己创建的新线程
cout << "---------- 2 ---------------" << endl;
return 0;
}
事实证明,我们的确成功在add函数执行完之前,promise对象被调用之前拿到了结果
注意:promise对象不能设置两次值的
pro.set_value(a + b);
pro.set_value(a - b);
terminate called after throwing an instance of 'std::future_error'
what(): std::future_error: Promise already satisfied
Aborted
ref可以在引用传参时保持引用属性
如果我们把thread t(add, a, b, ref(pro));改成thread t(add, a, b, pro);
那么编译时就会报错,因为引用属性丢失了
所以要么用ref
要么用指针
thread t(add, a, b, &pro);
2.packaged_task的引入
有了async和promise,感觉他们很棒了啊,但是他们仍然有一个缺点:
- async内部自带工作线程,无法适配我们的线程池
- promise需要当成对应函数的一个参数进行传入,这样的话就需要修改对应函数的函数签名,不太合适
因此引入了packaged_task
3.packaged_task
packaged_task跟function包装器长的很像,只不过两者的用途是不一样的
-
function包装器用来将函数名和函数参数绑定为一个完整的函数签名,简化回调机制的编写,它是为回调机制而生
-
packaged_task是为异步操作而生
下面我们先用一下,大家就明白了
七、异步线程池的编写
1.线程池的思想
线程池是一个基于生产者消费者模型的(一个交易场所+多个消费者)的一个对象
通过在内部维护一个线程容器来存放管理工作线程,维护一个任务队列作为交易场所
对外提供put接口供用户抛入任务,内部通过互斥锁和条件变量保证生产和生产的互斥,消费和消费的互斥,生产和消费的互斥与同步
即可实现一个线程池
2.框架
#pragma once
#include <future>
#include <thread>
#include <queue>
#include <vector>
#include <functional>
#include <mutex>
#include <condition_variable>
using namespace std;
class threadpool
{
// 所有函数类型都可以通过bind和lambda包装为void()类型的函数对象
using functor = function<void()>;
public:
threadpool(int thread_num)
: _threadnum(thread_num)
{
_thread_v.resize(_threadnum, thread(std::bind(&threadpool::take, this)));
}
//返回值类型:future<传入的函数返回值类型>
//通过decltype来实现类型推导,通过完美转发来保持引用原本的属性
template <class Fn, class... Args>
auto put(Fn &&fn, Args &&...args) -> future<decltype(fn(forward<Args>(args)...))>
{
}
private:
void take()
{
}
vector<thread> _thread_v;
queue<functor> _task_q;
mutex _mutex;
condition_variable _cv;
int _threadnum;
};
关于这个put函数,因为我们要实现异步操作,所以其必须返回对应函数返回值类型构成的future对象
可是我们不知道这个返回值类型是什么啊,所以返回值类型给auto,
但是只给auto作为返回值,代码可读性太差
因此后面用decltype来推导该返回值类型
中间别忘了用完美转发保持在右值引用原有的右值属性和左值引用本身的左值属性
别忘了加注释
//返回值类型:future<传入的函数返回值类型>
//通过decltype来实现类型推导,通过完美转发来保持引用原本的属性
template <class Fn, class... Args>
auto put(Fn &&fn, Args &&...args) -> future<decltype(fn(forward<Args>(args)...))>;
3.编写
#pragma once
#include <future>
#include <thread>
#include <queue>
#include <vector>
#include <functional>
#include <mutex>
#include <condition_variable>
using namespace std;
class threadpool
{
// 所有函数类型都可以通过bind和lambda包装为void()类型的函数对象
using functor = function<void()>;
public:
threadpool(int thread_num = 2)
: _threadnum(thread_num), _isrunning(true)
{
_thread_v.resize(_threadnum);
start();
}
~threadpool()
{
shutdown();
}
void start()
{
for (int i = 0; i < _threadnum; i++)
{
_thread_v[i] = thread(std::bind(&threadpool::take, this));
}
}
void shutdown()
{
if (!_isrunning)
return;
_isrunning = false;
_cv.notify_all();
for (auto &t : _thread_v)
t.join();
}
// 返回值类型:future<传入的函数返回值类型>
// 通过decltype来实现类型推导,通过完美转发来保持引用原本的属性
template <class Fn, class... Args>
auto put(Fn &&fn, Args &&...args) -> future<decltype(fn(forward<Args>(args)...))>
{
// 1. 绑定对应任务,将参数削减为0
auto func = bind(fn, forward<Args>(args)...);
// 2. 通过decltype获取对应返回值类型
using return_type = decltype(fn(forward<Args>(args)...));
// 3. 构建packaged_task对象
auto ptask_ptr = make_shared<packaged_task<return_type()>>(func);
// 4. 拿到future对象
future<return_type> fut = ptask_ptr->get_future();
// 5. 构建lambda表达式,削减返回值类型
// 传入ptask_ptr智能指针,将packaged_task对象管理起来,(它无法拷贝构造,因此只能通过传递智能指针来换一种方式进行'构造'),
//这里一定要是传值传参
/*
使用shared_ptr管理一些不能被拷贝的对象,因为shared_ptr能够被拷贝,
所以某种意义上,通过拷贝shared_ptr就能够实现不能被拷贝的一种资源共享(即:浅拷贝)
因此一个拷贝构造和拷贝赋值被删除了的对象,某种意义上讲,可以通过shared_ptr来实现一种“浅拷贝”的
*/
auto task = [ptask_ptr]()
{
(*ptask_ptr)(); };
// 6. 抛入任务队列当中
{
unique_lock<mutex> ulock(_mutex);
_task_q.push(task);
}
// 7. 通知一个线程
_cv.notify_one();
return fut;
}
private:
void take()
{
while (_isrunning)
{
functor task = nullptr;
// 1. 申请锁
{
unique_lock<mutex> ulock(_mutex);
// 2. 根据任务队列是否有任务决定是否需要等待条件变量
_cv.wait(ulock, [this]() -> bool
{
return !_task_q.empty() || !_isrunning; });
if (!_isrunning)
return;
// 返回true之后,才能从_cv中出去(防止伪唤醒)
// 注意:this指针无法被lambda使用引用来进行捕获
// 因为lambda支持拷贝构造,因此lambda可以通过创建副本在该函数外部存活
// 而如果引用捕捉this指针的话,那么在函数外部这个this指针就成为了野指针
// 因此lambda无法捕捉this
// 3.拿队首任务进行处理
task = _task_q.front();
_task_q.pop();
}
if (task != nullptr) // 代码的健壮性
task();
}
}
vector<thread> _thread_v;
queue<functor> _task_q;
mutex _mutex;
condition_variable _cv;
bool _isrunning;
int _threadnum;
};
#include "async_pool.hpp"
#include <iostream>
int add(int a, int b)
{
cout << "---------- 学徒 " << this_thread::get_id() << "执行任务" << endl;
return a + b;
}
int main()
{
threadpool pool;
pool.start();
future<int> f1 = pool.put(add, 1, 1);
future<int> f2 = pool.put(add, 2, 2);
future<int> f3 = pool.put(add, 3, 3);
future<int> f4 = pool.put(add, 4, 4);
cout << f1.get() << " " << f2.get() << " " << f3.get() << " " << f4.get() << "\n";
return 0;
}
这个线程池的代码注释已经很清楚了,大家肯定是能看懂的
需要注意的几点是:
- lambda不能引用捕捉this指针那里
- 使用shared_ptr实现不能被拷贝对象的一种“浅拷贝”(或者理解为延长作用域和生命周期)
- 这其实就是shared_ptr的一个非常好的作用:
shared_ptr允许我们跨多个作用域和函数边界安全地共享对象的所有权,同时自动管理对象的生命周期
以上就是项目第二弹:第三方工具选择与介绍、使用muduo库和protobuf搭建简易服务器和客户端、异步工作线程池实现的全部内容