skynet 学习笔记分享

skynet 学习

全篇是我在2020年8月底9月初,连续通宵两个星期完成的。

C 和 Lua 的相互调用

Lua 都是对虚拟栈顶进行操作

  • C 动态库

    require “mLualib” 会做如下:
    local path = “mLualib.dll”
    local f = package.loadlib(path,“luaopen_mLualib”) – 返回luaopen_mLualib函数
    f()

  • 在C中配置子模块(包),luaopen_main_sub();在lua层配置父模块,以子模块提供的功能为基础进一步封装。
    在lualib-src目录中C代码实现了子模块,lualib目录lua代码用子模块封装父模块;service亦然。

  • 全局
    1.我们所写的C服务在编译成so库以后,会在某个时机被加载到一个modules的列表中,当要创建该类服务的实例时,将从modules列表取出该服务的函数句柄,调用create函数创建服务实例,并且init之后,将实例赋值给一个新的context对象后,注册到下图所示的skynet_context list中,一个新的服务就创建完成了
    2.我们创建一个新的服务,首先要先找到对应服务的module,在创建完module实例并完成初始化以后,还需要创建一个skynet_context上下文,并将module实例和module模块和这个context关联起来,最后放置于skynet_context list中,一个个独立的沙盒环境就这样被创建出来了
    skynet 学习笔记分享_第1张图片

skynet 学习笔记分享_第2张图片

skynet模块

skynet.error

在skynet.core 定义(lua_skynet.c)
就是调试输出模块,不仅是error输出。

skynet.call(addr, typename, …)

skynet.lua.
同步调用,中间通过yield_call,主要是send发送…到对应的服务并阻塞携程直到返回,并返回结果。
它和send的区别就在于它是用send封装的,同时利用携程阻塞直到结果返回。在收到消息的服务里,如dispatch 通过skynet.ret函数返回结果给当前阻塞服务。所以,call 和 ret 应该是在逻辑上应该是成对出现的。

skynet_handle_findname()函数

通过名字查找uint32_t (地址)

skynet.uniqueservice 和 skynet.newservice 和skynet.queryservice

skynet.lua.
newservice通过skynet.call 调用.launcher中的LAUNCH 函数,创建snlua服务
其他都是通过skynet.call 调用.service 中不同的函数

skynet.getenv 和 skynet.setenv

skynet.lua.

  • conf配置信息已经写入到注册表中,通过该函数获取注册表的变量值
    skynet.getenv(varName) 。
  • 设置注册表信息,varValue一般是number或string,但是不能设置已经存在的varname
    skynet.setenv(varName, varValue)

skynet_send() 和 skynet_sendname()

skynet_server.c
skynet_send()是最底层的(相对)的数据传输服务,skynet_sendname()以register的name获取handle(skynet_handle_findname()得到)。

skynet.send

skynet.lua.
服务间通信

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

  • addr 目的地址

  • p.id 消息类型

  • session = 0 发送不需要应答的消息 session都是0

  • p.pack(…) 打包参数

  • 类似的方法有:
    function skynet.rawsend(addr, typename, msg, sz)
    local p = proto[typename]
    return c.send(addr, p.id, 0 , msg, sz)
    end

最后调用skynet_context_push ,将消息添加的对于目的服务的次级消息队列。
那么,消息怎么被接收?
在skynet.start.c 创建的woker线程,开始处理队列中的消息,最后调用skynet_server.c中的dispatch_message函数中调用服务注册的回调函数处理。
ctx->cb(ctx, ctx->cb_ud, type, msg->session, msg->source, msg->data, sz);

skynet.pack(…) 和 skynet.unpack(msg, sz)

主要功能在lua-seri.c

  • skynet.pack(…)打包后,会返回两个参数,一个是C指针msg指向数据包的起始地址,sz一个是数据包的长度。msg指针的内存区域是动态申请的。
  • skynet.unpack(msg,sz)解包后,会返回一个参数列表。需要注意这个时候C指针msg指向的内存不会释放掉。如果msg有实际的用途,skynet框架会帮你在合适的地方释放掉,如果没有实际的用途,自己想释放掉可以使用skynet.trash(msg, sz)释放掉。

