前两篇讲了sproto协议二进制文件的格式,这篇讲如何通过二进制文件构造协议.
关于二进制文件的生成请看这篇文章:
通过前两篇的分析我们知道了sproto协议的格式,主要分为两部分,sproto_type和protocol数组.构造协议时,也会有这两个大的步骤,请看代码:
static struct sproto *
create_from_bundle(struct sproto *s, const uint8_t * stream, size_t sz) {
const uint8_t * content;
const uint8_t * typedata = NULL;
const uint8_t * protocoldata = NULL;
int fn = struct_field(stream, sz);
int i;
if (fn < 0 || fn > 2)
return NULL;
stream += SIZEOF_HEADER;
content = stream + fn*SIZEOF_FIELD;
for (i = 0; i<fn; i++) {
int value = toword(stream + i*SIZEOF_FIELD);
int n;
if (value != 0)
return NULL;
n = count_array(content);
if (n<0)
return NULL;
if (i == 0) {
typedata = content + SIZEOF_LENGTH; // 1) 找到sproto头
s->type_n = n;
s->type = (struct sproto_type*)pool_alloc(&s->memory, n * sizeof(*s->type));
}
else {
protocoldata = content + SIZEOF_LENGTH; // 2) 找到protocol头
s->protocol_n = n;
s->proto = (protocol*)pool_alloc(&s->memory, n * sizeof(*s->proto));
}
content += todword(content) + SIZEOF_LENGTH;
}
for (i = 0; i<s->type_n; i++) { // 3) 填充sproto数组
typedata = import_type(s, &s->type[i], typedata);
if (typedata == NULL) {
return NULL;
}
}
for (i = 0; i<s->protocol_n; i++) {
protocoldata = import_protocol(s, &s->proto[i], protocoldata); // 4) 填充protocol数组
if (protocoldata == NULL) {
return NULL;
}
}
return s;
}
1), 2), 3), 4)很明显,找到并填充数据.
我们再看一次sproto格式的数据:
可以看出找到各部分数据区域的还是很简单的,因为有字段来标识来每部分数据的长度.读取下一部分的数据只要跳过这个偏移长度即可.
每个数据结构都分为几个数据部分,例如sproto分为sproto_type,和protocol,sproto_type又有name,field,field个数等部分.具体有多少个字段其实是在二进制协议文件中有标识.struct_field函数就是来解析这个的,实际上只需读取开头前2个字节即可.除了解析每个部分有多少个字段外,还有校验数据是否正确的功能,也就是看后面的数据大小是否正确,定义如下:
static int struct_field(const uint8_t * stream, size_t sz) {
const uint8_t * field;
int fn, header, i;
if (sz < SIZEOF_LENGTH)
return -1;
fn = toword(stream); //读取字段个数
header = SIZEOF_HEADER + SIZEOF_FIELD * fn;
if (sz < header)
return -1;
field = stream + SIZEOF_HEADER;
sz -= header;
stream += header;
for (i = 0; i<fn; i++) { //数据校验
int value = toword(field + i * SIZEOF_FIELD);
uint32_t dsz;
if (value != 0)
continue;
if (sz < SIZEOF_LENGTH)
return -1;
dsz = todword(stream);
if (sz < SIZEOF_LENGTH + dsz)
return -1;
stream += SIZEOF_LENGTH + dsz;
sz -= SIZEOF_LENGTH + dsz;
}
return fn;
}
前面说过sproto_type数据其实是sproto_type数据数组,如何知道他有多少个呢?
实际上从格式中就可以看出,每个部分由字段长度+数据构成,如果是数组数据也是这样的,数组数据连续排列,前面有数组数据大小的的总长度,即格式为 N·n1·data1·n2·data2...,N = n1+ n2 + ...
所以根据N,n1,n2,n3...确定数组的长度也不难. count_array就是来干这活的:
static int count_array(const uint8_t * stream) {
uint32_t length = todword(stream);
int n = 0;
stream += SIZEOF_LENGTH;
while (length > 0) {
uint32_t nsz;
if (length < SIZEOF_LENGTH)
return -1;
nsz = todword(stream);
nsz += SIZEOF_LENGTH;
if (nsz > length)
return -1;
++n;
stream += nsz;
length -= nsz;
}
return n;
}
接下来就是解析sproto_type数组了.每个sproto_type类型由import_type函数来解析:
static const uint8_t *
import_type(struct sproto *s, struct sproto_type *t, const uint8_t * stream) {
const uint8_t * result;
uint32_t sz = todword(stream);
int i;
int fn;
int n;
int maxn;
int last;
stream += SIZEOF_LENGTH;
result = stream + sz;
fn = struct_field(stream, sz); //sproto_type的数据的分为name和field两部分
if (fn <= 0 || fn > 2)
return NULL;
for (i = 0; i<fn*SIZEOF_FIELD; i += SIZEOF_FIELD) {
// name and fields must encode to 0
int v = toword(stream + SIZEOF_HEADER + i);
if (v != 0)
return NULL;
}
memset(t, 0, sizeof(*t));
stream += SIZEOF_HEADER + fn * SIZEOF_FIELD;
t->name = import_string(s, stream);
if (fn == 1) {
return result;
}
stream += todword(stream) + SIZEOF_LENGTH; // second data
n = count_array(stream); //计算field部分有多少个
if (n<0)
return NULL;
stream += SIZEOF_LENGTH;
maxn = n;
last = -1;
t->n = n;
t->f = (field*)pool_alloc(&s->memory, sizeof(struct field) * n);
for (i = 0; i<n; i++) {
int tag;
struct field *f = &t->f[i];
stream = import_field(s, f, stream); //逐个解析field字段
if (stream == NULL)
return NULL;
tag = f->tag;
if (tag <= last)
return NULL; // tag must in ascending order
if (tag > last + 1) {
++maxn;
}
last = tag;
}
t->maxn = maxn;
t->base = t->f[0].tag;
n = t->f[n - 1].tag - t->base + 1;
if (n != t->n) {
t->base = -1;
}
return result;
}
import_type数据分为name和field数据两部分. 解析完name和field个数之后就开始,就开始逐个解析field字段了,类似于import_type,都是读取数据填充到field结构体里.代码太长就不贴了.
解析protocol数组的过程类似于sproto_type,就不多说了.
最后分析一下sproto内存分配的策略.由于sproto字段个数不固定,而且内存分配频繁,所以采用内存池的方式分配内存是一个比较合理的方式.他的原理是,首先分配一个较大长度(例如1000字节)的空间,当要分配内存时,从这个空间中使用,并用current_used记录已使用的长度,和当前内存块的指针.现在分三种情况讨论一下内存使用的情况:
1) 将要使用的内存 + 已使用的内存 <= 内存块大小
这种情况最简单,直接使用后面的内存,并更新已使用的值就好
2) 将要使用的内存 + 已使用的内存 >= 内存块大小,但是将要使用的内存比已使用的小
这种情况的做法是放弃掉后面剩余的内存,重新分配一大块新的内存块,并更新当前节点current和头节点header指向,示意图如下:
3) 将要使用的内存 + 已使用的内存 >= 内存块大小,但是将要使用的内存比已使用的还大
这种情况下的做法是直接重新分配要使用的内存大小内存块.这样做的好处是之前利用的内存块还可以被利用.示意图如下:
记录的每个内存块的原因是为了以后的释放,全部代码如下:
static void
pool_init(struct pool *p) {
p->header = NULL;
p->current = NULL;
p->current_used = 0;
}
static void
pool_release(struct pool *p) {
struct chunk * tmp = p->header;
while (tmp) {
struct chunk * n = tmp->next;
free(tmp);
tmp = n;
}
}
static void *
pool_newchunk(struct pool *p, size_t sz) {
struct chunk * t = (chunk*)malloc(sz + sizeof(struct chunk));
if (t == NULL)
return NULL;
t->next = p->header;
p->header = t;
return t + 1;
}
static void *
pool_alloc(struct pool *p, size_t sz) {
// align by 8
sz = (sz + 7) & ~7;
if (sz >= CHUNK_SIZE) {
return pool_newchunk(p, sz);
}
if (p->current == NULL) {
if (pool_newchunk(p, CHUNK_SIZE) == NULL)
return NULL;
p->current = p->header;
}
if (sz + p->current_used <= CHUNK_SIZE) {
void * ret = (char *)(p->current + 1) + p->current_used;
p->current_used += sz;
return ret;
}
if (sz >= p->current_used) {
return pool_newchunk(p, sz);
}
else {
void * ret = pool_newchunk(p, CHUNK_SIZE);
p->current = p->header;
p->current_used = sz;
return ret;
}
}
通过这几篇的分析,我们只要理解了sproto协议的格式,对于解析就非常简单了.下篇分析数据通过sproto打包和解包.