tars-node:RPC框架分析(node-agent, deploy, rpc, monitor, stream(部分))

总体架构:

node-agent

TARS框架中Node.js程序启动器,提供生产环境所需的服务属性。

deploy

TARS框架服务打包工具

rpc

TARS框架中RPC通信模块

monitor

TARS框架中用于服务监控,特性监控上报。

stream

TARS框架的编解码工具

registry

TARS框架中的主控请求模块

utils

TARS框架辅助工具集合

winston-tars

基于winston的扩展,以提供符合TARS框架的日志格式与输出

notify

TARS框架中用于业务的(告警)消息上报

logs

TARS框架规范的日志组件,包括滚动(大小、时间)与染色日志

config

TARS框架中用于获取服务配置文件

dyeing

TARS染色基础模块

node-agent

@tars/node-agent

为了让Node.js应用于TARS框架中,node-agent将作为启动器来启动应用,提供生产环境所需的服务特性。它主要提供了如下的功能:

  • 内置负载均衡(通过Cluster模块实现)
  • 异常退出的监控与拉起
  • 日志搜索与处理
  • 支持TARS平台的管理命令
  • 支持HTTP(s)服务监控上报(在TARS平台上运行)
  • 支持服务用量上报(在TARS平台上运行)
安装
npm install @tar/node-agent -g

由于node-agent是一个CLI程序,所以一般需要用-g参数来安装

用法
node-agent app.js [options]
  • app.js为程序的入口脚本
  • [options]可选配置
例子

执行app.js文件:

$ node-agent app.js

以TARS服务的配置文件来启动:

$ node-agent app.js --config MTT.Test.conf

启动并命名应用为MTT.Test:

$ node-agent app.js --name MTT.Test

定义日志输出路径:

$ node-agent app.js --log ./logs/

传递子进程node的启动参数:

$ node-agent app.js --node-args="--debug=7001"

定义子进程数量:

$ node-agent app.js -i 4
入口点

node-agent启动时传入的第二个参数用来制定服务脚本执行的入口文件,其中:

  • 可以直接传入脚本文件用于执行,如:./app,js
  • 也可以传入脚本文件所在的目录,如:./
    当传入的为目录时,入口点根据如下顺序进行确认:
  1. 目录中存在package.json文件,则:
    i. 查找 nodeAgent.main
    ii. 查找 script.start(此配置节需要以node打头才可以识别)
    iii. 查找 main
  2. 查找目录中是否存在: server.js、app.js、start.js、index.js,只要其中的一项匹配则作为入口文件来执行,并不再往下匹配。
选项
Options:
-h, --help output usage information
-V, --version output the version number
-c, --config specify tars config file. NOTE: independent config will be override this
-n, --config specify tars config file. NOTE: independent config will be override this
-l, --log specify log file
-i, --instances launch [number] instances (for networked app)(load balanced)
--env <environment_name> specify environment to get specific env variables (for JSON declaration)
--http-address <http_address> specify http ip:port address to pass to script - e.g. 127.0.0.1:80
--script-args <script_args> space delimited arguments to pass to script - e.g. --use="https"
--node-args <node_args> space delimited arguments to pass to node - e.g. --node-args="--debug=7001 --trace-deprecation"
--run-as-user <run_as_user> The user or uid to run a managed process as
--run-as-group <run_as_group> The group or gid to run a managed process as
--max-memory-restart specify max memory amount used to autorestart (in megaoctets)
--graceful-shutdown specify graceful shutdown timeout (in millisecond), default is 8000ms
--exception-max <exp_max> The program will be terminated if an exceeding max exception count, default is 5
--exception-time <exp_time> The program will be terminated if an exception occurs within a particular period of time, default is 5000ms
--keepalive-time <detect_time> specify the interval for detecting the worker which could be set to [off] if you want to debug and the default value is 60s
--applog-max-files <applog_max_files> specify max number of rolling log, default is 10
--applog-max-size <applog_max_size> specify max file size for each rolling log, use human readable unit in [K|G|M], default is 10M
--applog-level <applog_level> define log level, default is DEBUG
--tars-node <tars_node> set tars node conncetion string, agent would send notifications to tars node - e.g. tars.tarsnode.ServerObj@tcp -h 127.0.0.1 -p 10000 -t 60000
--tars-local <tars_local> set local interface setup string, agent would receive the notifications from tars node - e.g. tcp -h 127.0.0.1 -p 10000 -t 3000
--tars-monitor <tars_monitor> enable or disable service monitor running in tars platform, and the default value is on
--tars-monitor-http-threshold <http_threshold> if the http(s) status code is large than the preseted threshold then this request will be considered error. default threshold is 400, set it "off" to disabled
--tars-monitor-http-seppath <http_seppath> separate url pathname as interface name, default is on

-c, --config
如果此服务为TARS服务,可以在此制定服务的配置文件。
配置文件将自动读入作为基础配置,通过设置其他的配置参数可以覆盖读入的基础配置。

-n, --name
可以在此制定服务名。

  • 如未配置,则使用脚本的文件名
  • 如魏TARS服务,则服务名必须为app.serverName格式

-l, -log
指定输出的日志文件根目录
如未配置,则所有日志输出采用stdout/stderr输出

-i, --instance
node-agent 采用Node.js原生的Cluster模块来实现负载均衡。
在此配置node-agent启动的子进程(业务进程)数量:

  • 未配置(或配置为 auto、0),启动的子进程的数量等于CPU物理核心的个数。
  • 配置为max,启动子进程数量等于CPU个数(所有核心数)。
    如果node-agent是由tarsnode启动的,会自动读取TARS配置文件中的tars.application.client.asyncthread配置节。也可以通过 TARS平台 ->遍及服务 -> 异步线程数 进行调整。

--env
设置服务启动时的环节变量,这里需要使用JSON格式进行描述
例如:可以通过这个配置来传入当前的运行环节(开发、生产)

{\"NODE_ENV"\:\"production"\}

请注意:当作为命令参数传递时,这里的双引号('')需要进行转移('')
如果此服务为TARS服务,则此参数以tarsnode可识别的方式读取并设置。

--http-address

设定服务脚本执行所需的ip:port
在脚本中可以使用环境变量HTTP_IP(IP)、HTTP_PORT(PORT)进行获取:

process.env.HTTP_IP
process.env.HTTP_PORT

如果此服务为TARS服务,则这里的值为配置文件中,第一个非TARS协议的Servant指明的ip:port

--script-args

设置服务脚本执行所需传入的参数
例如:

$ node-agent app.js --script-args=''--use="https"

等同于

$ node app.js --use="https"

--node-args

设置node cluster子进程所需的启动参数
例如:

$ node-agent app.js --node-args="--debug=7001 --trace-deprecation"

等同于:

$ node --debug=7001 --trace-deprecation app.js

--run-as-user, --run-as-group

制定node cluster子进程运行的用户(组)
可通过此服务脚本进行降权执行,如未配置权限等同于node-agent启动用户(组)

--max-memory-restart

制定服务所能使用到的最大内存。
如果子进程达到最大内存限制,将会抛出异常并退出。
此(资源形)也会纳入整体的异常进行处理。

--graceful-shutdown

正常情况下,node-agent在停止服务(进程)时会通过worker.disconnect()通知服务,让服务释放资源并退出。在这里可以设置超时时间,如果服务(进程)在给定的时间后仍然没有退出,node-agent则会强制kill掉进程,超时时间默认为8秒。
如果node-agent是由transnode启动的,会自动读取TARS配置文件中的tars.application.server.deactivating-timeout配置节。

--exception-max, --exception-time

如果(服务)子进程出现异常退出,并在一段时间内(--exception-time)异常退出的次数没有超过最大值(--exception-max)。node-agent将会自动拉起新的(服务)子进程,否则node-agent与服务也将异常退出、
以方便第三方管理工具对服务状态进行监控:
--exception-time默认为10s
--exception-max默认为2次

