skynet服务的名字我们在开发过程中会使用到,这篇文章试着讲解一下skynet服务名字的有关内容。
首先每个服务在初始化的时候都会调用skynet_command(ctx, "REG", ...)。例如snlua服务调用了skynet_command(ctx, "REG", NULL),logger服务调用skynet_command(ctx, "REG", ".logger"),最终会调用到cmd_reg这个函数,我们看下这个函数:
static const char *
cmd_reg(struct skynet_context * context, const char * param) {
if (param == NULL || param[0] == '\0') {
sprintf(context->result, ":%x", context->handle);
return context->result;
} else if (param[0] == '.') {
return skynet_handle_namehandle(context->handle, param + 1);
} else {
skynet_error(context, "Can't register global name %s in C", param);
return NULL;
}
}
snlua服务初始化的时候参数是NULL,所以基本上没有做什么事,context的result其实是一个辅助性的缓存区,在后面没有实际用途。给服务注册名字的api是skynet.register,他在manager.lua中,最终也会调用到上面的函数。注册的函数必须是带点.,否则就是全局服务名,这个在后面的文章中会讲到。
调用skynet_handle_namehandle函数就会把服务的handle id和名字写在全局的handle_storage结构体中。他的算法是全局结构体中有一个保存名字和handle id结构体的数组,注册时就往数组里写入。这里写入用到了一个常用的简单算法--二分法,具体请看:
const char * skynet_handle_namehandle(uint32_t handle, const char *name) {
rwlock_wlock(&H->lock);
const char * ret = _insert_name(H, name, handle);
rwlock_wunlock(&H->lock);
return ret;
}
static const char *_insert_name(struct handle_storage *s, const char * name, uint32_t handle) {
int begin = 0;
int end = s->name_count - 1;
while (begin<=end) {
int mid = (begin+end)/2;
struct handle_name *n = &s->name[mid];
int c = strcmp(n->name, name);
if (c==0) {
return NULL;
}
if (c<0) {
begin = mid + 1;
} else {
end = mid - 1;
}
}
char * result = skynet_strdup(name);
_insert_name_before(s, result, handle, begin);
return result;
}
static void _insert_name_before(struct handle_storage *s, char *name, uint32_t handle, int before) {
if (s->name_count >= s->name_cap) {
s->name_cap *= 2;
assert(s->name_cap <= MAX_SLOT_SIZE);
struct handle_name * n = skynet_malloc(s->name_cap * sizeof(struct handle_name));
int i;
for (i=0;i<before;i++) {
n[i] = s->name[i];
}
for (i=before;i<s->name_count;i++) {
n[i+1] = s->name[i];
}
skynet_free(s->name);
s->name = n;
} else {
int i;
for (i=s->name_count;i>before;i--) {
s->name[i] = s->name[i-1];
}
}
s->name[before].name = name;
s->name[before].handle = handle;
s->name_count ++;
}
代码很清晰,插入名字的时候会通过二分法找到要插入的位置(根据字符串ASCII顺序排位),然后写入插入的名字和handle id数组,累加数组大小。注意上面还有一个数组扩容的过程。
给服务命名是为了方便后面的调用。我们skynet.send或skynet.call的时候,可以使用服务的地址,但是更多的时候我们使用的是服务名,毕竟服务名方便记忆。在skynet.send或skynet.call函数的c底层实现中,首先会判断第一个参数是否为number,是的就调用skynet_send否则调用skynet_sendname。我们看看skynet_sendname的实现:
int skynet_sendname(struct skynet_context * context, uint32_t source, const char * addr , int type, int session, void * data, size_t sz) {
if (source == 0) {
source = context->handle;
}
uint32_t des = 0;
if (addr[0] == ':') {
des = strtoul(addr+1, NULL, 16);
} else if (addr[0] == '.') {
des = skynet_handle_findname(addr + 1);
if (des == 0) {
if (type & PTYPE_TAG_DONTCOPY) {
skynet_free(data);
}
return -1;
}
} else {
_filter_args(context, type, &session, (void **)&data, &sz);
struct remote_message * rmsg = skynet_malloc(sizeof(*rmsg));
copy_name(rmsg->destination.name, addr);
rmsg->destination.handle = 0;
rmsg->message = data;
rmsg->sz = sz;
skynet_harbor_send(rmsg, source, session);
return session;
}
return skynet_send(context, source, des, type, session, data, sz);
}
可以看到如果名字带.则会调用skynet_handle_findname在全局handle_storage中查找,查找依然是用比较高效的二分查找法。对比数组中的名字,获取handle id,最终还是会调用skynet_send。如果查找不到名字就会走harbor服务进行远程调用,这个后面有机会再讲。我们还注意到,查找的名字也支持:1000001这样的格式。
服务查找自己服务名字的api是skynet.self(),在c底层实际上是调用我们上面的cmd_reg返回":id"的字符串,然后转换为number。
单纯的查找服务名的api是skynet.localname,在c底层会调用cmd_query,最终会调用skynet_handle_findname,返回":%0xid"类型的字符串,然后在skynet.localname中转化为number。
服务除了自己给自己注册名字之外,还可以给其他服务名命名,api是skynet.name('name', handleid) ,他首先也会判断名字是不是全局名,不是则会调用c层的cmd_name,最终也是调用skynet_handle_namehandle。
根据上面的命名算法,我们可以看出,可以为同一个服务命名多个名字。而把同一个名字给多个服务就会失败。
最后,skynet.adress会将一个number类型的地址转换为形如":%08x"的字符串。
以上讨论的都是单节点服务名,全局服务名的用法后面再讲解。
欢迎加入QQ群 858791125 讨论skynet,游戏后台开发,lua脚本语言等问题。