基于Tarsnodejs快速实现云短信模块服务

导言

Tars 是将腾讯内部使用的微服务架构TAF(Total Application Framework)多年的实践成果总结而成的开源项目。Nodejs其js的语法对json处理的优势,可以适用于快速实现轻量级接口。

Tars-nodejs随着Tars开源之后与2018中旬一期发布,开启了Tars对于nodejs的支持。

下面以tarsnode整合腾讯云短信sdk为例,带大家掌握如何快速实现tarsnodejs的服务端与客户端。

服务端

Tars协议编写QSms.tars

代码语言:txt
复制
module QCloudSms

{

struct HeadReq

{

    0 require       string              requestId;//调用请求ID

};

//短信发送

struct SendSmsReq

{

    0 require       string              phoneNumber;   // 短信接收方手机号

    1 require       int                 templateId;    // 短信模板ID

    2 require       vector<string>      params;     //对应短信模板的数组值

};

//短信标准回包

struct SendSmsRsp

{

    0 require       int               result;//错误码,0 表示成功(计费依据),非 0 表示失败

    1 require       string            errmsg;//错误消息,result 非 0 时的具体错误信息

    2 require       string            ext;//用户的 session 内容,腾讯 server 回包中会原样返回

    3 require       string            sid;//本次发送标识 id,标识一次短信下发记录

    4 require       int               fee;//短信计费的条数

};

//请求短信验证码

struct SendSmsCodeReq

{

    0 require       string              phoneNumber;   // 短信接收方手机号

    1 require       int                 duration ;      // 过期时间单位分钟

};

//验证短信验证码

struct SmsCodeCheckReq

{

    0 require       string              phoneNumber;   // 短信接收方手机号

    1 require       string              vcode ;        // 验证码

};

//短信验证码消息回复

struct SendSmsCodeRsp

{

    0 require       int                  code;   // 消息码

    1 require       string               msg;    // 字符串内容

    2 require       string               data;   // 具体内容

};

interface SmsTars

{

    // 发送单条基于模板的短信

    int sendSingleSmsWithTpl(HeadReq head ,SendSmsReq req, out SendSmsRsp rsp);

    //发送短信验证码

    int sendSmsCode(HeadReq head ,SendSmsCodeReq req, out SendSmsCodeRsp rsp);

    //验证短信验证码

    int checkSmsCode(HeadReq head ,SmsCodeCheckReq req, out SendSmsCodeRsp rsp);

};

};

可以看到tars协议风格与protobuf比较类似,struct代表结构体类似pb的message,interface类似pb的service

使用tars2node将Tars IDL 定义文件转换为 JavaScript 语言所使用的版本,一般同时生成客户端及服务端,可以获得三个文件:QSms.js、QSmsImpl.js、QSmsProxy.js

QSmsImpl中生成对应的空方法

代码语言:txt
复制
QCloudSms.SmsTarsImp.prototype.sendSingleSmsWithTpl = async (current, head, req, rsp) => {

//TODO

}

QCloudSms.SmsTarsImp.prototype.sendSmsCode = async (current, head, req, rsp) => {

//TODO

}

QCloudSms.SmsTarsImp.prototype.checkSmsCode = async (current, head, req, rsp) => {

//TODO

}

接下来只需要继续完善 QSmsImpl.js 实现文件中具体的函数就可以了。

代码结构

代码语言:txt
复制
app

|--config(配置文件)

|--tars(tars协议及实现)

|--utils

main.js(入口)

package.json

在实现之前,我们先规划一下整个服务的代码结构。作为工具类微服务,我个人主张精简小巧,就像一个函数一样一目了然

QSmsImpl.js内方法实现

代码语言:txt
复制
QCloudSms.SmsTarsImp.prototype.sendSingleSmsWithTpl = async (current, head, req, rsp) => {

const logger = new tarsLogs('TarsDate', 'sendSingleSmsWithTpl');



const requestId = head.toObject().requestId;

const { phoneNumber, templateId, params } = req.toObject();

logger.debug(requestId, 'begin>>>', phoneNumber, templateId, params);

let rst = {};



if (!verifUtil.mobileVer(phoneNumber)) {

    rst = {

        result: 1,

        errmsg: 'invalid phoneNumber',

        ext: '',

        sid: '',

        fee: 0

    }

    rsp.readFromObject(rst);//将对象转为

    return current.sendResponse(rst.result, rsp);

}

try {

    rst = await smsUtils.sendSingleSms(phoneNumber, templateId, params);

    logger.debug(requestId, 'rst<<<', rst);//{"result":0,"errmsg":"OK","ext":"","sid":"8:MRasIKfs6eMBxstTmN020180910","fee":1}

    //错误码从1001~1036目前

} catch (err) {

    logger.error(requestId, 'err<<<', err);

    rst = {

        result: -1,

        errmsg: 'tars server error see details in sendSingleSmsWithTpl.log',

        ext: '',

        sid: '',

        fee: 0

    }

}

rsp.readFromObject(rst);

return current.sendResponse(rst.result, rsp);

};

函数从current之后开始,与interface中对应的参数保持一致

入参的.toObject()方法可以帮助你将内容快速转为和struct一致的json对象,清理掉其他无用的描述

出参的.readFromObject()可以帮助你将一个json内容复制进来

发送短信的具体实现被封装在了smsUtils工具类中,它主要集成了腾讯云短信nodejs版的SDK包,这里不再赘述。

main.js主入口