--keepalive-time

如果node-agent在一段时间(--keepalive-time)内未收到(服务)子进程发送的心跳,则判定(服务)子进程为僵尸进程(zombie process),将会直接杀死kill,并作为异常进行处理。
当服务器可用内存过小时不触发此逻辑。
如果您想对服务脚本进行(端点)调试,这需将此设置为 --keepalive-time=off
其默认值为5m

--applog-max-flies, --applog-max-size, --applog-level

制定服务默认的滚动日志大小(--applog-max-size)、总数(--applog-max-files)与日志级别(--applog-level)。
服务启动时会创建两份(滚动)日志:

  • app.serverName.log:所启动服务的 stdout/stderr/console
  • app.serverName_agent.log: node-agent的状态信息
    这个配置主要是影响上面两份(滚动)日志的输出参数

--tars-node, --tars-local

如果node-agent是由tarsnode启动的,则需要指定tarsnode的RPC连接参数(--tars-node)与本地被调的启动参数(--tars-local)。
此设置也可以通过TARS配置文件(--tars-config)进行指定。
node-agent会在服务启动时间向tarsnode上报服务的版本,并在服务运行过程中发送心跳包。
与此同时,node-agent本地启动的(被调)服务也将从tarsnode中接收下发的消息(shutdown/message),并进行响应。

--tars-monitor

如果您的服务在TARS平台上运行的,node-agent会自动向tarsstat上报服务的监控(用量)信息。
默认值为on,设置为off可关闭自动上报功能。
具体详情可查看,监控与用量上报节。

--tars-monitor-http-threshold

如果您服务的HTTP(s)返回码大于此阈值则此次请求将作为异常访问进行上报。
默认response.statusCode >= 400则为异常访问。
设置为off可以关闭此特性。
具体详情可查见,监控与用量上报节。

--tars-monitor-http-seppath

HTTP(s)服务在上报时是否需要区分不同路径。
默认为区分路径,其中url.pathname的部分会作为服务的接口名进行上报。
如果您的服务拥有非常多(大基数)的pathname(如RESTful),可设置为off。具体详情可查看监控与用量上报节。

配置

node-agent支持以多种匹配方式进行启动:

  • 命令行参数进行指定
  • 在服务脚本的package.son中指定
  • 在TARS服务的配置文件中指定
    其中:
  • 在package.json或TARS配置文件中指定的值,会覆盖掉命令行参数中指定的配置项。
  • 可以通过驼峰式写法将配置参数声明在package.json中nodeAgent的配置项。
  • TARS服务的配置文件中以配置参数原型直接进行声明。
    例如(以nobody用户启动子进程):
    命令行参数:
node-agent app.js -run-as-user=nobody

package.json:

{
  "nodeAgent" : {
    "runAsUser" : "nobody"
  }
}

TARS配置文件:

<tars>  
 <application>  
   <server>  
     run-as-user=nobody  
   </server>  
 </application>  
</tars>  
消息与事件

一般情况下,用户代码无需处理(关注)进程消息与事件,单如果您想处理(响应):进程退出、TARS管理命令,则需要进行处理、

process.on('disconnect', function)
关于此事件具体说明请参考 Cluster Event:‘disconnect’
默认情况下 node-agent 会对该事件进行处理,但如果用户代码监听(处理)了该事件则 node-agent 将不再进行处理。
请注意:您在处理完该事件后,请一定要显示调用process.exit()以确保进程可以正常退出。

process.on('message', object)
一旦node-agent收到了tarsnode的管理命令,将会通过进程消息发送给业务脚本。传递的消息object的格式为:

{
  cmd: String,
  data: String
}

支持的消息cmd有:

  • tars.viewstatus : 查看服务状态
  • tars.setloglevel:设置日志等级
  • tars.loadconfig:PUSH配置文件
  • tars.connection:查看当前连接情况
  • 自定义命令
    *node-agent会对自定义命令进行切分,命令中第一个空格前的字符作为cmd,后续的部分作为data。
日志

node-agent会将服务的输出(stdout|stderr管道以及console模块的输出)重定向到指定的文件(当使用 -l --log参数 启动时)或者管道。
日志的输出由winston-tars模块实现,其输出的日志格式为:日期 时间|PID|日志级别|文件名:行号|内容
服务脚本可以通过node自带的console模块输出不同级别的日志。

console.info = INFO
console.log = DEBUG
console.warn = WARN
console.error = ERROR

也可以通过服务stdout|stderr管道输出。

process.stdout = INFO
process.stderr = ERROR

日志级别的优先级为:INFO < DEBUG < WARN < ERROR < NONE
其中,默认的日志级别为:DEBUG

环境变量

node-agent通过环境变量向服务脚本提供所需的变量:

  • process.env.IP:HTTP(s)可监听的IP。
  • process.env.PORT:HTTP(s)可监听的端口。
  • process.env,WORKER_ID 进程顺序ID(例如启动8个进程,第一个为0,第二个为1,以此类推),重新启动的进程仍然使用之前的ID。
    如服务是由tarsnode启动的,还支持如下变量:
  • process.env.TARS_CONFIG:启动服务所使用的TARS配置文件的绝对路径。
  • process.env,TARS_MONITOR:是否开启监控(特性)上报(统计)。
    请注意:环境变量全为String类型

监控与用量上报

如果您的服务是在TARS平台上运行的,node-agent会自动向tarsstat上报服务的监控(用量)信息。

监控信息

监控信息的上报与您启动的服务及其调用者有关(可通过TARS平台->服务监控查看)

  • HTTP(s)
    • 服务端:response.statusCode >= 400为失败,所有请求超时为0
      • 可通过 --tars-monitor-http-threshold与--tars-monitor-http-seppath进行配置
        更多详情您可访问@tars/monitor.stat获取。
用量信息

无论您启用的服务是什么类型,用量信息总是上报(可通过 TARS平台->特性监控 查看):

  • memoryUsage: 内存用量,将会上报rss、heapUsed、heapTotal这三个用量(单位为字节)
  • cpuUsage: CPU用量,将会上报CPU使用率,数据汇总为逻辑单核(单位为百分比)
  • eventloopLag: 事件循环滞后(V8消息队列延迟),每隔2秒采样(单位为毫秒)
  • libuv: I/O用量,将会上报 activeHandles、activeRequests 这两个用量。
    所有的用量信息的统计策略均为:Avg、Max、Min
无损操作

如果您的服务是在TARS平台上运行的,每次无损重启或发布时:

  1. 设置流量状态为无流量(包括路由和第三方流量)
  2. 等待调用方获取配置(默认为2分13秒)
  3. 执行对应操作(重启或发布)
  4. 恢复流量状态
    请注意:如果大量节点同时进行无损操作,会同时屏蔽这些节点的流量,可能会造成服务不稳定,建议采用无损分批重启
预热

在无损操作的服务启动过程中,可以选择是否需要进行预热:

  1. 服务启动后美妙检查是否所有子进程都监听了端口(所有子进程状态均为ONLINE)
  2. 如果超高跟预热超时时间,且并非所有子进程都监听了端口,则无损操作流程失败并通知用户(邮件通知)
    我们强烈建议您:在任何情况下,请完成所有初始化操作后再监听(listen)端口
架构
architecture.png

node-agent在启动(也就是执行cluster.fork)服务脚本时,并不会直接载入对应脚本,而是载入node-agent/ProcessContainer.js来对服务脚本进行包装,之后再调用系统的require载入执行脚本

deploy

@tars/deploy
TARS框架服务打包工具,用于打包服务生成适合TARS框架的发布包。

安装
npm install -g @tars/deploy

由于tars-deploy是一个CLI程序,所以需要使用-g参数来安装

