本文根据云风博客为思路来解读databuffer在skynet老版本gate服务的应用。
源码为databuffer.h和service_gate.c
首先解释一下什么是ringbuffer
它是一个环(首尾相接的环),你可以把它用做在不同上下文(线程)间传递数据的buffer.
优点 它是一个数组(连续的内存),比链表快。在硬件级别,数组中的元素是会被预加载的,因此在ringbuffer当中,cpu无需时不时去主存加载数组中的下一个元素。
其次你可以为数组预先分配内存。不需要花大量时间用于垃圾回收。
ringbuffer在skynet gate服务中的应用
在旧的gate服务中,skynet启动gate服务,监听一个端口,然后该服务要处理连接到这个端口的所有TCP连接。Skynet使用了一个ringbuffer缓存了所有连接发过来的数据包。然后在一条一条数据去
进行逻辑分包。
ringbuffer的数据结构如下
struct messagepool_list {
struct messagepool_list *next;
struct message pool[MESSAGEPOOL]; //数组长度是1023
};
messagepoll数据绑定在gate数据上。messagepoll结构里有个messagepool_list的数组,即环形数组
struct messagepool {
struct messagepool_list * pool;
struct message * freelist;
};
databuffer数据是绑定在每一条tcp连接上。所以databuffer是n,messagepoll是1
struct databuffer {
int header; // 约定的包头长度 2或者4
int offset;
int size; // 该条tcp连接,已经接收到数据大小。 databuffer_push 是加 datebuffer_read是减
struct message * head; // 头指针 message数据最终是存在messagepool_list数组里
struct message * tail; // 尾指针
};
datebuffer_read 是 去messagepool读指定 sz(2字节或4字节)的消息。
databuffer_read(struct databuffer *db, struct messagepool *mp, char * buffer, int sz) {
assert(db->size >= sz); // sz 为2 或者 4
db->size -= sz; // size是该条连接已经缓存还没读的消息大小。 现在需要读sz个大小的数据出来.
for (;;) {
struct message *current = db->head;
int bsz = current->size - db->offset;
if (bsz > sz) { // 这条message的大小已经够读了,并且还有数据多。多出来的数据说明是下一个逻辑包的数据,这里需要用offset来标记。
memcpy(buffer, current->buffer + db->offset, sz);
db->offset += sz;
return;
}
if (bsz == sz) { // 已经读出了一个完整的包
memcpy(buffer, current->buffer + db->offset, sz);
db->offset = 0;
_return_message(db, mp); // 回收该内存 db->head往下跳一格
return;
} else { // 还没读完,需要跳到下一个message去继续读数据。
memcpy(buffer, current->buffer + db->offset, bsz);
_return_message(db, mp); // 回收该内存 db->head往下跳一格 并且标记messagepool的freelist为这个已经read的message
db->offset = 0;
buffer+=bsz;
sz-=bsz;
}
}
}
static void
databuffer_push(struct databuffer *db, struct messagepool *mp, void *data, int sz) {
struct message * m;
if (mp->freelist) {
m = mp->freelist;
mp->freelist = m->next;
} else { // ringbuff的数组初始化。
struct messagepool_list * mpl = skynet_malloc(sizeof(*mpl));
struct message * temp = mpl->pool;
int i;
for (i=1;i<MESSAGEPOOL;i++) {
temp[i].buffer = NULL;
temp[i].size = 0;
temp[i].next = &temp[i+1];
}
temp[MESSAGEPOOL-1].next = NULL;//最后一个m的next是空 但freelist指针指向数组最后一个元素时。数组会进行回绕。然后第38行到41行。会把当前的
//数组的元素往后移动一格。这里实现非常巧妙。
mpl->next = mp->pool;
mp->pool = mpl;
m = &temp[0]; // 第一个m存储当前push进来的数据
mp->freelist = &temp[1]; // freelist指向第二个m
}
m->buffer = data;
m->size = sz;
m->next = NULL;
db->size += sz; // 标记databuff又接收了sz个数据。等待下次read。
if (db->head == NULL) {
assert(db->tail == NULL);
db->head = db->tail = m;
} else {
db->tail->next = m;
db->tail = m; // 尾指针后移
}
}
static inline void
_return_message(struct databuffer *db, struct messagepool *mp) {
struct message *m = db->head;
if (m->next == NULL) {
assert(db->tail == m);
db->head = db->tail = NULL;
} else {
db->head = m->next;
}
skynet_free(m->buffer);
m->buffer = NULL;
m->size = 0;
m->next = mp->freelist; // freelist是下个tcp进来需要存放的地址,这里db->head的message进行回收,把freelist指针指向这里。下次数据进来就会存放在这里了。
mp->freelist = m;
}