skynet——网络模块学习

设计原理

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: ...
    }
}
  1. 先看 pipe 有没有 worker 发来的命令(listen / start / send / close等)

    Worker 不是直接调 epoll_ctl,而是把请求打包成 struct request_package,通过 sendctrl_fd写进 pipe,socket 线程在 ctrl_cmd()里读出来执行。这样 worker 和 socket 线程之间零锁------pipe 本身线程安全,slot 只有 socket 线程改。

  2. 再 sp_wait()等 epoll 事件

    有事件(新连接 / 可读 / 可写)就处理,产出 SOCKET_OPEN / SOCKET_ACCEPT / SOCKET_DATA / SOCKET_CLOSE等 message,填进 struct socket_message,返回给 skynet_socket_poll,再封装成 skynet_message投进 opaque对应服务的 mq。

相关推荐
tiantianuser1 小时前
NVME-oF IP 设计1 :为什么要设计它?
网络·网络协议·rdma·roce v2·nvme of
剑藏锋1 小时前
嵌入式学习(46)-基于STM32F407Hal库的DMA乒乓缓存机制
stm32·单片机·学习
xd1855785551 小时前
鸿蒙PC平台Flutter魔术教学应用开发实践:打造跨端沉浸式魔术学习体验
人工智能·学习·flutter·华为·harmonyos·鸿蒙
kyle~1 小时前
Linux---Snap(隔离式Linux 软件分发范式)
linux·运维·网络
Linging_242 小时前
基于Langchain得AI Agent相关知识学习
人工智能·学习·langchain
pt10432 小时前
思科网络自动化入门课程介绍
运维·网络·自动化
minglie111 小时前
高等代数试题
学习
CHENKONG_CK11 小时前
晨控CK-FR102ANS-EIP与基恩士KVX520系列PLC配置EtherNet/IP通讯连接手册
网络
Xiaofeng369312 小时前
GPT-5.6 发布后,我用了一个周末重新规划学习路线图
gpt·学习