skynet的消息发送:send和call

news2024/9/23 1:26:43

skynet是一个轻量级的游戏服务器框架。

skynet的核心是服务,服务之间通过消息来通信,消息的来源主要有:

  • 定时器
  • 网络
  • 服务之间的调用(skynet.sendskynet.call)

skynet.send和skynet.call

假设我们有两个服务A和B,A发了两条消息给B:
在这里插入图片描述

这里skynet.sendskynet.call的主要区别,在于call会阻塞,等待消息的返回值,而send将消息发送出去之后,就继续执行后续的指令。那么这里skynet.call之后,是怎么获取这个返回值的呢?我们来看看代码。

skynet.send的代码比较简单:

function skynet.send(addr, typename, ...)
	local p = proto[typename]
	return c.send(addr, p.id, 0, p.pack(...))
end

根据类型,将数据打包,然后调用底层的c.send将消息发送给目标地址。

再来看看skynet.call的代码:

function skynet.call(addr, typename, ...)
	--调试相关代码
	--...

	local p = proto[typename]
	local session = c.send(addr, p.id , nil , p.pack(...))
	if session == nil then
		error("call to invalid address " .. skynet.address(addr))
	end
	return p.unpack(yield_call(addr, session))
end

这里我们看到,skynet.callskynet.send在发送数据时,调用的底层函数是一样的,都是c.send,区别在于参数不同:

  • skynet.send调用c.send时,第三个参数是0,表示不用分配会话(session)
  • skynet.call调用c.send时,第三个参数是nil,表示需要分配会话ID(session)

这里的session,是系统一个自增的ID,每次分配时增加1,相当于给这一次的call分配一个唯一ID。

最后,skynet.call的返回是p.unpack(yield_call(addr, session))
p.unpack是解包数据,而yield_call,看名字就知道,是一个挂起的调用:

local function yield_call(service, session)
	watching_session[session] = service
	session_id_coroutine[session] = running_thread
	local succ, msg, sz = coroutine_yield "SUSPEND" 
	watching_session[session] = nil
	if not succ then
		error "call failed"
	end
	return msg,sz
end

这里,用到了刚刚分配的session,记录了session对应的服务地址执行协程,然后,调用coroutine_yield将线程挂起,参数是"SUSPEND",等到目标服务返回结果后,才重新回到这个协程。

处理消息并返回

服务A调用skynet.call发送消息给服务B之后,A的协程挂起了,收到消息的服务B,是怎么处理这个消息,并返回给服务A的呢?
skynet的体系中,每个服务都有一个消息处理函数。对于skynet的lua服务,在启动时,skynet.start的第一行代码,就是设置lua层面的回调函数:

function skynet.start(start_func)
	c.callback(skynet.dispatch_message)
	--...其他代码
end

skynet.dispatch_message中的第一句,则是以pcall的方式调用raw_dispatch_message,这个函数一共有5个参数:

  • ptototype: 消息类型
  • msg: 消息体
  • sz:消息长度
  • session:会话ID,使用send的话,则是0
  • source:消息来源的服务地址
local function raw_dispatch_message(prototype, msg, sz, session, source)
	-- skynet.PTYPE_RESPONSE = 1, read skynet.h
	if prototype == 1 then
		--...处理响应消息
	else
		local p = proto[prototype]
		if p == nil then
			--...错误处理
		end

		local f = p.dispatch
		if f then
			local co = co_create(f)                 -- 取得一个协程
			session_coroutine_id[co] = session      -- 并关联协程和会话
			session_coroutine_address[co] = source  -- 以及来源
			
            		--... trace调试相关代码
          
			suspend(co, coroutine_resume(co, session,source, p.unpack(msg,sz)))
		else
			--...错误处理
		end
	end
end

关键看这一句:suspend(co, coroutine_resume(co, session,source, p.unpack(msg,sz)))
从里向外,有三个函数调用:

  • p.unpack(msg, sz):根据消息类型预设好的unpack函数,来解析消息,返回解析后的参数。
  • coroutine_resume(co, session, source, …):执行协程,协程参数为session,source,以及解析后的参数。这里实际上就是执行到skynet.dispatch中设置的消息处理函数(上面示例代码中,serverB的函数f)。
  • suspend(co, …):处理完一条消息,挂起后的一些处理。