代码语言:txt
复制
'use strict';

const tarsLogs = require('@tars/logs');//日志

const logger = new tarsLogs('TarsDate', 'main');

const TARS = require("@tars/rpc");//RPC服务

const QCloudSms = require("./app/tars/svr/QSmsImp").QCloudSms;

const TarsConfigHepler = require('@tars/config');//加载外部配置服务

let smsConfig = require('./app/config/smsConfig');

let redisConfig = require('./app/config/redisConfig');

if (process.env.TARS_CONFIG) {

let tarsConfig = new TarsConfigHepler(process.env.TARS\_CONFIG);

logger.info('tars server is starting...');

TARS.server.getServant(process.env.TARS\_CONFIG).forEach(config => {

    let map, svr;

    map = {

        "demo.SmsSvr.SmsSvrObj": QCloudSms.SmsTarsImp,

    };

    if (map[config.servant]) {

        logger.info("Start Servant..." + config.servant);

        svr = TARS.server.createServer(map[config.servant]);

        svr.start(config);

        logger.info('tars server is started');

    } else {

        logger.info("Servant Not Exist..." + config.servant);

    }

});



//配置文件加载及更新

tarsConfig.loadConfig('SmsSvr.conf', { format: tarsConfig.FORMAT.JSON }).then(data => {

    logger.info("data:", data);

    smsConfig.init = data;

}, (err) => {

    logger.error("loadConfig SmsSvr err", err.response || err);

});

//配置文件加载及更新

tarsConfig.loadConfig('redis.conf', { format: tarsConfig.FORMAT.JSON }).then(data => {

    logger.info("redis data:", data);

    redisConfig.init = data;

}, (err) => {

    logger.error("loadConfig redis err", err.response || err);

});



tarsConfig.on('configPushed', (file) => {

    logger.info('configPushed', file);

    if (file === 'SmsSvr.conf') {

        tarsConfig.loadConfig(file, { format: tarsConfig.FORMAT.JSON }).then(data => {

            logger.info("SmsSvr data:", data);

            smsConfig.init = data;

        }, (err) => {

            logger.error("configPushed SmsSvr err", err.response || err);

        });

    }

    if (file === 'redis.conf') {

        tarsConfig.loadConfig(file, { format: tarsConfig.FORMAT.JSON }).then(data => {

            logger.info("redis data:", data);

            redisConfig.init = data;

        }, (err) => {

            logger.error("configPushed redis err", err.response || err);

        });

    }

});

} else {

logger.error('is not a online tars server');

}

作为服务端主入口,main.js判断了当前的环境是否是tars环境,并在环境变量中读取当前服务的的配置文件,尝试启动名为demo.SmsSvr.SmsSvrObj的Servant,并从当前这个服务中拉取SmsSvr.conf的服务配置,确保服务在执行过程中可以动态的替换配置参数,实现服务的灵活可配。


客户端

代码语言:txt
复制
const tarsLogs = require('@tars/logs');

const logger = new tarsLogs('TarsRotate','QcloudSmsUtils');//滚动调试日志

const Tars = require("@tars/rpc").client;

const QCloudSms = require("../tars_proxy/QSmsProxy.js").QCloudSms;

const prx = Tars.stringToProxy(QCloudSms.SmsTarsProxy, "demo.SmsSvr.SmsSvrObj");

class QcloudSmsUtil {

static async sendSingleSms(req, phoneNumber, templateId, params) {

    const requestId = (req && req.id) ? req.id : 'NoRequestId';

    logger.debug(requestId, 'sendSingleSms>>>', phoneNumber, templateId, params);

    try {

        let headReq = new QCloudSms.HeadReq();

        let sendSmsReq = new QCloudSms.SendSmsReq();

        headReq.requestId = requestId;

        sendSmsReq.readFromObject({

            phoneNumber: phoneNumber,

            templateId: templateId,

            params: params

        })

        let result = await prx.sendSingleSmsWithTpl(headReq, sendSmsReq);

        let rsp = result.response.arguments.rsp;

        logger.info(requestId, 'sendSingleSms<<<', "result.response.costtime:", result.response.costtime);

        logger.info(requestId, 'sendSingleSms<<<', "result.response.return:", result.response.return);

        logger.debug(requestId, 'sendSingleSms<<<', "result.response.arguments.rsp:", rsp);

        return rsp.toObject();

    } catch (err) {

        logger.debug(requestId, 'sendSingleSms>>>', phoneNumber, templateId, params);

        if (err.response) {//来自tars层面的错误

            logger.info(requestId, 'sendSingleSms<<<', "error.response.costtime:", err.response.costtime);

            logger.info(requestId, 'sendSingleSms<<<', "error.response.error.code:", err.response.error.code);

            logger.debug(requestId, 'sendSingleSms<<<', "error.response.error.message:", err.response.error.message);

            return Promise.reject(err.response.error.message);

        } else {

            logger.error(requestId, 'sendSingleSms error<<<', err.message);

            return Promise.reject(err.message);//node层面的错误

        }

    }

}

}

module.exports = QcloudSmsUtil;

客户端声明好对应调用的proxy地址将sendSingleSmsWithTpl函数的两个入参内容传入其中,就可以实现对服务端的调用了,需要注意的是,tars调用的错误内容被存放在error.response中,故在代码里进行了一次判断,如果直接将error抛出是无法得到所想要的错误信息的,还会因为内容太多而容易撑爆磁盘。