skynet 通讯模块笔记

skynet 版本信息:1.5.0

skynet是如何处理socket网络信息的?

  skynet框架总共有4种线程:

    1) worker线程:处理消息逻辑

    2) monitor线程:监控服务是否死循环卡死

    3) timer线程:处理定时逻辑

    4) socket线程:处理网络通讯

  skynet所有的网络通讯都会经过socket线程处理。

一个服务是如何监听端口的?以gateserver为例子

  当启动gateserver服务时,会调用到 lua-socket.c 的llisten 函数

复制代码
-- gateserver.lua

function CMD.open( source, conf )
    assert(not socket)
    local address = conf.address or "0.0.0.0"
    local port = assert(conf.port)
    maxclient = conf.maxclient or 1024
    nodelay = conf.nodelay
    skynet.error(string.format("Listen on %s:%d", address, port))
    socket = socketdriver.listen(address, port) -- 调用lua-socket.c库监听端口
    socketdriver.start(socket)
    if handler.open then
        return handler.open(source, conf)
    end
end
复制代码
复制代码
// lua-socket.c
static int
llisten(lua_State *L) {
    const char * host = luaL_checkstring(L,1);
    int port = luaL_checkinteger(L,2);
    int backlog = luaL_optinteger(L,3,BACKLOG);
    struct skynet_context * ctx = lua_touserdata(L, lua_upvalueindex(1));
    int id = skynet_socket_listen(ctx, host,port,backlog);
    if (id < 0) {
        return luaL_error(L, "Listen error");
    }

    lua_pushinteger(L,id);
    return 1;
}
复制代码

进而调用 skynet-socket.c里的skynet_socket_listen 获取到gateserver服务的handle后,在 socket_server.c中的socket_server_listen 函数进行handle与socket fd的绑定。

复制代码
// skynet-socket.c
int 
skynet_socket_listen(struct skynet_context *ctx, const char *host, int port, int backlog) {
    uint32_t source = skynet_context_handle(ctx);    // 获取到服务的handle
    return socket_server_listen(SOCKET_SERVER, source, host, port, backlog);
}

// socket_server.c
int 
socket_server_listen(struct socket_server *ss, uintptr_t opaque, const char * addr, int port, int backlog) {
    int fd = do_listen(addr, port, backlog);    // 监听端口获得socket的fd
    if (fd < 0) {
        return -1;
    }
    struct request_package request;
    int id = reserve_id(ss);            // 从socket_server中的socket库中获取到空闲的socket结构体
    if (id < 0) {
        close(fd);
        return id;
    }
        // 将服务的handle与socket fd信息绑定
    request.u.listen.opaque = opaque;
    request.u.listen.id = id;
    request.u.listen.fd = fd;
    send_request(ss, &request, 'L', sizeof(request.u.listen));
    return id;
}
复制代码
复制代码
// socket-server.c
static void
send_request(struct socket_server *ss, struct request_package *request, char type, int len) {
    request->header[6] = (uint8_t)type;
    request->header[7] = (uint8_t)len;
    const char * req = (const char *)request + offsetof(struct request_package, header[6]);
    for (;;) {
        ssize_t n = write(ss->sendctrl_fd, req, len+2);   // 将信息写入到socket_server的管道中,去触发epoll唤醒socket进行处理。 
        if (n<0) {
            if (errno != EINTR) {
                skynet_error(NULL, "socket-server : send ctrl command error %s.", strerror(errno));
            }
            continue;
        }
        assert(n == len+2);
        return;
    }
}
复制代码

  至此,gateserver执行完监听流程,等待socket执行listen监听。实际进行监听的仍然是socket线程,gateserver只是将自己服务的handle进行了注册操作。总的来说,就是gateserver调用socketdriver.listen,实际是将gateserver的服务id与socket的fd进行绑定然后将它写入pipe中,socket线程从pipe中获取到数据进行操作。

  接下来则是socket线程进行消息处理(上述的gateserver监听还没完成),在skynet启动时,会生成一个socket线程专门处理消息轮询:

复制代码
// skynet_start.c
static void *
thread_socket(void *p) {
    struct monitor * m = p;
    skynet_initthread(THREAD_SOCKET);
    for (;;) {
        int r = skynet_socket_poll();     // 执行消息处理
        if (r==0)
            break;
        if (r<0) {
            CHECK_ABORT
            continue;
        }
        wakeup(m,0); 
    }
    return NULL;
}    
View Code
复制代码

  最终会调用到socket_server.c里的ctrl_cmd函数,根据绑定时传入的'L' 去执行listen_socket进行真实的监听。

  在调用ctrl_cmd前,会先去调用select判断管道中是否有信息。

  

