skynet 网络模块解析

news2025/1/15 22:58:31

文章目录

  • 前言
  • 环境准备
  • sneak peek
  • 线程
  • 数据结构
    • 会话对象:持有基础套接字,封装了套接字的基础操作。
    • 会话管理器:持有并管理会话池,给外部模块提供网络接口。
  • 网络模块管理
    • 会话管理器的生命周期管理
    • 工作模式
  • 总结技术点
    • 原子数据
    • 管道描述符
    • 自定义锁
    • epoll
    • halfclose 状态
    • SO_REUSEADDR
    • dup(1)
    • opaque

前言

本文简要拆解和分析 skynet 网络模块的实现,可以作为一般游戏服务器的网关实现的参考。

环境准备

  • 拉取 skynet 仓库
  • skynet 的框架代码集中在 skynet-src 目录中,可以参考这个文件分类
  • 网络模块的全部内容在如下文件列表中:
    在这里插入图片描述

sneak peek

  • socket_server.h/c

    网络连接管理器接口实现(对 skynet 服务透明,至此以下的内容并不依赖 skynet 本身)

  • socket_epoll.h socket_kqueue.h socket_poll.h

    前两个文件是对 socket_poll.h 中声明的接口的实际定义,选择其中一种网络 io 事件通知机制进行搭配编译,epoll 用于 linux,kqueue 用于 mac

  • skynet_socket.h/c

    中间件,提供给 skynet 服务使用的网络接口封装,隐藏了 socket_server 中的接口调用细节。(skynet 服务机制依赖该中间件,该中间件依赖 socket_server。好处是,中间件提供的接口通常是稳定的,socket_server 内部的细节修改,例如 epoll/kqueue 的切换并不会对 skynet 的服务机制产生任何影响)

线程

  • skynet 只有一个网络线程。

  • 线程主循环:
    在这里插入图片描述

    • r == 0 时,网络线程退出工作状态,通过控制命令 ‘X’ 设置。
      在这里插入图片描述
      在这里插入图片描述
    • r < 0 时,检查是否还有 skynet 服务存在,如果没有则退出工作状态,有则继续工作。
      在这里插入图片描述
    • r > 0 时,检测当前正在工作的 worker 线程(承载 skynet 服务运转的线程)数量,如果都在 sleep,则触发信号试图唤醒一个正 sleep 的 worker 线程。
      在这里插入图片描述
    • 对于返回值 r > 0 和 r < 0,取决于一个变量 more,表示是否还有网络事件通知需处理,有则返回 r > 0,想要表达的是,网络事件大概都处理完了,是不是因为工作线程的工作不饱和导致的,所以去检测是否需要唤醒 worker 线程。不过,这只是一个 heuristic 处理,可以看到,前后流程都不是很慎重:
      在这里插入图片描述
      在这里插入图片描述
    • 注释有写到像这样“虚假地唤醒工作线程是无害的”,为什么说是虚假地唤醒,因为网络线程并不确定是否真的全局消息队列有服务消息待处理。在工作线程的工作代码中有看到解释为什么是无害的:
      在这里插入图片描述

数据结构

会话对象:持有基础套接字,封装了套接字的基础操作。

// file: socket_server.c
struct socket {
        uintptr_t opaque;
        struct wb_list high;
        struct wb_list low;
        int64_t wb_size;
        struct socket_stat stat;
        ATOM_ULONG sending;
        int fd;
        int id;
        ATOM_INT type;
        uint8_t protocol;
        bool reading;
        bool writing;
        bool closing;
        ATOM_INT udpconnecting;
        int64_t warn_size;
        union {
                int size;
                uint8_t udp_address[UDP_ADDRESS_SIZE];
        } p;
        struct spinlock dw_lock;
        int dw_offset;
        const void * dw_buffer;
        size_t dw_size;
};