用法
tars-deploy name [options]
  • name为服务的“服务名”,如您的服务名为Server,那么填写“Server”
  • [options]可选配置,详见选项节。
    打包时:请切换当前目录到服务的根目录()
选项
Options:
  -h, --help output usage information
  -V, --version output the version number
  -f, --force Force to Build Package

-f, --force
由于工具会打包当前的运行环节(如node可执行的二进制文件,在当前架构上重新编译C/C++ addon等)所以请在与目标运营架构相同的环境(linux)上执行打包工具。
打开此开关,可以跳过此限制。但同时我们强烈您,不要这么做!

rpc

TARS框架中的RPC通信模块
00-安装

$ npm install @tars/tars

01-tars简介
tars是Tars4NodeJS项目底层的RPC调用框架,提供了一个多服务器进程间进行RPC调用的基础设置。简单来说我们可以用这个模块做这些事情:

  • 使用tars2node将Tars文件翻译成客户端代理类代码后,供客户端调用任意的Tars服务。
  • 使用tars2node将Tars文件翻译成服务端代码后,可以实现标准的Tars服务,该服务可被任意使用TARS/TUP协议的客户端直接调用。
  • 远程日志、染色日志、属性上报、告警上报、tarsnode与服务通信等框架内服务。
  • 创建自定义通信协议的客户端代理类(比如使用JSON格式的协议)。
  • 创建自定义通信协议的服务端(比如使用JSON格式的协议)。
  • 模块:@tars/registry,功能:根据服务Obj名字到主控查询该服务可用的IP列表。
    tars分为客户端和服务器端两个部分。客户端部分提供了rpc代理生成,消息路由和网络通讯等功能。服务器端提供了远程服务暴露,请求派发,网络通讯等功能。
    02-关于协议、Tars文件以及翻译工具tars2node的说明
    在深入学习了tars的相关知识之前,我们先理清:TARS编码协议、TUP组包协议、TARS组包协议三者之间的关系:
  • TARS编码协议是一种数据解码规则,它将整形、枚举值、字符串、序列、字典、自定义结构体等数据类型按照一定的规则编码到二进制数据流中。对端接收到二进制数据流之后,按照相应的规则反序列化可得到原始数值。
  • TARS编码协议是一种数据编解码规则,它将整形、枚举值、字符串、序列、字典、自定义结构体等数据结构按照一定的规则编码到二进制数据流中,对端接收到二进制数据流之后,按照相应的规则反序列化可得到原始数据。
  • TARS编码协议使用一种叫做TAG的整型值(unsigned char)来标识变量,比如某个变量A的TAG值为100(该值由开发者自定义),我们将变量值编码的同时,也将该TAG值编码进去。对端需要读取变量A的数值时,就到数据流中寻找TAG值为100的数据段,找到后按规则独处数据部分即是变量A的数值。
  • TARS编码协议的定位是一套编码规则。tars协议序列化之后的数据不仅可以进行网络传输,同时还可以存储到数据库或者DCache中。
  • TUP组包协议是TARS编码协议的上层封装,定义为通信协议。它使用变量名为变量的关键字,编码时,客户端将变量名打包到数据流中;解码时,对端根据变量名寻找对应的数据区,然后根据数据类型对该数据区进行反序列化得到原始数值。
  • TUP组包协议内置一个TARS编码协议的Map类型,该Map的关键字就是变量名,Map的值是将变量的数据值经过TARS编码序列化的二进制数据。
  • TUP组包协议封装的数据包可以直接发给Tars服务端,而服务端可以直接反序列化得到原始值。
  • TARS组包协议是对RequestPacket(请求结构体)和ResponsePacket(结果结构体)使用TARS编码协议封装的通信协议。结构体包含比如请求序列号、协议类型、RPC参数序列化之后二进制数据等重要信息。
    TARS编码协议的编码规则以及Tars文件的编写方法,请参考@tars/steam文档
    由Tars文件生成客户端或服务端代码的方法:
    首先安装tars2node模块,这个模块是一个命令行应用程序,所以需要全局安装
npm install -g @tars/tars2node

通过tars2node xxxx.tars --client命令得到client端代理类
通过tars2node xxxx.tars --server命令得到server端实现类
tars2node工具简介
学习Tars文件的编写方法之后,我们可以自己来定义通信描述文件,然后使用tars2node的不同命令行选项生成不同的代码文件:

$ tars2node Protocol.tars

上述命令将忽略interface描述段,只转换文件中定义的“常量”、“枚举值”、“结构体”等数据类型,供开发者当不使用Tars框架作为调用工具时的编解码库文件。生成的文件名称为“ProtocolTars.js”。

$ tars2node Protocol.tars --client

上述命令不仅转换文件中定义的"常量"、"枚举值"、"结构体"等数据类型,同时将interface的描述段翻译成RPC调用框架。生成的文件名称为"ProtocolProxy.js",该文件供调用方使用。开发者引入该文件之后,可以直接调用服务端的服务。具体的使用方法请参考"npm install tars"模块的说明文档。

$ tars2node Protocol.tars --server
选项 作用
--tars-lib-path=<DIRECTORY> 指定@tars/stream模块的路径,默认使用NodeJS的目录
--with-tars 是否允许"tars"作为命名空间(因为tars这个命名空间主要用于框架服务的tars文件定义)
--dir=<DIRECTORY> 生成文件的输出目录。
--relative 限定所有的Tars文件都在当前目录寻找。
--tarBase=<DIRECTORY> 指定Tars文件的搜索目录
--r 转换嵌套的Tars文件(比如在A.tars中包含了B.tars和C.tars,使用该参数,在翻译A.tars的同时也将B.tars和C.tars翻译成JS代码。)
--client 生成客户端的调用类代码。
--server 生成服务端的框架代码。

上述命令不仅转换文件中定义的"常量"、"枚举值"、"结构体"等数据类型,
同时将interface的描述段翻译成服务端的接口文件。生成的文件名称为"Protocol,js"以及"Protocolmp.js",开发者不要改动"Protocol,js",只需要继续完善"Protocollmp.js",实现文件中具体的函数,即可作为Tars服务端提供服务。具体的使用方法请参考"npm install tars"模块的说明文档。
tars2node支持的命令行参数及其作用:

选项 作用
--tars-lib-path=<DIRECTORY> 指定@tars/stream模块的路径,默认使用NodeJS的目录
--with-tars 是否允许"tars"作为命名空间(因为tars这个命名空间主要用于框架服务的tars文件定义)
--dir=<DIRECTORY> 生成文件的输出目录。
--relative 限定所有的Tars文件都在当前目录寻找。
--tarBase=<DIRECTORY> 指定Tars文件的搜索目录
--r 转换嵌套的Tars文件(比如在A.tars中包含了B.tars和C.tars,使用该参数,在翻译A.tars的同时也将B.tars和C.tars翻译成JS代码。)
--client 生成客户端的调用类代码。
--server 生成服务端的框架代码。
03-tars示例和开发步骤

文档看不下去了,马上动手实测!
第一步,下载rpc模块代码
第二步,在rpc模块根目录

$ npm install

第三步,在/rpc/examples/rpc-tars/demo.1/server.node.1目录下

$ node main.js

启动rpc服务端程序
第四步,在/rpc/examples/rpc-tars/demo.1/client.node.proxy目录下

$ node main.js

启动rpc客户端程序
使用tars模块的开发步骤
第一步,编写tars文件,定义客户端与服务端同学用到的常量、枚举值、结构体、函数等通信协议。我们使用如下tars文件作为示例:
一般而言Tars文件通常由服务端开发指定、维护和提供。

module TRom
{
  struct User_t
  {
    0 optional int id = 0;
    1 optional int score = 0;
    2 optional string name = "";
  };

  struct Result_t
  {
    0 optional int id = 0;
    1 optional int iLevel = 0;
  };

  interface NodeJsComm
  {
    int test();
     