复制代码
// socket_server.c
static int
has_cmd(struct socket_server *ss) {
    struct timeval tv = {0,0};
    int retval;

    FD_SET(ss->recvctrl_fd, &ss->rfds);

    retval = select(ss->recvctrl_fd+1, &ss->rfds, NULL, NULL, &tv);
    if (retval == 1) {
        return 1;
    }
    return 0;
}

// return type
static int
ctrl_cmd(struct socket_server *ss, struct socket_message *result) {
    int fd = ss->recvctrl_fd;
    // the length of message is one byte, so 256+8 buffer size is enough.
    uint8_t buffer[256];
    uint8_t header[2];
    block_readpipe(fd, header, sizeof(header));
    int type = header[0];
    int len = header[1];
    block_readpipe(fd, buffer, len);
    // ctrl command only exist in local fd, so don't worry about endian.
    switch (type) {
    ...
    case 'L':
        return listen_socket(ss,(struct request_listen *)buffer, result);
    ...
    };

    return -1;
}

// return type
int 
socket_server_poll(struct socket_server *ss, struct socket_message * result, int * more) {
    for (;;) {
        if (ss->checkctrl) {
            if (has_cmd(ss)) {
                int type = ctrl_cmd(ss, result);
                if (type != -1) {
                    clear_closed_event(ss, result, type);
                    return type;
                } else
                    continue;
            } else {
                ss->checkctrl = 0;
            }
        }
        ...
        }
    }
}
View Code
复制代码

  这里有个问题,为什么has_cmd在读取管道中的数据时,用select而不是用epoll?

  从解决的问题上出发,比较select与epoll的区别,epoll主要解决了select中有大量不活跃的fd。在skynet中,select监控的fd只有这个管道,是所有服务的通讯fd,则表明了这个fd是非常活跃的,也仅此一个fd,那么select执行获得到的fd也只有一个,即该管道fd,仅当该管道有消息时,才会返回数据,那么就不存在使用select的效率低于使用epoll的问题。

  select在内核中是做的轮询操作,判断是否有fd是有数据的,而epoll的实现机制是需要通过硬件中断触发后将这个fd的信息保存到就绪状态中,所以如果大量的活跃fd中,使用select会比较高效。
  epoll的内部结构比较复杂,在触发回调的时候需要做更复杂的处理;在内核中,select是去做轮询操作判断是否有消息过来,而epoll是需要中断来回调加入到就绪队列中,个人认为中断的代价比轮询大(在活跃度趋近于1或者连接数量少的情况下)。

  上述既是gateserver监听端口的执行过程。

 

接下来,一个玩家socket上来后,是怎么启动到agent服的(此处一个玩家一个agent的设计)?

客户端连接到agent启动

  当gateserver启动好后,其实socket已经在替gateserver监听了端口,当有新的连接上来时,socket线程会去接收网络消息,接收完后,会根据epoll_event里的信息获取到该消息是属于哪个服务的,然后将消息分配到各服务的消息队列里,等待worker线程的调用。

  当有gateserver的消息时,会带着fd等参数调用到回调函数的 MSG.open(),进而调用到gate.lua 里的 connect函数,转到watchdog里的SOCKET.open函数进行创建一个agent的服务,在agent启动时,会调回gateserver服务与新socket的fd的绑定,然后gateserver会维护socket的fd与agent的fd的映射。

  当客户端发送协议给服务器时,socket线程会分发到gateserver服务当中,进而转发到watchdog中,再转到具体agent。

复制代码
-- gateserver.lua
function MSG.open(fd, msg)
    if client_number >= maxclient then
        socketdriver.shutdown(fd)
        return
    end
    if nodelay then
        socketdriver.nodelay(fd)
    end
    connection[fd] = true
    client_number = client_number + 1
    handler.connect(fd, msg)
end

-- gate.lua
function handler.connect(fd, addr)
    local c = {
        fd = fd,
        ip = addr,
    }
    connection[fd] = c
    skynet.send(watchdog, "lua", "socket", "open", fd, addr)
end

-- watchdog.lua
function SOCKET.open(fd, addr)
    skynet.error("New client from : " .. addr)
    agent[fd] = skynet.newservice("agent")
    skynet.call(agent[fd], "lua", "start", { gate = gate, client = fd, watchdog = skynet.self() })
end
View Code
复制代码

 

上述文章即是skynet监听端口,接收客户端的网络消息的过程。

 

posted @   小乐虎  阅读(587)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示