skynet.call的返回

从上面的消息处理来看,并没有对skynet.call做特别的处理,实际上,对于skynet.call的消息,我们必须手动调用skynet.retpack来返回数据。
通常,在消息处理函数中,我们可以通过session,来判断要不要使用skynet.retpack:

if session > 0 then
    skynet.retpack(func(...))
else
    func(...)
end

skynet.retpack实际上是对skynet.ret的调用:

在这里插入图片描述

  1. 前面收到消息时,记录了当前协程对应的session,这里取出session。
  2. 如果session等于0,表示是send的消息,不需要返回。
  3. 前面收到消息时,还记录当前协程对应的消息来源,这里,给来源地址source发送一个PTYPE_RESPONSE类型的消息,成功将数据返回。

上面这些返回的操作,是在服务B中,而在服务A中,就收到了一个PTYPE_RESPONSE消息。此时前面发送skynet.call时的协程co还处于挂起的状态。

前面讲到raw_dispatch_message的时候,略过了PTYPE_RESPONSE的处理,现在再来看一下:

在这里插入图片描述

  1. 通过session取得处理协程,在skynet.call => yield_call中,挂起之前,记录的session对应哪个协程,这里取回挂起的协程。
  2. RESPONSE并不只是skynet.ret才会用到,还有可能是skynet.timeout的定时时间到了,也会发送RESPONSE,这时co是一个字符串"BREAK"
  3. 收到一个未知的response的处理。
  4. 正常的skynet.call在这里获得返回值,这里的coroutine_resume,执行co协程,就是回到前面的yield_call

在这里插入图片描述

  1. 挂起的协程co恢复执行后,接收succ,msg,sz参数,最终yield_call返回的是msgsz
  2. yield_call的返回值,通过unpack解析之后,最终返回给调用者。至此,skynet.call终于取到了返回值。

Maybe forgot response session … from …

假设消息B在收到一个skynet.call的消息后,没有调用skynet.ret返回,那么会输出一个报错:Maybe forgot response session ... from ...skynet系统是怎么知道没有返回的呢?
前面在讲到消息处理raw_dispatch_message函数中,有一个步骤是从协程池中获取一个协程,并调用设置好的dispatch函数(示例中serviceB的函数f),实际上,这里并不是直接调用f,而是加了一层封装,我们来看看co_create的代码:
在这里插入图片描述

  1. 从池子里取出一条协程。
  2. 池子里没有协程时,创建协程。
  3. 协程的主函数,首先执行f(即传入的dispatch函数)。
  4. 执行完成之后,判断当前协程是否记录着session,当调用skynet.ret时,会清掉这个session。如果此时的session不等于0,就表示收到一个call之后没有使用skynet.ret返回,就在这里报个错。
  5. 清理数据。
  6. 将当前协程放入池子里,等待循环使用。
  7. 将协程挂起。
  8. 下一将调用co_create时,如果能从池子里找到co,则在这里开始执行协程,传入f,继续执行。

延迟返回

一般情况下,在处理call消息的协程中,我们必须调用skynet.retpack来返回数据,否则的话,会报错误Maybe forgot response
但有些情况下,我们希望在其他协程中返回数据(例如skynet.newservice 简介:服务的启动讲到的launch),这时候,我们可以使用skynet.response来生成一个响应函数。

function skynet.response(pack)
	pack = pack or skynet.pack

	local co_session = assert(session_coroutine_id[running_thread], "no session")
	session_coroutine_id[running_thread] = nil
	local co_address = session_coroutine_address[running_thread]
	if co_session == 0 then
		--  do not response when session == 0 (send)
		return function() end
	end
	local function response(ok, ...)
		if ok == "TEST" then
			return unresponse[response] ~= nil
		end
		if not pack then
			error "Can't response more than once"
		end

		local ret
		if unresponse[response] then
			if ok then
				ret = c.send(co_address, skynet.PTYPE_RESPONSE, co_session, pack(...))
				if ret == false then
					-- If the package is too large, returns false. so we should report error back
					c.send(co_address, skynet.PTYPE_ERROR, co_session, "")
				end
			else
				ret = c.send(co_address, skynet.PTYPE_ERROR, co_session, "")
			end
			unresponse[response] = nil
			ret = ret ~= nil
		else
			ret = false
		end
		pack = nil
		return ret
	end
	unresponse[response] = co_address

	return response