skynet.command

skynet_core
传的第一个参数为函数名,其余为该函数的参数,在C层查找该函数名,并直接调用它。

skynet.callback

skynet.core
在skynet.start中被使用注册dispath_message方法,其主要作用就是将回调函数和当前服务绑定(skynet_server.c中skynet_context 中关于callback的参数设置为回调函数地址)
void skynet_callback(struct skynet_context * context, void *ud, skynet_cb cb) { context->cb = cb; context->cb_ud = ud; }

skynet.dispatch_message

skynet.lua
主要调用了raw_dispatch_message ,分析msg是否需要response等。
消息处理的分为两种情况,一种是其他服务send过来的消息,还有一种就是自己发起同步rpc调用(调用call)后,获得的返回结果(返回消息的类型是PTYPE_RESPONSE)

  • 每条 skynet 消息由 6 部分构成:消息类型、session 、发起服务地址 、接收服务地址 、消息 C 指针、消息长度。
  • 每个 skynet 服务都可以处理多类消息。在 skynet 中,是用 type 这个词来区分消息的。但与其说消息类型不同,不如说更接近网络端口 (port) 这个概念。每个 skynet 服务都支持 255 个不同的 port 。消息分发函数可以根据不同的 port 来为不同的消息定制不同的消息处理流程。skynet 预定义了一组消息类型,需要开发者关心的有:回应消息、网络消息、调试消息、文本消息、Lua 消息、错误。

skynet.start(start_func)

skynet.lua
一个服务的入口函数,对某个服务来说,先newservice ,在C层所有相关工作做好后然后才到skynet.start。详见一下blog的 "skynet如何启动一个lua服务"部分:
Manistein’s blog

function skynet.start(start_func)
	c.callback(skynet.dispatch_message)  --通过core为当前service注册回调函数
	init_thread = skynet.timeout(0, function()
		skynet.init_service(start_func) -- 下一帧运行传入的函数进行初始化
		init_thread = nil
	end)
end

skynet.register_protocol 和 skynet.dispatch

skynet.lua

注册协议,传入的class里一般包括协议name ,id , unpack解包函数 和 dispatch 分发处理函数

function skynet.register_protocol(class)
	local name = class.name
	local id = class.id
	assert(proto[name] == nil and proto[id] == nil)
	assert(type(name) == "string" and type(id) == "number" and id >=0 and id <=255)
	proto[name] = class
	proto[id] = class
end

对已有的协议注册dispatch分发函数,典型应用就是gateserver.lua和watchdog.lua里对lua类消息的处理函数配置

function skynet.dispatch(typename, func)
	local p = proto[typename]
	if func then
		local ret = p.dispatch
		p.dispatch = func
		return ret
	else
		return p and p.dispatch
	end
end

skynet.trace

skynet.core
skynet 加了一个 skynet.trace() 命令,可以开启对当前执行序的跟踪统计

skynet.ret(msg, sz)

skynet.lua
请求的返回。返回给当前占用协程的服务。

skynet.redirect(address, source, typename, session, …)

它和 skynet.send 功能类似,但更细节一些。它可以指定发送地址(把消息源伪装成另一个服务),指定发送的消息的 session 。注:address 和 source 都必须是数字地址,不可以是别名。skynet.redirect 不会调用 pack ,所以这里的 … 必须是一个编码过的字符串,或是 userdata 加一个长度。

skynet lua层 gate,watchdog,agent之间的关系

