本文将聚焦 Skynet 网络通讯的核心线程 thread_socket
,并深入探讨 skynet_socket_poll
、forward_message
、socket_server_poll
等关键函数如何协作,实现高效的网络数据收发与消息分发。
1. 背景与目标
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线程 以及消息分发机制有一个清晰的认知。
2. thread_socket:多线程网络 IO
在 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;
}
2.1 关键流程解析
- 无限循环 :循环调用
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能够及时处理到达的数据或断开事件。
3. skynet_socket_poll()
:轮询网络事件
cpp
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;
}
3.1 分步骤说明
-
调用
socket_server_poll
:背后是一个底层 socket_server 实例(支持 epoll/kqueue/etc.)- 返回 :
type
表示事件类型 (DATA/OPEN/CLOSE...) result
是 socket_message(包含了 id/ud/data等信息)
- 返回 :
-
switch(type):根据事件类型进行分发
- SOCKET_EXIT => return 0 => 让上层
thread_socket
break - 其他 => 调用
forward_message(具体类型, bool padding, &result)
- SOCKET_EXIT => return 0 => 让上层
-
more
:有时 poll 结果一次返回多个事件 =>if (more) return -1;
=> 让上层循环再 poll
4. forward_message
:转发 socket 事件给 Skynet Service
cpp
static 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
orACCEPT
等,需要把 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 事件
5. socket_server_poll
:底层 epoll/kqueue
cpp
// 根据操作系统进行切换
#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<n;i++) {
e[i].s = ev[i].data.ptr;
unsigned flag = ev[i].events;
e[i].write = (flag & EPOLLOUT) != 0;
e[i].read = (flag & EPOLLIN) != 0;
e[i].error = (flag & EPOLLERR) != 0;
e[i].eof = (flag & EPOLLHUP) != 0;
}
return n;
}
--------------------------------------------------------------------------
// epoll的触发模式
static int
sp_add(int efd, int sock, void *ud) {
struct epoll_event ev;
// 由这个参数决定触发模式,如果 包含 EPOLLET 标志,则是 边缘触发模式(ET)。这是默认 水平触发
ev.events = EPOLLIN;
ev.data.ptr = ud;
if (epoll_ctl(efd, EPOLL_CTL_ADD, sock, &ev) == -1) {
return 1;
}
return 0;
}
过程:
sp_wait
-> epoll_wait(或 kevent) -> 返回就绪事件 存到ss->ev[]
- 逐个处理
ev[i]
=> 解析socket 是 READ/WRITE/ACCEPT => forward => 生成SOCKET_DATA
/SOCKET_ACCEPT
/ etc. - 只要拿到第一个可返回的事件 => return type
这样 就串联 了Linux epoll (C函数) => socket_server_poll
=> skynet_socket_poll
=> forward_message
=> skynet_context_push
=> 目标服务 queue => Worker => 回调处理**的网络 I/O 数据流。
6. 小结流程:从网络事件到 Service
thread_socket
:无限循环 =>skynet_socket_poll()
skynet_socket_poll
:调用socket_server_poll
=> 得到 1 条socket_eventforward_message
:构造skynet_socket_message
=> 通过skynet_context_push
=> push到目标服务的消息队列- Worker => detect queue => pop => sees PTYPE_SOCKET => 执行Lua (如
socket_message_handler()
)
7. 常见疑问
1) "为什么 forward_message 里有 padding?"
- 当 type = "OPEN/ACCEPT/ERROR" 等,result->data 仅是一小段字符串(可在内存中马上失效) => 需要复制到 skynet_socket_message 后面。
- 其他 case (DATA) 直接
sm->buffer = result->data
=> 不拷贝 => "零拷贝" 原则, 只把指针交给 skynet.
2) "如何区分 TCP/UDP?"
- 在 socket_server 层,根据
s->protocol
= PROTOCOL_TCP/UDP => forward到SOCKET_DATA 或 SOCKET_UDP。 - Skynet Lua 层就通过PTYPE_SOCKET + type="DATA" or "UDP"区分.
3) "发送数据在哪里发生?"
- 发送 (
skynet_socket_send
) => socket_server => 先放发送缓冲 => poll事件可写 => 继续写 => 直到 send 成功/出错 => 生成 event => forward_message => "CLOSE/ERROR" 等.
8. 总结
Skynet 的网络通讯可以用一句话概括:单独"socket线程"轮询 epoll/kqueue 事件 -> 将事件转换为 Skynet 自定义 "socket_message" -> push到目标服务消息队列 -> Worker线程 消费。
8.1 优点
- 多线程(Socket专线 + Worker) 并行 => 不阻塞
- 零拷贝(DATA情况下) => 减少内存复制, 提升性能
- 统一消息队列 => Worker处理网络和自定义消息保持一致
- 支持 UDP / TCP(socket_server区分)