skynet sproto协议剖析(三)

前两篇讲了sproto协议二进制文件的格式,这篇讲如何通过二进制文件构造协议.

关于二进制文件的生成请看这篇文章:

skynet 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打包和解包.






  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值