    int getall(User_t stUser, out Result_t stResult);
    
    int getUserName(string sUsrName, out string sValue1, out string sValue2);
    
    int setRequest(vector<byte> binRequest, out vector<byte> binResponse);
  };
};

将上述内容保存为:NodeJsComm.tars。
第二步,根据tars文件生成客户端的调用代码

$ tars2node --client NodeJsComm,tars

第三步,客户端程序

//  STEP01 引入系统模块以及工具生成的代码
var Tars = require("@tars/tars").client;
var TRom = require("./NodeJsCommProxy,js").TRom;

//  STEP02  初始化Tars客户端
//  该步骤非必选项,后续文档将介绍[tars].client.initalize函数在什么情况下需要调用以及它做了哪些工作
//  initalize函数只需要调用一次,初始化之后全局可用
//  在演示程序中,我们不需要使用过多的特性,所以先将其注释
//  Tars.initalize("./config.conf");

//  STEP03  生成服务端调用代理类实例
var prx = Tars.stringToProxy(TRom.NodeJsCommProxy, "TRom.NodeJsTestServer.NodeJsCommObj@tcp -h 127.0.0.1 -p 14002 -t 60000");

//  STEP04  客户端调用采用Promise机制进行回调,这里编写成功以及失败的回调函数
var success = function (result) {
  console.log("result.response.costtime:", result.response.costtime);
  console.log("result.response.return:", result.response.return);
  console.log("result.response,arguments.stResult", result.response,arguments.stResult);
} 

var error = function () {
  console.log("result.response.costtime:", result.response.costtime);
  console.log("result.response.error.code:", result.response.error.code);
  console.log("result.response,error.message", result.response,error.message);
}

//  STEP05 初始化接口参数,开始调用RPC接口
var stUser = new TRom.User_t();
stUser.name = "tencent-mig";

prx.getall(stUser).then(success, error).done();

将上述代码保存为client.js,使用如下命令即可调用服务端。

$ node client.js
result.response.costtime: 7
result.response.return: 200
result.response.arguments.stResult: {id: 10000, iLevel: 10001}

如果我们只是调用方,写到这里已经足矣。按照刚才的示例,拿到相应Tars文件我们就可以调用C++的Tars服务、Java的Tars服务或者NodeJS的Tars服务。
第四步,实现一个NodeJS版本的Tars服务。
首先,完形填空。完成Tars文件中定义的RPC函数,实现自己的业务逻辑。
tars2node的--server选项将Tars文件生成服务端的代码。使用该选项翻译工具不仅转换文件中定义的“常量”、“枚举值”、“结构体”等数据类型,同时将interface描述段翻译成服务端的接口文件。主要生成两个文件,比如在当前例子中会生成NodeJsComm.js和NodeJsCommImp.js。开发者不需要也尽量不要改动NodeJsComm.js,该文件主要实现了:结构体编解码、函数参数编解码、函数分发等功能。NodeJsComImp.js继承于NodeJsComm.js,该文件主要供开发者填补定义的RPC函数,实现业务逻辑。

var TRom = require('./NodeJsComm.js').TRom;
module.exports.TRom = TRom;

TRom.NodeJsCommImp.prototype.initialize = function() {
  //TODO::
  
}

TRom.NodeJsCommImp.prototype.test = function (current) {
  //TODO::

}

TRom.NodeJsCommImp.prototype.getall = function (current, stUser, stResult) {
  //TODO::
    //初始时,每个RPC函数都为空,需要开发者自己完形填空,补齐这里缺失的业务逻辑。
    //补齐业务逻辑之后,开发者调用current的sendResponse函数,返回数据给调用方。
    //需要注意:每个函数的sendResponse都是不一样的,它的参数与当前函数的返回值和出参相对应。
    //  如果当前函数有返回值,那么current.sendResponse的第一个参数应该是该返回值。
    //  示例中当前函数的返回值为int类型,解决返回值的问题之后,我们按顺序写入当前的出参即可。参数的编解码和网络传输由框架解决。
    stResult.id = 10000;
    stResult.iLevel = 10001;

    current.sendResponse(200, stResult);
}

TRom.NodeJsCommImp.prototype.getUsrName = function(current, sUsrName, sValue1, sValue2) {
  //TODO::

}

TRom.NodeJsCommImp.prototype.setRequest = function(current, binRequest, binResponse) {
  //TODO::

}

接下来,创建一个服务入口文件。它主要负责读取配置文件、配置端口、设置协议解析器、启动服务等等工作。

var Tars = require("@tars/tars").server;
var TRom = require("./NodeJsCommImp,js").TRom;

var svr = Tar.createServer(TRom.NodeJsCommImp);
svr.start({
  name : "TRom.NodeJSTestServer.NodeJSCommObjAdapetr",
  servant : "TRom.NodeJSTestServer.NodeJsCommObj",
  endpoint : "tcp -h 127.0.0.1 -p 14002 -t 10000",
  protocol : "tars",
  maxconns : 200000
});

console.log("server started.");

将上述代码保存为server.js,使用如下命令启动。

$ node server.js
server started

04-客户端的初始化函数[tars].client.initialize

在演示代码中我们提到initialize不一定要显示调用,我们用其他方式同样可以设置我们需要的参数。
首先我们看下配置文件的格式和必要参数:

<tars>
  <applictaion>
    <client>
      locator = tars.tarsregistry.Queryobj@tcp -h 172.27.208.171 -p 17890 ##定义主控地址
      async-invoke-timeout=60000 ##异步调用的超时时间(ms)
    </client>
  </applictaion>
</tars>

这个配置文件正是由tarsnode生成的,我们主要使用“tars.application.client.locator”和“tars.application.client.async-invoke-timeout”这两个配置项。
什么情况下可以不用调用initialize函数?
如果我们在生成服务端代理时,每个服务端都使用直连的模式,也就是在stringToProxy中指定Ip地址就可以不用初始化了。

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

Tars.set("locator", "tars.tarsregistry.QueryObj@tcp -h 172.27.208.171 -p 17890");
Tars.set("timeout", 60000);

上述的调用方法,与使用initialize+配置文件的方式等价。

05-Tars服务的创建方法

tars有三种方法创建一个标准的Tars服务:
第一种,使用tarsnode生成的配置文件。
使用这种方法与TARS4C++的使用方式一样。
首先需要我们在TARS管理平台配置服务的Obj,然后在启动程序时由tarsnode生成包含监听端口的配置文件,然后服务框架再依赖该配置绑定端口+启动服务。

deploy.png

tarsnode生成的配置文件类似于如下:

<tars>
  <applictaion>
    enableset=n
      setdivision=NULL
      <server>
        node=tars.tarsnode.ServerObj@tcp -h 127.0.0.1 -p 19386 -t 60000
        app=TRom
        server=NodeJsTestServer
        localip=127.0.0.1
        netthread=2
        local=tcp -h 127.0.0.1 -p 1002 -t 3000
        basepath = /usr/local/app/tars/tarsnode/data/MTT.NodeJSTest/bin/
        datapath = /usr/local/app/tars/tarsnode/data/MTT.NodeJSTest/data/
        logpath=/usr/local/app/tars/app_log//
        logsize=15M
        config=tars.tarsconfig.ConfigObj
        notify=tars.tarsnotify.NotifyObj
        log=tars.tarslog.LogObj
        deactivating-timeout=3000
        openthreadcontext=0
        threadcontextnum=10000
        threadcontextstack=32768
        closeout=0
        <TRom.NodeJsTestServer.NodeJsCommObjAdapter>
          allow
          endpoint=tcp -h 127.0.0.1 -p 14002 -t 60000
          maxconns=200000
          protocol=tars
          queuecap=10000
          queuetimeout=60000
          servant=TRom.NodeJsTestServer.NodeJsCommObj
          shmcap=0
          shmkey=0
          thread=5
        <TRom.NodeJsTestServer.NodeJsCommObjAdapter/>  
      </server>
      <client>
        locator=tars.tarsregistry.Queryobj@tcp -h 127.0.0.1 -p 17890:tcp -h 127.0.0.1 -p 17890
        refresh=endpoint-interval=60000
        stat=tars.tarsstat.StatObj
        property=tars.tarsproperty.PropertyObj
        report-interval=60000
        sample-rate=1000
        max-sample-count=100
        sendthread=1
        recvthread=1
        asyncthread=3
        modulenam=TRom.NodeJsTestServer
        async-invoke-timeout=60000
        sync-invoke-timeout=3000
      </client>
    </nav>
  </applictaion>
