skynet 源码阅读 -- 「揭秘 Skynet 网络通讯」

本文将聚焦 Skynet 网络通讯的核心线程 thread_socket,并深入探讨 skynet_socket_pollforward_messagesocket_server_poll 等关键函数如何协作,实现高效的网络数据收发与消息分发。


1. 背景与目标

Skynet 之所以能轻量高效,网络 I/O 模块的功劳不可忽视。它利用一个独立线程 不断poll 网络事件,把事件打包成 socket message 再转交给目标服务处理。要掌握 Skynet 的"多线程 + Actor"模型,socket 线程是必读的。

本文主要将回答以下几个问题:

  1. thread_socket 线程在做什么?
  2. skynet_socket_poll 如何获取网络事件并将其转为 Skynet Message?
  3. forward_message 如何将 socket_event 转发给对应的 Service(通过 handle)?
  4. 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 关键流程解析

  1. 无限循环 :循环调用 skynet_socket_poll() 获取下一条网络事件。
  2. r == 0 :表示 SOCKET_EXIT => 说明 socket_server 已退出 => 线程可以 break
  3. r < 0 :通常代表无事件EINTR 等错误 => CHECK_ABORT 做安全检查
  4. 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 分步骤说明

  1. 调用 socket_server_poll :背后是一个底层 socket_server 实例(支持 epoll/kqueue/etc.)

    • 返回type 表示事件类型 (DATA/OPEN/CLOSE...)
    • result 是 socket_message(包含了 id/ud/data等信息)
  2. switch(type):根据事件类型进行分发

    • SOCKET_EXIT => return 0 => 让上层 thread_socket break
    • 其他 => 调用 forward_message(具体类型, bool padding, &result)
  3. 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);
	}
}
  1. bool padding : 如果是 SOCKET_OPEN or ACCEPT等,需要把 result->data 复制到 sm+1 => 避免后续失效。
  2. struct skynet_socket_message *sm:Skynet自己定义的 socket层消息结构(带 type/id/ud/buffer)
  3. 构造 struct skynet_message message;sz中保存了**(size | PTYPE_SOCKET)** => Worker处理时能知道这是"Socket类型消息"。
  4. 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;
}

过程:

  1. sp_wait -> epoll_wait(或 kevent) -> 返回就绪事件 存到 ss->ev[]
  2. 逐个处理 ev[i] => 解析socket 是 READ/WRITE/ACCEPT => forward => 生成 SOCKET_DATA / SOCKET_ACCEPT / etc.
  3. 只要拿到第一个可返回的事件 => return type

这样串联Linux epoll (C函数) => socket_server_poll => skynet_socket_poll => forward_message => skynet_context_push => 目标服务 queue => Worker => 回调处理**的网络 I/O 数据流。


6. 小结流程:从网络事件到 Service

  1. thread_socket :无限循环 => skynet_socket_poll()
  2. skynet_socket_poll :调用 socket_server_poll => 得到 1 条socket_event
  3. forward_message :构造 skynet_socket_message => 通过 skynet_context_push => push到目标服务的消息队列
  4. 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_DATASOCKET_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 优点

  1. 多线程(Socket专线 + Worker) 并行 => 不阻塞
  2. 零拷贝(DATA情况下) => 减少内存复制, 提升性能
  3. 统一消息队列 => Worker处理网络和自定义消息保持一致
  4. 支持 UDP / TCP(socket_server区分)
相关推荐
半个番茄2 小时前
C 或 C++ 中用于表示常量的后缀:1ULL
c语言·开发语言·c++
我想学LINUX8 小时前
【2024年华为OD机试】 (C卷,200分)- 机器人走迷宫(JavaScript&Java & Python&C/C++)
java·c语言·javascript·python·华为od·机器人
今天_也很困10 小时前
牛客周赛 Round 78 A-C
c语言·开发语言·算法
sushang~11 小时前
leetcode27. 移除元素
c语言·数据结构
Winston-Tao13 小时前
Skynet实践之「Lua C 模块集成—优先级队列」
c语言·lua·游戏开发·skynet·游戏服务器框架
2401_8582861113 小时前
L29.【LeetCode笔记】丢失的数字
c语言·开发语言·算法
艺杯羹14 小时前
二级C语言题解:统计奇偶个数以及和与差、拼接字符串中数字并计算差值、提取字符串数组中单词尾部字母
c语言·数据结构·算法
艺杯羹14 小时前
二级C语言题解:孤独数、找最长子串、返回两数组交集
c语言·开发语言·数据结构·算法
比特在路上14 小时前
ListOJ13:环形链表(判断是否为环形链表)
c语言·开发语言·数据结构·链表