Skynet(3)-Sproto协议的使用
文章整理和来源:https://github.com/cloudwu/skynet/wiki/Sproto
1.首先看下Sproto的协议的格式:
.Person {
name 0 : string
id 1 : integer
email 2 : string
.PhoneNumber {
number 0 : string
type 1 : integer
}
phone 3 : *PhoneNumber
}
.AddressBook {
person 0 : *Person
}
我们可以看到,凡以“.”开头定义的皆为一种自定义的类型,例如这里的.Person,.AddressBook都是一种类型,而且类型之中可以包含其他的类型。而 “ * ” 加变量名代表一个数组。Sproto中支持以下5种基本类型。
- string : string
- binary : binary string (it's a sub type of string)
- integer : integer, the max length of an integer is signed 64bit. It can be a fixed-point number with specified precision.
- double : double, floating-point number.
- boolean : true or false
我们再看一个符合这个协议的sproto数据:
local ab = {
person = {
{
name = "Alice",
id = 10000,
phone = {
{ number = "123456789" , type = 1 },
{ number = "87654321" , type = 2 },
}
},
{
name = "Bob",
id = 20000,
phone = {
{ number = "01234567890" , type = 3 },
}
}
}
}
2.Parser(解析器):
local parser = require "sprotoparser"
parser .parse将sproto架构解析为二进制字符串。
解析sproto架构需要解析器。你可以利用它离线将spoto数据生成二进制字符串,
程序运行时就不再需要sproto数据和解析器。
3.Lua API(这部分没啥好讲的,直接从云风的wiki里面copy过来了)
local sproto = require "sproto"
local sprotocore = require "sproto.core" -- optional
sproto.new(spbin)
creates a sproto object by a schema binary string (generates by parser).sprotocore.newproto(spbin)
creates a sproto c object by a schema binary string (generates by parser).sproto.sharenew(spbin)
share a sproto object from a sproto c object (generates by sprotocore.newproto).sproto.parse(schema)
creares a sproto object by a schema text string (by calling parser.parse)sproto:exist_type(typename)
detect whether a type exist in sproto object.sproto:encode(typename, luatable)
encodes a lua table with typename into a binary string.sproto:decode(typename, blob [,sz])
decodes a binary string generated by sproto.encode with typename. If blob is a lightuserdata (C ptr), sz (integer) is needed.sproto:pencode(typename, luatable)
The same with sproto:encode, but pack (compress) the results.sproto:pdecode(typename, blob [,sz])
The same with sproto.decode, but unpack the blob (generated by sproto:pencode) first.sproto:default(typename, type)
Create a table with default values of typename. Type can be nil , "REQUEST", or "RESPONSE".
4.RPC API:
所谓RPC,就是远程过程调用,我们通常客户端与服务端的数据传输都会用到这个,Sproto提供了RPC的接口,这部分比较实用。
为了帮助我们理解,先来听听Sproto作者云风在博客中对Sproto RPC的描述:
https://blog.codingnow.com/2015/04/sproto_rpc.html
首先我们需要定义一个消息包的主体格式。它必须有一个叫 type 的字段,描述 RPC 到底是哪一条消息。还需要有一个 session 字段来表示回应消息的对应关系。通常这两个字段都被定义成 integer 。
.package {
type 0 : integer
session 1 : integer
}
使用 sproto 的 rpc 框架,每条消息都会以这条消息开头,接上真正的消息内容;连接在一起后用 sproto 的 0-pack 方式打包。注意,这个数据包并不包含长度信息,所以真正在网络上传输,还需要添加长度信息,方便分包,也就是说,实际我们传送的包都是(数据长度+消息包)。当然,如果你使用 skynet 的 gate 模块的话,约定了以两字节大端表示的长度加内容的方式分包。
(1)构造一个 sproto rpc 的消息处理器,应使用:
-- packagename 默认值为 "package" 即对应前面的 .package 类型。你也可以起别的名字。
local host = sproto:host(packagename)
这条调用会返回一个 host 对象,用于处理接收的消息。host对象通过调用dispatch处理消息包,返回请求类型(注意不是消息类型,而是“REQUEST ”或“RESPONSE ”)以及具体的内容,然后我们根据请求类型来自行处理。
host:dispatch(msgcontent)
这里的 msgcontent 也是一个字符串,或是一个 userdata(指针)加一个长度。它应符合上述的以 sproto 的 0-pack 方式打包的包格式。
dispatch 调用有两种可能的返回类别,由第一个返回值决定:
-
REQUEST : 第一个返回值为 "REQUEST" 时,表示这是一个远程请求。如果请求包中没有 session 字段,表示该请求不需要回应。这时,第 2 和第 3 个返回值分别为消息类型名(即在 sproto 定义中提到的某个以 . 开头的类型名),以及消息内容(通常是一个 table );如果请求包中有 session 字段,那么还会有第 4 个返回值:一个用于生成回应包的函数。
-
RESPONSE :第一个返回值为 "RESPONSE" 时,第 2 和 第 3 个返回值分别为 session 和消息内容。消息内容通常是一个 table ,但也可能不存在内容(仅仅是一个回应确认)。
(2)对外发送请求(一般指客户端向服务端发送请求或服务端向客户端发起请求),应使用attach:
local sender = host:attach(sp) -- 这里的 sp 是向外发出的消息协议定义。
attach 可以构造一个打包函数,用来将对外请求打包编码成可以被 dispatch 正确解码的数据包。这个生成的函数可以将 type session content 三者打包成一个串,这个串可以被对方的 host:dispatch 正确处理。
这个 sender 函数接受三个参数(name, args, session)。name 是消息的字符串名(其实就是消息的类型,如登录消息,授权消息等)、args 是一张保存用消息内容的 table ,而 session 是你提供的唯一识别号,用于让对方正确的回应。 当你的协议不规定需要回应时,session 可以不给出。同样,args 也可以为空。
类似Protocol Buffers(但不同于json),sproto消息是强类型的,并且本身不是自描述的。必须用特殊语言定义消息结构。
可以使用sprotoparser库将模式文本解析为二进制字符串,这样sproto库就可以使用它。可以脱机(离线)解析它们并保存字符串,也可以在程序运行期间解析它们。
架构文本如下:
.Person { # . means a user defined type
name 0 : string # string is a build-in type.
id 1 : integer
email 2 : string
.PhoneNumber { # user defined type can be nest.
number 0 : string
type 1 : integer
}
phone 3 : *PhoneNumber # *PhoneNumber means an array of PhoneNumber.
height 4 : integer(2) # (2) means a 1/100 fixed-point number.
data 5 : binary # Some binary data
weight 6 : double # floating number
}
.AddressBook {
person 0 : *Person(id) # (id) is optional, means Person.id is main index.
}
foobar 1 { # define a new protocol (for RPC used) with tag 1
request Person # Associate the type Person with foobar.request
response { # define the foobar.response type
ok 0 : boolean
}
}
当然如果要使用自描述的sproto协议也是可以的:
.type {
.field {
name 0 : string
buildin 1 : integer
type 2 : integer # type is fixed-point number precision when buildin is SPROTO_TINTEGER; When buildin is SPROTO_TSTRING, it means binary string when type is 1.
tag 3 : integer
array 4 : boolean
key 5 : integer # If key exists, array must be true, and it's a map.
}
name 0 : string
fields 1 : *field
}
.protocol {
name 0 : string
tag 1 : integer
request 2 : integer # index
response 3 : integer # index
confirm 4 : boolean # response nil where confirm == true
}
.group {
type 0 : *type
protocol 1 : *protocol
}
5.Sproto Loader
由于 skynet 采用的是多 lua 虚拟机。如果在每个 VM 里都加载相同的 sproto 协议定义就略显浪费。所以 skynet 还提供了一个叫 sprotoloader 的模块来共享它们。
其实现原理是在 C 模块中提供了 16 个全局的 slot ,可以通过 sprotoloader.register 或 sprotoloader.save 在初始化时,加载需要的协议,并保存在这些 slot 里。通常我们只需要两个 slot ,一个用于保存客户端到服务器的协议组,另一个用于保存服务器到客户端的协议组。分别位于 slot 1 和 2 。
这样,在每个 vm 内,都可以通过 sprotoloader.load 把协议加载到 vm 中。
注意:这套 api 并非线程安全。所以必须自行保证在初始化完毕后再做 load 操作。(load 本身是线程安全的)。