核心字段:

  • uintptr_t opaque;

    opaque 翻译是隐晦的、不清楚的。实际存储的是 skynet 服务的 id。之所以用 opaque 来命名,就是想传达这么一种设计理念,网络模块跟 skynet 服务机制是完全解耦的。网络模块不需要了解 opaque 具体存放的内容的用法,只是相当于个外部透传,在适当时机再传递给外部使用的自定义数据。

  • struct wb_list high; struct wb_list low;

    在这里插入图片描述
    这两条链表存放的都是待发送的消息,high 和 low 的区别是优先级。优先发送 high 链表中的消息,直到 high 链表中的消息全部发送完成,才会发送 low 链表中的消息。一条消息可能需要发送多次才能全部发送完,这条消息未发送完成的状态下一定是处于 high 链表的头,如果它本来是在 low 链表中,也会因此而上升转移到 high 链表中。
    在这里插入图片描述

  • ATOM_ULONG sending;

    记录已经由外部(通常是某个服务,线程是 worker 线程)发出,还未被会话对象接收到待发送列表中的消息数量。外部服务通过管道消息与网络线程的会话管理器通信。

  • int fd;

    套接字 ID

  • int id;

    会话 ID,同时是会话管理器分配的会话对象池的数组索引。总共支持 65535 个会话,当然,包括了监听套接字对象在内。

  • ATOM_INT type;

    既标识了 socket 的用途,也标识了 socket 的状态。
    在这里插入图片描述

  • uint8_t protocol;

    标识协议类型。
    在这里插入图片描述

  • bool reading;

  • bool writing;

  • bool closing;

    这三个变量都是 bool 类型,reading 和 writing 标识会话是否接收读事件和写事件,也即是是否注册读或写监听到 epoll 对象中。closing 为 true 是一个很特殊的状态,简单来说就是处于一个半关闭状态,不会再从 socket 读取数据,但是可以往对方发送数据(有可能发送失败),socket 在没有数据需要发送之后会从半关闭转换到完全关闭,然后清理数据。

会话管理器:持有并管理会话池,给外部模块提供网络接口。

struct socket_server {
        volatile uint64_t time;
        int reserve_fd; // for EMFILE
        int recvctrl_fd;
        int sendctrl_fd;
        int checkctrl;
        poll_fd event_fd;
        ATOM_INT alloc_id;
        int event_n;
        int event_index;
        struct socket_object_interface soi;
        struct event ev[MAX_EVENT];
        struct socket slot[MAX_SOCKET];
        char buffer[MAX_INFO];
        uint8_t udpbuffer[MAX_UDP_PACKAGE];
        fd_set rfds;
};

核心字段:

  • int reserve_fd;

    这个字段是为了解决接入连接时文件描述符不够用的情况下,可以有效的通知到客户端。初始化管理器时,用该变量存放标准输出文件描述符的副本,它同样指向标准输出,但是关闭它不会影响到实际的标准输出描述符状态。当 accept() 失败,错误码是 EMFILE 或者 ENFILE 时,skynet 会先 close 掉这个描述符以空出一个描述符的空间,然后立即重新调用 accept(),如果正确接入连接,需要立即关闭它(达到了通知对端连接不可用的目的),然后重新调用 dup(1) 继续保留标准输出描述符的副本。初始化和实际使用的代码如下:
    在这里插入图片描述
    在这里插入图片描述
    这里有个疑问,dup(1) 复制出来的描述符,是否占据进程可打开的描述符数量呢?如果不占据,则即使 close 掉保留的描述符,也不能空出空间来接入连接。经过测试发现 dup(1) 复制出来的描述符是会占用可打开的描述符数量的。测试代码和结果如下:
    在这里插入图片描述

  • int recvctrl_fd;

  • int sendctrl_fd;

  • int checkctrl;

  • fd_set rfds;

    这一组变量是用于外部工作线程往网络线程发送控制消息用。通过创建两个管道套接字,一个用于接收控制消息,一个用于发送控制消息。需要注意的是,管道套接字的读写都是原子性的,所以有如下代码片段:
    在这里插入图片描述

  • poll_fd event_fd;

  • int event_n;

  • int event_index;

  • struct event ev[MAX_EVENT]

    epoll 或 kqueue 对象句柄,触发的事件集合、数量、当前处理到第几个事件的索引。

  • ATOM_INT alloc_id;

    用于会话 id 分配策略,记录上一个分配出去的会话 id。分配下一个时,自增。值得注意的是,通常分配 id 这一操作是在网络线程之外进行的,避免多个外部线程的竞争,用了原子类型的变量和原子操作。
    在这里插入图片描述

  • struct socket_object_interface soi;

    在这里插入图片描述
    针对待发送的 buffer 的抽象接口,自定义从一块内存获取待发送数据的接口。buffer() 获取发送数据的起始地址,size() 获取待发送数据的长度,free() 作为待发送数据的释放接口,在数据发送失败或者发送完成的情况下会进行调用。初衷应该是用于 lua 的 lightuserdata 数据的传递抽象出来的消息构建接口对象,在代码中没有搜到实际设置 soi 的地方。
    在这里插入图片描述

  • struct socket slot[MAX_SOCKET];

    会话池(连接池)。存放所有 socket 对象。

