导言
Tars 是将腾讯内部使用的微服务架构TAF(Total Application Framework)多年的实践成果总结而成的开源项目。Nodejs其js的语法对json处理的优势,可以适用于快速实现轻量级接口。
Tars-nodejs随着Tars开源之后与2018中旬一期发布,开启了Tars对于nodejs的支持。
下面以tarsnode整合腾讯云短信sdk为例,带大家掌握如何快速实现tarsnodejs的服务端与客户端。
服务端
Tars协议编写QSms.tars
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中生成对应的空方法
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 实现文件中具体的函数就可以了。
代码结构
app
|--config(配置文件)
|--tars(tars协议及实现)
|--utils
main.js(入口)
package.json
在实现之前,我们先规划一下整个服务的代码结构。作为工具类微服务,我个人主张精简小巧,就像一个函数一样一目了然
QSmsImpl.js内方法实现
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主入口
'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的服务配置,确保服务在执行过程中可以动态的替换配置参数,实现服务的灵活可配。
客户端
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抛出是无法得到所想要的错误信息的,还会因为内容太多而容易撑爆磁盘。