gate和gateserver是同一个服务。gate和gateserver是同一个服务,gate引用了gateserver文件.gateserver才是各种socket操作的核心,监听和接收数据都在这里进行。消息都是在gateserver里接收的,通过CMD表和MSG来处理这些消息,会用调用到在gate里声明的函数。

  • gate是模板lualib/snax/gateserver.lua使用范例,系统自带的,位于service/gate.lua,是一个实现完整的网关服务器。
    watchdog和agent都是在应用层写的脚本,可以参考源码中example目录下的watchdog.lua和agent.lua
    在启动脚本main.lua(一般情况下)启动了watchdog服务(而watchdog的启动函数内又启动了gate服务),并向watchdog发送了start命令,而watchdog对start命令的处理就是向gate发送open命令,其实是让gate服务开启监听端口,此时gate记录了watchdog的的地址。
    当一个客户端连接过来的时候,首先是经过gate处理(MSG.open – > handler.connect),先建立fd -> connection的映射,然后转发给watchdog(SOCKET.open)处理(目的是为了创建agent并得到之),在其中新建一个agent服务,然后建立fd->agent服务地址的映射,然后向agent服务发送start命令,agent在start命令的处理中又向gate发送forward命令,gate在forward的处理中是通过建立agent -> connection的映射来建立转发,此时的 connection包含agent服务地址,然后调用gateserver.openclient(调用socketdriver.start ,这是c语言实现的)放行消息(传入的fd 就是c层的id,这个id应该就是socket,所以gateserver.openclient或者说socketdriver.start的作用就是开始侦听该socket的消息)。以后在客户端发来消息时,如果已经建立的转发,则直接转发到对应的agent服务上,否则,则发送给watchdog处理。

  • main.lua 首先引导watchdog.lua,使用网络配置参数调用其 start 接口。(newservice + start 是skynet中常用的组合)
    看门狗启动时引导 gate.lua,main.lua对前者start的调用中又调用了gate的open命令,在gateserver.lua模板中启动socketdriver.listen和start。至此网络接口打开等候连接进入,引导过程完成。
    gate.lua是gateserver.lua的应用,后面一旦有连接进来,都会执行gate.lua设置的handler,其中接入过程,会在gate.lua中通知回watchdog,后者就新建一个agent,并start之。
    agent引导起来后,start过程中会主动调用gate.lua的forward接口,为其安装agent相关信息,再之后的过程就是 gate.lua中接收到数据都会redirect到agent了(gate 的handler.message函数)。

网上的一篇教程上的图:
skynet 学习笔记分享_第3张图片

那么,客户端发来的消息是如何从C层传到lua的呢?
在gateserver.start里注册了name = "socket"的protocol,这里是Lua层接收到socket消息的入库。
发送的具体函数在在skynet_socket.c 中的forward_message(),组装了一个skynet_message结构体供lua使用,发出register_protocol函数注册的“skynet.PTYPE_SOCKET”类型消息

skynet 中的消息 msg:
C 结构体

struct {
	uint32_t source;
	int session;
	void * data;
	size_t sz;
};

在skynet_message结构体 data上搭载了skynet_socket_message, skynet_message结构体经过netpack.filter() 处理才能才能得到dispatch函数—— “function (_, _, q, type, …)”需要的后三个参数组合。 dispatch被调用的**本质(记住dispatch函数的输入参数)**是:

proto[prototype].dispatch(
    session,
    source, 
    proto[prototype].unpack(msg,sz))

因此定义了协议的解包函数:

unpack = function ( msg, sz )
        return netpack.filter( queue, msg, sz)
    end

unpack过程如下:
netpack.filter( queue, msg, sz) 调用链:netpack.filter( queue, msg, sz) -> c: lfilter() -> filter_data() -> filter_data_()
执行完成unpack,大致如下:

proto[prototype].dispatch(
    session,
    source, 
    queue,"data",id,ptr,size)