网络模块管理

在 skynet_socket.h/c 文件中,skynet 实现了一系列网络接口的封装,提供给框架中其他模块使用。主要有下面几部分:

会话管理器的生命周期管理

  • skynet_socket_init

    分配内存,初始化会话管理器,在 skynet 中,会话管理器是单例存在。
    在这里插入图片描述

  • skynet_socket_exit

    发送控制消息给网路线程,停止工作。

  • skynet_socket_free

    释放会话管理器的内存。

  • skynet_socket_updatetime

    对时。

工作模式

  • 提供的接口如下图:
    在这里插入图片描述
  • 这些接口的调用者通常为非网络线程,利用几个关键的原子变量,允许多个外部线程同时调用且能保证线程安全。
  • 网络线程尽量精简,只做必要的事情,轮询事件、建立连接、接纳连接、维护连接、收包、发包。其他的,例如会话 id 的分配、监听套接字的初始化都由外部线程自己预先处理,然后通过控制消息通知到网络线程,控制消息如下:
    在这里插入图片描述

总结技术点

原子数据

  • 针对 c11 标准和非 c11 标准对这套原子操作有不同的宏定义方案,详情参考 atomic.h。
    在这里插入图片描述
  • 部分变量的原子性,使得外部线程可以直接处理部分事务。合理的划分线程职责,有效的降低网络线程的压力。

管道描述符

  • 外部模块对网络线程的访问通过管道消息来跨线程实现交互。
  • 内核保证原子性读写其内容。

自定义锁

  • 锁的应用只在发送消息时,外部线程可以先试图直接往套接字写入数据,这里可能会和网络线程往套接字写入数据产生冲突,所以需要加锁。
  • 封装了自旋锁的调用,添加了加锁计数,避免在同一流程的多个函数中反复加锁造成死锁。
    在这里插入图片描述

epoll

  • 对 io 事件通知模块进行抽象,实现了 epoll 和 kqueue 两种具体的 io 机制的封装,提高了代码的可移植性。

halfclose 状态

  • 半关闭状态使得关闭连接的流程更优雅,处理更完善。

SO_REUSEADDR

  • for TIME_WAIT。

dup(1)

  • 保留一个套接字空位的做法,优雅地解决了 accept() 时出现 EMFILE 和 ENFILE 错误,确保有效的通知对端。

opaque

  • 清晰的传达作者的设计理念,合理的规定模块职责,依赖关系。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/866692.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

漫话拥塞控制:BBR 是个单流模型

概要(便于检索主题)&#xff1a;单流&#xff0c;多流收敛&#xff0c;probe buffer 挤压带宽&#xff0c;maxbw-filter wnd。 我曾经经常说 BBR 是个单流模型&#xff0c;而不是多流收敛模型&#xff0c;也做过不少评论&#xff0c;最近在复听 IETF 的大会&#xff0c;在 IET…