</tars>

我们使用该配置文件创建一个服务,代码如下:

//  STEP01 引入关键模块
var Tars = require("@tars/tars");
var TRom = require("./NodeJsCommImp.js");

//  STEP02 创建一个服务的实例
//  注意这里的配置,在正式环境时,用process.env.TARS_CONFIG来表示配置文件的路径
//  也就是:svr.initialize(process.env.TARS_CONFIG, function (server){...});
var svr = new Tars.server();
svr.initialize("./TRom.NodeJsTestServer.config.conf", function (server){
  server.addServant(TRom.NodeJsCommImp, server.Application + "." + server.ServerName + ".NodeJsCommObj");
});

//  STEP03 上步初始化服务之后,开始启动服务
svr.start();

第二种,显示化服务端信息

//  STEP01 引入关键模块
var Tars = require("@tars/tars").server;
var TRom = require("./NodeJsCommImp,js").TRom;

//  STEP02 创建一个服务的实例
//  注意这里的“endpoint”和“protocol”为必选项,格式必须如下实例相同
var svr = Tars.createServer(TRom.NodeJsCommImp);
svr.start({
  name : "TRom.NodeJsTestServer.AdminObjAdapter",
  servant : "TRom.NodeJsTestServer.AdminObj",
  endpoint : "tcp -h 127.0.0.1 -p 14002 -t 10000"
  maxconns : 200000
  protocol : "tars"
});

console.log("server started");

第三种,从tarsnode生成的配置文件中,选取部分服务来启动

//  STEP01 引入关键模块
var Tars = require("@tars/tars");
var TRom = require("./NodeJsCommImp.js");

Tars.server.getServant('./TRom.NodeJsTestServer.config.conf').forEach(function (config){
  var svr, map;
  map = {
    'TRom.NodeJsTestServer.NodeJsCommObj' : TRom.NodeJsCommImp 
  };

  svr = Tars.server.createServer(map[config.servant]);
  svr.start(config);
});

06-Tars客户端的实现原理

client.png

07-Tars服务端的实现原理

server.png

08-tars作为客户端调用第三方协议服务的示例

首先我们先定一个双方认可的通信协议,比如我们以Json格式作为通信协议,格式假定:

//  客户端 --> 服务端
{
  p_RequestId : 0,  // 本次调用的序列号
  p_FuncName : 'test',  // 本次调用的函数名称
  p_Arguments : ['aa', 'bb'......]  // 本次调用的函数参数
}

//  客户端 <-- 服务端  
{
  p_RequestId : 0,  // 本次调用的序列号
  p_FuncName : 'test',  // 本次调用的函数名称
  p_Arguments : ['ee', 'ff', ......]  // 本次调用的返回参数
}

实现协议解析类

//  将文件保存为Protocol.js
var EventEmitter = require("events").EventEmitter;
var util = require("@tars/util");

var stream = function() {
  EventEmitter.call(this);
  this._data = undefined;
  this._name = "json";
}

util.inherits(stream, EventEmitter);

stream.prototype.__defineGetter__("name", function () { return this._name; });

module.exports = stream;

/**
 * 根据传入数据进行打包的方法
 * @param request
 * request.iRequestId : 框架生成的请求序列号
 * request.sFuncName : 函数名称
 * request.arguments : 函数的参数列表
 */
stream.prototype.compose = function(data) {
  var str = JSON.stringify({
    p_RequestId : data.iRequstId,
    p_FuncName : data.sFuncName,
    p_Arguments : data.arguments
  });

  var len = 4 + Buffer.byteLength(str);
  var buf = new Buffer(len);
  buf.writeUInt32BE(len, 0);
  buf.write(str, 4);

  return buf;
}

/**
 * 网络收取包之后,填入数据判断是否完整包
 * @param data 传入的data数据可能是TCP的各个分片,不一定是一个完整的数据请求,解析类内部做好数据缓存工作。
 *
 * 当有一个完整的请求时,解包函数抛出事件,需按照如下格式补充事件的数据成员
 *
 * {
 *  iRequestId : 0, // 本次请求的序列号
 *  sFuncName : “”, // 本次请求的函数名称
 *  Arguments : []  // 本次请求的参数列表
 * }
 */
stream.prototype.feed = function(data) {
  var BinBuffer = data;
  if (this._data != undefined) {
    var temp = new Buffer(this._data.length + data.length);
    this._data.copy(temp, 0);
    data.copy(temp, this._data.length);
    this._data = undefined;
    BinBuffer = temp;
  }

  for(var pos = 0; pos < BinBuffer.length;) {
    if (BinBuffer.length - pos < 4) {
      break;
    }
    var Length = BinBuffer.readUInt32BE(pos);
    if (pos + Length > BinBuffer.length) {
      break;
    }
    var result = JSON.parse(BinBuffer.slice(pos + 4, pos + Length).toString());
    var request =
    {
      iRequestId : result.P_RequestId,
      sFuncName : result.p_FuncName,
      Arguemnts : result.p_Arguments
    };

    this.emit("message", request);
    pos += Length;
  }

  if (pos != BinBuffer.length) {
    this._data = new Buffer(BinBuffer.length - pos);
    BinBuffer.copy(this._data, 0, pos);
  }
}

/**
 * 重置当前协议解析器
 */
stream.prototype.reset = function () {
  delete this._data;
  this._data = undefined;
}

客户端使用该协议解析器,调用服务端的代码:

var Tars = require("@tars/tars").client;
var Protocol = require("./ProtocolClient.js");

var prx = Tars.stringToProxy(Tars.ServantProxy, "test@tcp -h 127.0.0.1 -p 12306 -t 60000");
prx.setProtocol(Protocol)
prx.rpc.createFunc("echo");

var success = function (result) {
  console.log("success");
  console.log("result.response.costtime:", result.response.costtime);
  console.log("result.response.arguments:", result.response.arguments);
}

var error = function () {
  console.log("error");
  console.log("result.response.error.code:", result.response.error.code);
  console.log("result.response.error.message:", result.response.error.message);
}

prx.rpc.echo("tencent", "mig", "abc").then(success, error);

另外,如果想请求某个特征来发到某台固定的机器,可以使用如下方法:

prx.getUsrName(param, {
  hashCode.userId
}).then(success, error).done();

获得客户端对象之后,调用服务端接口函数,此时可以传入hashCode参数,tars会根据hashCode来吧请求分配到连接列表中固定的一台机器,需要注意的是:

  • 这里的userId是一个数字,二进制、八进制、十六进制都可以,但是转换成10进制的数字最好在16位数一下。javascript处理高精度数字会有精度丢失的问题。
  • 服务端机器列表固定时,同一hashCode会被分配到固定的一台机器,当服务端机器列表发生变化时,会重新分配hashCode对应的机器。

09-tars作为第三方协议服务的示例

首先实现RPC函数处理类,注意框架的分发逻辑:
A. 如果客户端传来的函数名,是处理类的函数,那么框架有限调用对应函数。
B. 如果客户端出来的函数不是处理的函数,那么调用该处理类的onDispatch函数,由该函数负责处理该请求。
C. 如果也没有onDispatch函数,则报错

