skynet热更新之inject

news2024/9/18 18:56:47

游戏服务器的热更新是一种常见的需求,skynet可以通过inject的方式,来修改一个服务的消息处理函数,达到热更新的效果。

skynet内置服务debug_console

skynet自带了一个调试控制台服务。inject注入代码需要先启动这个服务。

skynet.newservice("debug_console", "127.0.0.1", "9666")

启动之后,我们可以用telnet或者nc等指令来登录调试控制台。

> nc 127.0.0.1 9666

输入list指令,可以得到当前系统中所有服务的地址:

list
:00000004       snlua cdummy
:00000006       snlua datacenterd
:00000007       snlua service_mgr
:00000008       snlua main
:00000009       snlua debug_console 127.0.0.1 9666
:0000000a       snlua serviceA
<CMD OK>

输入inject指令,我们可以将某个代码文件,注入到指定的服务中:

inject :0000000a service/hotfix.lua

<CMD OK>

更多的debug_console指令可以参考这里

inject实例

我们在系统启动时,打开debug_console,然后启动服务serviceA,接着设置每隔5秒给serviceA发送两个lua消息,一个参数bar,一个参数foo,代码如下:

--main.lua
local skynet = require "skynet"
skynet.start(function()
    skynet.newservice("debug_console", "127.0.0.1", "9666")
    local addr = skynet.newservice("serviceA")
    
    local function tick()
        skynet.send(addr, "lua", "foo")
        skynet.send(addr, "lua", "bar")
        skynet.timeout(500, tick)
    end

    skynet.timeout(500, tick)
end)

在服务serverA中,我们根据参数,调用不同的处理函数:

--serviceA.lua
local skynet = require "skynet"
local handles = {}

handles.foo = function()
    print("foo")
end

skynet.start(function()
    skynet.dispatch("lua", function(session, source, cmd, ...)
        local handle = handles[cmd]
        if handle then
            handle()
        else
            print("cmd not found", cmd)
        end
    end)
end)

现在我们启动skynet,可以看到每隔5秒输出:

foo
cmd not found   bar

现在我们新建一个文件hotfix.lua

--hotfix.lua
local handles = _P.lua.handles
local print = _G.print
handles.foo = function()
    print("foo after hotfix")
end

handles.bar = function()
    print("bar after hotfix")
end

接下来连接到控制台,并输入inject指令:

echo 'inject :0000000a services/hotfix.lua' | nc 127.0.0.1 9666

等到下次输出的时候,我们看到的就是:

foo after hotfix
bar after hotfix

更新完成,修改了foo函数,新增了bar函数。

使用inject调用hotfix.lua时,print函数是被修改过成debug_console的返回输出函数,所以如果要用到print的话,需要使用全局变量_G.print

对upValue的处理

如果我们的serviceA是这样的:

--serviceA.lua
local skynet = require "skynet"
local handles = {}

local N = 1
local T = {
    count = 0,
}

handles.foo = function()
    N = N + 2
    T.count = T.count + 1
    print("foo", N, T.count)
end

handles.bar = function()
    N = N - 1
    print("bar", N)
end

skynet.start(function()
    skynet.dispatch("lua", function(session, source, cmd, ...)
        local handle = handles[cmd]
        if handle then
            handle()
        else
            print("cmd not found", cmd)
        end
    end)
end)

foo函数带有两个upValue: NTbar函数带有一个upValue: N 如果hotfix.lua文件没有做特殊处理,直接覆盖函数的话,那么就会丢失这些upValue。那么,要怎么处理这些upValue呢?这里需要用到luadebug库,主要是两个函数:

  • debug.getupvalue(f, i): 获取函数f中的第iupValue的变量名和值。
  • debug.upvaluejoin(f1, i, f2, j):让函数f1的第iupValue引用f2中的第jupValue

热更新带有upValue的函数,我们的hotfix.lua分三步走:

  1. 定义一个函数get_up,来获取原有的函数的upValue列表。
  2. 定义新的处理函数。
  3. 定义一个函数uv_join,将新函数的upValue和旧函数的upValue绑定起来。