SQL | 检索数据

1-检索数据 1.1-检索单个列 SELECT prod_name FROM Products; 上述SELECT语句从Products表中检索一个名为prod_name的列。 所要查找的列在select后面&#xff0c;from关键字指出从那个表查询数据。 输出如下&#xff1a; prod_name8 inch teddy bear12 inch teddy bear18…

linux鲁班猫代码初尝试[编译镜像][修改根文件系统重编译][修改设备树改屏幕为MIPI]

编译镜像 官方百度云盘资料:https://doc.embedfire.com/linux/rk356x/quick_start/zh/latest/quick_start/baidu_cloud/baidu_cloud.html 解压虚拟机压缩包:"鲁班猫\8-SDK源码压缩包\开发环境虚拟机镜像\ubuntu20.04.7z"后既可以用VMware打开,打开后可以看到已经有…

【前端】JQ生成二维码

提供两种方法&#xff0c;两种都是借助JQ插件生成。 所需文件&#xff1a;https://download.csdn.net/download/qq_25285531/88204985https://download.csdn.net/download/qq_25285531/88204985 方法一&#xff1a; <script type"text/javascript" src"/s…

【JavaEE基础学习打卡02】是时候了解JavaEE了

目录 前言一、为什么要学习JavaEE二、JavaEE规范介绍1.什么是规范&#xff1f;2.什么是JavaEE规范&#xff1f;3.JavaEE版本 三、JavaEE应用程序模型1.模型前置说明2.模型具体说明 总结 前言 &#x1f4dc; 本系列教程适用于JavaWeb初学者、爱好者&#xff0c;小白白。我们的天…

【刷题笔记8.11】LeetCode题目:二叉树中序遍历、前序遍历、后序遍历

LeetCode题目&#xff1a;二叉树中序遍历、前序遍历、后序遍历 题目1&#xff1a;二叉树中序遍历 &#xff08;一&#xff09;题目描述 给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 &#xff08;二&#xff09;分析 二叉树中序遍历&#xff0c;遍历…

6-Ngnix配置反向代理