//  将该文件保存为:EchoHandle.js
var Handle = function () {
  
}

Handle.prototype.initialize = function () {}
Handle.prototype.echo = function (current, v1, v2, v3) {
  console.log("EchoHandle.echo::", v1, v2, v3);

  current.sendResponse("TX", "TX-MIG");
}

Handle.prototype.onDispatch = function(v1, v2, v3) {
  console.log("EchoHandle.onDispatch::", v1, v2, v3);
}

module.exports = Handle;

服务端启动函数的代码示例:

var Tars = require("@tars/tars").server;
var Protocol = require("./ProtocolClient.js");
var Handle = require("./EchoHandle.js");

var svr = Tars.createServer(Handle);
svr.start({
  endpoint : "tcp -h 127.0.0.1 -p 12306 -t 10000",
  protocol : Protocol
});

09-tars客户端请求参数

tars客户端代理对象调用协议接口函数时,最后一个参数可以传入一个对象,配置一些请求参数,目前支持4种请求参数。
dyeing
请求染色对象。染色对象生成方式详见@tars/dyeing
context
请求上下文对象
packetType
请求类型参数,1位单向请求,其他为普通请求
hashCode
请求hash,必须填入js精度安全范围内的数字(Math.pow(2, 53) - 1)
示例:

prx.getUsrName(param, {
  dyeing: dyeingObj,
  context: {xxx:xxx},
  packetType: 1,
  hashCode: userId
}).then(success, error);

Monitor

TARS框架中用于服务监控、特性监控上报

Monitor是TARS(TUP)服务与特性监控上报模块。
它由2个子模块构成:

  • 服务监控(stat)
  • 特性监控(property)
    安装
npm install @tars/monitor
初始化

如果服务通过node-agent(或TARS平台)运行,则无需执行该方法。
初始化是通过调用特点模块的init(data)方法实现。
data: 可以为tars配置文件路径或已配置的(@tars/utils).Config实例。

服务监控(stat)
var stat = require('@tars/monitor').stat;

服务监控主要统计(上报)每个请求的成功、失败、超时的调用量,并当调用成功时额外搜集调用耗时。
因为其他模块已经集成了本模块,所以一般情况下,服务脚本无需显式使用此模块。
已集成的模块如下:

  • TUP Client&Server由@tars/rpc进行上报。
  • HTTP(S) Server由node-agent进行上报,但由于HTTP(S)协议的特性所以:
    • 不统计超时的调用量,所有请求的超时上报为0。
    • response.statusCode >= 400为失败调用,否则为成功调用。
      如您确定要手动进行上报,可通过如下代码进行:
stat.report(headers, type[, timeout]);

headers:

  • masterName: 主调模块名
  • slaveName: 被调模块名
  • interfaceName: 被调模块接口名
  • masterIp: 主调IP
  • slaveIp: 被调IP
  • slavePort: 被调端口
  • bFromClient: 客户端上报为true,服务端上报为false
  • returnValue: 返回值,默认为0
    如果被调为set则headers还需包括如下信息:
  • slaveSetName: 被调set名
  • slaveSetArea: 被调set地区名
  • slaveSetID: 被调set组名
    如果主调为set则headers还需包含如下信息:
  • masterSetInfo: 主调set信息(由setName.setArea.setID构成)
    参数type的取值为stat.Type中的一种,如下所示:
    stat.TYPE:
  • SUCCESS: 成功
  • ERROR: 失败
  • TIMEOUT: 超时
    如果type === stat.TYPE.SUCCESS必须上报响应耗时timeout(整型)
    数据上报后,用户可在服务监控选项卡中查看上报的数据。

特性监控(property)

var property = require('@tars/monitor').property;

特性监控上报的是服务脚本的自定义特性,它由特性名、特性值、以及统计方法构成(key/value pairs)。

property.create(name, policies)

调用create方法,会返回(或创建)一个上报对象,可以通过调用返回对象的report(value)方法进行上报。其中name为上报的特性值名,而policies是统计方法类的实例数组(指定了数据的统计方法)。

property.create('name', [new property.POLICY.Count, new  property.POLICY.Max]);

policies数组中的实例对象不能有重复的统计方法。
请注意:不要每次上报都调用create获取上报对象,这样会造成性能损耗

obj.report(value)

property.create会返回一个上报对象,可以通过调用对象的report方法进行上报。
每次调用report只能上报一次数据,并且value在一般情况下必须为数值。
数据上报后,用户可以在特性监控中查看上报的数据。

统计方法

特性监控 所上报的数据(也就是在调用create时)需要制定一种或几种统计方法,以便统计在一段时间内的值,这些方法都在POLICY中定义,它们分别是:

  • POLICY.Max:统计最大值
  • POLICY.Min:统计最小值
  • POLICY.Count:统计一共有多少个数据
  • POLICY.Sum:将所有数据进行相加
  • POLICY.Avg:计算数据的平均值
  • POLICY.Distr:分区统计
    除了property.POLICY.Distr其它均不需要传递构造参数

property.POLICY.Distr(ranges)

Distr为分区间统计,事先划分区间,在上报时会自动统计落入这个区间的value数量。
同时,在进行数据展示时,分区间统计展示为饼图类型。
其中,参数ranges为数组,其中的每一项为一个数值(Int),并以从小到大的顺序排列。
例如:

var monitor = property.create('name', [new property.POLICY.Distr([0, 10, 100, 1000])]);
monitor.report(2);
monitor.report(20);
monitor.report(200);

上面的例子统计的统计结果为:

[0 - 10] = 1
(10 - 100] = 1
(100 - 1000] = 1

每个区间都包含右侧不包含左侧(除了第一个区间)

上报间隔

在监控上报中,并非每次调用report方法均会上报数据,模块会搜集一定时间内提交的数据,并进行整合统计后一次性上报(单向调用)。
请注意:配置的上报间隔不可低于10s,亦不可高于TARS主控的刷新时间(也就是tars.application.client.refresh-endpoint-interval 配置节)
为了防止循环调用,上报模块本身的调用情况由被调方上报(也就是单向调用的上报逻辑)。

stream

TARS框架的编解码工具

00-安装
$ npm install @tars/stream
01-stream模块基本介绍和使用方法

stream模块用作Tars(tars/TUP)基础协议编解码库,使用该模块可以基于tars协议描述格式对数据流进行编解码,并能够与目前使用tars协议的TARS服务端以及终端进行无障碍通信。
tars编解码模块工作流方式一般有如下三种:
第一种,以tars文件作为调用方和服务方的通信桥梁(双方约定最终协议以tars文件为准)
该tars文件也就是我们常识说的以".tars"结尾的协议描述文件。
该tars文件一般由后台开发制定,前台开发需向后台开发索求经评审确定的tars文件,然后经工具转换成适用于NodeJS的编解码源代码文件。

module TRom 
{
  struct User_t
  {
    0 optional int id = 0; 
    1 optional float score = 0;
    2 optional string name = "";
  };

  struct Result_t 
  {
    0 optional int id = 0;
  };

  interface NodeJsComm
  {
    int test();

    int getall(User_t stUser, out Result_t stResult);

    int getUsrName(string sUsrName, out string sValue1, out string sValue2);

    int secRequest(vector<byte> binRequest, out vector<byte> binResponse);
  };
};

比如,我们将如上内容保存为"Protocol.tars"后,可以使用如下的命令生成不同的文件:

$ tars2node Protocol.tars

上述命令将忽略interface描述段,只转换文件中定义的"常量"、"枚举值"、"结构体"等数据类型,供开发者当不使用Tars框架作为调用工具时的编解码库文件。生成的文件名称为"Protocol.js"。

$ tars2node Protocal,tars --client

上述命令不仅转换文件中定义的"常量"、"枚举值"、"结构体"等数据类型,同时将interface的描述段翻译成RPC调用框架。生成的文件名称为"ProtocolProxy.js",该文件供调用方使用。开发者引入该文件之后,可以直接调用服务端的服务,具体的使用方法请参考"npm install rpc"模块的说明文档。

