使用过skynet的童鞋都知道,一般新启动一个服务是调用skynet.newservice函数。至于他的代码实现,相信没有多少人会去看,这篇文章会讲明白为什么skynet.newservice要这么实现。
刚开始的时候,我以为sky.newservice只是启动了一个参数是lua文件名的snlua服务,例如skynet.newservice('room'),我天真的认为他的实现如下:
local svr1 = skynet.launch("snlua", "room")
其中skynet.launch的实现在manager.lua文件里,代码如下:
function skynet.launch(...)
local addr = c.command("LAUNCH", table.concat({...}," "))
if addr then
return tonumber("0x" .. string.sub(addr , 2))
end
end
调用底层c.command("LAUNCH", table.concat({...}," "))就是新产生一个snlua服务,要执行lua文件里的逻辑必须生成一个snlua的服务,文件名就是这个服务的参数。这个在skynet怎么启动lua文件有介绍。
假设room服务的实现很简单,只实现了一个lua协议的回调函数,如下:
local skynet = require "skynet"
skynet.start(
function()
skynet.dispatch("lua", function(session, address, cmd)
print('cmd is ', cmd)
end
)
end
)
上述代码虽然新生成了一个room服务,但是还有一些问题没有解决。例如这时候给room服务发送消息可能会报错,即使room服务有相应的协议回调函数,为什么呢?
原因是虽然skynet.launch返回了room服务的地址,但是此时room服务可能并没有执行第一条消息,导致回调函数dispatch在被call时没有被注册,这样call的一方就会有如下错误:
所以第一个要解决的问题是同步问题,即在新的服务生成时要保证该服务已经执行了skynet.start()里面的函数。
某位计算机大师说过,“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”。我们可以先尝试用一个中间服务的办法来解决问题。我们将这个中间服务叫做launcher。以下是我的构想:
让我们来分析上面的过程,A服务给launcher服务调用call请求,内容是创建B服务,此时A服务将挂起。等B服务初始化完成,也就是执行了start里面的参数,然后给launcher回复一条ok消息。最后是launcher给A服务response,这样A服务就恢复了,同时也保证了B服务初始化完成。
注意,A给launcher服务call请求时,launcher服务会记录A发送消息的session和source,所以一旦B给launcher发送了ok消息,launcher就会给A回复。
仔细观察上图的童鞋肯定会有疑问,A给launcher服务发送call请求时,如何保证launcher服务的回调函数已经注册呢?又回到了上面的问题。凡是都有特例,launcher就是这样一个特殊的服务,它保证在生成这个服务之后的,执行lua文件时在全局就已经注册dispatch函数了,因为他的skynet.dispatch是在文件作用域中,而不是在skynet.start函数中,不相信的童鞋可以去看下源码。
那么既然launcher服务在全局中注册dispatch,那其他任何服务都可以啊,这样一来,只要遵守这个规则,一旦一个服务生成,就里面可以给这个服务call请求了。是没错,但是这并不是好的设计,因为你不能强制这样做。所以在skynet中是统一用newservice来生成新的服务的,他的实现很简单,就是像launcher服务发送一个call请求:
function skynet.newservice(name, ...)
return skynet.call(".launcher", "lua" , "LAUNCH", "snlua", name, ...)
end
统一调用newservice生成新的服务还有其他的作用,例如对服务作监管,常见的例子就是我们要计算整个服务占用内存,对指定服务做GC等等。如果我们自己按照调用skynet.launch这种方式来生成一个新的服务,则这个服务的一些信息将不受整个服务体系的监管,除非你手动把这个服务加入到整个服务体系中,这将对开发很不友好。有机会再剖析一下有关特殊服务的作用。
欢迎加入QQ群 858791125 讨论skynet,游戏后台开发,lua脚本语言等问题。