写给go开发者的Tars教程-Tars协议基础

2023-06-18
5分钟阅读时长

简介

Tars这个名字取自于电影"星际穿越"中的机器人,它是基于名字服务使用Tars协议的高性能RPC开发框架,配套一体化的运营管理平台,并通过伸缩调度,实现运维半托管服务。

Tars是腾讯从2008年到今天一直在使用的后台逻辑层的统一应用框架TAF(Total Application Framework),目前支持C++、Java、PHP、Nodejs、Go语言。该框架为用户提供了涉及到开发、运维、以及测试的一整套解决方案,帮助一个产品或者服务快速开发、部署、测试、上线。 它集可扩展协议编解码、高性能RPC通信框架、名字路由与发现、发布监控、日志统计、配置管理等于一体,通过它可以快速用微服务的方式构建自己的稳定可靠的分布式应用,并实现完整有效的服务治理。

目前该框架在腾讯内部,各大核心业务都在使用,颇受欢迎,基于该框架部署运行的服务节点规模达到上万个。

支持平台

目前运行的操作系统平台如下:

  • Linux

  • Mac(>=2.1.0 support)

支持语言

目前支持的开发语言如下:

  • C++

  • Java

  • Nodejs

  • PHP

  • Go

版本管理

Tars由多种模块组成, 分散在多个仓库中, 并且基础框架版本和语言版本可以独立发展, 鉴于此, 从2.1.0版本开始, 框架的版本TAG打在TarsFramework仓库上, 不再体现在Tars这个仓库上.

Tars IDL

所谓序列化通俗来说就是把内存的一段数据转化成二进制并存储或者通过网络传输,而读取磁盘或另一端收到后可以在内存中重建这段数据

1、tars协议是跨语言跨平台的序列化协议。

2、tars协议本身也可以被用于非RPC场景,如存储

json xml都是一种序列化的方式,只是他们不需要提前预定义idl,且具备可读性,当然他们传输的体积也因此较大,可以说是各有优劣。

关键字

void,struct,bool,byte,short,int,double,float,long,string,vector,map,key,routekey,module,interface,out,require,optional,false,true,enum,const

注释

采用 c++的注释规范:

  • //表示注释一行
  • /**/表示注释范围中的所有代码。

基本类型

基本类型会涉及到不同语言和编码方式,这里只整理tars和go类型对照表

.tars TypeGo TypeNotes
void函数无返回值只能在函数的返回值表示
boolbool布尔类型
byteint8有符号字符
unsigned byteuint8无符号字符
shortint16有符号短整型
unsigned shortuint16无符号短整形
intint32有符号整型
unsigned intuint32无符号整形
longint64有符号长整型
floatfloat3232位浮点数
doublefloat6464位浮点数
stringstring字符串

复杂类型

.tars TypeGo TypeNotes
enumint32枚举类型会转为int32
vectorslice序列
mapmap字典

枚举

枚举类型的定义如下:

enum EnumType
{
    EnumType1,
    EnumType2,
    EnumType3
};

说明:

  • 枚举类型支持在指定枚举变量的值,例如支持:EnumType1 = 1 这种定义方式
  • 第一个定义的枚举类型值为 0,这里 EnumType1 的值为 0

序列

序列用 vector 来定义,如下:

vector<int> vi;

字典

字典用 map 来定义,如下:

map<int, string> m;

常量

Tars 文件中可以定义常量,例如:

const int a = 0;
const string s = “abc”;

说明:

  • 由于 map,vector 没有描述常量的值,因此不支持 map,vector 的定义;

嵌套

任何 struct,map,vector 都可以嵌套;

结构

结构定义如下:

struct Test
{
    0  require  string s;
    1  optional int  i = 23;
};

key[Test, s, i];

说明:

  • 第一列数字表示该字段的标识(tag),无论结构增减字段,该字段得值都不变,必须和响应的字段对应;
  • Tag 的值必须要>=0 且<=255;
  • require 表示该字段必选;
  • optional 表示该字段可选;
  • 对于 optional 字段,可以有一个缺省值,缺省值在编码时默认不打包;

key 说明:

  • 在TarsGo中不做处理,在TarsCpp会按下面规则处理。
  • 表示结构的小于比较符号,缺省时 Struct 是没有小于操作的,如果定义了 key,则生成小于比较符。

key 详细说明:

  • key[Stuct, member…]:
  • Struct:表示结构的名称
  • Member:表示该结构的成员变量,可以有多个;
  • 生成的小于比较操作符,按照 key 中成员变量定义的顺序进行优先<比较;
  • 生成小于比较操作符以后,该结构就可以作为 map 的 key;

接口

接口定义如下,例如:

interface Demo
{
    int get(out vector<map<int, string>> v);
    int set(vector<map<int, string>> v);
};

说明:

  • 表示输出参数
  • 接口定义后,通过自动代码生成工具(如:tars2go)会生成同步接口和异步接口等代码

名字空间

所有的 struct,interface 必须在名字空间中,例如:

module MemCache
{
    struct Key
    {
        0 require string s;
    };

