写自己的游戏 -云风skynet

上午粗略浏览了poco c++ library,理解了一下poco notification&event部分的设计。中午吃完饭感觉有点累,就躺在床上,可是怎么都睡不着,所以就起床即兴打开云风Skynet的源码,然后把自己的思考记录下来。没办法,这是强迫症。

关于webgame对于游戏proto的处理,有很多都是采用notification的机制实现的,用比较C方式的话来解释,那就是注册flag+callback来实现,无疑,需要一个NotificationCenter采用map或者是vector的方式保存k_v<flag,callback>。至于协议如何设计,都是仁者见仁智者见智的事情了,按照需求来吧,不过总体变动不大,我以前的做法是xml映射flag,在Socket解析出来proto id,然后读取xml中的flag,利用动态语言本身的反射去创建对应的协议类.这种做法就是根据语言本身的特性做出的部分变动,就像c++,本身并没有反射机制,不像java和as3。自然使用binary buffer还是class来承载proto,都是数据在模块中流动的承载方式,并无太大不同,只是更趋近于oo的特性。

对于skynet里面的dispatch,handler以及context etc这些,我上次阅读源码的时候就能够大致理解,这次联系reactor理解了一下,发现有不同的思考。我不知道将skynet说成是注册服务组件消息转发framework是不是不准确,但是我觉得符合我现在的想法。skynet的设计宗旨可以去参考云风大大的博客,这里我不再过多说明了。skynet提供两种service注册实现方式,一种是c动态库编写服务组件,而就像云风说的一样,Skynet核心只是数据转发,不做数据处理,由开发者自己实现。另外一种方式是通过lua实现,不过无论是哪种方式实现,都需要在skynet启动的时候主动启动。所以这里的动态组件的意义体现在服务的独立性以及扩展性,而非程序运行时的动态性。

每一个skynet注册的Service都需要有一个callback。而且是按照Skynet_cb的方式:

typedef int (*skynet_cb)(struct skynet_context * context, void *ud, int type, int session, uint32_t source , const void * msg, size_t sz);

好吧,下面我们来看一个具体的service。每一个service我们只需要关注他的三个方法就可以了,为什么? 看看下面这段源码吧。

static int
_open_sym(struct skynet_module *mod) {
    size_t name_size = strlen(mod->name);
    char tmp[name_size + 9]; // create/init/release , longest name is release (7)
    memcpy(tmp, mod->name, name_size);
    strcpy(tmp+name_size, "_create");
    mod->create = dlsym(mod->module, tmp);
    strcpy(tmp+name_size, "_init");
    mod->init = dlsym(mod->module, tmp);
    strcpy(tmp+name_size, "_release");
    mod->release = dlsym(mod->module, tmp);

    return mod->init == NULL;
}

 

看到这里并没有什么,这三个方法分别在skynet_context_new创建和销毁服务的时候会执行。但是看到这里,还是没有看到关键的代码,因为我们并没有看到skynet_cb在哪里。好吧,不着急,继续看service的源码,我挑选的harbor的源码。

int
harbor_init(struct harbor *h, struct skynet_context *ctx, const char * args) {
    h->ctx = ctx;
    int sz = strlen(args)+1;
    char master_addr[sz];
    char local_addr[sz];
    int harbor_id = 0;
    sscanf(args,"%s %s %d",master_addr, local_addr, &harbor_id);
    h->master_addr = strdup(master_addr);
    h->master_fd = _connect_to(h, master_addr, true);
    if (h->master_fd == -1) {
        fprintf(stderr, "Harbor: Connect to master failed\n");
        exit(1);
    }
    h->local_addr = strdup(local_addr);
    h->id = harbor_id;

    _launch_gate(ctx, local_addr);
    skynet_callback(ctx, h, _mainloop); // skynet_cb
    _request_master(h, local_addr, strlen(local_addr), harbor_id);

    return 0;
}

 

上面的注释代码,为ctx注册了skynet_cb。自然业务处理的逻辑也都在_mainloop里面了。这个先不看,因为skynet在有两种消息传播的方式,一种是单向的,一种是双向的,后面再说。下面我们来看一下lua service部分的实现。

ctx = skynet_context_new("snlua", "launcher");

 

源码中有这么一句代码,这里的snlua是什么? 按照前面我们对skynet的理解,这里应该是一个service的动态库名字才对。好吧,我相信会有人在这里栽倒,不过没关系。看一下makefile吧。

all : \
  skynet \
  service/snlua.so \ // here
  service/logger.so \
  service/gate.so \
  service/client.so \
  service/master.so \
  service/multicast.so \
  service/tunnel.so \
  service/harbor.so \
  service/localcast.so \
  luaclib/skynet.so \
  luaclib/socketdriver.so \
  luaclib/int64.so \
  luaclib/mcast.so \
  luaclib/bson.so \
  luaclib/mongo.so \
  client

 

此时我只想对你说,忍住,云风大大是我偶像,不准乱说话。大神的想法我们是不会懂的,所以也没有资格评论。 这里这个问题说明一下,其实发现这个也并不是什么难事,只是很多人并不太了解库的概念,这才是重点,好吧,这不是本文的重点。

