什么是sproto
我们先看看云风设计sproto的初衷以及sproto的改进历史
1,云风的 BLOG: 设计一种简化的 protocol buffer 协议 (codingnow.com)
2,云风的 BLOG: sproto 的实现与评测 (codingnow.com)
3,云风的 BLOG: 给 sproto 增加 unordered map 的支持 (codingnow.com)
4,云风的 BLOG: skynet 近期更新及 sproto 若干 bug 的修复 (codingnow.com)
5,云风的 BLOG: sproto rpc 的用法 (codingnow.com)
6,云风的 BLOG: sproto 的缺省值处理 (codingnow.com)
7,云风的 BLOG: sproto 的一些更新 (codingnow.com)
8,云风的 BLOG: 为什么 skynet 提供的包协议只用 2 个字节表示包长度 (codingnow.com)
官方项目
高亮语法:
sproto的原理
底层实现原理
sproto 数据格式图解 - 简书 (jianshu.com)
sproto的使用
说到sproto的使用,就不能不提skynet中的几个文件
1,sprotoparser.lua
2,sprotoloader.lua
3,sproto.lua
以及在examples/proto.lua的样例
更重要的是sproto项目中的测试样例
学习、理解以上的的内容,差不多就可以使用了
1,协议的构建
简单的结构如下:
client_to_server 1000 {
request {
name 0 : string
}
response {
ret 0 : integer
}
}
或者
server_to_client 2000 {
request {
msg 0 : string
}
response {
ret 0 : integer
}
}
这是项目中客户端和服务器协议的定义,非常简单
2,协议支持的结构
sproto支持以下基本结构:
1,integer // *integer 整数
2,string // *string 字符串
3,double // *double 浮点型
4,boolean // *boolean 布尔型
5,binary // *binary 二进制,我没用过
其实在云大的github/sproto项目中,testall.lua文件可以看到所有sproto支持的结构和类型
可以自己运行看看,实践对比一下运行结果。
注:加*号,表示其为数组,github中的描述:
You can add * before the typename to declare an array
用户自定义的结构
.SERVER {
index 0 : integer
ip 1 : string
port 2 : integer
is_work 3 : boolean
cluster 4 : *SERVER
}
用户自定义的结构,. name {} 来进行构造,里面是一些基础的类型,
里面依然可以嵌套用户自定义的结构
integer(2) // *integer(2) 的样式
在github的描述是这样的:
integer(2) # (2) means a 1/100 fixed-point number.
即我们说的精确到第几位。
这里需要提醒:如果你需要传输的数据是整数,但是你用了integer(2),则会解码成浮点数,6变为6.0,或者被进行四舍五入:6.245 变为 6.25。这是我测试testall.lua知道的,注意一下就行。
其实这算不上它的结构,但我认为放到这里一起也是可以的。
先定义一个结构conf
.conf {
id 0 : integer
name 1 : string
desc 2 : string
value 3 : integer
}
以下是结构使用:
info 0 : *conf(id)
data 1 : *conf() //这样会报错,不知为什么
显然这是2种不同的使用方法。
第一种,在github的描述是:
You can also specify a main index with the syntax likes *array(id), the array would be encode as an unordered map with the id field as key.
即该数据以conf的id作为主键key,进行编码,并是有序的。
利用此操作,我们游戏中在服务器传配置给客户端时,就可以使用。
客户端在解码出来时候,就不再需要遍历一边配置,按id为key另保存配置。
而是可以直接按id检索配置,非常方便。
第二种,在github的描述是:
For empty main index likes *array(), the array would be encoded as an unordered map with the first field as key and the second field as value.
即该数据会以第一项的值作为key,第二项为value,进行编码,是无序的。
但是我这个没有实验出来,如果直接使用 data 1 : *conf() 会报错,需要这样使用:
data 1 : *conf
则没有问题。有知道为什么会这样的朋友评论告诉我一下:)
对于以上的结果,我是自己去测试了一下的出来,大家可以根据自己的疑问去使用一下
以上的8种结构或者说用法,足以覆盖项目中很多的需求。
可能有浮点数的问题。
但是浮点数一般都会乘于10000等较大的数,转化为整型进行传输,客户端收到后再转化
或者直接转化为string进行传输
3,协议的使用
仔细看下面的文字,理解之后才继续往下:
-———————————————————————————————–
每当我们发送一次远程请求,需要传输的数据就有三项:
请求的类型
一个请求方自己保证唯一的 session id
以及请求的数据内容
服务方收到请求后,应根据请求的类型对请求的数据内容解码,并根据类型分发给相应的处理器。同时应该把 session id 记录下来。
等处理器处理完毕后,根据类型去打包回应的消息,并附加上 session id ,发送回客户端。注意:回应是不需要传输消息类型的。
这是因为 session id 就唯一标识了这是对哪一条请求的回应。
而 session id 是客户端保证唯一的,它在产生 session id 时,
就保存了这个 session 对应的请求的类型,所以也就有能力对回应消息解码。
另外 : 如果只是单向推送消息(也就是 publish/subscribe 模式),直接省略 session 就可以了,也不需要回应
你需要定义一个叫做 package 的消息类型,里面包含 type 和 session 两项。如
.package {
type 0 : integer --请求的类型
session 1 : integer -- 一个请求方自己保证唯一的 session id
}
对于每个包,都以这个 package 开头,后面接上 (padding)消息体。
最后连在一起,用 sproto 自带的 0-pack 方式压缩。
对于type和session的作用,也非常明确了。这也回答了为什么服务方收到请求后要把session id 记录下来,打包回去的消息不需要带类型。为什么单向推送的消息不需要session。
-———————————————————————————————-
首先我们来看看sproto的简单API
sproto提供4个简单 RPC API的封装函数:
sproto:request_encode(protoname, tbl) //encode a request message with protoname.//对请求进行编码
sproto:response_encode(protoname, tbl) //encode a response message with protoname.//对回复进行编码
sproto:request_decode(protoname, blob [,sz]) //decode a request message with protoname.//对请求进行解码
sproto:response_decode(protoname, blob [,sz] //decode a response message with protoname.//对回复进行解码
这组 api 不会帮你处理 type session 这些信息,而是留给你处理。
它只是在你知道一条消息的内容在已知是请求还是回应包时,可以调用对应的 api 来编解码。
一般,我们都使用能帮我们处理session 和 type 的RPC接口
接着看下一段文字:
-———————————————————————————————-
你需要定义一个叫做 package 的消息类型,里面包含 type 和 session 两项。依然是:
.package {
type 0 : integer //请求的类型
session 1 : integer //一个请求方自己保证唯一的 session id
}
对于每个包,都以这个 package 开头,后面接上 (padding)消息体。最后连在一起,用 sproto 自带的 0-pack 方式压缩。
你可以用 sproto:host 这个 api 生成一个消息分发器 host ,用来处理上面这种 rpc 消息。默认每个 rpc 处理端都有处理请求和处理回应的能力。
也就是每个 rpc 端都同时可以做服务器和客户端。
所以 host:dispatch 这个 api 可以处理消息包,返回它是请求还是回应,以及具体的内容。
如果 host 要对外发送请求,它可以用 host:attach 生成一个打包函数。
这个生成的函数可以将 type session content 三者打包成一个串,这个串可以被对方的 host:dispatch 正确处理。
-———————————————————————————————-
所以,很简单来说,就是:
我们只需要 sproto:host 生成消息分发器host,用来处理外部的请求,host:dispatch 这个api会自动帮我们处理REQUEST和RESPONSE类型的消息,编码,打包,返回消息等
我们还需要 host:attach 还可以往外发消息是,被对方的 host:dispatch 解析
4,RPC的使用
1,定义协议
local client_to_server = sproto.parse [[
.package {
type 0 : integer
session 1 : integer
}
c_to_s 1 {
request {
name 0 : string
}
response {
id 0 : integer
}
}
]]
local server_to_client = sproto.parse [[
.package {
type 0 : integer
session 1 : integer
}
s_to_c 1 {
request {
id 0 : integer
}
response {
name 0 : string
}
}
]]
2,构建收发器
local server = client_to_server:host "package" --建立服务器的消息分发器
local client = server_to_client:host "package" --建立客户端的消息分发器
local client_request = client:attach(client_to_server) --客户端往外发消息
local server_request = server:attach(server_to_client) --服务端往外发消息
3,发送/接收消息
--客户端写入参数,发出请求。client_request即为client:attach
local session = 1000 -- 这个自定义的编号
session = session + 1
local cli_req = client_request("c_to_s", { name = "i am client, i need a id !" }, session)
--服务器读取client消息,分发到对应的函数处理
--type 是REQUEST
--name 是协议名
--request 是请求的数据
--response 是回复的函数
local type, name, request, response = server:dispatch(cli_req)
local ret = {
id = 416
}
--打包回复
local svr_rep = response(ret)
--客户端收到回复,进行解包等,获得response
local type, session, response = client:dispatch(svr_rep)
-- session将会是1001,就是刚才发出的session
服务器发消息的就不再写了,一样的流程。
需要注意的是,client有些请求是不需要传输是内容的。可以直接这样写:
client_request(“协议名”, nil, session号)
或者某些请求不需要回复的
client_request(“协议名”, {data}, nil)
更多使用,查看sproto中得testrpc.lua文件
5,实际项目中的使用
在实际项目中,对于sproto的RPC使用,肯定不是像上面一样,一个个协议名,协议号来写。
多个虚拟机也可以共享一份协议文件,以减少内存占用。这就用到 “sprotoloader” 和"protoloader"
这些在skynet的项目中都是有的,根据上面的sproto的简单使用
去看skynet项目中的云大给出的例子,就很好懂了。
有空再写1篇实际项目中会用到的目录结构,在开发中会很方便和清晰,但也只是做一些使用方法的处理,根本的原理和接口是完全一样的