    struct Value
    {
        0 require string s;
    };

    interface MemCacheI
    {
        int get(Key k, out Value v);
        int set(Key k, Value v);
    };
};

说明:

  • 名字空间不能嵌套;
  • 可以引用其他名字空间,例如:Demo1::Key

使用已有协议类型(import)

一个 tars 文件可以 include 另外一个 tars 文件, 只需要在头部如下引用其他文件即可: #include "other.tars"

即可引用其他 tars 文件中的结构体了

完整协议定义示例

所有标识符不能带有’tars_’符号,且必须以字母开头,同时不能和关键字冲突。

#include "other.tars"

module Demo {
    const int a = 0;
    const string s = "abc";
    //表示注释一行
    /*表示注释范围中的所有代码。*/
    struct Demo {
        0 require bool a;
        1 optional byte b;
        2 optional unsigned byte c;
        3 optional short d;
        4 optional unsigned short e;
        5 optional int f;
        6 optional unsigned int g;
        7 optional long h;
        8 optional float i;
        9 optional double k;
        10 optional string l;
        11 optional vector<string> m; // 序列
        12 optional map<string,int> n; // 字典
    };
    key[Demo, a, b];

    enum EnumType
    {
        EnumType1,
        EnumType2,
        EnumType3
    };

    interface DemoIter
    {
        int get(out vector<map<int, string>> v);
        int set(vector<map<int, string>> v);
        void demo(Demo demoIn, out Demo demoOut);
    };
};

生成Go代码

逼迫篇幅代码过长,这里只给出了struct转换后的Go语言代码

// Demo struct implement
type Demo struct {
	A bool             `json:"a"`
	B int8             `json:"b"`
	C uint8            `json:"c"`
	D int16            `json:"d"`
	E uint16           `json:"e"`
	F int32            `json:"f"`
	G uint32           `json:"g"`
	H int64            `json:"h"`
	I float32          `json:"i"`
	K float64          `json:"k"`
	L string           `json:"l"`
	M []string         `json:"m"`
	N map[string]int32 `json:"n"`
}

Tars协议数据编码

基本结构

每一个数据由两个部分组成:头信息 + 实际数据,而其中头信息包括:Type(4 bits) + Tag 1(4 bits) + Tag 2(1 byte) Tag 2 是可选的,当 Tag 的值不超过 14 时,只需要用 Tag 1 就可以表示;当 Tag 的值超过 14 而小于 256 时,Tag 1 固定为 15,而用 Tag 2 表示 Tag 的值。Tag 不允许大于 255。

Type 表示类型,用 4 个二进制位表示,取值范围是 0~15,用来标识该数据的类型。不同类型的数据,其后紧跟着的实际数据的长度和格式都是不一样的,详见一下的类型表。

TagTag 1Tag 2 一起表示。取值范围是 0~255,即该数据在结构中的字段 ID,用来区分不同的字段,也就是说一个struct中最多有256个属性。

编码类型表

注意,这里的类型与 tars 文件定义的类型是两个不同的概念,这里的类型只是标识数据存储的类型,而不是数据定义的类型。

Type取值类型备注
0int1紧跟 1 个字节整型数据
1int2紧跟 2 个字节整型数据
2int4紧跟 4 个字节整型数据
3int8紧跟 8 个字节整型数据
4float紧跟 4 个字节浮点型数据
5double紧跟 8 个字节浮点型数据
6String1紧跟 1 个字节长度,再跟内容
7String4紧跟 4 个字节长度,再跟内容
8Map紧跟一个整型数据表示 Map 的大小,再跟[key, value]对列表
9List紧跟一个整型数据表示 List 的大小,再跟元素列表
10自定义结构开始自定义结构开始标志
11自定义结构结束自定义结构结束标志,Tag 为 0
12数字 0表示数字 0,后面不跟数据
13SimpleList简单列表(目前用在 byte 数组),紧跟一个类型字段(目前只支持 byte),紧跟一个整型数据表示长度,再跟 byte 数据

各类型详细描述

  1. 基本类型(包括 int1、int2、int4、int8、float、double),头信息后紧跟数值数据。char、bool 也被看作整型。所有的整型数据之间不做区分,也就是说一个 short 的值可以赋值给一个 int。

  2. 数字 0,头信息后不跟数据,表示数值 0。所有基本类型的 0 值都可以这样来表示。这是考虑到数字 0 出现的概率比较大,所以单独提一个类型,以节省空间。

  3. 字符串(包括 String1、String4)

    - String1 跟一个字节的长度(该长度数据不包括头信息),接着紧跟内容。

    - String4 与之类似。

  4. Map紧跟一个整形数据(包括头信息)表示 Map 的大小,然后紧跟[Key 数据(Tag 为 0),Value 数据(Tag 为 1)]对列表。

  5. List紧跟一个整形数据(包括头信息)表示 List 的大小,然后紧跟元素列表(Tag 为 0)

  6. 自定义结构体开始标志,后面紧跟字段数据,字段按照 tag 升序顺序排列

  7. 自定义结构体结束标志,Tag 为 0