end

这里实际上就是把返回需要用到的sessionsource用作一个函数的upValue,并返回这个函数,同时,清除session_coroutine_id中当前co对应的session,这样就不会触发到Maybe forgot response的警告了。

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

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

相关文章

AI+服装电商细分赛道的落地应用:图应AI模特的进化史干货篇

文章目录 AI绘制人物的效果进化史2022年2023年2024年 摄影师、设计师、模特三方在AI商拍领域的位置国家统计局的一些服装行业数据遇到的一些问题以及相应的解决方案图应AI这个产品未来可能怎么走统一回答某些投资人的一个问题 AI绘制人物的效果进化史 2022年 还记得我2022年从…

Understanding the Overheads of Launching CUDA Kernels (理解启动 CUDA Kernels 的开销)

Understanding the Overheads of Launching CUDA Kernels {理解启动 CUDA Kernels 的开销} Understanding the Overheads of Launching CUDA Kernels1. INTRODUCTION2. MICRO-BENCHMARKS USED IN OUR STUDY3. OVERHEAD OF LAUNCHING KERNELS3.1. Experimental Environment3.2. …

【Validation + i18n】✈️运行时参数校验+国际化上下文实现自定义参数校验规则

目录 👋前言 👀一、环境准备 📫二、代码实现 2.1 Validation 自定义验证类 2.2 自定义注解代码实现 💞️三、测试 🌱四、章末 👋前言 小伙伴们大家好,最近在和一位读者讨论国际化上下文工具…

SpringBoot-01-全局异常处理器

在之前的项目中每一个异常的地方都要进行处理&#xff0c;十分的麻烦。 在springBoot项目中&#xff0c;提供了全局的异常处理器&#xff0c;可能出现异常的地方直接抛出即可。 RestControllerAdvice public class GlobalException {ExceptionHandlerpublic Result<String…

Golang | Leetcode Golang题解之第342题4的幂

题目&#xff1a; 题解&#xff1a; func isPowerOfFour(n int) bool {return n > 0 && n&(n-1) 0 && n%3 1 }

【电路笔记】-桥接 T 型衰减器

桥接 T 型衰减器 文章目录 桥接 T 型衰减器1、概述2、桥接 T 型衰减器示例 13、可变桥接 T 型衰减器4、完全可调衰减器5、可切换桥接 T 型衰减器Bridged-T 衰减器是另一种电阻衰减器设计,它是标准对称 T 垫衰减器的变体。 1、概述 顾名思义,桥接 T 形衰减器具有一个额外的电…

Chapter 39 Python多线程编程

欢迎大家订阅【Python从入门到精通】专栏&#xff0c;一起探索Python的无限可能&#xff01; 文章目录 前言一、并行执行二、threading模块 前言 现代操作系统如 macOS、UNIX、Linux 和 Windows 等&#xff0c;均支持多任务处理。本篇文章详细讲解了并行执行的概念以及如何在 …

苍穹外卖-day03(SpringBoot+SSM的企业级Java项目实战)

苍穹外卖-day03 课程内容 公共字段自动填充 新增菜品 菜品分页查询 删除菜品 修改菜品 功能实现&#xff1a;菜品管理 菜品管理效果图&#xff1a; 1. 公共字段自动填充 1.1 问题分析 在上一章节我们已经完成了后台系统的员工管理功能和菜品分类功能的开发&#xff0c…

本地ComfyUI安装全记录

资料 先看我写的stable diffusion全记录 ComfyUI 完全入门&#xff1a;安装部署 ComfyUI 完全入门&#xff1a;图生视频 ComfyUI【强烈推荐】 秋葉aaaki comfy UI整合包 可以使用stable diffusion的大模型&#xff0c;通过修改文件重新指向 修改路径即可 下载秋叶大佬的…

Linux 实操-权限管理:深入了解rwx的作用

&#x1f600;前言 本篇博文是关于Linux文件权限管理的基本知识和实际操作&#xff0c;希望你能够喜欢 &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文章可以帮助到大家&#xff0c;您的满意是…

git rebase 重建清爽的历史提交

