手撕基于AMQP协议的简易消息队列-2(所用第三方库的介绍与简单使用)
第三方库的介绍
Protobuf
什么是Protobuf(Protocol Buffer)?
- Protobuf是数据结构序列化和反序列化框架
Protobuf的特点有哪些?
- 通用性:语⾔⽆关、平台⽆关。即 ProtoBuf ⽀持 Java、C++、Python 等多种语⾔,⽀持多个平台
- ⾼效:即⽐ XML 更⼩、更快、更为简单
- 扩展性、兼容性好:你可以更新数据结构,⽽不影响和破坏原有的旧程序
Protobuf的使用
-
使用流程:
- 编写后缀为.proto的文件,在该文件中定义结果对象及属性内容
- 使⽤ protoc 编译器编译 .proto ⽂件,⽣成⼀系列接⼝代码,存放在新⽣成头⽂件和源⽂件中
- 依赖⽣成的接⼝,将编译⽣成的头⽂件包含进我们的代码中,实现对 .proto ⽂件中定义的字段进⾏设置和获取,和对自定义对象进⾏序列化和反序列化
-
使用示例
-
.proto文件的编写,以学生类为例
//指定proto3语法,若没有指定,编译器会使用proto2语法 syntax = "proto3"; //Protocol Buffers 语⾔版本3,简称 proto3,是 .proto ⽂件最新的语法版本。proto3 简化了 Protocol Buffers 语⾔,既易于使⽤,⼜可以在更⼴泛的编程语⾔中使⽤。它允许你使⽤ Java,C++,Python 等多种语⾔⽣成 protocol buffer 代码。在 .proto ⽂件中,要使⽤ syntax = "proto3"; 来指定⽂件语法为 proto3,并且必须写在除去注释内容的第⼀⾏。 如果没有指定,编译器会使⽤proto2语法。package Student;//确定命名空间 //package 是⼀个可选的声明符,能表⽰ .proto ⽂件的命名空间,在项⽬中要有唯⼀性。它的作⽤是为了避免我们定义的消息出现冲突。message Student//自定义结构 {uint64 StudentNumber = 1;string StudentName = 2; }; //ProtoBuf 就是以 message 的⽅式来⽀持我们定制协议字段,后期帮助我们形成类和⽅法来使⽤。
-
.proto文件的编写规范:
- ⽂件命名应该使⽤全⼩写字⺟命名,多个字⺟之间⽤ _ 连接,如student.proto
- 书写 .proto ⽂件代码时,应使⽤ 2 个空格的缩进
-
定义消息字段;
-
字段定义格式为:字段类型 字段名 = 字段唯⼀编号;
-
字段名称命名规范:全⼩写字⺟,多个字⺟之间⽤ _ 连接
-
字段类型分为:标量数据类型 和 特殊类型(包括枚举、其他消息类型等)
.proto 类型 C++ 类型 说明 double
double
双精度浮点数 float
float
单精度浮点数 int32
int32
变长编码的32位整数。负数编码效率低,若字段可能为负值,建议改用 sint32
。int64
int64
变长编码的64位整数。负数编码效率低,若字段可能为负值,建议改用 sint64
。uint32
uint32
变长编码的无符号32位整数 uint64
uint64
变长编码的无符号64位整数 sint32
int32
变长编码的32位整数,ZigZag编码优化负数效率,适用于可能包含负值的场景。 sint64
int64
变长编码的64位整数,ZigZag编码优化负数效率,适用于可能包含负值的场景。 fixed32
uint32
定长4字节的无符号32位整数,数值常大于2^28时更高效(相比 uint32
)。fixed64
uint64
定长8字节的无符号64位整数,数值常大于2^56时更高效(相比 uint64
)。sfixed32
int32
定长4字节的有符号32位整数 sfixed64
int64
定长8字节的有符号64位整数 bool
bool
布尔值( true
/false
)string
std::string
UTF-8或ASCII编码字符串,长度不超过2^32。 bytes
std::string
任意二进制字节序列,长度不超过2^32(实际存储为 std::string
,但语义上区分于字符串)。enum
枚举类型 用户自定义的枚举类型,对应C++的 enum
或enum class
。message
类(class) 用户自定义的消息类型,对应C++中的类结构,可嵌套其他消息或枚举。 repeated
std::vector<T>
动态数组,存储相同类型的多个元素(例如 repeated int32
对应std::vector<int32>
)。map<K, V>
std::unordered_map<K, V>
键值对集合,键类型需为整数、布尔或字符串,值类型可为任意有效类型(例如 map<string, int32>
对应std::unordered_map<string, int32>
)。 -
字段唯⼀编号:⽤来标识字段,⼀旦开始使⽤就不能够再改变。
-
-
字段唯⼀编号的范围:1 ~ 536,870,911 (2^29 - 1) ,其中 19000 ~ 19999 不可⽤。19000 ~ 19999 不可⽤是因为:在 Protobuf 协议的实现中,对这些数进⾏了预留。如果⾮要在.proto⽂件中使⽤这些预留标识号,例如将 name 字段的编号设置为19000,编译时就会报警
-
-
编译.proto文件
protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.proto //将.proto文件编译为cpp文件的格式 // protoc 是 Protocol Buffer 提供的命令⾏编译⼯具。// --proto_path 指定 被编译的.proto⽂件所在⽬录,可多次指定。可简写成 -I IMPORT_PATH 。如不指定该参数,则在当前⽬录进⾏搜索。当某个.proto ⽂件 import 其他.proto ⽂件时,或需要编译的 .proto ⽂件不在当前⽬录下,这时就要⽤-I来指定搜索⽬录。// --cpp_out= 指编译后的⽂件为 C++ ⽂件。// OUT_DIR 编译后⽣成⽂件的⽬标路径。// path/to/file.proto 要编译的.proto⽂件
-
编译student.proto文件命令如下
protoc --cpp_out=./ ./student.proto
-
编译生成的cpp代码内容:
- 对于每个message,都会生成一个对应的消息类
- 在消息类中,编译器为每个字段提供了获取和设置⽅法,以及⼀下其他能够操作字段的⽅法
- 编辑器会针对于每个 .proto ⽂件⽣成 .h 和 .cc ⽂件,分别⽤来存放类的声明与类的实现student.pb.h、student.pb.cc 部分代码展⽰
-
-
序列化与反序列化的使用
-
示例程序
#include<iostream> #include"student.pb.h"int main() {//设置学生的属性Student::Student s1;s1.set_studentname("张三");s1.set_studentnumber(1000);//将学生进行序列化std::string str;str = s1.SerializeAsString();//将学生信息反序列化Student::Student ret;ret.ParseFromString(str);//输出结果std::cout<<ret.studentname()<<std::endl<<ret.studentnumber()<<std::endl;return 0; }
-
对程序编译并执行
g++ -o testStudent student.pb.cc test_student.cc -std=c++11 -lprotobuf//-lprotobuf:链接protobuf库⽂件 -std=c++11:⽀持C++11
-
程序结果
-
-
Muduo库的介绍
什么是Muduo库?
-
Muduo由陈硕⼤佬开发,是⼀个基于⾮阻塞IO和事件驱动的C++⾼并发TCP⽹络编程库。 它是⼀款基于主从Reactor模型的⽹络库,其使⽤的线程模型是one loop per thread, 所谓one loop per thread指的是:
-
⼀个线程只能有⼀个事件循环(EventLoop), ⽤于响应计时器和IO事件
-
⼀个⽂件描述符只能由⼀个线程进⾏读写,换句话说就是⼀个TCP连接必须归属于某个EventLoop管理
-
-
Muduo库常用接⼝介绍
-
muduo::net::TcpServer类基础介绍
// TcpConnection 的共享指针类型别名,用于安全地管理 TcpConnection 对象的生命周期 typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;// 连接事件回调类型(连接建立/关闭时触发) // 参数:const TcpConnectionPtr& 表示关联的 TCP 连接对象 typedef std::function<void (const TcpConnectionPtr&)> ConnectionCallback;// 消息到达回调类型(当接收到数据时触发) // 参数: // - const TcpConnectionPtr&: 关联的 TCP 连接对象 // - Buffer*: 指向接收缓冲区的指针,存储待处理的字节数据 // - Timestamp: 消息到达的时间戳(通常为系统时间或相对时间) typedef std::function<void (const TcpConnectionPtr&,Buffer*,Timestamp)> MessageCallback;// IP 地址封装类(继承自可 基类,允许对象拷贝) // 用途:表示一个网络端点(IP + 端口),支持 IPv4/IPv6 class InetAddress : public muduo::copyable {public:// 构造函数// 参数:// - ip: 字符串形式的 IP 地址(如 "192.168.1.1" 或 "2001:db8::1")// - port: 16 位端口号// - ipv6: 是否为 IPv6 地址(默认 false,即 IPv4)InetAddress(StringArg ip, uint16_t port, bool ipv6 = false); };// TCP 服务器类(继承自不可 基类,禁止对象拷贝) // 功能:管理 TCP 连接监听、处理客户端连接和消息收发 class TcpServer : noncopyable {public:// 端口复用选项枚举enum Option {kNoReusePort, // 不启用 SO_REUSEPORT(默认)kReusePort, // 启用 SO_REUSEPORT,允许多进程/线程绑定同一端口};// 构造函数// 参数:// - loop: EventLoop 事件循环对象,用于驱动网络事件处理// - listenAddr: 服务器监听的地址和端口// - nameArg: 服务器名称标识(用于日志或调试)// - option: 端口复用选项(默认 kNoReusePort)TcpServer(EventLoop* loop,const InetAddress& listenAddr,const string& nameArg,Option option = kNoReusePort);// 设置 IO 线程池的大小(用于处理连接的线程数)// 注:通常设置为 CPU 核心数,0 表示所有操作在 EventLoop 线程执行void setThreadNum(int numThreads); // 启动服务器,开始监听并处理连接void start(); // 设置新连接建立/关闭时的回调函数void setConnectionCallback(const ConnectionCallback& cb){ connectionCallback_ = cb; } // 赋值给成员变量 connectionCallback_// 设置接收到消息时的业务处理回调函数void setMessageCallback(const MessageCallback& cb){ messageCallback_ = cb; } // 赋值给成员变量 messageCallback_
-
muduo::net::EventLoop类基础介绍
// 事件循环类(不可 ,遵循 one loop per thread 设计) // 核心职责:驱动 IO 事件监听、定时器调度、跨线程任务执行 class EventLoop : noncopyable {public:/// 启动事件循环(无限循环,阻塞直到调用 quit())/// **必须**在创建该对象的线程中调用void loop();/// 终止事件循环/// **非完全线程安全**:通过裸指针调用时需同步,建议通过 shared_ptr 调用确保安全void quit();/// 在指定时间点运行定时器回调/// 参数:/// - time: 触发时间戳(基于 Timestamp 的时序)/// - cb: 定时器触发时的回调函数/// 返回值:TimerId 用于后续取消定时器/// **线程安全**:可从其他线程调用TimerId runAt(Timestamp time, TimerCallback cb);/// 在延迟指定秒数后运行回调/// 参数:/// - delay: 延迟时间(单位:秒)/// - cb: 延迟结束后的回调函数/// 返回值:TimerId/// **线程安全**:可从其他线程调用TimerId runAfter(double delay, TimerCallback cb);/// 每隔指定间隔重复运行回调/// 参数:/// - interval: 间隔时间(单位:秒)/// - cb: 每次触发时的回调函数/// 返回值:TimerId/// **线程安全**:可从其他线程调用TimerId runEvery(double interval, TimerCallback cb);/// 取消指定定时器/// 参数:/// - timerId: 由 runAt/runAfter/runEvery 返回的 ID/// **线程安全**:可从其他线程调用void cancel(TimerId timerId);private:std::atomic<bool> quit_; // 原子标志位,控制循环退出std::unique_ptr<Poller> poller_; // 事件监听器(如 epoll/kqueue)mutable MutexLock mutex_; // 互斥锁,保护 pendingFunctors_std::vector<Functor> pendingFunctors_ GUARDED_BY(mutex_); // 跨线程提交的待执行任务队列 };
-
muduo::net::TcpConnection类基础介绍
/*** TCP 连接核心类(不可 ,支持共享指针安全访问)* 职责:封装已建立的 TCP 连接,管理连接生命周期及事件处理* 设计特点:基于 Reactor 模式,通过 Channel 监听 socket 事件,实现非阻塞 IO*/ class TcpConnection : noncopyable, // 禁止拷贝语义 public std::enable_shared_from_this<TcpConnection> // 支持安全共享指针[6,7] {public:/*** 构造函数(由 TcpServer/TcpClient 内部调用,用户不应直接创建)* @param loop 所属事件循环(sub-Reactor)* @param nameArg 连接名称标识(用于日志)* @param sockfd 已连接的套接字描述符* @param localAddr 本地端点地址* @param peerAddr 对端端点地址* 初始化流程:* 1. 创建 Socket 和 Channel 对象[2,4]* 2. 注册 Channel 的事件回调(读->handleRead,写->handleWrite)[2,4]* 3. 设置初始状态为 kConnecting*/TcpConnection(EventLoop* loop,const string& name,int sockfd,const InetAddress& localAddr,const InetAddress& peerAddr);/// 连接状态查询(线程安全)bool connected() const { return state_ == kConnected; } // 已建立连接bool disconnected() const { return state_ == kDisconnected; } // 已断开/// 数据发送接口(支持多种数据格式,线程安全)void send(string&& message); // C++11 移动语义优化void send(const void* message, int len); // 原始数据指针void send(const StringPiece& message); // 字符串片段void send(Buffer* message); // 交换缓冲区所有权/// 关闭连接(非线程安全,需避免并发调用)void shutdown();/// 上下文数据存取(支持任意类型扩展)void setContext(const boost::any& context) { context_ = context; }const boost::any& getContext() const { return context_; }boost::any* getMutableContext() { return &context_; }/// 回调函数设置接口void setConnectionCallback(const ConnectionCallback& cb) { connectionCallback_ = cb; } // 连接状态变化回调void setMessageCallback(const MessageCallback& cb) { messageCallback_ = cb; } // 消息到达回调private:/// 连接状态机(原子操作保障线程安全)enum StateE { kDisconnected, // 初始/已断开状态kConnecting, // 正在建立连接(Acceptor 创建后)kConnected, // 已建立连接(Channel 注册到 Poller)kDisconnecting // 正在关闭连接(主动调用 shutdown)};// 核心成员变量EventLoop* loop_; // 所属事件循环(保证IO操作线程安全)ConnectionCallback connectionCallback_; // 连接建立/关闭回调MessageCallback messageCallback_; // 数据到达回调WriteCompleteCallback writeCompleteCallback_; // 发送完成回调(需额外设置)[1]boost::any context_; // 用户自定义上下文(如会话ID) };
-
muduo::net::TcpClient类基础介绍
/*** TCP 客户端核心类(不可 ),基于 muduo 网络库设计* 职责:管理客户端与服务器的单条 TCP 连接,实现异步连接与消息处理* 设计特点:通过 Connector 发起连接,连接成功后创建 TcpConnection 管理会话*/ class TcpClient : noncopyable {public:/*** 构造函数* @param loop 事件循环对象(sub-Reactor)* @param serverAddr 服务器地址(IP + 端口)* @param nameArg 客户端名称标识(用于日志)* 初始化流程:* 1. 创建 Connector 对象发起异步连接* 2. 连接成功后创建 TcpConnection 对象*/TcpClient(EventLoop* loop,const InetAddress& serverAddr,const string& nameArg);/// 强制显式定义析构函数(因包含 unique_ptr 成员)~TcpClient();/// 发起异步连接(线程安全,可在任意线程调用)void connect();/// 关闭连接(非线程安全,需在 IO 线程调用)void disconnect();/// 停止客户端(停止重连机制)void stop();/// 获取当前连接对象(需加锁保证线程安全)TcpConnectionPtr connection() const {MutexLockGuard lock(mutex_); // 互斥锁保护连接对象return connection_;}// 回调函数设置接口void setConnectionCallback(ConnectionCallback cb) { connectionCallback_ = std::move(cb); } // 连接状态变化回调void setMessageCallback(MessageCallback cb) { messageCallback_ = std::move(cb); } // 消息到达回调private:EventLoop* loop_; // 所属事件循环(sub-Reactor)ConnectionCallback connectionCallback_; MessageCallback messageCallback_;TcpConnectionPtr connection_ GUARDED_BY(mutex_); // 当前连接(受互斥锁保护) };/*** 倒计时闩锁类(不可 ),用于线程间同步控制* 用途:确保异步操作完成前阻塞等待(如等待 TCP 连接建立)* 原理:基于条件变量实现的线程同步原语*/ class CountDownLatch : noncopyable {public:/// 初始化计数器(count > 0)explicit CountDownLatch(int count);/// 阻塞等待直到计数器归零(线程安全)void wait() {MutexLockGuard lock(mutex_);while (count_ > 0) { // 避免虚假唤醒condition_.wait(); // 条件变量等待}}/// 计数器减一(线程安全),归零时唤醒所有等待线程void countDown() {MutexLockGuard lock(mutex_);--count_;if (count_ == 0) {condition_.notifyAll(); // 广播唤醒}}private:mutable MutexLock mutex_; // 互斥锁Condition condition_ GUARDED_BY(mutex_); // 条件变量int count_ GUARDED_BY(mutex_); // 计数器 };
-
muduo::net::Buffer类基础介绍
/*** 网络数据缓冲区类(支持高效拷贝)* 设计目标:提供高效的读写操作,避免内存拷贝,支持协议解析常用操作* 特性:* - 预留头部空间(kCheapPrepend)便于添加协议头* - 自动扩容机制保证写入空间* - 提供网络字节序与主机字节序转换接口*/ class Buffer : public muduo::copyable { public:static const size_t kCheapPrepend = 8; ///< 头部预留空间(用于添加协议头等场景)static const size_t kInitialSize = 1024; ///< 初始缓冲区大小(不包含预留空间)/*** 构造函数* @param initialSize 初始可用空间大小(默认1024字节)* 总缓冲区大小 = kCheapPrepend + initialSize*/explicit Buffer(size_t initialSize = kInitialSize);/// 交换两个缓冲区内容(高效无拷贝)void swap(Buffer& rhs);/// 获取可读数据字节数size_t readableBytes() const { return writerIndex_ - readerIndex_; }/// 获取可写空间字节数size_t writableBytes() const { return buffer_.size() - writerIndex_; }/// 获取指向可读数据的指针(不会消费数据)const char* peek() const { return begin() + readerIndex_; }/// 查找第一个CRLF("\r\n")位置,用于行协议解析const char* findEOL() const;const char* findEOL(const char* start) const;/// 消费指定长度数据(移动读指针)void retrieve(size_t len);/// 消费整型数据(自动转换网络字节序到主机字节序)void retrieveInt64() { retrieve(sizeof(int64_t)); }void retrieveInt32() { retrieve(sizeof(int32_t)); }void retrieveInt16() { retrieve(sizeof(int16_t)); }void retrieveInt8() { retrieve(sizeof(int8_t)); }/// 获取并消费所有可读数据为字符串string retrieveAllAsString();/// 获取并消费指定长度数据为字符串string retrieveAsString(size_t len);/// 追加数据到缓冲区(自动扩容)void append(const StringPiece& str);void append(const char* data, size_t len);void append(const void* data, size_t len);/// 获取可写空间起始指针(写前需确保足够空间)char* beginWrite() { return begin() + writerIndex_; }const char* beginWrite() const { return begin() + writerIndex_; }/// 确认已写入数据长度(移动写指针)void hasWritten(size_t len) { writerIndex_ += len; }/// 追加整型数据(自动转换主机字节序到网络字节序)void appendInt64(int64_t x);void appendInt32(int32_t x);void appendInt16(int16_t x);void appendInt8(int8_t x);/// 读取并消费整型数据(自动转换网络字节序)int64_t readInt64();int32_t readInt32();int16_t readInt16();int8_t readInt8();/// 查看整型数据(不移动读指针)int64_t peekInt64() const;int32_t peekInt32() const;int16_t peekInt16() const;int8_t peekInt8() const;/// 在头部预留空间添加数据(常用于添加协议头)void prependInt64(int64_t x);void prependInt32(int32_t x);void prependInt16(int16_t x);void prependInt8(int8_t x);void prepend(const void* data, size_t len);private:std::vector<char> buffer_; ///< 数据存储容器size_t readerIndex_; ///< 读指针(指向可读数据起始位置)size_t writerIndex_; ///< 写指针(指向可写空间起始位置)static const char kCRLF[]; ///< 行结束符常量"\r\n"/// 获取缓冲区首地址char* begin() { return &*buffer_.begin(); }const char* begin() const { return &*buffer_.begin(); }/// 空间不足时自动扩容(保证至少有len字节可写空间)void ensureWritableBytes(size_t len); };// 示例用法: // Buffer buf; // buf.appendInt32(123); // 写入4字节网络序整数 // int32_t n = buf.readInt32(); // 读取并转换为主机序 // buf.prependInt32(0xdeadbeef); // 在头部插入魔数
-
Muduo库的快速上手
-
使用Muduo库搭建一个简单的英译汉服务器和客户端
-
大体流程:
一、服务端搭建流程(TranslateServer)
-
初始化事件循环与服务器
EventLoop loop; // 创建主事件循环 InetAddress listenAddr(8888); // 监听端口 TcpServer server(&loop, listenAddr, "TranslateServer");
-
注册回调函数
// 连接建立/关闭回调 server.setConnectionCallback(std::bind(&onConnection, ...)); // 消息到达回调 server.setMessageCallback(std::bind(&onMessage, ...));
-
启动服务
server.start(); // 启动监听线程 loop.loop(); // 进入事件循环(阻塞在此)
-
实现核心回调函数
void onConnection(const TcpConnectionPtr& conn) {// 连接状态变化处理if (conn->connected()) { /* 记录新连接 */ }else { /* 清理连接资源 */ } }void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp) {string msg = buf->retrieveAllAsString(); // 读取数据string result = translate(msg); // 业务处理conn->send(result); // 发送响应 }
二、客户端搭建流程(TranslateClient)
-
创建IO线程与客户端
EventLoopThread ioThread; // 创建IO线程 TcpClient client(ioThread.startLoop(), serverAddr, "Client");
-
设置回调函数
client.setConnectionCallback(std::bind(&onClientConnection, ...)); client.setMessageCallback(std::bind(&onClientMessage, ...));
-
连接服务器
client.connect(); // 异步连接 latch.wait(); // 等待连接建立(使用CountDownLatch)
-
实现客户端回调
void onClientConnection(const TcpConnectionPtr& conn) {if (conn->connected()) {latch_.countDown(); // 连接成功通知conn_ = conn; // 保存连接对象} }void onClientMessage(const TcpConnectionPtr&, Buffer* buf, Timestamp) {cout << "收到响应: " << buf->retrieveAllAsString() << endl; }
-
发送请求
conn_->send("hello"); // 使用保存的连接对象发送数据
三、关键机制说明
-
Reactor模型
-
单线程处理所有IO事件(用户代码无需考虑锁)
-
EventLoop::loop()
驱动事件分发
-
-
回调机制
通过std::bind
绑定成员函数,处理三类事件:-
连接建立/断开
-
消息到达
-
写完成通知(示例未使用)
-
-
Buffer设计
-
自动管理读写指针(
retrieveAllAsString()
消费数据) -
支持高效预置空间(处理协议头)
-
-
线程安全
-
客户端
send()
需在IO线程调用(示例用CountDownLatch
同步) -
服务端无需显式锁(muduo保证回调在IO线程)
-
-
-
音译汉TCP服务器代码
#include "muduo/net/EventLoop.h" #include "muduo/net/TcpConnection.h" #include "muduo/net/TcpServer.h" #include <functional> #include <iostream> #include <sys/types.h> #include <unordered_map>/*** 基于muduo网络库的简易翻译服务器* 功能:接收客户端英文单词,返回中文翻译(示例实现简单字典查询)*/ class TranslateServer { public:/*** 构造函数* @param port 服务器监听端口号* 初始化流程:* 1. 创建事件循环对象(_baseloop)* 2. 创建TCP服务器对象(_server),绑定全零地址(0.0.0.0)表示监听所有网络接口* 3. 设置连接回调与消息回调*/TranslateServer(uint16_t port): _server(&_baseloop, // 事件循环对象muduo::net::InetAddress("0.0.0.0", port), // 监听地址(IPv4任意地址)"TranslateServer", // 服务器名称(用于日志标识)muduo::net::TcpServer::kReusePort) // 启用端口复用选项(允许多线程监听相同端口){// 绑定连接状态变化回调(lambda通过std::bind转换为函数对象)_server.setConnectionCallback(std::bind(&TranslateServer::ConnectionCallback, this, std::placeholders::_1));// 绑定消息到达回调(参数占位符分别表示连接对象、缓冲区、时间戳)_server.setMessageCallback(std::bind(&TranslateServer::MessageCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));}/// 启动服务器(开始监听并进入事件循环)void Start(){_server.start(); // 启动监听线程_baseloop.loop(); // 进入主事件循环(阻塞在此处)}private:/*** 连接状态变化回调函数* @param connection TCP连接智能指针* 触发时机:当新连接建立或现有连接关闭时*/void ConnectionCallback(const muduo::net::TcpConnectionPtr &connection){if (connection->connected()) {std::cout << "新连接建立 [" << connection->peerAddress().toIpPort() << "]" << std::endl;} else {std::cout << "连接关闭 ["<< connection->peerAddress().toIpPort() << "]" << std::endl;}}/*** 消息到达回调函数(核心业务逻辑)* @param connection TCP连接对象* @param buffer 接收缓冲区(存储客户端发送的数据)* @param timestamp 消息到达时间戳* 处理流程:* 1. 从缓冲区提取全部数据为字符串* 2. 调用翻译函数获取结果* 3. 通过TCP连接返回翻译结果*/void MessageCallback(const muduo::net::TcpConnectionPtr &connection,muduo::net::Buffer *buffer,muduo::Timestamp timestamp){// 提取缓冲区所有数据(自动移动读指针)std::string request = buffer->retrieveAllAsString(); // 执行翻译(示例实现为内存字典查询)std::string answer = translate(request);// 返回结果给客户端(send方法线程安全)if (!answer.empty()) {connection->send(answer + "\r\n"); // 添加换行符作为消息分隔符} else {connection->send("未找到翻译: " + request + "\r\n");}}/*** 简单字典翻译函数(实际项目应替换为数据库查询或API调用)* @param str 待翻译的英文单词* @return 中文翻译结果,未找到返回空字符串* 特点:* - 使用静态哈希表减少重复初始化开销* - 当前仅支持全小写单词查询*/const std::string translate(const std::string &str){// 静态字典(初始化一次,后续查询共享)static std::unordered_map<std::string, std::string> dictionary{{"hello", "你好"}, {"love", "爱"},{"computer", "计算机"},{"server", "服务器"}};auto it = dictionary.find(str);return (it != dictionary.end()) ? it->second : "";}private:muduo::net::EventLoop _baseloop; // 主事件循环对象(Reactor核心)muduo::net::TcpServer _server; // TCP服务器实例(管理监听和连接) };int main() {// 创建服务器实例(监听8888端口)TranslateServer tmpserver(8888);std::cout << "翻译服务器启动,监听端口:8888" << std::endl;// 启动服务(阻塞在此处直到程序终止)tmpserver.Start();return 0; }
-
音译汉客户端代码
#include "muduo/base/CountDownLatch.h" #include "muduo/net/EventLoopThread.h" #include "muduo/net/TcpClient.h" #include "muduo/net/TcpConnection.h" #include <functional> #include <iostream> #include <sys/types.h>/*** 基于muduo网络库的翻译客户端* 功能:连接翻译服务器,发送英文单词并接收中文翻译结果*/ class TanslateClient // 注意类名拼写建议改为 TranslateClient { public:/*** 构造函数* @param ip 服务器IP地址(默认本地环回)* @param port 服务器端口(默认8888)* 初始化流程:* 1. 创建事件循环线程(_loopthread)* 2. 创建TCP客户端(_client),绑定到指定服务器地址* 3. 设置连接和消息回调*/TanslateClient(const std::string &ip = "127.0.0.1", const int &port = 8888): _loopthread(), // 事件循环线程(自动启动)_client(_loopthread.startLoop(), // 获取事件循环对象(启动IO线程)muduo::net::InetAddress(ip, port), // 服务器地址"TanslateClient" // 客户端名称(日志标识)),_count_down_lantch(1), // 连接同步闩锁(初始计数器1)_conn_ptr(nullptr) // 当前连接指针(线程安全需改进){// 绑定连接状态变化回调_client.setConnectionCallback(std::bind(&TanslateClient::ConnectionCallback, this, std::placeholders::_1));// 绑定消息到达回调_client.setMessageCallback(std::bind(&TanslateClient::MessageCallback, this,std::placeholders::_1, // TcpConnectionPtrstd::placeholders::_2, // Buffer*std::placeholders::_3 // Timestamp));}/// 连接到服务器(阻塞直到连接建立或失败)void Connect(){_client.connect(); // 发起异步连接_count_down_lantch.wait(); // 阻塞等待连接成功信号}/// 发送翻译请求到服务器(线程安全)void Send(const std::string &buffer){// 需添加互斥锁保护_conn_ptr访问(当前实现存在线程安全隐患)if (_conn_ptr && _conn_ptr->connected()) {_conn_ptr->send(buffer + "\r\n"); // 添加消息分隔符} else {std::cerr << "连接未就绪,发送失败" << std::endl;}}private:/*** 连接状态变化回调* @param conn TCP连接智能指针* 功能:处理连接建立/断开事件,更新连接状态*/void ConnectionCallback(const muduo::net::TcpConnectionPtr &conn){_conn_ptr = conn; // 需用互斥锁保护(当前非线程安全)if (conn->connected()) {std::cout << "成功连接服务器 [" << conn->peerAddress().toIpPort() << "]" << std::endl;_count_down_lantch.countDown(); // 释放等待线程} else {std::cout << "与服务器断开连接" << std::endl;// 建议添加自动重连逻辑}}/*** 消息到达回调* @param buffer 接收缓冲区(包含服务器响应)* 功能:处理服务器返回的翻译结果*/void MessageCallback(const muduo::net::TcpConnectionPtr &conn,muduo::net::Buffer *buffer,muduo::Timestamp timestamp){// 提取并打印所有接收数据std::cout << "[翻译结果] " << buffer->retrieveAllAsString() // 清空缓冲区<< std::endl;}private:muduo::net::EventLoopThread _loopthread; // 事件循环线程(IO线程)muduo::net::TcpClient _client; // TCP客户端实例muduo::net::TcpConnectionPtr _conn_ptr; // 当前连接(需线程安全访问)muduo::CountDownLatch _count_down_lantch; // 连接同步闩锁 };int main() {// 创建客户端实例(连接指定服务器)TanslateClient myclient("112.74.40.147", 8888);try {myclient.Connect(); // 阻塞直到连接建立// 交互式发送模式while (true) {std::cout << "请输入英文单词(输入quit退出): ";std::string input;std::cin >> input;if (input == "quit") break;myclient.Send(input); // 发送查询请求}} catch (const std::exception& e) {std::cerr << "客户端异常: " << e.what() << std::endl;}return 0; }
-
使用Makefile文件进行编译(记得更改为自己的路径)
# 默认构建目标(当直接运行make时,会构建这两个目标) all: TranslateServer TanslateClient# 构建客户端可执行文件 TanslateClient(注意目标名称可能存在拼写错误,建议改为TranslateClient) # 依赖关系:需要 TanslateClient.cpp 文件 TanslateClient: TanslateClient.cpp# 编译命令说明:# -o $@ : 输出文件名为目标名称(即TanslateClient)# $^ : 输入文件为所有依赖项(即TanslateClient.cpp)# -std=c++11 : 使用C++11标准# -I ../include : 添加头文件搜索路径(指向上级目录的include文件夹)# -L ../lib : 添加库文件搜索路径(指向上级目录的lib文件夹)# -lmuduo_net -lmuduo_base : 链接muduo网络库和基础库# -lpthread : 链接pthread线程库g++ -o $@ $^ -std=c++11 -I ../include -L ../lib -lmuduo_net -lmuduo_base -lpthread# 构建服务端可执行文件 TranslateServer # 依赖关系:需要 TranslateServer.cpp 文件 TranslateServer: TranslateServer.cpp# 参数说明同上g++ -o $@ $^ -std=c++11 -I ../include -L ../lib -lmuduo_net -lmuduo_base -lpthread# 声明伪目标(防止存在同名文件时make误判) # 注:原写法不够规范,建议改为 .PHONY: all clean .PHONY: # 清理生成的可执行文件 clean:# 强制删除客户端和服务端可执行文件# 风险提示:rm -rf 需谨慎使用rm -rf TanslateClient TranslateServer
-
运行结果
-
服务器
-
客户端
-
-
-
基于Muduo库函数实现protobuf协议的通信
-
这里需要用到Muduo库里自带的协议处理器ProtobufCodec,以及自带的请求分发器ProtobufDispatcher,与前面我们使用自己制定的协议不同,在使用Protobuf进行通信时,我们在 设置服务器接受消息时调用的回调函数时(_server.setMessageCallback())并不是我们自己设定的消息回调函数了,而是使用协议处理器ProtobufCodec中的ProtobufCodec::onMessage()函数。也就是说服务器接收到消息之后,立即传给协议处理器ProtobufCodec,由它进行解析后,传给请求分发器ProtobufDispatcher去找到对应的消息处理函数,对应的函数处理完请求后,通过_codec.send(conn, resp);将结果发回给协议处理器,经过协议处理再发给客户端。
-
客户端的接受服务器的Protobuf请求流程与服务器差不多,这里就不过多赘述
-
定义具体的业务请求类型(也就是编写request.proto文件)
// 定义协议版本和包名 syntax = "proto3"; // 使用proto3语法 package HQJ; // 包命名空间(防⽌命名冲突)// 翻译服务请求消息(客户端 -> 服务端) message tanslate_request { string word = 1; // 待翻译的单词(如:输入"hello") };// 翻译服务响应消息(服务端 -> 客户端) message tanslate_respond { string word = 1; // 翻译结果(如:返回"你好") };// 加法计算请求消息(客户端 -> 服务端) message add_request {uint64 a = 1; // 第⼀个操作数(示例值:100)uint64 b = 2; // 第⼆个操作数(示例值:200) };// 加法计算响应消息(服务端 -> 客户端) message add_respond {uint64 result = 1; // 计算结果(示例值:300) };
-
编译request.proto文件
protoc --cpp_out=./ ./request.proto
-
编写服务端代码
#include "muduo/protobuf/codec.h" #include "muduo/protobuf/dispatcher.h" #include "muduo/base/Logging.h" #include "muduo/base/Mutex.h" #include "muduo/net/EventLoop.h" #include "muduo/net/TcpServer.h" #include "request.pb.h" // 包含protobuf生成的头文件 #include <iostream> // 使用muduo库中的noncopyable类禁止拷贝 class protobuf_server : public muduo::noncopyable { public: // 定义protobuf消息的智能指针类型 typedef std::shared_ptr<HQJ::add_request> add_request_ptr; typedef std::shared_ptr<HQJ::add_respond> add_respond_ptr; typedef std::shared_ptr<HQJ::tanslate_request> tanslate_request_ptr; typedef std::shared_ptr<HQJ::tanslate_respond> tanslate_respond_ptr; // 构造函数,初始化服务器和消息处理逻辑 protobuf_server(int port = 8888) : _server(&_loop, muduo::net::InetAddress("0.0.0.0", port), "protobuf_server", muduo::net::TcpServer::kReusePort), // 注册未知消息处理函数 _dispatcher(std::bind(&protobuf_server::onUnknownMessage, 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<HQJ::tanslate_request>( std::bind(&protobuf_server::onTranslate, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<HQJ::add_request>( std::bind(&protobuf_server::onAdd, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); // 设置连接回调 _server.setConnectionCallback( std::bind(&protobuf_server::onConnection, this, std::placeholders::_1)); // 设置消息回调,用于处理接收到的消息 _server.setMessageCallback( std::bind(&ProtobufCodec::onMessage, &_codec, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); } // 启动服务器 void start() { _server.start(); _loop.loop(); } private: // 简单的翻译函数 const std::string translate(const std::string &str) { static std::unordered_map<std::string, std::string> dictionary{{"hello", "你好"}, {"love", "爱"}}; auto cur = dictionary.find(str); if (cur == dictionary.end()) { return ""; } else { return cur->second; } } // 未知消息处理函数 void onUnknownMessage(const muduo::net::TcpConnectionPtr &conn, const MessagePtr &message, muduo::Timestamp) { LOG_INFO << "onUnknownMessage: " << message->GetTypeName(); conn->shutdown(); } // 处理翻译请求 void onTranslate(const muduo::net::TcpConnectionPtr &conn, const tanslate_request_ptr&message, muduo::Timestamp) { std::string request = message->word(); std::string res = translate(request); HQJ::tanslate_respond resp; resp.set_word(res); _codec.send(conn, resp); } // 处理加法请求 void onAdd(const muduo::net::TcpConnectionPtr &conn, const add_request_ptr &message, muduo::Timestamp) { int num1 = message->a(); int num2 = message->b(); int result = num1 + num2; HQJ::add_respond resp; resp.set_result(result); _codec.send(conn, resp); } // 连接回调 void onConnection(const muduo::net::TcpConnectionPtr &connection) { if (connection->connected()) { std::cout << "创建新的连接!" << std::endl; } else { std::cout << "关闭连接!" << std::endl; } } private: muduo::net::EventLoop _loop; // 事件循环 muduo::net::TcpServer _server; // TCP服务器 ProtobufDispatcher _dispatcher; // 请求分发器 ProtobufCodec _codec; // Protobuf协议编解码器 }; int main() { protobuf_server prot_server(8888); prot_server.start(); return 0; }
-
编写客户端代码
#include "muduo/base/CountDownLatch.h" #include "muduo/base/Logging.h" #include "muduo/base/Mutex.h" #include "muduo/net/EventLoop.h" #include "muduo/net/EventLoopThread.h" #include "muduo/net/TcpClient.h" #include "muduo/protobuf/codec.h" #include "muduo/protobuf/dispatcher.h"#include "request.pb.h" #include <iostream>class Client {public:typedef std::shared_ptr<google::protobuf::Message> MessagePtr;typedef std::shared_ptr<HQJ::add_respond> add_respondPtr;typedef std::shared_ptr<HQJ::tanslate_respond> tanslate_respondPtr;Client(const std::string &sip, int sport) : _latch(1),_client(_loopthread.startLoop(), muduo::net::InetAddress(sip, sport), "Client"),_dispatcher(std::bind(&Client::onUnknownMessage, 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<HQJ::tanslate_respond>(std::bind(&Client::onTranslate, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<HQJ::add_respond>(std::bind(&Client::onAdd, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));// 设置客户端接收到消息时调用的函数,交给协议处理机中的消息回调函数,不用我们自己写了_client.setMessageCallback(std::bind(&ProtobufCodec::onMessage, &_codec,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));// 设置客户端连接成功时调用的函数,这个是需要自己写的_client.setConnectionCallback(std::bind(&Client::onConnection, this, std::placeholders::_1));}void connect(){_client.connect();_latch.wait(); // 阻塞等待,直到连接建立成功}//制造并发送一个翻译请求void Translate(const std::string &msg){HQJ::tanslate_request req;req.set_word(msg);send(&req);}//制造并发送一个加法请求void Add(int num1, int num2){HQJ::add_request req;req.set_a(num1);req.set_b(num2);send(&req);}private:// 连接时调用void onConnection(const muduo::net::TcpConnectionPtr &conn){if (conn->connected()){_latch.countDown(); // 唤醒主线程中的阻塞_conn = conn;}else{// 连接关闭时的操作_conn.reset();}}bool send(const google::protobuf::Message *message){if (_conn->connected()){ // 连接状态正常,再发送,否则就返回false_codec.send(_conn, *message);return true;}return false;}void onUnknownMessage(const muduo::net::TcpConnectionPtr &,const MessagePtr &message,muduo::Timestamp){LOG_INFO << "onUnknownMessage: " << message->GetTypeName();}void onTranslate(const muduo::net::TcpConnectionPtr &conn, const tanslate_respondPtr &message, muduo::Timestamp){std::cout << "翻译结果:" << message->word() << std::endl;}void onAdd(const muduo::net::TcpConnectionPtr &conn, const add_respondPtr &message, muduo::Timestamp){std::cout << "加法结果:" << message->result() << std::endl;}private:muduo::CountDownLatch _latch; // 实现同步的muduo::net::EventLoopThread _loopthread; // 异步循环处理线程muduo::net::TcpConnectionPtr _conn; // 客户端对应的连接muduo::net::TcpClient _client; // 客户端ProtobufDispatcher _dispatcher; // 请求分发器ProtobufCodec _codec; // 协议处理器 };int main() {Client client("127.0.0.1", 8888);client.connect();client.Translate("hello");client.Add(11, 22);sleep(1);return 0; }
-
使用Makefile编译
# 默认构建目标(当直接运行make时,会构建client和server两个目标) all: client server# 构建客户端可执行文件 # 依赖文件:protobuf_client.cpp + request.pb.cc + muduo的protobuf编解码器实现 # 编译说明: # -std=c++11 : 使用C++11标准 # $^ : 表示所有依赖文件(即冒号后的全部文件) # -o $@ : 输出文件名为目标名称(即client) # -I../include : 添加头文件搜索路径(指向muduo头文件目录) # -L../lib : 添加库文件搜索路径(指向muduo库目录) # -l参数链接的库:muduo网络库/基础库、protobuf库、zlib压缩库 # -pthread : 启用POSIX线程支持 client: protobuf_client.cpp request.pb.cc ../include/muduo/protobuf/codec.ccg++ -std=c++11 $^ -o $@ -I../include -L../lib -lmuduo_net -lmuduo_base -pthread -lprotobuf -lz# 构建服务端可执行文件(参数含义与客户端相同) server: protobuf_server.cpp request.pb.cc ../include/muduo/protobuf/codec.ccg++ -std=c++11 $^ -o $@ -I../include -L../lib -lmuduo_net -lmuduo_base -pthread -lprotobuf -lz# 声明伪目标 .PHONY:# 清理生成的可执行文件 clean:rm -rf client server # 强制删除客户端和服务端可执行文件
-
执行结果
-
客户端
-
服务端
-
-
SQLlite3的快速上手
SQLite3的官方文档
https://www.sqlite.org/c3ref/funclist.html
SQLite3是什么?
- SQLite是⼀个进程内的轻量级数据库,它实现了⾃给⾃⾜的、⽆服务器的、零配置的、事务性的 SQL数据库引擎。它是⼀个零配置的数据库,这意味着与其他数据库不⼀样,我们不需要在系统中配置。像其他数据库,SQLite 引擎不是⼀个独⽴的进程,可以按应⽤程序需求进⾏静态或动态连接,SQLite直接访问其存储⽂件
为什么选择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 C/C++ API介绍
sqlite3操作流程:0. 查看当前数据库在编译阶段是否启动了线程安全int sqlite3_threadsafe(); 0-未启⽤; 1-启⽤需要注意的是sqlite3是有三种安全等级的:1. ⾮线程安全模式2. 线程安全模式(不同的连接在不同的线程/进程间是安全的,即⼀个句柄不能⽤于多线程间)3. 串⾏化模式(可以在不同的线程/进程间使⽤同⼀个句柄)1. 创建/打开数据库⽂件,并返回操作句柄int sqlite3_open(const char *filename, sqlite3 **ppDb) 成功返回SQLITE_OK//若在编译阶段启动了线程安全,则在程序运⾏阶段可以通过参数选择线程安全等级int sqlite3_open_v2(const char *filename, sqlite3 **ppDb, int flags, constchar *zVfs );
flag:
SQLITE_OPEN_READWRITE -- 以可读可写⽅式打开数据库⽂件SQLITE_OPEN_CREATE -- 不存在数据库⽂件则创建SQLITE_OPEN_NOMUTEX--多线程模式,只要不同的线程使⽤不同的连接即可保证线程安全SQLITE_OPEN_FULLMUTEX--串⾏化模式返回:SQLITE_OK表⽰成功2. 执⾏语句int sqlite3_exec(sqlite3*, char *sql, int (*callback)(void*,int,char**,char**), void* arg, char **err)int (*callback)(void*,int,char**,char**)void* : 是设置的在回调时传⼊的arg参数int:⼀⾏中数据的列数char**:存储⼀⾏数据的字符指针数组char**:每⼀列的字段名称这个回调函数有个int返回值,成功处理的情况下必须返回0,返回⾮0会触发ABORT退出程序返回:SQLITE_OK表⽰成功3. 销毁句柄int sqlite3_close(sqlite3* db); 成功返回SQLITE_OKint sqlite3_close_v2(sqlite3*); 推荐使⽤--⽆论如何都会返回SQLITE_OK获取错误信息const char *sqlite3_errmsg(sqlite3* db);
SQLite3 C/C++ API 的简单使用
- SqliteHelper类的编写
- 用来方便我们进行数据库的操作
#include <iostream>
#include <sqlite3.h>
#include <string>
#include <vector>
#include"Logger.hpp"class SqliteHelper
{ public: // 定义一个回调函数类型,用于sqlite3_exec的回调 typedef int (*SqliteCallback)(void *, int, char **, char **); // 构造函数,接收数据库文件名 SqliteHelper(const std::string dbfilename) : _dbfilename(dbfilename) { } // 打开数据库 // 参数safe_leve用于指定打开数据库的附加模式,默认为SQLITE_OPEN_FULLMUTEX bool open(int safe_leve = SQLITE_OPEN_FULLMUTEX) { // 使用sqlite3_open_v2函数打开或创建数据库 int ret = sqlite3_open_v2(_dbfilename.c_str(), &_handler, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | safe_leve, nullptr); if (ret != SQLITE_OK) { // std::cout << "创建/打开sqlite数据库失败: "; // std::cout << sqlite3_errmsg(_handler) << std::endl; ELOG("创建/打开sqlite数据库失败: %s",sqlite3_errmsg(_handler));return false; } return true; } // 关闭数据库 void close() { // 使用sqlite3_close_v2函数关闭数据库 if (_handler) sqlite3_close_v2(_handler); } // 执行SQL语句 // 参数sql为要执行的SQL语句,cb为回调函数,arg为回调函数的参数 bool exec(const std::string &sql, SqliteCallback cb, void *arg) { // 使用sqlite3_exec函数执行SQL语句 int ret = sqlite3_exec(_handler, sql.c_str(), cb, arg, nullptr); if (ret != SQLITE_OK) { // std::cout << sql << std::endl; // std::cout << "执行语句失败: "; // std::cout << sqlite3_errmsg(_handler) << std::endl; ELOG("执行语句:%s 失败!\t错误原因: %s",sql.c_str(),sqlite3_errmsg(_handler));return false; } return true; } private: std::string _dbfilename; // 数据库文件名 sqlite3 *_handler; // 数据库句柄
};
-
SqliteHelper类的测试,编写main.cc文件
#include "test_sqlite.hpp" // 包含SQLite操作的辅助类头文件 #include <cassert> // 包含断言头文件,用于检查程序中的假设// SQLite查询回调函数,用于处理查询结果 int select_stu_callback(void *arg, int col_count, char **result, char **fields_name) {std::vector<std::string> *arry = (std::vector<std::string> *)arg; // 将void*类型的参数转换为std::vector<std::string>*类型arry->push_back(result[0]); // 将查询结果的第一列添加到向量中return 0; // 返回0表示成功 }int main() {SqliteHelper helper("./test.db"); // 创建一个SqliteHelper对象,用于操作数据库// 1. 创建/打开库文件assert(helper.open()); // 打开数据库文件,如果文件不存在则创建// 2. 创建表(不存在则创建), 学生信息: 学号,姓名,年龄std::string ct = "create table if not exists student(sn int primary key, name varchar(32), age int);";assert(helper.exec(ct, nullptr, nullptr)); // 执行创建表的SQL语句// 3. 新增数据 , 修改, 删除, 查询std::string insert_sql = "insert into student values(1, '小明', 18), (2, '小黑', 19), (3, '小红', 18);";assert(helper.exec(insert_sql, nullptr, nullptr)); // 执行插入数据的SQL语句std::string update_sql = "update student set name='张小明' where sn=1";assert(helper.exec(update_sql, nullptr, nullptr)); // 执行更新数据的SQL语句std::string delete_sql = "delete from student where sn=3";assert(helper.exec(delete_sql, nullptr, nullptr)); // 执行删除数据的SQL语句std::string select_sql = "select name from student;";std::vector<std::string> arry;assert(helper.exec(select_sql, select_stu_callback, &arry)); // 执行查询SQL语句,并使用回调函数处理查询结果for (auto &name : arry){std::cout << name << std::endl; // 输出查询结果}// 4. 关闭数据库helper.close(); // 关闭数据库连接return 0; }
-
编译
g++ -std=c++11 main.cc -o main -lsqlite3
-
运行结果
对GTest的快速上手
什么是GTest?
- GTest是⼀个跨平台的 C++单元测试框架,由google公司发布。gtest是为了在不同平台上为编写C++单元测试⽽⽣成的。它提供了丰富的断⾔、致命和⾮致命判断、参数化等等
GTest的使用
-
TEST宏
TEST(test_case_name, test_name) TEST_F(test_fixture,test_name)
-
TEST:主要⽤来创建⼀个简单测试, 它定义了⼀个测试函数, 在这个函数中可以使⽤任何C++代码并且使⽤框架提供的断⾔进⾏检查
-
TEST_F:主要⽤来进⾏多样测试,适⽤于多个测试场景如果需要相同的数据配置的情况, 即相同的数据测不同的⾏为
-
GTest中的断言
-
分类:
- ASSERT_系列:如果当前点检测失败则退出当前函数_
- EXPECT_系列:如果当前点检测失败则继续往下执⾏
-
常用断言的介绍
// bool值检查 ASSERT_TRUE(参数),期待结果是true ASSERT_FALSE(参数),期待结果是false //数值型数据检查 ASSERT_EQ(参数1,参数2),传⼊的是需要⽐较的两个数 equal ASSERT_NE(参数1,参数2),not equal,不等于才返回true ASSERT_LT(参数1,参数2),less than,⼩于才返回true ASSERT_GT(参数1,参数2),greater than,⼤于才返回true ASSERT_LE(参数1,参数2),less equal,⼩于等于才返回true ASSERT_GE(参数1,参数2),greater equal,⼤于等于才返回true
-
简单的断言测试程序(assert.cpp)
#include <gtest/gtest.h> // 引入Google Test框架的头文件 #include <iostream> using std::cout; using std::endl; // 定义一个测试用例,属于"test"测试套件,用例名称为"testname_less_than" TEST(test, testname_less_than) { int age = 20; // 定义一个整型变量age,并初始化为20 EXPECT_LT(age, 18); // 使用EXPECT_LT断言宏,期望age小于18,若不满足则测试失败,但继续执行后续测试 } // 定义一个测试用例,属于"test"测试套件,用例名称为"testname_great_than" TEST(test, testname_great_than) { int age = 20; // 定义一个整型变量age,并初始化为20 EXPECT_GT(age, 18); // 使用EXPECT_GT断言宏,期望age大于18,若满足则测试通过,否则测试失败,但继续执行后续测试 } // 程序的主函数,程序的执行入口 int main(int argc,char* argv[]) { testing::InitGoogleTest(&argc,argv); // 初始化Google Test框架 RUN_ALL_TESTS(); // 运行所有已定义的测试用例 return 0; // 程序正常结束,返回0 }
-
编译
g++ -o assert assert.cpp -std=c++11 -lgtest
-
结果
-
GTest中的事件机制
- 事件机制的最⼤好处就是能够为我们各个测试⽤例提前准备好测试环境,并在测试完毕后⽤于销毁环境,这样有个好处就是如果我们有⼀端代码需要进⾏多种不同⽅法的测试,则可以通过测试机制在每个测试⽤例进⾏之前初始化测试环境和数据,并在测试完毕后清理测试造成的影响。
-
测试程序:⼀个测试程序只有⼀个main函数,也可以说是⼀个可执⾏程序是⼀个测试程序。该级别的事件机制是在程序的开始和结束执⾏
-
测试套件:代表⼀个测试⽤例的集合体,该级别的事件机制是在整体的测试案例开始和结束执⾏
-
测试⽤例:该级别的事件机制是在每个测试⽤例开始和结束都执⾏
-
GTest提供的三种常见事件:
-
全局事件:针对整个测试程序。实现全局的事件机制,需要创建⼀个⾃⼰的类,然后继承
testing::Environment类,然后分别实现成员函数 SetUp 和 TearDown ,同时在main函数内进
⾏调⽤ testing::AddGlobalTestEnvironment(new MyEnvironment); 函数添加全局的事件机制
-
简单的测试程序
#include <gtest/gtest.h> // 引入Google Test框架的头文件 #include <iostream> #include <map> using std::cout; // 使用std命名空间中的cout对象,用于标准输出 using std::endl; // 使用std命名空间中的endl对象,用于输出换行符 using std::string; // 自定义环境类,继承自testing::Environment class MyEnvironment : public testing::Environment { public: // 重写SetUp方法,用于单元测试前的环境初始化 virtual void SetUp() override { std::cout << "单元测试执行前的环境初始化!!\n"; } // 重写TearDown方法,用于单元测试后的环境清理 virtual void TearDown() override { std::cout << "单元测试执行完毕后的环境清理!!\n"; } }; // 定义两个测试用例,均属于MyEnvironment测试套件 TEST(MyEnvironment, test1) { std::cout << "单元测试1\n"; } TEST(MyEnvironment, test2) { std::cout << "单元测试2\n"; } // 定义一个全局的map,用于测试 std::map<string, string> mymap; // 自定义的MyMapTest环境类,继承自testing::Environment class MyMapTest : public testing::Environment { public: // 重写SetUp方法,用于单元测试前的环境初始化,向map中插入数据 virtual void SetUp() override { cout << "测试mymap单元测试" << endl; mymap.insert(std::make_pair("hello", "你好")); mymap.insert(std::make_pair("no", "不要")); } // 重写TearDown方法,用于单元测试后的环境清理,清空map virtual void TearDown() override { mymap.clear(); cout << "单元测试执行完毕" << endl; } }; // 定义两个测试用例,均属于MyMapTest测试套件 TEST(MyMapTest, test1) { // 期望mymap的大小为2 EXPECT_EQ(mymap.size(), 2); // 从mymap中删除键为"no"的元素 mymap.erase("no"); } TEST(MyMapTest, test2) { // 期望mymap的大小仍为2(但由于test1中已经删除了一个元素,这个期望实际上是不正确的) EXPECT_EQ(mymap.size(), 2); } // 程序的主函数,程序的执行入口 int main(int argc, char* argv[]) { testing::InitGoogleTest(&argc, argv); // 初始化Google Test框架 testing::AddGlobalTestEnvironment(new MyMapTest); // 注册MyMapTest环境 testing::AddGlobalTestEnvironment(new MyEnvironment); // 注册MyEnvironment环境 RUN_ALL_TESTS(); // 运行所有已定义的测试用例 return 0; // 程序正常结束,返回0 }
- 编译
g++ -o global global.cpp -std=c++11 -lgtest
-
结果
-
-
TestSuite事件:针对⼀个个测试套件。测试套件的事件机制我们同样需要去创建⼀个类,继承⾃testing::Test ,实现两个静态函数 SetUpTestCase 和 TearDownTestCase ,测试套件的事件机制不需要像全局事件机制⼀样在 main 注册,⽽是需要将我们平时使⽤的 TEST 宏改为 TEST_F 宏。
-
SetUpTestCase() 函数是在测试套件第⼀个测试⽤例开始前执⾏
-
TearDownTestCase() 函数是在测试套件最后⼀个测试⽤例结束后执⾏
-
需要注意TEST_F的第⼀个参数是我们创建的类名,也就是当前测试套件的名称,这样在
TEST_F宏的测试套件中就可以访问类中的成员了
-
简单的测试程序
#include<iostream> // 包含标准输入输出流库 #include<gtest/gtest.h> // 包含Google Test框架的头文件 #include<map> // 包含标准库中的map容器 // 使用声明,避免每次调用时都需要std::前缀 using std::string; using std::cout; using std::endl; using std::make_pair; // 定义测试套件SuitTest,继承自testing::Test class SuitTest : public testing::Test { public: // 在测试套件中的所有测试开始之前调用 static void SetUpTestCase() { std::cout << "环境1第一个TEST之前调用\n"; } // 在测试套件中的所有测试结束之后调用 static void TearDownTestCase() { std::cout << "环境1最后一个TEST之后调用\n"; } // 在每个测试用例之前调用,用于初始化测试环境 virtual void SetUp() override { mymap.insert(make_pair("hsq","哈士奇")); cout<<"这是每个单元测试自己的初始化"<<endl; } // 在每个测试用例之后调用,用于清理测试环境 virtual void TearDown() { cout<<"这是每个单元自己的结束函数"<<endl; } public: std::map<std::string,std::string> mymap; // 测试用例共享的成员变量 }; // 定义第一个测试用例testInsert,测试插入操作 TEST_F(SuitTest,testInsert) { mymap.insert(make_pair("nihao","你好")); EXPECT_EQ(mymap.size(),1); // 期望map的大小为1 } // 定义第二个测试用例testSize,测试map的大小 TEST_F(SuitTest,testSize) { EXPECT_EQ(mymap.size(),1); // 期望map的大小为1 } // 主函数,程序的入口点 int main(int argc,char* argv[]) { testing::InitGoogleTest(&argc,argv); // 初始化Google Test RUN_ALL_TESTS(); // 运行所有测试用例 return 0; }
-
编译
g++ -o suit suit.cpp -std=c++11 -lgtest
-
结果
-
-
-
TestCase事件: 针对⼀个个测试⽤例。测试⽤例的事件机制的创建和测试套件的基本⼀样,不同地⽅在于测试⽤例实现的两个函数分别是 SetUp 和 TearDown , 这两个函数也不是静态函数
- SetUp()函数是在⼀个测试⽤例的开始前执⾏
- TearDown()函数是在⼀个测试⽤例的结束后执⾏
- 也就是说,在TestSuite/TestCase事件中,每个测试⽤例,虽然它们同⽤同⼀个事件环境类,可以访问其中的资源,但是本质上每个测试⽤例的环境都是独⽴的,这样我们就不⽤担⼼不同的测试⽤例之间会有数据上的影响了,保证所有的测试⽤例都使⽤相同的测试环境进⾏测试。
-