skynet 网络模块解析

文章目录

  • 前言
  • 环境准备
  • 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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

opencv实战项目 手势识别-手势控制键盘

手势识别是一种人机交互技术&#xff0c;通过识别人的手势动作&#xff0c;从而实现对计算机、智能手机、智能电视等设备的操作和控制。 1. opencv实现手部追踪&#xff08;定位手部关键点&#xff09; 2.opencv实战项目 实现手势跟踪并返回位置信息&#xff08;封装调用&am…

计算机竞赛 - 基于机器视觉的图像拼接算法

前言 图像拼接在实际的应用场景很广&#xff0c;比如无人机航拍&#xff0c;遥感图像等等&#xff0c;图像拼接是进一步做图像理解基础步骤&#xff0c;拼接效果的好坏直接影响接下来的工作&#xff0c;所以一个好的图像拼接算法非常重要。 再举一个身边的例子吧&#xff0c;…

Java基础入门篇——修饰符

在Java中&#xff0c;修饰符&#xff08;Modifiers&#xff09;是一种用于修改类、方法、变量和其他实体的访问权限、行为或特性的关键字。Java提供了一组修饰符&#xff0c;可以用于实现对代码的封装、继承、多态和访问控制等功能。 1、访问修饰符&#xff08;Access Modifie…

【Spring Boot】夺名连环问(持续更新ing)

Spring的了解与特性 简单介绍&#xff1a;快速开发Spring项目的脚手架。简化Spring应用的初始搭建以及开发过程。 特性 提供了很多内置的Starter结合自动配置&#xff0c;对主流框架的无配置集成、开箱即用。即不需要自己去引入很多依赖。 并且管理了常用的第三方依赖的版本&…

Node.js |(二)Node.js API:fs模块 | 尚硅谷2023版Node.js零基础视频教程

学习视频&#xff1a;尚硅谷2023版Node.js零基础视频教程&#xff0c;nodejs新手到高手 文章目录 &#x1f4da;文件写入&#x1f407;writeFile 异步写入&#x1f407;writeFileSync 同步写入&#x1f407;appendFile / appendFileSync 追加写入&#x1f407;createWriteStrea…

LeetCode150道面试经典题--验证回文串(简单)

1.题目 如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后&#xff0c;短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。 字母和数字都属于字母数字字符。 给你一个字符串 s&#xff0c;如果它是 回文串 &#xff0c;返回 true &#xff1b;否…

pve组网实现公网访问pve,访问电脑,访问pve中的openwrt同时经过openwrt穿透主路由地址nginx全公网访问最佳办法测试研究...

一台路由器 做主路由 工控机 装pve虚拟机 虚拟机里面装一个openwrt, 外网可以直接访问pve,可以访问pve里的openwrt 一台主机 可选择连 有4个口&#xff0c;分别eth0,eth1,eth2,eth3 pve有管理口 这个情况下 &#xff0c;没有openwrt 直接电脑和pve管理口连在一起就能进pve管理界…

TCP协议的报头格式和滑动窗口

文章目录 TCP报头格式端口号序号和确认序号确认应答&#xff08;ACK&#xff09;机制超时重传机制 首部长度窗口大小报文类型URGACKSYNPSHFINRST 滑动窗口滑动窗口的大小怎么设定怎么变化滑动窗口变化问题 TCP报头格式 端口号 两个端口号比较好理解&#xff0c;通过端口号来找…

el-tree-select那些事

下拉菜单树形选择器 用于记录工作及日常学习涉及到的一些需求和问题 vue3 el-tree-select那些事 1、获取el-tree-select选中的任意层级的节点对象 1、获取el-tree-select选中的任意层级的节点对象 1-1数据集 1-2画面 1-3代码 1-3-1画面代码 <el-tree-selectv-model"s…

ELK常见部署架构以及出现的问题及解决方案

ELK常见部署架构以及出现的问题及解决方案 ELK 已经成为目前最流行的集中式日志解决方案&#xff0c;它主要是由Beats 、Logstash 、Elasticsearch 、 Kibana 等组件组成&#xff0c;来共同完成实时日志的收集&#xff0c;存储&#xff0c;展示等一站式的解决方案。本文将会介…