#skynet socket相关
在gateserver.lua 中 gateserver.start方法下,CMD.open方法里调用了socketdriver.listen()开始监听端口,socketdriver.start()开始accept(存疑,因为我后来发现是在agent创建后调用gate的forward函数,才开始openclient,说明socketdriver.start()的调用是在客户端连接之后,这和accept明显无关;上面一段说到,openclient用来放行消息,socketdriver.start是把ctx也就是gateserver和socket也就是id或fd关联起来,用gateserver侦听对于socket消息。)。这个socketdriver需要require “skynet.socketdriver” ,其使用C语言实现底层功能,在lua-socket.中。

  • 在skynet_start.c中调用skynet_socket.c中的socket_server_create方法,其中调用sp_create设置了epoll,通过其返回值创建了静态变量SOCKET_SERVER;然后在开启了socket线程,循环skynet_socket_poll(),该方法通过 int type = socket_server_poll(ss, &result, &more);获取type和result,并根据type 处理forward_message,把数据传到lua,socket_server_poll通过sp_wait(在socket_epoll.h中定义)监听epoll事件,设置阻塞。
    • 在forward_message方法中调用了skynet_context_push发送挂载了skynet_socket_message的skynet_message发送结构体到对应的服务地址,skynet_context_push((uint32_t)result->opaque, &message),这里的result->opaque是在哪被设置的呢?
    • 是在lua-socket.c中,这里面定义的socket.driver子模块,其start方法调用skynet_socket_start函数,这个该函数传入两个参数,一个是当前的服务context(应该是gateserver),另一个s是listen得到的socket(id),其内部调用 skynet_context_handle获取handle地址,然后socket_server_start中设置了该handle 和(socket)id,即opaque。(id在lua层叫做fd)

gateserver

这是lua层的一个封装好了的服务,用于处理一些基本的连接逻辑判断,在官方的示例中根据不同的逻辑间接gate.lua中的handle方法,我们可以仿照gate.lua设计自己的业务方法。

Lua 查找一个表元素时的规则

1.在表中查找,如果找到,返回该元素,找不到则继续
2.判断该表是否有元表,如果没有元表,返回 nil,有元表则继续。
3.判断元表有没有 __index 方法,如果 __index 方法为 nil,则返回 nil;如果 __index 方法是一个表,则重复 1、2、3;如果 __index 方法是一个函数,则返回该函数的返回值。

skynet sproto

组装.sproto文件流程如下:
(1). 用lpeg库解析.sproto文件内容,把信息保存在一个lua表里
(2). 依次组装所有types,对每一个type先组装名称,再组装它的fields
(3). 依次组装所有protos

sptoto组装

.package {
	type 0 : integer
	session 1 : integer
}

使用 sproto 的 rpc框架,每条消息都会以这条消息开头,接上真正的消息内容(包头);连接在一起后用 sproto 的 0-pack方式打包。注意,这个数据包并不包含长度信息,所以真正在网络上传输,还需要添加长度信息,方便分包。当然,如果你使用 skynet 的 gate 模块的话,约定了以两字节大端表示的长度加内容的方式分包。

构造一个 sproto rpc 的消息处理器,应使用:
local host = sproto:host(packagename) -- packagename 默认值为 "package" 即对应前面的 .package 类型。你也可以起别的名字。
这条调用会返回一个 host 对象,用于处理接收的消息。

host:dispatch(msgcontent)
用于处理一条消息。这里的 msgcontent 也是一个字符串,或是一个 userdata(指针)加一个长度。它应符合上述的以 sproto 的 0-pack 方式打包的包格式。
dispatch 调用有两种可能的返回类别,由第一个返回值决定:

  • REQUEST : 第一个返回值为 “REQUEST” 时,表示这是一个远程请求。如果请求包中没有 session 字段,表示该请求不需要回应。这时,第 2 和第 3 个返回值分别为消息类型名(即在 sproto 定义中提到的某个以 . 开头的类型名),以及消息内容(通常是一个 table );如果请求包中有 session 字段,那么还会有第 4 个返回值:一个用于生成回应包的函数。
    该函数应该在attach返回的函数基础上已经内定了回应包的name 和 session,因此其输入参数仅为args.
    改正:04/01/2021 15:41上述黑体字应该错的,因为attach产生的函数和生成回应包不是用的同一个sproto,尽管效果可能相同,但实质不同;而且后面绑定C++ 的sproto的笔记部分发现,是根据type(也就是这里的name)判断request和response,根据session判断是否有回应包函数,回应包的type就是0,不需要再特别指定,session可能沿用了请求包的session,也不需要再指定。

  • RESPONSE :第一个返回值为 “RESPONSE” 时,第 2 和 第 3 个返回值分别为 session 和消息内容。消息内容通常是一个 table ,但也可能不存在内容(仅仅是一个回应确认)。