$ tars2node Protocol.tars --server

上述命令不仅转换文件中定义的"常量"、"枚举值"、"结构体"等数据类型,同时将interface的描述段翻译成服务端的接口文件。生成的文件名称为"Protocol.js"以及"ProtocolImp.js",开发者不要改动"Protocol.js",只需要继续完善"ProtocolImp,js",实现文件中的具体函数,即可作为Tars服务端提供服务、具体的使用方法请参考"npm install rpc"模块的说明文档。
第二种,没有协议描述文件,需要我们自己手工书写编解码代码时。
比如服务后台提供购买某件商品的功能,它需要“用户号码”、“用户昵称”、“商品编号”、“商品数量”等四个参数。后台对这四个参数的编号(也就是tars中所指的tag)分别为0、1、2、3。

//  第一步,引入tars/TUP编解码库
var Tars = require("@tars/stream");

//  第二步,客户端按照服务端要求,对输入参数进行编码
var ost = new Tars.OutputStream();
ost.writeUnit32(0, 155069599);  //  写入"用户号码";在服务端“0”代表“用户号码”
ost.writeString(1, "KevinTian");  //  写入“用户昵称”;在服务端“1”代表“用户昵称”
ost.writeUnit32(2, 1002121); //  写入“商品编号”:在服务端“2”代表“商品编号”
ost.writeUnit32(3, 10); //  写入"商品数量";在服务端“3”代表“商品数量”

//  第三步,客户端将打包后的二进制Buffer发送给服务端
send (ost.getBinBuffer().toNodeBuffer()) to server

//  第四步,服务端从客户端接收完整的请求二进制Buffer
recv (var requestBuffer = new Buffer()) from client

//  第五步,将该请求进行解码反序列化
var ist = new Tars.InputStream(new Tars.BinBuffer(requestBuffer));

var uin = ist.readUInt32(0, true);  //  根据编号"0"读取“用户号码”
var name = ist.readString(1, true); //  根据编号“1”读取“用户昵称”
var gid = ist.readInt32(2, true); //  根据编号“2”读取“商品编号”
var num = ist.readUInt32(3, true);  //  根据编号“3”读取“商品数量”

//  第六步,根据相关传入参数进行相应的逻辑操作
console.log("name:", name);
console.log("num:", num);
......
第三种,服务端接受TUP协议格式的数据
//  第一步,引入tars/TUP编解码库
var Tars = require("@tars/stream");

//  第二步,客户端按照服务端要求,对输入参数进行编码
var tup_encode = new Tars.Tup();
tup_encode.writeInt32("uin", 155069599);  //  服务端接口函数”用户号码“的变量名称为”uin“
tup_encode.writeString("name", "KevinTian");  //  服务端接口函数”用户昵称“的变量名称为”name“
tup_encode.writeUInt32("gid", 1002121); //  服务端接口函数“商品编号”的变量名称为“gid”
tup_encode.writeUInt32("num", 10);  //  服务端接口函数“商品数量”的变量名称为“uum”

var BinBuffer = tup_encode.encode(true);

//  第三步,客户端将打包后的二进制Buffer发送给服务端
send (BinBuffer.toNodeBuffer()) to server

//  第四步,服务端从客户端接收完整的请求二进制Buffer
recv (var requestBuffer = new Buffer()) from client

//  第五步,将请求进行解码反序列化
var tup_decode = new Tars.Tup();
tup_decode.decode(new Tars.BinBuffer(requestBuffer));

var uin = tup_decode.readInt32("uin");  // 服务端根据变量名“uin”读取“用户号码”
var name = tup_decode.readString("name"); //  服务端根据变量名“name”读取”用户昵称“
var num = tup_decode.readInt32("num");  //  服务端根据变量名“num”读取“商品数量”
var gid = tup_decode.readInt32("gid");  //  服务端根据变量名“gid”读取“商品编号”

//  第六步,根据相关传入参数进行相应的逻辑操作
console.log("name:", name);
console.log("num:", num);
......

02-stream支持的数据类型以及使用方法

数据类型 对应C++语言的数据类型
布尔值 bool
整型 char(int8)、short(int16)、int(int32)、long long(int64)
整型 unsigned char(uint8)、unsigned short(uint16)、unsigned int(uint32)
数值 float(32位)、double(64位)
字符串 std::string
数据类型 对应C++语言的数据类型
结构体 struct(在Tars框架中需要使用tars2node根据tars文件来生成JavaScript中的类)
二进制Buffer vector<char>(在NodeJs中使用[stream].BinBuffer类型来模拟)
数组 vector<DataType>(在NodeJs中使用[stream].List(vproto)类型来模拟)
词典 map<KeyType, DataType> (在NodeJs中使用[stream],Map(kproto, vproto)类型来模拟)

基本数据类型(见上表)

数据类型 对应C++语言的数据类型
结构体 struct(在Tars框架中需要使用tars2node根据tars文件来生成JavaScript中的类)
二进制Buffer vector<char>(在NodeJs中使用[stream].BinBuffer类型来模拟)
数组 vector<DataType>(在NodeJs中使用[stream].List(vproto)类型来模拟)
词典 map<KeyType, DataType> (在NodeJs中使用[stream],Map(kproto, vproto)类型来模拟)

复杂数据类型(见上表)
关于NodeJs中数据类型的特别说明
[1] : “复杂数据类型”与“基本数据类型”,或者“复杂数据类型”与“复杂数据类型”组合使用可以组成其他高级数据类型。
[2] : 虽然NodeJS中支持Float和Double数据类型,但我们不推荐使用,因为在序列化之后,数值存在精度缺失,某些情况下会对业务逻辑造成伤害。
[3] : 我们这里实现的64位整型实际上是伪64位,在NodeJs它的原型仍然是Number。
我们都知道Js中的Number类型采用IEEE754双精度浮点数标准来表示。IEEE754规定有效数字第一位默认为1,再加上后面的52位来表示数值。
也就是说IEEE754提供的有效数字的精度为53个二进制位,这就意味着NodeJs的Number数值或者说我们实现的Int64数据类型只能精确表示绝对值小于2的53次方的整数。
[4] : 在Javascript中String类型是Unicode编码,在tars编解码时我们将其转换成了UTF8编码格式;
后台服务程序接受到的字符串是UTF8编码,如果需要按照GBK编码的方式处理字符串,需要后台程序先做下转码(UTF8->GBK);
后台服务程序如果使用的是GBK,发送字符串之前,需要将其转成UTF8编码。

03-基本类型使用方法

//  必须引入stream模块
var Tars = require('@tars/stream');

//  使用Tars.OutputStream对数据进行序列化
var os = new Tars.OutputStream();

os.writeBoolean(0, false);
os.writeInt8(1, 10);
os.writeInt16(2, 32767);
os.writeInt32(3, 0x7FFFFFFE);
os.writeInt64(4, 8589934591);
os.writeUInt8(5, 200);
os.writeUInt16(6, 65535);
os.writeUInt32(7, 0xFFFFFFEE);
os.writeString(8, "我的测试程序");

//  使用Tars.InputStream对数据进行反序列化
var is = new Tars.InputStream(os.getBinBuffer());

var tp0 = is.readBoolean(0, true, false);
console.log("BOOLEAN:", tp0);

var tp1 = is.readInt8(1, true, 0);
console.log("INT8:", tp1);

var tp2 = is.readInt16(2, true, 0);
console.log("INT16:", tp2);

var tp3 = is.readInt32(3, true, 0);
console.log("INT32:", tp3);

var tp4 = is.readInt64(4, true, 0);
console.log("INT64:", tp4);

var tp5 = is.readUInt8(5, true, 0);
console.log("UINT8:", tp5);

