Skynet第三方之:sproto协议

JavenLaw

什么是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)

官方项目

cloudwu/sproto: Yet another protocol library like google protocol buffers , but simple and fast. (github.com)

高亮语法:

cloudwu/sproto: Yet another protocol library like google protocol buffers , but simple and fast. (github.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,协议的使用

仔细看下面的文字,理解之后才继续往下:

-———————————————————————————————–

参考:云风的 BLOG: sproto rpc 的用法

每当我们发送一次远程请求,需要传输的数据就有三项:

请求的类型

一个请求方自己保证唯一的 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篇实际项目中会用到的目录结构,在开发中会很方便和清晰,但也只是做一些使用方法的处理,根本的原理和接口是完全一样的