完整代码如下:

local handles = _P.lua.handles
local print = _G.print

local function get_up(f)
    local u = {}
    if not f then
        return u
    end
    local i = 1
    while true do
        local name = debug.getupvalue(f, i)
        if name == nil then
            return u
        end
        u[name] = i
        i = i + 1
    end
    return u
end

local function uv_join(f, old_f, old_uv)
    local i = 1
    while true do
        local name = debug.getupvalue(f, i)
        if not name then
            break
        end

        if old_uv[name] then
            debug.upvaluejoin(f, i, old_f, old_uv[name])
        end
        i = i + 1
    end
end

local foo = handles.foo
local up = get_up(foo)

local N, T      --定义两个upValue,否则函数里会变成全局变量
handles.foo = function()
    N = N + 200
    T.count = T.count + 100
    print("foo", N, T.count)
end
uv_join(handles.foo, foo, up)

这里的get_up函数只取了传入函数的upValue,如果要嵌套处理函数中的函数,可以参考lualib/skynet/inject.lua中的getupvaluetable函数。

inject实现原理

debug_console服务的代码位于service/debug_console.lua文件中,其对inject指令的处理,其实就是发送一条debug类型的消息到目标服务:

--debug_console.lua
function COMMAND.inject(address, filename, ...)
	address = adjust_address(address)
	local f = io.open(filename, "rb")
	if not f then
		return "Can't open " .. filename
	end
	local source = f:read "*a"
	f:close()
	local ok, output = skynet.call(address, "debug", "RUN", source, filename, ...)
	if ok == false then
		error(output)
	end
	return output
end

在我们的服务中,当我们require 'skynet'的时候,会自动注册debug消息类型的处理:

--lualib/skynet.lua
-- Inject internal debug framework
local debug = require "skynet.debug"
debug.init(skynet, {
	dispatch = skynet.dispatch_message,
	suspend = suspend,
	resume = coroutine_resume,
})
--lualib/skynet/debug.lua
    skynet.register_protocol {
		name = "debug",
		id = assert(skynet.PTYPE_DEBUG),
		pack = assert(skynet.pack),
		unpack = assert(skynet.unpack),
		dispatch = _debug_dispatch,
	}

其中,参数RUN是这样处理的

--lualib/skynet/debug.lua
function dbgcmd.RUN(source, filename, ...)
    local inject = require "skynet.inject"
    local args = table.pack(...)
    local ok, output = inject(skynet, source, filename, args, export.dispatch, skynet.register_protocol)
    collectgarbage "collect"
    skynet.ret(skynet.pack(ok, table.concat(output, "\n")))
end

追溯代码,来到最终的inject函数:
在这里插入图片描述

  1. 修改print函数,可以返回输出内容给debug_console服务。
  2. 在上一层调用的时候,传进来的...实际上两个函数skynet.dispatch_messageskynet.register_protocol,这里将这两个函数,以及函数中包含的子函数,所用到的upVluae都收集起来,存入表u中。
  3. protoskynet.register_protocol中用到的一个upValue,存放着当前服务所注册的消息类型。遍历proto,将每个消息的处理函数用到的upValue收集起来,存放到表p中。
  4. 设置环境,调用传入的热更新文件。现在我们知道,在上面的例子中的hotfix.lua,用到的_P,就是存放各种消息类型的处理函数的upValue表。

在控制台调用inject指令时,还可以传入额外的参数,例如:inject :0000000a services/hotfix.lua xxx yyy,最终这两个参数,就是这里的inject函数中的第四个参数args,可以在hotfix.lua中直接使用这两个参数。

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

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

相关文章

linux自动化构建工具--make/makefile

目录 1.make/makefile介绍 1.1基本认识 1.2依赖关系、依赖方法 1.3具体操作步骤 1.4进一步理解 1.5默认设置 1.6make二次使用的解释 1.7两个文件的时间问题 1.8总是被执行 1.9特殊符号介绍 1.make/makefile介绍 1.1基本认识 make是一个指令&#xff0c;makefile是一…

