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; }
最终会调用到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; } } ... } } }
这里有个问题,为什么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
上述文章即是skynet监听端口,接收客户端的网络消息的过程。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)