其实snlua.so是处理lua部分的过渡层,实现转换。源码在makefile中可以找得到,看makefile就是咯。

service/snlua.so : service-src/service_lua.c service-src/luacode_cache.c service-src/luaalloc.c
    gcc $(CFLAGS) $(SHARED) -Iluacompat $^ -o $@ -Iskynet-src

 

好了,下面我们看看源码吧。snlua_init,snlua_create,snlua_realease都在,没有任何问题,看看重点。

struct snlua *
snlua_create(void) {
    struct snlua * l = malloc(sizeof(*l));
    memset(l,0,sizeof(*l));
#ifdef PREALLOCMEM
    l->L = lua_newstate(skynet_lua_alloc, skynet_lalloc_new(PREALLOCMEM));
#else
    l->L = luaL_newstate();
#endif
    l->init = _init;
    return l;
}

 

创建了一个lua虚拟机。追踪了一下源码。发现,服务并不是在注册之后立即启动的,而是通过skynet_send发送了一条启动的消息到meesage queue.

snlua_init(struct snlua *l, struct skynet_context *ctx, const char * args) {
    int sz = strlen(args);
    char * tmp = malloc(sz+1);
    memcpy(tmp, args, sz+1);
    skynet_callback(ctx, l , _launch);
    const char * self = skynet_command(ctx, "REG", NULL);
    uint32_t handle_id = strtoul(self+1, NULL, 16);
    // it must be first message
    skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY,0, tmp, sz+1); // here
    return 0;
}

 

但是服务究竟什么时候启动的呢?好吧,这个问题虽然转了一个弯,但是也比较好理解。注意一下skynet在启动的时候启动了三条非工作线程,也可说是管理线程。

    create_thread(&pid[0], _monitor, m);
    create_thread(&pid[1], _timer, m);
    create_thread(&pid[2], _socket, m);

 

mointer线程是管理线程,_socket则是监控采用epoll模式管理的Socket是否有数据,然后唤醒工作线程处理任务。到这里,线程如何工作也说清楚了。最后就是_timer.

static void *
_timer(void *p) {
    struct monitor * m = p;
    for (;;) {
        skynet_updatetime();
        CHECK_ABORT
        wakeup(m,m->count-1); // 注意这里,启动的只是一条线程
        usleep(2500);
    }
    // wakeup socket thread
    skynet_socket_exit();
    // wakeup all worker thread
    pthread_cond_broadcast(&m->cond);
    return NULL;
}

 

在我注释的地方,也就是定时线程每隔一段时间就会唤醒一条工作线程去处理任务,为什么要这么做? 很简单了,就是去处理service初始化的。接着前面继续看吧。初始化进入snlua里面,进入_init

在加载Lua文件的方法里面,看到下面这段代码:

#ifdef NOCODECACHE
    int r = luaL_loadfile(L,tmp);
#else
    int r = luacode_loadfile(L,tmp);
#endif

 好吧这里我们想一下为什么要使用cache。 每一次workThread执行任务的时候都会调用skynet_cb,如下:

static void
_dispatch_message(struct skynet_context *ctx, struct skynet_message *msg) {
    assert(ctx->init);
    CHECKCALLING_BEGIN(ctx)
    int type = msg->sz >> HANDLE_REMOTE_SHIFT;
    size_t sz = msg->sz & HANDLE_MASK;
    if (type == PTYPE_MULTICAST) {
        skynet_multicast_dispatch((struct skynet_multicast_message *)msg->data, ctx, _mc);
    } else {
        int reserve = ctx->cb(ctx, ctx->cb_ud, type, msg->session, msg->source, msg->data, sz); // 执行处理逻辑
        reserve |= _forwarding(ctx, msg);
        if (!reserve) {
            free(msg->data);
        }
    }
    CHECKCALLING_END(ctx)
}

 

依据对上面源码的分析,每次都要重新加载不同的lua service。我对Lua加载.lua文件的效率没什么概念,上升到的层次也就是c io读取文件嘛,因为lua是c写的,不知道这样理解有没有错,反正涉及到Io总是有消耗的嘛。 所以如果每次切换可以实现cache级别的切换,那么无疑性能将提升不少。好吧,这里又可以pass了。接着看下面吧。

下面就直接到service下面的lua文件了,那就先看看main.lua吧。main.lua里面加载了skynet模块,追踪一下lua源码到lua-skynet.c,方法依旧,对着makefile找吧。

 

额,写到这里,下面就是Lua部分的处理了,由于对lua的理解还不够,所以暂时就不写了,最近抽时间看看lua与c交互方面的处理,再考虑全面分析一下这方面的。请原谅我沉默的方式。不过可以讨论一下标题其他部分的内容。

 

skynet的源码2/3都已经讨论过了,虽然只是大体浏览,但是也算是有了个大概的了解。就到这里吧,篇幅也不长,有不到之处欢迎拍砖。

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(写自己的游戏 -云风skynet)