基于GRPC实现注册模块发送邮件的功能
1、用户、服务器、grpc服务端模型描述
- 用户输入邮箱并点击获取验证码:用户通过前端页面提交请求,输入他们的邮箱并点击“获取验证码”按钮。这个操作将触发前端发送请求给服务器。
- 服务器接受到用户发来的请求:服务器接收用户的请求,该请求包含用户输入的邮箱地址。此时服务器扮演 HTTP API 的角色,处理客户端(用户)的请求。
- 服务器作为 gRPC 客户端,向 gRPC 服务端获取验证码:服务器作为 gRPC 客户端,向另外一个 gRPC 服务端发起请求,要求生成一个验证码。这个 gRPC 服务端专门负责生成并处理验证码。
- gRPC 服务端将验证码发送给用户:gRPC 服务端生成验证码后,可以通过邮件系统(或其他消息系统)直接将验证码发送到用户的邮箱。
因此,简要流程为:
-
用户 → 服务器:用户请求获取验证码,服务器接收请求。
-
服务器 → gRPC 服务端:服务器作为 gRPC 客户端向 gRPC 服务端请求生成验证码。
-
gRPC 服务端 → 用户:gRPC 服务端生成验证码并通过邮件发送给用户。
流程图如下:
在本篇博客中,将侧重点放在第2、3步,对第1步不会太详细去讲
RPC 的工作流程:
- 服务定义:在一个
.proto
文件中定义服务和消息结构。 - 代码生成:通过
protoc
生成客户端和服务器的代码存根(Stub)。 - 客户端调用:客户端通过 gRPC 框架调用生成的存根,发起远程服务调用。
- 服务器响应:服务器端实现生成的接口,并处理客户端请求,返回结果。
服务定义
- 在项目的根目录下创建一个proto名字为message.proto
syntax = "proto3";
package message;
// 定义 VarifyService 服务,其中包含用于验证码验证的 RPC 方法
service VeifyService {
// 定义名为 GetVarifyCode 的 RPC 方法,接收 GetVarifyReq 消息并返回 GetVarifyRsp 消息
rpc GetVerifyCode (GetVerifyReq) returns (GetVerifyRsp) {}
}
// 定义 GetVarifyCode RPC 方法的请求消息
message GetVerifyReq {
string email = 1; // 要发送验证码的电子邮件地址
}
// 定义 GetVarifyCode RPC 方法的响应消息
message GetVerifyRsp {
int32 error = 1; // 错误码 (0 表示成功,非零表示失败)
string email = 2; // 与请求相关联的电子邮件地址,通常回传以确认
string code = 3; // 为该电子邮件地址生成的验证码
}
代码生成
利用grpc编译后生成的proc.exe生成proto的grpc的头文件和源文件
protoc -I="." --grpc_out="." --plugin=protoc-gen-grpc=/usr/bin/grpc_cpp_plugin message.proto
protoc -I="." --cpp_out="." message.proto
两条命令执行完后会多出四个文件
message.grpc.pb.cc
message.grpc.pb.h
message.pb.cc
message.pb.h
2、基于C++实现gRPC客户端
VerifyGrpcClient
是一个 gRPC 客户端,用于向远程 gRPC 服务请求验证码。
在本例中VerifyGrpcClient
可以设计成一个单例类,意味着程序运行过程中只有一个实例,下面介绍单例类的实现.
2.1 单例类的实现
在这里,我将会粗略的介绍单例类,在后面的博客中,我会详细介绍单例类。
#include <memory>
#include <mutex>
#include <iostream>
template <typename T>
class Singleton {
protected:
Singleton() = default;
Singleton(const Singleton<T>&) = delete;
Singleton& operator=(const Singleton<T>& st) = delete;
static std::shared_ptr<T> _instance;
public:
static std::shared_ptr<T> GetInstance() {
static std::once_flag s_flag;
std::call_once(s_flag, [&]() {
_instance = std::shared_ptr<T>(new T); //这里要用new,不能用到make_shared
});
return _instance;
}
void PrintAddress() {
std::cout << _instance.get() << std::endl;
}
~Singleton() {
std::cout << "this is singleton destruct" << std::endl;
}
};
template <typename T>
std::shared_ptr<T> Singleton<T>::_instance = nullptr;
2.2 VerifyGrpcClient的实现
由于
VerifyGrpcClient
基于 gRPC 连接的生命周期和并发请求,我们可以通过实现连接池来减少了重复创建和销毁连接的开销。这里着重讲业务逻辑,连接池的逻辑下面在讲。
VerifyGrpcClient
类:这是业务逻辑层的 gRPC 客户端,它通过单例模式确保只有一个实例存在。该类利用 RPConPool
连接池来高效复用 gRPC 连接,并提供 GetVerifyCode
方法,用于请求远程服务器的验证码服务。如果 gRPC 调用成功,则返回服务器响应;如果失败,返回包含错误码的响应。
#include <grpcpp/grpcpp.h>#include <queue>
#include <atomic>
#include <condition_variable>
#include <mutex>
#include "message.pb.h"
#include "../head.h"
#include "message.grpc.pb.h"
#include "../ConfigMgr/ConfigMgr.h"
#include "../LogicSystem/Singleton.h"
using grpc::Channel;
using grpc::Status;
using grpc::ClientContext;
using message::GetVerifyReq;
using message::GetVerifyRsp;
using message::VerifyService;
// VerifyGrpcClient 类:业务逻辑层的 gRPC 客户端,通过连接池与 gRPC 服务器交互。
// 该类通过单例模式实现,确保在应用程序中只存在一个 gRPC 客户端实例。
// 它利用 RPConPool 连接池来高效地复用 gRPC 连接,执行验证码请求并返回结果。
class VerifyGrpcClient : public Singleton<VerifyGrpcClient>
{
// 友元声明,允许 Singleton 类访问 VerifyGrpcClient 的私有构造函数
friend class Singleton<VerifyGrpcClient>;
public:
// GetVerifyCode 方法:请求验证码的核心逻辑
// 输入:用户的电子邮件地址
// 输出:包含验证码或错误信息的 GetVerifyRsp 响应
GetVerifyRsp GetVerifyCode(std::string email)
{
ClientContext context; // gRPC 客户端上下文,管理 RPC 的生命周期
GetVerifyReq request; // gRPC 请求对象
GetVerifyRsp response; // gRPC 响应对象
// 设置请求中的电子邮件地址
request.set_email(email);
// 从连接池中获取 gRPC 存根(stub)
auto stub = pool_->getConnection();
// 调用 gRPC 服务的 GetVerifyCode 方法
Status status = stub->GetVerifyCode(&context, request, &response);
// 如果 gRPC 调用成功,返回响应并归还连接
if (status.ok()) {
pool_->returnConnection(std::move(stub)); // 将连接归还到池中
return response;
}
// 如果调用失败,设置错误码,归还连接并返回错误响应
else {
pool_->returnConnection(std::move(stub)); // 将连接归还到池中
response.set_error(ErrorCodes::RPCFailed); // 设置错误信息
return response;
}
}
private:
// 私有构造函数,只有 Singleton 类可以创建 VerifyGrpcClient 实例
VerifyGrpcClient()
{
// 从配置管理器中读取 gRPC 服务的主机和端口
// 这里的ConfigMgr可以看我前面的博客有讲过如何从config.ini配置文件读取配置信息
std::string host = ConfigMgr::getInstance()["VarifyServer"]["Host"];
std::string port = ConfigMgr::getInstance()["VarifyServer"]["Port"];
// 初始化 gRPC 连接池,指定连接池的大小
pool_.reset(new RPConPool(5, host, port));
}
// gRPC 连接池的智能指针,管理并复用与远程服务器的 gRPC 连接
std::unique_ptr<RPConPool> pool_;
};
2.3 RPConPool的实现
PConPool
类:该类负责管理 gRPC 连接池的创建、获取和归还。
- 通过预先创建多个 gRPC 连接(存根
Stub
),减少了每次请求重新建立连接的开销。 - 它使用互斥锁和条件变量确保多线程访问的安全性,并通过标记和通知机制控制连接池的生命周期。
// RPConPool 类:负责管理 gRPC 连接的池化机制,允许多个客户端共享有限数量的 gRPC 连接。
// 它通过创建和复用 gRPC 连接,减少每次请求都重新建立连接的开销,并提供线程安全的连接获取与归还机制。
class RPConPool {
public:
// 构造函数,初始化指定数量的 gRPC 连接池
// 参数:
// poolSize - 连接池中初始连接的数量
// host - gRPC 服务器的主机地址
// port - gRPC 服务器的端口号
RPConPool(size_t poolSize, std::string host, std::string port)
: poolSize_(poolSize), host_(host), port_(port), b_stop_(false) {
// 创建并初始化 poolSize_ 个 gRPC 连接(stub)并存储在队列中
for (size_t i = 0; i < poolSize_; ++i) {
std::shared_ptr<Channel> channel = grpc::CreateChannel(host + ":" + port,
grpc::InsecureChannelCredentials());
connections_.push(VerifyService::NewStub(channel));
}
}
// 析构函数,确保在销毁时释放所有连接,并通知所有等待的线程
~RPConPool() {
std::lock_guard<std::mutex> lock(mutex_); // 保证线程安全
Close(); // 标记停止并唤醒所有等待的线程
// 清空连接池队列
while (!connections_.empty()) {
connections_.pop();
}
}
// 获取连接池中的 gRPC 连接(stub)
// 如果连接池为空,当前线程会阻塞,直到有可用连接或连接池停止
std::unique_ptr<VerifyService::Stub> getConnection() {
std::unique_lock<std::mutex> lock(mutex_); // 线程安全地访问连接队列
// 等待直到有可用连接或连接池标记为停止
cond_.wait(lock, [this] {
if (b_stop_) {
return true; // 如果连接池已停止,立即返回
}
return !connections_.empty(); // 如果连接池非空,继续执行
});
// 如果连接池已标记为停止,则返回空指针
if (b_stop_) {
return nullptr;
}
// 从队列中取出一个 gRPC 连接(stub)
auto context = std::move(connections_.front());
connections_.pop();
return context;
}
// 归还 gRPC 连接,将连接放回连接池供其他请求使用
void returnConnection(std::unique_ptr<VerifyService::Stub> context) {
std::lock_guard<std::mutex> lock(mutex_); // 确保归还操作线程安全
if (b_stop_) {
return; // 如果连接池已停止,忽略归还操作
}
connections_.push(std::move(context)); // 将连接放回队列
cond_.notify_one(); // 唤醒一个正在等待的线程
}
// 关闭连接池,通知所有等待的线程连接池已停止
void Close() {
b_stop_ = true; // 标记连接池停止
cond_.notify_all(); // 唤醒所有阻塞等待的线程
}
private:
std::atomic<bool> b_stop_; // 标记连接池是否已停止,确保线程安全
size_t poolSize_; // 连接池的大小
std::string host_; // gRPC 服务器的主机地址
std::string port_; // gRPC 服务器的端口号
std::queue<std::unique_ptr<VerifyService::Stub>> connections_; // 存储 gRPC 连接(stub)的队列
std::mutex mutex_; // 保护队列访问的互斥锁
std::condition_variable cond_; // 条件变量,用于管理线程等待与唤醒
};
2.4 如何使用?
在与用户客户端的逻辑处理层通过如下代码使用:
auto email = src_root["email"].asString(); // 通过json解析出用户的emali
GetVerifyRsp rsp = VerifyGrpcClient::GetInstance()->GetVerifyCode(email); // 把email传递给gRPC服务端并收到回复
std::cout << "email is " << email << std::endl;
3、基于JS实现gRPC服务端
将创建好的proto文件放入项目目录中,并且新建一个proto.js来解析proto文件
// 原生模块 'path',用于处理文件和目录路径
const path = require('path');
//,'@grpc/grpc-js',用于在 Node.js 中实现 gRPC 客户端和服务端
const grpc = require('@grpc/grpc-js');
// 用于加载 .proto 文件并将其转换为 gRPC 可用的格式
const protoLoader = require('@grpc/proto-loader');
// 定义 .proto 文件的路径,使用 path 模块将当前目录与 'message.proto' 文件组合成完整路径
const PROTO_PATH = path.join(__dirname, 'message.proto');
// 使用 protoLoader 加载 .proto 文件,传入一些选项以配置解析方式
// keepCase: 保持字段名称不变,longs: 将长整型数字表示为字符串,enums: 将枚举值表示为字符串
// defaults: 为消息字段设置默认值,oneofs: 支持 oneof 特性
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
// 将加载的 .proto 文件转换为 gRPC 可用的格式
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
// 获取 .proto 文件中定义的 message 服务对象
const message_proto = protoDescriptor.message;
// 将 message_proto 导出,便于在其他模块中使用该 gRPC 服务定义
module.exports = message_proto;
然后设置你自己的邮箱的SMTP服务
读取配置
因为我们要实现参数可配置,所以要读取配置,先在文件夹内创建一个config.json文件
mysql和redis目前还用不到,大家暂时可以不用理会
注意:user是我的邮箱地址,pass是邮箱的授权码,只有有了授权码才能用代码发邮件。
{
"email": {
"user": "xxxxxxxx",
"pass": "xxxxxxxx"
},
"mysql":{
"host" : "0.0.0.0",
"port" : 3316
},
"redis":{
"host" : "0.0.0.0",
"port" : 6380,
"passwd": 123456
}
}
另外我们也要用到一些常量和全局得变量,所以我们定义一个const.js
let code_prefix = "code_";
const Errors = {
Success : 0,
RedisErr : 1,
Exception : 2,
};
module.exports = {code_prefix, Errors}
新建config.js来读取配置
const fs = require('fs');
let config = JSON.parse(fs.readFileSync('config.json', 'utf8'));
let email_user = config.email.user;
let email_pass = config.email.pass;
let mysql_host = config.mysql.host;
let mysql_port = config.mysql.port;
let redis_host = config.redis.host;
let redis_port = config.redis.port;
let redis_passwd = config.redis.passwd;
let code_prefix = "code_";
module.exports = {
email_pass, email_user,
mysql_host, mysql_port,
redis_host, redis_port, redis_passwd,
code_prefix
}
接下来封装发邮件的模块,新建一个email.js文件
const nodemailer = require('nodemailer');
const config_module = require("./config")
/**
* 创建发送邮件的代理
*/
let transport = nodemailer.createTransport({
host: 'smtp.qq.com', // 这里要写你们邮箱的那个smtp服务器,这里是qq的
port: 465,
secure: true,
auth: {
user: config_module.email_user, // 发送方邮箱地址
pass: config_module.email_pass // 邮箱授权码或者密码
}
});
/**
* 发送邮件的函数
* @param {*} mailOptions_ 发送邮件的参数
* @returns
*/
function SendMail(mailOptions_){
return new Promise(function(resolve, reject){
transport.sendMail(mailOptions_, function(error, info){
if (error) {
console.log(error);
reject(error);
} else {
console.log('邮件已成功发送:' + info.response);
resolve(info.response)
}
});
})
}
module.exports.SendMail = SendMail
我们新建server.js,用来启动grpc server
const grpc = require('@grpc/grpc-js')
const config = require('./config')
const message_proto = require('./proto')
const const_module = require('./const')
const {v4:uuidv4} = require('uuid')
const emailModule = require('./email')
const { selectLbConfigFromList } = require('@grpc/grpc-js/build/src/load-balancer')
async function GetVarifyCode(call, callback) {
console.log("email is", call.request.email)
try{
uniqueId = uuidv4();
console.log("uniqueId is ", uniqueId)
let text_str = '您的验证码为'+ uniqueId +'请三分钟内完成注册'
//发送邮件
let mailOptions = {
from: config.email_user,
to: call.request.email,
subject: '验证码',
text: text_str,
};
let send_res = await emailModule.SendMail(mailOptions);
console.log("send res is ", send_res)
callback(null, { email: call.request.email,
error:const_module.Errors.Success
});
}catch(error){
console.log("catch error is ", error)
callback(null, { email: call.request.email,
error:const_module.Errors.Exception
});
}
}
function main() {
var server = new grpc.Server()
server.addService(message_proto.VarifyService.service, { GetVarifyCode: GetVarifyCode })
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
console.log('grpc server started')
})
}
main()
这样子就可以实现我们所需的功能啦!