var tp6 = is.readUInt16(6, true, 0);
console.log("UINT16:", tp6);

var tp7 = is.readUInt32(7, true, 0);
console.log("UINT32:", tp7);

var tp8 = is.readString(8, true, "");
console.log("STRING:", tp8);

04-复杂类型前传-用于表示复杂类型的类型原型

首先,我们理解一下什么是类型原型
在C++中,我们可以按如下方法声明一个字符串的容器向量:

#include <string>
#include <vector>

std::vector<std::string> vec;
vec.push_back("qzone");
vec.push_back("wechat");

其中std::vector<std::string> vec,std::vector表示容器类型,而std::string则表示该容器所容纳的类型原型
那我们如何在NodeJs中表示该类型?并能使之于tars编码库无缝的融合?
为了解决这个问题,我们使用如下的方法对std::vector进行模拟,以达到上述C++代码能完成的功能:

var Tars = require("@tars/stream");

var abc = new Tars.List(Tars.String);
abc.push("qzone");
abc.push("wechat");

其中:Tars.List(Tars.String),Tars.List表示数组类型,而Tars.String则用来表示该容器所容纳的类型原型
至此,我们明白类型原型主要是用来与复杂数据类型组合,表示更加复杂的数据类型

数据类型 描述
布尔值 [stream].Boolean
整型 [stream].Int8, [stream].Int16, [stream].32, [stream].64, [stream].UInt8, [stream].UInt16, [stream].UInt32
数值 [stream].Float, [stream].Double
字符串 [stream].String
枚举值 [stream].Enum
数组 [stream].List
字典 [stream].Map
二进制Buffer [stream].BinBuffer

目前的版本中,我们支持如上的类型原型定义:
为了让大家更加清晰的理解该概念,我们提前描述一部分复杂类型在NodeJs中的表示方法。
数据类型的详细使用方法,请参考后续的详细说明。

var Tars = require("@tars/stream");

// c++语法:std::vector<int>
var abc = new Tars.List(Tars.Int32);
abc.push(10000);
abc.push(10001);

//  c++语法:std::vector<std::vector<std::string>>
var abc = new Tars.List(Tars.List(Tars.String));
var ta = new Tars.List(Tars.String);
    ta.push("ta1");
    ta.push("ta2");
var tb = new Tars.List(Tars.String);
    tb.push("tb1");
    tb.push("tb2");
abc.push(ta);
abc.push(tb);

// c++语法:std::map<std::string, std::string>
var abc = new Tars.Map(Tars.String, Tars.String);
abc.insert("key1", "value1");
abc.insert("key2", "value2");

//  c++语法:std::map<std::string, std::vector<string>>
var abc = new Tars.Map(Tars.String, Tars.List(Tars.String));
var ta = new Tars.List(Tars.String);
    ta.push("ta1");
    ta.push("ta2");
var tb = new Tars.List(Tars.String);
    tb.push("tb1");
    tb.push("tb2");
abc.insert("key_a", ta);
abc.insert("key_b", tb);

//  c++语法:std::vector<char>
var abc = new Tars.BinBuffer();
abc.writeInt32(10000);
abc.writeInt32(10001);

05-复杂类型-struct(结构体)的使用方法说明

module Ext 
{
  struct ExtInfo {
      0 optional string sUerNamel
      1 optional map<string, vector<byte>> data;
      2 optional map<string, map<string, vector<byte>>> cons;
  };
};

将上述内容保存为文件"Demo.tars",然后使用命令"tars2node Demo.tars"生成编码文件"Demo.js"。
"Demo,js"内容如下所示:

var TarsStream = require("@tars/stream");

var Ext = Ext || {};
module.exports.Ext = Ext;

Ext.ExtInfo = function() {
  this.sUserName = "";
  this.data = new TarsStream.Map(TarsStream.String, TarsStream.BinBuffer);
  this.cons = new TarsStream.Map(TarsStream.String, TarsStream.Map(TarsStream.String, TarsStream.BinBuffer));
};

Ext.ExtInfo._write = function (os, tag, value) {os.writeStruct(tag, value);}
Ext.ExtInfo._read = function (is, tag, def) { return is.readStruct(tag, true, def); }
Ext.ExtInfo._readFrom = function(is) {
  var tmp = new Ext.ExtInfo();
  tmp.sUserName = is.readString(0, false, "");
  tmp.data = is.readMap(1, false, TarsStream.Map(TarsStream.String, TarsStream.BinBuffer));
  tmp.cons = is.readMap(2, false, TarsStream.Map(TarsStream.String, TarsStream.Map(TarsStream.String, TarsStream.BinBuffer)));
  return tmp;
};
Ext.ExtInfo.prototype._writeTo = function(os) {
  os.writeString(0, this.sUserName);
  os.writeMap(1, this.data);
  os.writeMap(2, this.cons);
};
Ext.ExtInfo.prototype._equal = function(anItem) {
  return anItem.sUserName === this.sUserName
  && anItem.data === this.data
  && anItem.cons === this.cons;
};
Ext.ExtInfo.prototype._genKey = function() {
  if (!this._proto_struct_name_) {
      this._proto_struct_name_ = 'STRUCT' + Math.random();
  }
  return this._proto_struct_name_;
};
Ext.ExtInfo.prototype.toBinBuffer = function() {
  var os = new TarsStream.OutputStream();
  this._writeTo(os);
  return os.getBinBuffer();
};
Ext.ExtInfo.create = function(is) {
   return Ext.ExtInfo._readFrom(is);
};
方法 限制 描述
_write 开发者不可用 静态函数。当结构体作用类型原型时使用
_read 开发者不可用 静态函数。当结构体作用类型原型时使用
_readFrom 开发者不可用 静态函数。从数据流中读取结构体的数据成员值,并生成一个权限的结构体示例返回。
_writeTo 开发者不可用 成员函数。将当前结构体的数据成员写入指定的数据流中。
_equal 开发者不可用 成员函数。将当前结构体用作字典类型Key值时的比较函数。
_genKey 开发者不可用 成员函数。将当前结构体用作字典类型Key值时,内部使用该函数获得当前结构体的别名。
toBinBuffer 开发者可用 成员函数。将当前结构体序列化成二进制Buffer,返回值类型为require("@tars/stream").BinBuffer。
create 开发者可用 成员函数。从数据流返回一个全新的结构体。

对"module Ext"的说明
Ext在C++中就是命名空间,在JavaScript中我们将它翻译成一个Object,该命名空间下所有的常量、枚举值、结构体、函数都挂载在该Object之下。
tars文件中描述的结构体的表示方法
首先,结构体翻译成一个Object。翻译程序根据数据类型以及tars文件中定义的默认值,生成数据成员。除tars中定义的数据成员之外,根据编解码的需要,翻译程序为结构体添加了若干辅助函数。这些函数如:_writeTo,在需要结构体序列化为数据流的地方,被编解码库调用,该函数逐个将数据成员写入数据流中。
翻译程序默认添加的辅助函数

方法 限制 描述
_write 开发者不可用 静态函数。当结构体作用类型原型时使用
_read 开发者不可用 静态函数。当结构体作用类型原型时使用
_readFrom 开发者不可用 静态函数。从数据流中读取结构体的数据成员值,并生成一个权限的结构体示例返回。
_writeTo 开发者不可用 成员函数。将当前结构体的数据成员写入指定的数据流中。
_equal 开发者不可用 成员函数。将当前结构体用作字典类型Key值时的比较函数。
_genKey 开发者不可用 成员函数。将当前结构体用作字典类型Key值时,内部使用该函数获得当前结构体的别名。
toBinBuffer 开发者可用 成员函数。将当前结构体序列化成二进制Buffer,返回值类型为require("@tars/stream").BinBuffer。
create 开发者可用 成员函数。从数据流返回一个全新的结构体。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容