1.前提 虚拟机能连接外网 仿真http应用需在本虚拟机启用(原因&#xff1a;只有一台虚拟机做测试) http_8080和http_8081要启用&#xff08;http测试应用&#xff09; [rootcent79-2 ~]# ls -l http_* -rwxr-xr-x 1 root root 6391676 Jul 19 13:39 http_8080 -rwxr-xr-x 1 …

【C# Programming】C#第一课(自己学习的笔记)

目录 一、C# 介绍 1.1 托管代码(Manage Code ) &#xff1a; 1.2 基础类型库 (Base Class Library)&#xff1a; 1.3 程序集(Assembly)&#xff1a; 1.4 .NET 框架&#xff1a; 1.5 公共中间语言(Common Intermediate Language)&#xff0c;简称 IL。 1.6 C#编译器将源代…

RISC-V在快速发展的处理器生态系统中找到立足点

原文&#xff1a;RISC-V Finds Its Foothold in a Rapidly Evolving Processor Ecosystem 作者&#xff1a;Agam Shah 转载自&#xff1a;https://thenewstack.io/risc-v-finds-its-foothold-in-a-rapidly-evolving-processor-ecosystem/ 以下是正文 But the open source pr…

【用unity实现100个游戏之6】制作一个战旗自走棋类游戏(附源码)

文章目录 前言导入素材开始1. 设置瓦片间隙2. 放置全图瓦片3. 美化瓦片地图4. 添加树木障碍物5. 设定不同的排序图层6. 瓦片交互6. 瓦片交互优化6. 瓦片是否允许角色7. 添加角色8. 新增游戏管理脚本9. 角色移动范围逻辑10. 角色移动范围可视化11. 角色移动12. 重置瓦片颜色12. …

Spark(38):Streaming DataFrame 和 Streaming DataSet 转换

目录 0. 相关文章链接 1. 基本操作 1.1. 弱类型 api 1.2. 强类型 1.3. 直接执行 sql 2. 基于 event-time 的窗口操作 2.1. event-time 窗口理解 2.2. event-time 窗口生成规则 3. 基于 Watermark 处理延迟数据 3.1. 什么是 Watermark 机制 3.2. update 模式下使用 w…

【计算机视觉|生成对抗】条件生成对抗网络(CGAN)

本系列博文为深度学习/计算机视觉论文笔记&#xff0c;转载请注明出处 标题&#xff1a;Conditional Generative Adversarial Nets 链接&#xff1a;[1411.1784] Conditional Generative Adversarial Nets (arxiv.org) 摘要 生成对抗网络&#xff08;Generative Adversarial…

04_Hudi 集成 Spark、保存数据至Hudi、集成Hive查询、MergeInto 语句

本文来自"黑马程序员"hudi课程 4.第四章 Hudi 集成 Spark 4.1 环境准备 4.1.1 安装MySQL 5.7.31 4.1.2 安装Hive 2.1 4.1.3 安装Zookeeper 3.4.6 4.1.4 安装Kafka 2.4.1 4.2 滴滴运营分析 4.2.1 需求说明 4.2.2 环境准备 4.2.2.1 工具类SparkUtils 4.2.2.2 日期转换…

读《Flask Web开发实战》(狼书)笔记 | 第1、2章

前言 2023-8-11 以前对网站开发萌生了想法&#xff0c;又有些急于求成&#xff0c;在B站照着视频敲了一个基于flask的博客系统。但对于程序的代码难免有些囫囵吞枣&#xff0c;存在许多模糊或不太理解的地方&#xff0c;只会照葫芦画瓢。 而当自己想开发一个什么网站的时&…

限流在不同场景的最佳实践

目录导读 限流在不同场景的最佳实践1. 前言2. 为什么要限流3. 有哪些限流场景3.1 限流场景分类3.2 限流与熔断降级之间的关系3.3 非业务限流3.4 业务限流 4. 有哪些限流算法4.1 计数器限流算法4.2 漏桶限流算法4.3 令牌桶限流算法4.4 滑动时间窗限流算法4.5 限流算法选型 5. 限…

【数据结构与算法】稀疏数组

文章目录 一&#xff1a;为什么会使用稀疏数组1.1 先看一个实际的需求1.2 基本介绍1.2.1 稀疏数组的处理方法1.2.2 数组的举例说明1.2.3 应用实例1.2.4 整体思路分析二维数组转稀疏数组的思路稀疏数组转原始的二维数组的思路 二&#xff1a;代码实现2.1 创建一个原始的11*11二维…

​LeetCode解法汇总1572. 矩阵对角线元素的和

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 描述&#xff1a; 给你一个正…

探秘金和OA:解析任意文件读取漏洞的潜在威胁

是喜是悲&#xff0c;但可以慰藉的是&#xff0c;你总不枉在这世界上活了一场&#xff0c;有了这样的认识&#xff0c;你就会珍重生活&#xff0c;而不会玩世不恭&#xff1b;同时也会给人自身注入一种强大的内在力量…… 漏洞复现 访问url&#xff1a; 构造payload /C6/Jh…

【网络编程(二)】NIO快速入门

NIO Java NIO 三大核心组件 Buffer&#xff08;缓冲区&#xff09;&#xff1a;每个客户端连接都会对应一个Buffer&#xff0c;读写数据通过缓冲区读写。Channel&#xff08;通道&#xff09;&#xff1a;每个channel用于连接Buffer和Selector&#xff0c;通道可以进行双向读…

日常问题——使用Java转将long类型为date类型,日期是1970年

&#x1f61c;作 者&#xff1a;是江迪呀✒️本文关键词&#xff1a;日常BUG、BUG、问题分析☀️每日 一言 &#xff1a;存在错误说明你在进步&#xff01; 一、问题描述 long类型的日期为&#xff1a;1646718195 装换为date类型&#xff1a; Date date new Dat…