前言 在代码评审时遇到分支上有多个commit信息&#xff0c;对于评审者来说是非常头疼的&#xff0c;因为太混乱了。遇到这样的情况&#xff0c;就需要让开发人员把commit压缩一下&#xff0c;简单来说就是将多个commit合并为一个&#xff0c;这样看起来就比较整洁了&#xff0…

【颠覆传统!】SmartEDA引领潮流:在线实时仿真,Multisim与Proteus望尘莫及的新纪元!

在电子设计自动化的浩瀚星空中&#xff0c;两款老牌软件——Multisim与Proteus&#xff0c;如同璀璨星辰&#xff0c;长久以来照亮了工程师们的设计之路。它们以强大的仿真功能和丰富的元件库&#xff0c;赢得了无数设计者的青睐。然而&#xff0c;时代的车轮滚滚向前&#xff…

关于FreeRTOS使用相关API函数导致程序阻塞的问题

前言&#xff1a; 如题。近日在给项目移植FreeRTOS的时候&#xff0c;发现调用如下API函数会阻塞&#xff1a; xTaskNotifyGive(xTaskGetHandle(Task_PrintCtrl_attributes.name)); 首先猜测可能是xTaskGetHandle有问题导致。通过printf打印调试信息&#xff0c;发现执行xTask…

乐凡三防平板定制:为行业量身打造的移动解决方案

在数字化转型的大潮中&#xff0c;移动设备成为企业提升效率、优化流程的关键工具。三防平板&#xff0c;以其坚固耐用、适应恶劣环境的特性&#xff0c;成为工业、物流、建筑、军事等领域不可或缺的选择。而三防平板的定制化服务&#xff0c;则进一步满足了不同行业对设备性能…

Linux | Linux进程万字全解:内核原理、进程状态转换、优先级调度策略与环境变量

目录 1、从计算机组成原理到冯诺依曼架构 计算机系统的组成 冯诺依曼体系 思考&#xff1a;为什么计算机不能直接设计为 输入设备-CPU运算-输出设备 的结构&#xff1f; 2、操作系统(Operator System) 概念 设计OS的目的 描述和组织被管理对象 3、进程 基本概念 进程id和父进程…

亲测好用,吐血整理 ChatGPT 3.5/4.0 新手使用手册~

废话不多说&#xff0c;直接分享正文~ 以下是小编为大家搜集到的最新的ChatGPT国内站&#xff0c;各有优缺点。 1、AI Plus&#xff08;稳定使用&#xff09; 推荐指数&#xff1a;⭐⭐⭐⭐⭐ yixiaai.com 该网站已经稳定运营了1年多了。2023年3月份第一批上线的网…

linux网络配置脚本

通过脚本&#xff0c;设置静态ip以及主机名 因为企业9的网络配置文件和企业7的不一样所以&#xff0c;我们以rhel9和rhel7为例 rhel7/centos7/openeuler #!/bin/bash cat > /etc/sysconfig/network-scripts/ifcfg-$1 << EOF DEVICE$1 ONBOOTyes BOOTPROTOnone IPAD…

数据埋点系列 14|跨平台和多源数据整合:构建全面数据视图的策略与实践

在当今复杂的数字生态系统中&#xff0c;组织的数据通常分散在多个平台和来源中。有效整合这些数据不仅可以提供全面的业务洞察&#xff0c;还能支持更准确的决策制定。本文将探讨如何实现跨平台和多源数据的有效整合。 目录 1. 数据整合的重要性2. 数据整合的挑战3. 数据整合…

695. 岛屿的最大面积(中等)

695. 岛屿的最大面积 1. 题目描述2.详细题解3.代码实现3.1 Python3.2 Java 1. 题目描述 题目中转&#xff1a;695. 岛屿的最大面积 2.详细题解 该题是典型的深度优先搜索题&#xff0c;深度优先搜索的基本思想是&#xff1a;从某个节点出发&#xff0c;尽可能深地搜索图的分支…

Redis未授权访问漏洞利用合集

一、基本信息 靶机&#xff1a;IP:192.168.100.40 攻击机&#xff1a;IP:192.168.100.60 二、漏洞 & 过程 Redis 未授权访问漏洞利用无口令远程登录靶机 靶机 cd redis-4.0.8/src./redis-server ../redis.conf 攻击机 ./redis-cli -h 192.168.100.40 Redis 未授权访问…