本文将聚焦 Skynet 网络通讯的核心线程 thread_socket
,并深入探讨 skynet_socket_poll
、forward_message
、socket_server_poll
等关键函数如何协作,实现高效的网络数据收发与消息分发。
Skynet 之所以能轻量高效,网络 I/O 模块的功劳不可忽视。它利用一个独立线程不断poll网络事件,把事件打包成 socket message 再转交给目标服务处理。要掌握 Skynet 的“多线程 + Actor”模型,socket 线程是必读的。
本文主要将回答以下几个问题:
thread_socket
线程在做什么?skynet_socket_poll
如何获取网络事件并将其转为 Skynet Message?forward_message
如何将 socket_event 转发给对应的 Service(通过 handle)?socket_server_poll
又是什么,里面epoll or kqueue 机制如何衔接?读完后,能对 Skynet 的网络I/O线程以及消息分发机制有一个清晰的认知。
在 Skynet 启动时,会创建一个名为 thread_socket
的线程(参见 start(...)
函数中 create_thread(&pid[2], thread_socket, m)
). 其源码如下:
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;
}
skynet_socket_poll()
获取下一条网络事件。r == 0
:表示 SOCKET_EXIT
=> 说明 socket_server 已退出 => 线程可以 breakr < 0
:通常代表无事件或EINTR等错误 => CHECK_ABORT
做安全检查wakeup(m,0)
:唤醒Monitor机制 or Worker(具体在 Skynet Monitor 结构中), 让 Worker 线程有机会处理队列因此,这个线程持续 poll网络事件,然后唤醒其他线程,这样Worker能够及时处理到达的数据或断开事件。
skynet_socket_poll()
:轮询网络事件int
skynet_socket_poll() {
struct socket_server *ss = SOCKET_SERVER;
assert(ss);
struct socket_message result;
int more = 1;
int type = socket_server_poll(ss, &result, &more);
switch (type) {
case SOCKET_EXIT:
return 0;
case SOCKET_DATA:
forward_message(SKYNET_SOCKET_TYPE_DATA, false, &result);
break;
case SOCKET_CLOSE:
forward_message(SKYNET_SOCKET_TYPE_CLOSE, false, &result);
break;
case SOCKET_OPEN:
forward_message(SKYNET_SOCKET_TYPE_CONNECT, true, &result);
break;
case SOCKET_ERR:
forward_message(SKYNET_SOCKET_TYPE_ERROR, true, &result);
break;
case SOCKET_ACCEPT:
forward_message(SKYNET_SOCKET_TYPE_ACCEPT, true, &result);
break;
case SOCKET_UDP:
forward_message(SKYNET_SOCKET_TYPE_UDP, false, &result);
break;
case SOCKET_WARNING:
forward_message(SKYNET_SOCKET_TYPE_WARNING, false, &result);
break;
default:
skynet_error(NULL, "Unknown socket message type %d.",type);
return -1;
}
if (more) {
return -1;
}
return 1;
}
调用 socket_server_poll
:背后是一个底层 socket_server 实例(支持 epoll/kqueue/etc.)
type
表示事件类型 (DATA/OPEN/CLOSE...)result
是 socket_message(包含了 id/ud/data等信息)switch(type):根据事件类型进行分发
thread_socket
breakforward_message(具体类型, bool padding, &result)
more
:有时 poll 结果一次返回多个事件 => if (more) return -1;
=> 让上层循环再 poll
forward_message
:转发 socket 事件给 Skynet Servicestatic void
forward_message(int type, bool padding, struct socket_message * result) {
struct skynet_socket_message *sm;
size_t sz = sizeof(*sm);
if (padding) {
if (result->data) {
size_t msg_sz = strlen(result->data);
if (msg_sz > 128) {
msg_sz = 128;
}
sz += msg_sz;
} else {
result->data = "";
}
}
sm = (struct skynet_socket_message *)skynet_malloc(sz);
sm->type = type;
sm->id = result->id;
sm->ud = result->ud;
if (padding) {
sm->buffer = NULL;
memcpy(sm+1, result->data, sz - sizeof(*sm));
} else {
sm->buffer = result->data;
}
struct skynet_message message;
message.source = 0;
message.session = 0;
message.data = sm;
message.sz = sz | ((size_t)PTYPE_SOCKET << MESSAGE_TYPE_SHIFT);
if (skynet_context_push((uint32_t)result->opaque, &message)) {
// todo: report somewhere to close socket
// don't call skynet_socket_close here (It will block mainloop)
skynet_free(sm->buffer);
skynet_free(sm);
}
}
bool padding
: 如果是 SOCKET_OPEN
or ACCEPT
等,需要把 result->data 复制到 sm+1
=> 避免后续失效。struct skynet_socket_message *sm
:Skynet自己定义的 socket层消息结构(带 type/id/ud/buffer)struct skynet_message message;
并sz中保存了**(size | PTYPE_SOCKET)** => Worker处理时能知道这是“Socket类型消息”。skynet_context_push(result->opaque, &message)
=> 这会投递给目标服务(handle=opaque) => 服务就能收到此 socket 事件socket_server_poll
:底层 epoll/kqueue// 根据操作系统进行切换
#ifdef __linux__
#include "socket_epoll.h"
#endif
#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined (__NetBSD__)
#include "socket_kqueue.h"
#endif
--------------------------------------------------------------------------
int socket_server_poll(struct socket_server *ss, struct socket_message * result, int * more) {
for (;;) {
if (ss->checkctrl) {
// 先check ctrl_cmd
...
}
if (ss->event_index == ss->event_n) {
ss->event_n = sp_wait(ss->event_fd, ss->ev, MAX_EVENT); // epoll_wait / kevent
ss->checkctrl = 1;
...
ss->event_index = 0;
if (ss->event_n <= 0) {
...
continue;
}
}
// 处理 ev[ss->event_index++]
// 找到 socket, 读取 or accept or error ...
// return some socket event type
}
}
--------------------------------------------------------------------------
// sp_wait 实际底层调用的是 epoll_wait -- linux 系统下
static int
sp_wait(int efd, struct event *e, int max) {
struct epoll_event ev[max];
int n = epoll_wait(efd , ev, max, -1);
int i;
for (i=0;i
过程:
sp_wait
-> epoll_wait(或 kevent) -> 返回就绪事件存到 ss->ev[]
ev[i]
=> 解析socket是 READ/WRITE/ACCEPT => forward => 生成 SOCKET_DATA
/ SOCKET_ACCEPT
/ etc.这样就串联了Linux epoll(C函数) => socket_server_poll
=> skynet_socket_poll
=> forward_message
=> skynet_context_push
=> 目标服务 queue => Worker => 回调处理**的网络 I/O 数据流。
thread_socket
:无限循环 => skynet_socket_poll()
skynet_socket_poll
:调用 socket_server_poll
=> 得到 1 条socket_eventforward_message
:构造 skynet_socket_message
=> 通过 skynet_context_push
=> push到目标服务的消息队列socket_message_handler()
)1) “为什么 forward_message 里有 padding?”
sm->buffer = result->data
=> 不拷贝 => “零拷贝” 原则, 只把指针交给 skynet.2) “如何区分 TCP/UDP?”
s->protocol
= PROTOCOL_TCP/UDP => forward到SOCKET_DATA 或 SOCKET_UDP。3) “发送数据在哪里发生?”
skynet_socket_send
) => socket_server => 先放发送缓冲 => poll事件可写 => 继续写 => 直到 send 成功/出错 => 生成 event => forward_message => “CLOSE/ERROR” 等.Skynet 的网络通讯可以用一句话概括:单独“socket线程”轮询 epoll/kqueue 事件 -> 将事件转换为 Skynet 自定义 “socket_message” -> push到目标服务消息队列 -> Worker线程 消费。