local sender = host:attach(sp)
– 这里的 sp 是向外发出的消息协议定义
attach 可以构造一个发送函数,用来将对外请求打包编码成可以被 dispatch 正确解码的数据包。
这个 sender 函数接受三个参数(name, args, session)。name 是消息的字符串名、args 是一张保存用消息内容的 table ,而 session 是你提供的唯一识别号,用于让对方正确的回应。其返回值是一个数据包,但是example中还要加入“>s2”才能发出 当你的协议不规定需要回应时,session 可以不给出。同样,args 也可以为空。将args设置为协议中定义的参数来设置消息是“response”还是“request”,如在sproto.lua中设置:

get 2 {
	request {
		what 0 : string
	}
	response {
		result 0 : string
	}
}

主要就是消息的打包和解包,分清楚两个函数attach和dispatch的输入和输出,sproto就很容易理解。
总结一下重点:
1、attach(sp)输入向外发出协议的定义,输出了一个打包函数sender。
sender接受三个参数,name,args,session,输出一个数据包,args(通常是一个 table )和sesion都可以为空,若sesion为空,则表明这种请求不需要回应,这个数组包再加上“>s2”处理就变成了最后的发出包。
2、dispath接受发来的msg,sz ,输出三到四个值,第一个type(“REQUEST” 或 “RESPONSE”);若type =="REQUEST"表示这是一个远程请求,如果请求包中没有 session 字段,则第二、三个输出为 name,args,若有session则此时有第四个返回值,一个生成回应包的函数(打包函数),和输入参数仅为args(相比sender可认为name和session已内定)。若type =="RESPONSE"表示这是一个回应包,可能是之前发出的某个请求的回应,第 2 和 第 3 个返回值分别为 session和消息内容。消息内容通常是一个 table,但也可能不存在内容(仅仅是一个回应确认)。

  • 云风的blog 中的一段
    你需要定义一个叫做 package 的消息类型,里面包含 type 和 session 两项。
    对于每个包,都以这个 package 开头,后面接上 (padding)消息体。最后连在一起,用 sproto 自带的 0-pack 方式压缩。
    你可以用 sproto:host 这个 api 生成一个消息分发器 host ,用来处理上面这种 rpc 消息。
    默认每个 rpc 处理端都有处理请求和处理回应的能力。也就是每个 rpc 端都同时可以做服务器和客户端。所以 host:dispatch 这个 api 可以处理消息包,返回它是请求还是回应,以及具体的内容。
    如果 host 要对外发送请求,它可以用 host:attach 生成一个打包函数。这个生成的函数可以将 type session content 三者打包成一个串,这个串可以***被对方的 host:dispatch*** 正确处理(所以其传入的参数为对方的sp)。

几个消息函数的输入输出

  • skynet.redirect(address, source, typename, session, …) 把消息发给agent,协议类型为typename,session将是接收函数的第一个输入,…一般是消息载体msg 和sz

  • proto[prototype].dispatch(
    session,
    source,
    proto[prototype].unpack(msg,sz))消息的接收函数前两个输入是固定的,session和来源地址

  • skynet.send(address, typename, …)前两个目的地址和协议类型,…为消息载体(可变),内部它会先经过事先注册的 pack 函数打包 … 的内容,然后传给目标的接收服务。非阻塞。

  • skynet.call(address, typename, …) 这条 API 则不同,它会在内部生成一个唯一 session ,并向 address 提起请求,并阻塞等待对 session 的回应(可以不由 address 回应)。当消息回应后,还会通过之前注册的 unpack 函数解包。表面上看起来,就是发起了一次 RPC ,并阻塞等待回应。

  • skynet.rawcall(address, typename, message, size) 它和 skynet.call 功能类似(也是阻塞 API)。但发送时不经过 pack 打包流程,收到回应后,也不走 unpack 流程。

skynet.genid() 生成一个唯一 session 号。

skynet.pack 和 skynet.unpack