Stable Diffusion - 哥特 (Goth) 风格服装与背景的 LoRA 配置

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/132177882 图像来源于 Goth Clothing 的 LoRA 效果&#xff0c;配合哥特 (Goth) 风格服饰的相关提示词。 测试模型&#xff1a;DreamShaper 8 哥…

STM32CubeMX之freeRTOS互斥量

这是大哥保护小弟的故事 高中低等级的任务 互斥量就是谁要敢插我小弟的队&#xff0c;我就要打他&#xff0c;不能让其他人插我小弟的队 互斥量的使用是默认开启的不用手动开启&#xff01; 最高优先级任务&#xff1a;延时&#xff08;10ms&#xff09;再上厕所 中间&#x…

【网络编程·传输层】UDP和TCP的经典八股文

目录 一、端口号划分 二、部分指令 1、pidof&#xff08;用于查看进程id&#xff09; 2、netstat&#xff08;查看网络状态&#xff09; 三、UDP协议 1、UDP协议格式 2、UDP协议如何进行封装、解包、分用 2.1封装、解包 2.2分用 3、UDP协议的特点 3.1UDP协议的特点 …

【百度翻译api】中文自动翻译为英文

欸&#xff0c;最近想做一些nlp的项目&#xff0c;做完了中文的想做做英文的&#xff0c;但是呢&#xff0c;国内爬虫爬取的肯定都是中文 &#xff0c;爬取外网的技术我没有尝试过&#xff0c;没有把握。所以我决定启用翻译&#xff0c;在这期间chatGPT给了我非常多的方法&…

hive修改表或者删除表时卡死问题的解决(2023-08-08)

背景&#xff1a;前阶段在做hive表的改表名时&#xff0c;总是超时&#xff0c;表是内部表&#xff0c;数据量特别大&#xff0c;无论你是修改表名还是删除表都是卡死的状态&#xff0c;怎么破&#xff1f; 终于&#xff1a;尝试出来一个新的方法 将内部表转化成外部表&#…

24.Netty源码之合理管理堆内存

highlight: arduino-light 合理管理 Netty 堆外内存 内存使用目标 •内存占用少(空间) •应用速度快(时间) 即多快好省 对 Java 而言&#xff1a;减少 Full GC 的 STW(Stop the world)时间 内存使用技巧 • 减少对象本身大小 md 例 1&#xff1a;用基本类型就不要用包装类。 例…

嵌入式虚拟仿真实验教学平台之串口发送数据

嵌入式虚拟仿真实验教学平台课程系列 串口发送数据实验 课程内容 本实验使用 STM32 的串口发送数据。开始仿真后,打开串口监视器&#xff0c;串口监视器会打印出要发送的数据。 课程目标 学习配置使用GPIO功能学习配置使用复用功能学习配置使用UART功能 硬件设计 本课程…

css小练习:案例6.炫彩加载

一.效果浏览图 二.实现思路 html部分 HTML 写了一个加载动画效果&#xff0c;使用了一个包含多个 <span> 元素的 <div> 元素&#xff0c;并为每个 <span> 元素设置了一个自定义属性 --i。 这段代码创建了一个简单的动态加载动画&#xff0c;由20个垂直排列的…

allegro中不可选时,如何对find进行可选操作

allegro出现不可选时&#xff0c;只能尝试其他单一的操作&#xff0c;但这样效率不高&#xff1b;可以通过菜单栏Display下拉菜单点击Element&#xff0c;即可实现FIND下选择需要调整的选项。

selenium爬虫与配置谷歌浏览器的driver问题

用selenium爬虫时&#xff0c;明明已经安装了selenium模块&#xff0c;程序却运行不了。在使用selenium之前必须先配置浏览器对应版本的webdriver 本文主要涉及驱动有问题driver 网上有很多手动的方法&#xff08;查看谷歌浏览的版本然后在其他博主分享的webdriver中下载与自己…