Dify中语音和文字间转换问题的一种暂时注释方式

本文主要解释了Dify中语音和文字间转换可能会遇到的问题&#xff0c;并给出了一种暂时注释的解决方案。 一.文本转语音可能问题 本地部署文本转语音时&#xff0c;如果遇到如下问题&#xff0c;安装ffmpeg即可。但是如果安装后&#xff0c;重启系统还是遇到这个问题该如何办&…

02 Golang面向对象编程_20240727 课程笔记

视频课程 最近发现越来越多的公司在用Golang了&#xff0c;所以精心整理了一套视频教程给大家&#xff0c;这个是其中的第二部&#xff0c;后续还会有很多。 视频已经录制完成&#xff0c;完整目录截图如下&#xff1a; 课程目录 01 结构体的声明.mp402 使用var根据结构体…

Firefox扩展程序和Java程序通信

实现Firefox扩展程序&#xff0c;和Java RMI Client端进行通信。 在Firefox工具栏注册按钮&#xff0c;点击按钮后弹出Popup.html页面&#xff0c;引用Popup.js脚本&#xff0c;通过脚本向Java RMI client发送消息&#xff0c;Java RMI Client接收消息后转发到Java RMI Server…

Docker————数据卷容器,容器互联,镜像创建

1、Docker的数据管理 管理Docker容器中的数据&#xff0c;主要有两种方式&#xff1a;数据卷&#xff08;Data Volumes&#xff09;和数据卷容器&#xff08;DataVolumes Containers&#xff09;. docker run [-i -t] [--name 容器名] 镜像名&#xff1a;标签 [容器启动命令]…

RK3568 Linux 平台开发系列讲解(内核入门篇):从内核的角度看外设芯片的驱动

在嵌入式 Linux 开发中,外设芯片的驱动是实现操作系统与硬件之间交互的关键环节。对于 RK3568 这样的处理器平台,理解如何从内核的角度构建和管理外设芯片的驱动程序至关重要。 1. 外设驱动的基础概念 外设驱动(Device Driver)是操作系统与硬件设备之间的桥梁。它负责控…

智能浇花机器人·设计说明

智能浇花机器人 目录&#xff1a; 第一章 :项目描述 1 1.1 项目简介 1 1.1.1 服务范围&#xff1a; 1 1.1.2 所处行业&#xff1a; 1 1.2 项目背景 1 1.3 创新点与项目特色 3 第二章 :设计说明书 4 2.1 主要构成&#xff1a; 4 2.1.1 循迹小车 4 2.1.2 机械…

网络编程——wireshark抓包、tcp粘包

目录 一、前言 1.1 什么是粘包 1.2 为什么UDP不会粘包 二、编写程序 文件树 客户端程序 服务器程序 tcp程序 头文件 makefile 三、 实验现象 四、改进实验 五、小作业 一、前言 最近在做网络芯片的驱动&#xff0c;验证功能的时候需要借助wireshark这个工具&…

DataX(二):DataX安装与入门

1. 官方地址 下载地址&#xff1a;http://datax-opensource.oss-cn-hangzhou.aliyuncs.com/datax.tar.gz 源码地址&#xff1a;GitHub - alibaba/DataX: DataX是阿里云DataWorks数据集成的开源版本。 2. 前置要求 Linux JDK(1.8 以上&#xff0c;推荐 1.8) Python(推荐 Pyt…

C语言内存函数精讲

目录 引言 1.内存分配函数malloc 2.内存释放函数free 3.内存拷贝函数memcpy 4.内存移动函数memmove 5.内存设置函数memset 6.内存比较函数memcmp 总结 引言 在C语言编程中&#xff0c;内存管理是核心技能之一。C语言提供了一系列内存操作函数&#xff0c;这些函数在动…

机器学习 第7章-贝叶斯分类器

