一 简介
sproto是云风设计的一个客户端服务端通讯协议,设计思想可以看云风的blog, 其实现中提供了一个sproto.parse 方法, 可以将sproto的schema描述文件转变为二进制字符,这样,项目中只需要使用转变后的二进制字符就可以知道客户端与服务端之间的通讯协议了。本篇博客主要就是分析parse方法的实现。
二 sproto schema 要做哪些事
.Person {
#类型 Person
name 0 : string # 变量名 name, tag 0 用来作为属性的索引, 类型为string
id 1 : integer
email 2 : string
.PhoneNumber {
# 子类型PhoneNumber, 在Person中都可以被直接使用, 在外面应该要使用Person.PhoneNumber
number 0 : string
type 1 : integer
}
phone 3 : *PhoneNumber # PhoneNumber数组
gjy 4 : *integer(2) #.后精度为2的小数, 比如13.12
}
.AddressBook {
person 0 : *Person(id) #Person Dictionary, 索引为Person.id
others 1 : *Person
}
foobar 1 { # 协议名footbar, 协议tag = 1
request Person # request 数据类型为Person
response { ok 0 : boolean } #respone 数据类型为匿名类型
}
上面是一个比较完善的sproto schema, 那么将其转换成二进制字符串需要做那些事呢?
最先想到的应该是文本处理,我们要从上面的长字符串中得到我们需要的信息,比如我们要知道一个类型叫AddressBook, 该类型中定义了两个变量,每次定义的变量名是什么, tag是什么,变量类型是什么。之后,我们需要做一些错误检测,比如变量重名, 变量类型不存在,Dictionary 索引不存在等。最后就是将其转换成二进制字符串。
三 文本处理
假设我们读到的第一个非空字符是’.’, 那么我们就知道接下来的应该是一个类型名称,在接下来是一个’{‘, 再之后读到n 我们就知道这是一个变量名, 读到0知道是一个tag, 读到 : 知道后面是一个类型, 当我们读到的字符不是我们所期望的时候,我们就知道发生了语法错误。
当然, 上面只是对文本处理进行一个简单的描述,事实上并不是这样实现的,sproto中采用了LPEG库来进行了字符匹配与捕获。
local typedef = P {
"ALL",
FIELD = namedpat("field", (name * blanks * tag * blank0 * ":" * blank0 * (C"*")^-1 * typename * (mainkey + decimal)^0)),
STRUCT = P"{" * multipat(V"FIELD" + V"TYPE") * P"}",
TYPE = namedpat("type", P"." * name * blank0 * V"STRUCT" ),
SUBPROTO = Ct((C"request" + C"response") * blanks * (typename + V"STRUCT")),
PROTOCOL = namedpat("protocol", name * blanks * tag * blank0 * P"{" * multipat(V"SUBPROTO") * P"}"),
ALL = multipat(V"TYPE" + V"PROTOCOL"),
}
以下时对上面单个字符的解释:
- FIELD 定义一个变量的文本,比如phone 3 : *PhoneNumber # PhoneNumber数组
- STRUCT {}及其中间的所有内容
- TYPE 定义一个类型的所有文本
- SUBPROTO 协议中的request或者response
- PROTOCOL 整个协议
- ALL 所有文本
- name 变量名,类型名,协议名
- tag 数字
- blanks 最少一个空格
- blank0 0或多个空格
- typename 不仅可以匹配name, 还可以匹配Person.PhoneNumber
- mainkey 匹配 (id)
- decimal 匹配(2)
- namedpat(name, pat) namedpat返回一个新的pat,其匹配规则和pat一样,但是会将name存储到type变量中,这样我们就知道这个pat匹配的是什么,比如field,或者protocol 或者是type
- multipat(pat) 则是匹配多个pat
匹配之后我们会捕获如下内容:
{ [1] =
{ [1] = Person, #name
[2] = { #STRUCT
[1] = { [3] = string,[1] = name,[2] = 0,["type"] = field,} ,#FIELD
[2] = { [3] = integer,[1] = id,[2] = 1,["type"] = field,} ,#FIELD
[3] = { [3] = string,[