设计原理
epoll/kqueue + 非阻塞
设计巧思:socket 线程只负责 IO 和封 message,不碰 Lua;worker 只从 mq 取 message 调 Lua,不碰 fd。两边靠 mq 解耦,完全没有"IO 线程 vs 业务线程抢同一份数据"的问题。
设计初衷
Skynet 不强制协议,不定义 RPC,不规定编码,甚至"不定义出类似 TCP 连接的概念"------socket 层只搬字节流。
与其他语言对比
-
Go net包:bufio.Reader、textproto、http全家桶都绑在 net 上
-
Python asyncio.protocol:data_received()给你 bytes,但至少帮你分了 Protocol/Transport 两层
-
Skynet socket:C 层连分包都不管,一个 SOCKET_DATA就是一段 raw bytes,分包/协议解析全甩给 Lua 层的 Gate(gateserver.lua模板做 \r\n或自定义分包)

设计缘由
- gate 是先于 socket 模块存在的历史产物,早期直接在内核做接入;后来抽了 C 层 socket API 出来,但协议层故意不放 C------游戏协议千奇百怪(二进制 / protobuf / sproto / 自定义),C 层写死了反而僵。Lua 层写 Gate 模板,用户自己改分包逻辑更方便。
- 网络 IO 和业务消息共用同一套调度------worker 不用区分"这是网络事件还是服务 A 发给服务 B 的消息",都是 mq 里的一条
- socket 线程和 worker 之间不用任何 mutex------pipe 发命令,mq 是 worker 单线程消费(每个 worker 拿自己绑的 mq 槽),天然无锁,代价是多一次 mq 入队 + 一次 Lua 层 dispatch,延迟比"epoll 直接回调"高一点点
socket模块
数据接收
客户端 TCP
↓
C 层 socket_server(独立 socket 线程,epoll_wait)
↓ pipe 发命令 / 收事件
↓ socket_server_poll 返回 SOCKET_DATA / OPEN / CLOSE ...
↓ 封装成 skynet_message 投进 对应服务的次级 mq
↓ 全局 mq
↓ worker 线程消费 → 调 Lua 服务 callback(Gate/Agent)
核心数据结构
全局上下文
c
// socket_server.c
struct socket_server {
int recvctrl_fd; // pipe 读端(socket 线程读 worker 的命令)
int sendctrl_fd; // pipe 写端(worker 写命令给 socket 线程)
poll_fd event_fd; // epoll fd(Linux) / kqueue fd(Mac)
int alloc_id; // 下一个分配的 socket id(全局递增)
int event_n; // 本次 epoll_wait 返回的事件数
int event_index; // 正在处理的事件下标
struct socket slot[MAX_SOCKET]; // socket 池,默认 16K 个
fd_set rfds; // select 用来非阻塞探 pipe
};
单个连接
c
struct socket {
uintptr_t opaque; // 绑定的 Lua 服务地址(如 Gate),数据最终投给这
struct wb_list high; // 高优发送队列
struct wb_list low; // 低优发送队列
int64_t wb_size; // 发送缓冲区总字节(用来做限流报警)
int fd; // 内核 fd
int id; // slot 下标,lua 层看到的"socket id"
uint8_t protocol; // TCP / UDP
uint8_t type; // 状态机:LISTEN / CONNECTING / CONNECTED / CLOSE ...
};
工作流
c
// 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) continue; // 这帧没出 message,跳过
wakeup(m, 0); // 有消息产生了,唤醒 worker 来取
}
}
c
int socket_server_poll(struct socket_server *ss,
struct socket_message *result,
int *more)
{
for (;;) {
// ① 先看 pipe 有没有待处理命令
if (ss->checkctrl && has_cmd(ss)) {
int type = ctrl_cmd(ss, result); // ⭐在 socket 线程里执行
if (type != -1) return type;
continue;
}
// ② epoll_wait 拿一批事件
if (ss->event_index == ss->event_n) {
ss->event_n = sp_wait(ss->event_fd, ss->ev, MAX_EVENT);
ss->checkctrl = 1;
ss->event_index = 0;
if (ss->event_n <= 0) return -1;
}
// ③ 处理事件:accept / tcp-read / udp / error ...
struct event *e = &ss->ev[ss->event_index++];
struct socket *s = e->s;
if (s == NULL) continue; // pipe 事件已在上一步消化
// default 分支:TCP 可读 → forward_message_tcp
// TCP 可写 → flush write buffer
}
}
ctrl_cmd的 switch
c
static int ctrl_cmd(struct socket_server *ss, struct socket_message *result) {
uint8_t header[2];
block_readpipe(ss->recvctrl_fd, header, 2);
int type = header[0];
int len = header[1];
uint8_t buffer[256];
block_readpipe(ss->recvctrl_fd, buffer, len);
switch (type) {
case 'L': return listen_socket(ss,(...*)buffer, result);
case 'O': return open_socket(ss,(...*)buffer, result); // connect
case 'K': return close_socket(ss,(...*)buffer, result);
case 'D': return send_socket(ss, ..., PRIORITY_HIGH, NULL); // write-high
case 'P': return send_socket(ss, ..., PRIORITY_LOW, NULL); // write-low
case 'S': return start_socket(ss,(...*)buffer, result); // 开始监听读
case 'R': return resume_socket(ss,(...*)buffer, result);
case 'X': return SOCKET_EXIT;
default: ...
}
}
-
先看 pipe 有没有 worker 发来的命令(listen / start / send / close等)
Worker 不是直接调 epoll_ctl,而是把请求打包成 struct request_package,通过 sendctrl_fd写进 pipe,socket 线程在 ctrl_cmd()里读出来执行。这样 worker 和 socket 线程之间零锁------pipe 本身线程安全,slot 只有 socket 线程改。
-
再 sp_wait()等 epoll 事件
有事件(新连接 / 可读 / 可写)就处理,产出 SOCKET_OPEN / SOCKET_ACCEPT / SOCKET_DATA / SOCKET_CLOSE等 message,填进 struct socket_message,返回给 skynet_socket_poll,再封装成 skynet_message投进 opaque对应服务的 mq。