对象持久化

对于自定义结构体的持久化,由开始标志与结束标志来标识。

比如如下结构定义:

struct TestInfo
{
    1  require  int    ii  = 34;
    2  optional string s   = "abc";
};

struct TestInfo2
{
    1  require TestInfo  t;
    2  require int       a = 12345;
};

其中,默认的 TestInfo2 结构编码后结果为:

tars

Tars 底层RPC消息格式

TUP 底层协议完全采用 Tars 定义,与 Tars 的底层数据包定义一致,其中 require 的字段为 TUP 必须的字段,optional 为访问 Tars 服务时额外需要用到的字段。

请求包

//请求包体
struct RequestPacket
{
    1  require short        iVersion;         //版本号
    2  optional byte        cPacketType;      //包类型
    3  optional int         iMessageType;     //消息类型
    4  require int          iRequestId;       //请求ID
    5  require string       sServantName;     //servant名字
    6  require string       sFuncName;        //函数名称
    7  require vector<byte> sBuffer;          //二进制buffer
    8  optional int         iTimeout;         //超时时间(毫秒)
    9  optional map<string, string> context;  //业务上下文
    10 optional map<string, string> status;   //框架协议上下文
};

响应包

//响应包体
struct ResponsePacket
{
    1 require short         iVersion;       //版本号
    2 optional byte         cPacketType;    //包类型
    3 require int           iRequestId;     //请求ID
    4 optional int          iMessageType;   //消息类型
    5 optional int          iRet;           //返回值
    6 require vector<byte>  sBuffer;        //二进制流
    7 optional map<string, string> status;  //框架协议上下文
    8 optional string       sResultDesc;    //结果描述
    9 optional map<string, string> context; //业务上下文
};

TARS定义的返回码

//返回值
const int TAFSERVERSUCCESS       = 0;       //服务器端处理成功
const int TAFSERVERDECODEERR     = -1;      //服务器端解码异常
const int TAFSERVERENCODEERR     = -2;      //服务器端编码异常
const int TAFSERVERNOFUNCERR     = -3;      //服务器端没有该函数
const int TAFSERVERNOSERVANTERR  = -4;      //服务器端没有该Servant对象
const int TAFSERVERRESETGRID     = -5;      //服务器端灰度状态不一致
const int TAFSERVERQUEUETIMEOUT  = -6;      //服务器队列超过限制
const int TAFASYNCCALLTIMEOUT    = -7;      //异步调用超时
const int TAFINVOKETIMEOUT       = -7;      //调用超时
const int TAFPROXYCONNECTERR     = -8;      //proxy链接异常
const int TAFSERVEROVERLOAD      = -9;      //服务器端超负载,超过队列长度
const int TAFADAPTERNULL         = -10;     //客户端选路为空,服务不存在或者所有服务down掉了
const int TAFINVOKEBYINVALIDESET = -11;     //客户端按set规则调用非法
const int TAFCLIENTDECODEERR     = -12;     //客户端解码异常
const int TAFSERVERUNKNOWNERR    = -99;     //服务器端位置异常

tars2go 使用

安装

go install github.com/TarsCloud/TarsGo/tars/tools/tars2go@latest

验证安装

# tars2go
Usage: tars2go [flags] *.tars
       tars2go -I tars/protocol/res/endpoint [-I ...] QueryF.tars
       tars2go -include="dir1;dir2;dir3"
  -E	Generate code before fmt for troubleshooting
  -I value
    	Specify a specific import path
  -add-servant
    	Generate AddServant function (default true)
  -debug
    	enable debug mode
  -dispatch-reporter
    	Dispatch reporter support
  -include string
    	set search path of tars protocol
  -json-omitempty
    	Generate json omitempty support
  -module string
    	current go module path
  -module-cycle
    	support jce module cycle include(do not support jce file cycle include)
  -module-upper
    	native module names are supported, otherwise the system will upper the first letter of the module name
  -outdir string
    	which dir to put generated code
  -tarsPath string
    	Specify the tars source path. (default "github.com/TarsCloud/TarsGo/tars")
  -without-trace
    	no call chain tracking logic required

核心参数

  • -module 控制当前go模块路径,默认值为空
  • -module-cycle 支持jce模块循环include(不支持jce文件循环include)
  • -module-upper 支持原生模块名称,否则系统会将模块名称的首字母大写
  • -outdir 将生成的代码放在哪个目录
  • -tarsPath 指定 tars 源路径。 (默认“github.com/TarsCloud/TarsGo/tars”)
  • -without-trace 是否生成Tars调用链跟踪相关代码,默认会生成
  • -add-servant 生成 AddServant 函数(默认为 true)
  • -include设置tars协议的include搜索路径
  • -json-omitempty是否生成 json omitempty 支持

使用

tars2go \
	-without-trace=true \
	-add-servant=false \
	-tarsPath github.com/TarsCloud/TarsGo/tars \
	-module github.com/TarsCloud/TarsGo/tars/protocol/res \
	*.tars

参考资料

关注公众号获得更多精彩文章

公众号:程序员大兵