基于GRPC实现注册模块发送邮件的功能

基于GRPC实现注册模块发送邮件的功能

1、用户、服务器、grpc服务端模型描述

  • 用户输入邮箱并点击获取验证码:用户通过前端页面提交请求,输入他们的邮箱并点击“获取验证码”按钮。这个操作将触发前端发送请求给服务器。
  • 服务器接受到用户发来的请求:服务器接收用户的请求,该请求包含用户输入的邮箱地址。此时服务器扮演 HTTP API 的角色,处理客户端(用户)的请求。
  • 服务器作为 gRPC 客户端,向 gRPC 服务端获取验证码:服务器作为 gRPC 客户端,向另外一个 gRPC 服务端发起请求,要求生成一个验证码。这个 gRPC 服务端专门负责生成并处理验证码。
  • gRPC 服务端将验证码发送给用户:gRPC 服务端生成验证码后,可以通过邮件系统(或其他消息系统)直接将验证码发送到用户的邮箱。

因此,简要流程为:

  • 用户 → 服务器:用户请求获取验证码,服务器接收请求。

  • 服务器 → gRPC 服务端:服务器作为 gRPC 客户端向 gRPC 服务端请求生成验证码。

  • gRPC 服务端 → 用户:gRPC 服务端生成验证码并通过邮件发送给用户。

流程图如下

image-20240912134709031

在本篇博客中,将侧重点放在第2、3步,对第1步不会太详细去讲

RPC 的工作流程:
  • 服务定义:在一个 .proto 文件中定义服务和消息结构。
  • 代码生成:通过 protoc 生成客户端和服务器的代码存根(Stub)。
  • 客户端调用:客户端通过 gRPC 框架调用生成的存根,发起远程服务调用。
  • 服务器响应:服务器端实现生成的接口,并处理客户端请求,返回结果。
服务定义
  1. 在项目的根目录下创建一个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()

这样子就可以实现我们所需的功能啦!

grpc-server-spring-boot-starter是一个基于Spring Boot框架的gRPC服务器的启动器。gRPC(Google Remote Procedure Call)是一种高性能的远程过程调用框架,它使用Protocol Buffers作为接口定义语言,并支持多种编程语言。 grpc-server-spring-boot-starter提供了一系列简化配置和集成的功能,使得在Spring Boot应用中启动和配置gRPC服务器变得更加容易。它提供了自动装配的功能,可以根据应用的配置自动创建和启动gRPC服务器。用户只需要在配置文件中设置相应的参数,如服务器的端口号、TLS证书等,即可完成服务器的启动配置。 在使用grpc-server-spring-boot-starter时,用户可以方便地定义服务接口和实现类。通过使用gRPC的接口定义语言(protobuf)定义接口,并生成对应的Java代码。然后,用户只需要在实现类中实现相应的接口方法即可。 在服务器启动后,grpc-server-spring-boot-starter会根据定义的接口和实现类,自动创建相应的gRPC服务,并将其注册到服务器中。当客户端发起远程调用时,服务器会根据接口定义和方法参数,将请求转发给对应的实现类,并返回执行结果给客户端。 grpc-server-spring-boot-starter还支持对gRPC服务器进行拦截器的配置。拦截器可以在请求和响应的过程中拦截和修改消息,用于实现日志记录、鉴权、性能监控等功能。 总之,grpc-server-spring-boot-starter简化了在Spring Boot应用中使用gRPC的配置和集成过程,使得开发者可以更加便捷地构建和部署gRPC服务器。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值