机器学习 第7章-贝叶斯分类器 7.1 贝叶斯决策论 贝叶斯决策论(Bayesian decision theory)是概率框架下实施决策的基本方法。对分类任务来说&#xff0c;在所有相关概率都已知的理想情形下&#xff0c;贝叶斯决策论考虑如何基于这些概率和误判损失来选择最优的类别标记。下面我…

Linux Vim全能攻略:实战代码,轻松掌握文本编辑神器

1. Vim简介与安装 1.1 Vim的历史与发展 Vim&#xff08;Vi IMproved&#xff09;是一款高度可配置的文本编辑器&#xff0c;它起源于1976年由Bill Joy开发的Vi编辑器。Vi是Unix系统上最古老的文本编辑器之一&#xff0c;因其强大的功能和高效的编辑方式而广受欢迎。随着时间的…

流媒体服务器一:搭建RTMP流媒体服务器搭建

1 安装和测试srs流媒体服务器 服务器&#xff1a;SRS(Simple RTMP Server&#xff0c;⽀持RTMP、HTTP-FLV&#xff0c;HLS) 推流端&#xff1a;ffmpeg OBS 拉流端&#xff1a;ffplay VLC srs播放器 1.1 安装srs流媒体服务器 官网 SRS (Simple Realtime Server) | SRS 码…

【一图流】Git下载与安装教程

下载Git Git官网&#xff1a;https://git-scm.com/?hlzh-cn 安装Git

全栈嵌入式C++、STM32、Modbus、FreeRTOS和MQTT协议:工业物联网(IIoT)可视化系统设计思路(附部分代码解析)

项目概述 随着工业4.0时代的到来&#xff0c;工业物联网&#xff08;IIoT&#xff09;在提高生产效率、降低运营成本和实现智能制造方面得到了广泛应用。本项目旨在开发一个全面的工业物联网监控系统&#xff0c;能够实时监测设备的温度、压力、振动和电流等参数&#xff0c;并…

浅析Jeecgboot中mybatisplus不支持Postgres SKIP LOCKED语法问题

目录 1、场景及问题 2、数据库及各框架版本信息 3、错误回放 4、根因分析及确认 5、解决问题 6、总结 1、场景及问题 场景&#xff1a; 在调用腾讯位置服务时有用到key值&#xff0c;因为每个key值都有自己的额度&#xff0c;所以在表里存了多个key&#xff0c;简称key池&…

基于Java的城市公交管理系统/SSM的城市公交查询系统/计算机专业/课设

摘 要 网络技术的不断发展&#xff0c;使网络成为人们的日常生活中不可缺少的一部分&#xff0c;而城市公交管理系统是网络的一种新型体现&#xff0c;它以其特有的便捷和快速的特点得到了广泛的认可。当前的城市公交管理系统不仅没有建立起整体的管理系统&#xff0c;为企业定…

Go语言中常见的多线程同步方法

什么是线程、进程、协程 Go 源文件经过编译器处理后&#xff0c;会产生可执行文件&#xff0c;不同系统有不同的格式。可执行文件在操作系统上执行一次&#xff0c;就对应一个进程 进程可以理解为执行中的程序&#xff0c;是一个动态的概念&#xff0c;同一份可执行文件执行多…

Django 表单error_messages , 表单校验提示

在Django中&#xff0c;error_messages是表单字段的一个参数&#xff0c;允许你为特定的验证错误自定义错误消息。默认情况下&#xff0c;Django的表单字段会为常见的验证错误提供默认的错误消息。但是&#xff0c;你可能想要为你的应用提供更加用户友好的或者本地化的错误消息…

成为git砖家(2): gitk 介绍

大家好&#xff0c;我是白鱼。这篇我们介绍 gitk。 gitk 和 fork 界面对比 当我们在 macOS 上执行 brew install git 后&#xff0c; 得到了 git 命令行工具。 然而这条命令并不会安装 gitk. gitk 是 git 自带的图形化界面工具&#xff0c;也可以称为“穷人版 fork”&#xf…