skynet 默认提供了一套对 lua 数据结构的序列化方案。即上一节提到的 skynet.pack 以及 skynet.unpack 函数。skynet.pack 可以将一组 lua 对象序列化为一个由 malloc 分配出来的 C 指针加一个数字长度。你需要考虑 C 指针引用的数据块何时释放的问题(通过skynet.trash,如下gate.lua代码块)。当然,如果你只是将 skynet.pack 填在消息处理框架里时,框架解决了这个管理问题。skynet 将 C 指针发送到其他服务,而接收方会在使用完后释放这个指针。
如果你想把这个序列化模块做它用,建议使用另一个 api skynet.packstring 。和 skynet.pack 不同,它返回一个 lua string 。而 skynet.unpack 即可以处理 C 指针,也可以处理 lua string 。

if agent then
		-- It's safe to redirect msg directly , gateserver framework will not free msg.
		skynet.redirect(agent, c.client, "client", fd, msg, sz)
	else
		skynet.send(watchdog, "lua", "socket", "data", fd, skynet.tostring(msg, sz))
		-- skynet.tostring will copy msg to a string, so we must free msg here.
		skynet.trash(msg,sz)
	end

gate.lua里管理socket的几张表

--这里的每个成员是什么意思 fd : socket句柄 ;client :伪装的消息来源(消息从客户端获得) ;agent : 代理服务;ip:客户端ip地址;
local connection = {}	-- fd -> connection : { fd , client, agent , ip, mode },通过 fd获得connect
local forwarding = {}	-- agent -> connection , 通过c.agent获取c

sproto 的组织方式

  • 我以为,从sproto_dump()C语言函数的输出可以看出:
    分为两种,数据类型(type) 和 消息类型(protocol)。
    数据类型可用 . 来自定义;消息类型可用已有的数据类型来组织,其格式一定是包含request和response的表,来表明数据接收的格式和发出的格式(针对lua而言);对C++的binding,事实上消息类型由自定义的类来确定,在编码时只用到sproto.pb二进制中自定义的数据类型就可以了;那是否意味着在C++ 的sproto中不需要用到消息类型了呢?否,因为和skynet lua通信,交换的数据包先是“>s2”编码带长度的吧,然后是skynet中规定的package头 和 主体的信息拼接而成,package头包括type 和session,type就是消息类型中对应的tag,所以消息类型必须。
.package {
	type 0 : integer
	session 1 : integer
}
  • C端处理lua发来的数据包
    首先,数据包需要先处理2字节的长度信息再对剩余的二进制字段sproto_unpack得到二进制串bin,然后把bin分为两部分,header和content,header长度由.package编码后的二进制串长度确定,这得事先就用参数记好。然后,先将header 进行decode,得到type 和session(有个问题,session是0代表nil吗,还是0也是有效session,这里假设是前者),用type值通过query_proto函数查询得到 struct protocol 结构体指针,里面有其request和response对应的sproto_type。然后可以用sproto_type来解码content。
if header.type then
   	-- request
   	local proto = queryproto(self.__proto, header.type)
   	local result
   	if proto.request then
   		result = core.decode(proto.request, content)
   	end
   	if header_tmp.session then
   		return "REQUEST", proto.name, result, gen_response(self, proto.response, header_tmp.session), header.ud
   	else
   		return "REQUEST", proto.name, result, nil, header.ud
   	end
   else
   	-- response
   	local session = assert(header_tmp.session, "session not found")
   	local response = assert(self.__session[session], "Unknown session")
   	self.__session[session] = nil
   	if response == true then
   		return "RESPONSE", session, nil, header.ud
   	else
   		local result = core.decode(response, content)
   		return "RESPONSE", session, result, header.ud
   	end
   end
根据这段host:dispatch的lua源代码可知,根据type(也就是tag)判断是否需要回复(request(非 nil) 或 response(nil值)),也侧面说明了不存在tag为nil的protocol。在消息为request类型的情况,根据session判断是(非nil)否(nil值)有回复函数;在response类型情况,根据session判断收到的回复数据发往何处。
  • 封装C端发送给lua的数据包
    先encode 头header,附上所用的protocol的tag,如果需要恢复,就把session设为非0值。然后encode 所用的协议及填充内容得到content。把两端二进制数据连接起来得到bin。对bin进行sproto_pack,再加上两字节的长度信息,大端。

你可能感兴趣的:(lua,linux,c语言)