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.core 定义(lua_skynet.c)
就是调试输出模块,不仅是error输出。
skynet.lua.
同步调用,中间通过yield_call,主要是send发送…到对应的服务并阻塞携程直到返回,并返回结果。
它和send的区别就在于它是用send封装的,同时利用携程阻塞直到结果返回。在收到消息的服务里,如dispatch 通过skynet.ret函数返回结果给当前阻塞服务。所以,call 和 ret 应该是在逻辑上应该是成对出现的。
通过名字查找uint32_t (地址)
skynet.lua.
newservice通过skynet.call 调用.launcher中的LAUNCH 函数,创建snlua服务
其他都是通过skynet.call 调用.service 中不同的函数
skynet.lua.
skynet_server.c
skynet_send()是最底层的(相对)的数据传输服务,skynet_sendname()以register的name获取handle(skynet_handle_findname()得到)。
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);
主要功能在lua-seri.c
skynet_core
传的第一个参数为函数名,其余为该函数的参数,在C层查找该函数名,并直接调用它。
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.lua
主要调用了raw_dispatch_message ,分析msg是否需要response等。
消息处理的分为两种情况,一种是其他服务send过来的消息,还有一种就是自己发起同步rpc调用(调用call)后,获得的返回结果(返回消息的类型是PTYPE_RESPONSE)
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.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.core
skynet 加了一个 skynet.trace() 命令,可以开启对当前执行序的跟踪统计
skynet.lua
请求的返回。返回给当前占用协程的服务。
它和 skynet.send 功能类似,但更细节一些。它可以指定发送地址(把消息源伪装成另一个服务),指定发送的消息的 session 。注:address 和 source 都必须是数字地址,不可以是别名。skynet.redirect 不会调用 pack ,所以这里的 … 必须是一个编码过的字符串,或是 userdata 加一个长度。
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函数)。
那么,客户端发来的消息是如何从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.中。
这是lua层的一个封装好了的服务,用于处理一些基本的连接逻辑判断,在官方的示例中根据不同的逻辑间接gate.lua中的handle方法,我们可以仿照gate.lua设计自己的业务方法。
1.在表中查找,如果找到,返回该元素,找不到则继续
2.判断该表是否有元表,如果没有元表,返回 nil,有元表则继续。
3.判断元表有没有 __index 方法,如果 __index 方法为 nil,则返回 nil;如果 __index 方法是一个表,则重复 1、2、3;如果 __index 方法是一个函数,则返回该函数的返回值。
组装.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,但也可能不存在内容(仅仅是一个回应确认)。
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 默认提供了一套对 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
--这里的每个成员是什么意思 fd : socket句柄 ;client :伪装的消息来源(消息从客户端获得) ;agent : 代理服务;ip:客户端ip地址;
local connection = {} -- fd -> connection : { fd , client, agent , ip, mode },通过 fd获得connect
local forwarding = {} -- agent -> connection , 通过c.agent获取c
.package {
type 0 : integer
session 1 : integer
}
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判断收到的回复数据发往何处。