skynet 服务相关api

1、启动服务相关

我们都知道开启一个新服务的方式是skynet.newservice(‘name’)。他的实现为:

function skynet.newservice(name, ...)
	return skynet.call(".launcher", "lua" , "LAUNCH", "snlua", name, ...)
end

所有首先要开启launcher服务。其实launcher服务首先会在bootstrap文件里被启用:

local launcher = assert(skynet.launch("snlua","launcher"))

skynet.name(".launcher", launcher)

skynet.launch是在skynet manager.lua实现的:

function skynet.launch(...)
	local addr = c.command("LAUNCH", table.concat({...}," "))
	if addr then
		return tonumber("0x" .. string.sub(addr , 2))
	end
end

也就是说skynet.newservice(‘name’)最后调用为:

local addr = c.command("LAUNCH", "snlua launcher")
skynet.call(addr, "lua" , "LAUNCH", "snlua", 'name')

c.command在c层里实现:

static const char *
cmd_launch(struct skynet_context * context, const char * param) {
	size_t sz = strlen(param);
#ifdef _MSC_VER
	assert(sz <= 1024);
	char tmp[1024+1];
#else
	char tmp[sz+1];
#endif
	strcpy(tmp,param);
	char * args = tmp;
	char * mod = strsep(&args, " \t\r\n");
	args = strsep(&args, "\r\n");
	struct skynet_context * inst = skynet_context_new(mod,args);
	if (inst == NULL) {
		return NULL;
	} else {
		id_to_hex(context->result, inst->handle);
		return context->result;
	}
}

可以看到他是创建一个新的slua实例,然后返回全局实例句柄。这个新的实例会启用launcher.lua文件。他的回调函数执行时的调用为:

local function launch_service(service, ...)
	local param = table.concat({...}, " ")
	local inst = skynet.launch(service, param)
	local response = skynet.response()
	if inst then
		services[inst] = service .. " " .. param
		instance[inst] = response
	else
		response(false)
		return
	end
	return inst
end

function command.LAUNCH(_, service, ...)
	launch_service(service, ...)
	return NORET
end

可以看出他还是调用了skynet.launch(),也就是最后会调用c.command(“LAUNCH”, “snlua name”)。最终启动那个lua文件。

我们看到新启动一个服务是call launcher服务来实现的。其实我们完全可以一步到位,直接skynet.launch(“snlua”, “xxx_name”),或者更为直接的是

c.command(“LAUNCH”, “snlua xxx_name”), 其中c为"skynet.core",为什么通过launcher服务来间接来启动一个服务呢?

我们从launcher服务一些命令诸如REMOVE, MEM, LIST, STAT可以看出,这么做的目的是为了其中管理,操纵新的服务。

与启动服务相关的还有service_mgr服务,他也是在bootstrap中启动的。他的作用是启动一个唯一,或者等待服务启动,官网api的描述为:

uniqueservice(name, …) 启动一个唯一服务,如果服务该服务已经启动,则返回已启动的服务地址。
queryservice(name) 查询一个由 uniqueservice 启动的唯一服务的地址,若该服务尚未启动则等待。

2、命名服务相关

为服务命名的函数是skynet.register,他实质上是调用c层的cmd_reg:

static const char *
cmd_reg(struct skynet_context * context, const char * param) {
	if (param == NULL || param[0] == '\0') {
		sprintf(context->result, ":%x", context->handle);
		return context->result;
	} else if (param[0] == '.') {
		return skynet_handle_namehandle(context->handle, param + 1);
	} else {
		skynet_error(context, "Can't register global name %s in C", param);
		return NULL;
	}
}

可以看出给服务取名时前面要加.号,否则失败,或者干脆为空名则返回的是服务handle。正常情况下调用skynet_handle_namehandle,他的算法是在全局handle_storage中的handle_name字段中利用二分法依次添加名字。

相反的,根据服务名字获取hanle的函数是skynet.localname(name),他调用c层的c.command(“QUERY”, ‘xxx_name’)。算法是利用二分法查找上面注册过的名字,并取出包含handle的字符串,在lua层返回为16进制数。

所以要想获取本身服务handle的最好办法是直接调用c.command(“REG”),第二个参数为空,所以会返回handle。实际上skynet有这个api,其实现就是这么干的:

local self_handle
function skynet.self()
	if self_handle then
		return self_handle
	end
	self_handle = string_to_handle(c.command("REG"))
	return self_handle
end

3、 调用服务相关

调用服务skynet.call()第一个参数既可以是注册的字符串名字,也可以是服务的handle(可以通过调用skynet.self()得到),即一个整形。skynet.call实际上调用c.send,在c层,他会判断传过来的是字符串还是整形,如果是字符串(必须带.),会通过skynet_handle_findname()二分查找返回整形。

5.1、服务地址

每个服务都有一个 32bit 的数字地址,这个地址的高 8bit 表明了它所属的节点。
(1)skynet.self() 用于获得服务自己的地址。
(2)skynet.harbor() 用于获得服务所属的节点。
(3)skynet.address(address) 把一个地址数字转换为一个可用于阅读的字符串。
同时,我们还可以给地址起一个名字方便使用。
(4)skynet.register(name) 可以为自己注册一个别名。(在 32 个字符以内)
(5)skynet.name(name, address) 为一个地址命名。
(6)skynet.name(name, skynet.self()) 和skynet.register(name) 功能等价。
这个名字一旦注册,是在 skynet 系统中通用的,你需要自己约定名字的管理的方法。
以 . 开头的名字是在同一 skynet 节点下有效的,跨节点的 skynet 服务对别的节点下的 . 开头的名字不可见。不同的 skynet 节点可以定义相同的 . 开头的名字。
以字母开头的名字在整个 skynet 网络中都有效,你可以通过这种全局名字把消息发到其它节点的。原则上,不鼓励滥用全局名字,它有一定的管理成本。管用的方法是在业务层交换服务的数字地址,让服务自行记住其它服务的地址来传播消息。
(7)skynet.localname(name) 用来查询一个 . 开头的名字对应的地址。它是一个非阻塞 API ,不可以查询跨节点的全局名字。

下面的 API 说明中,如非特别提及,所有接受服务地址的参数,都可以传入这个地址的字符串别名。

5.2、消息的分发和回应

(1)skynet.dispatch(type, function(session, source, …) … end) 注册特定类消息的处理函数。 type用字符串表示类型,表示对应的操作

local CMD = {}
skynet.dispatch("lua", function(session, source, cmd, ...)
	local f = assert(CMD[cmd])
	f(...)
end)

(1) 注册了 lua 类消息的分发函数。通常约定 lua 类消息的第一个元素是一个字符串,表示具体消息对应的操作。我们会在脚本中创建一个 CMD 表,把对应的操作函数定义在表中。每条 lua 消息抵达后,从 CMD 表中查到处理函数,并把余下的参数传入。这个消息的 session 和source 可以不必传递给处理函数,因为除了主动向 source 发送类别为 “response” 的消息来回应它以外,还有更简单的方法。框架记忆了这两个值。
(2)skynet.register_protocol注册新的消息类别。例如你可以注册一个以文本方式编码消息的消息类别。通常用 C 编写的服务更容易解析文本消息。skynet 已经定义了这种消息类别为 skynet.PTYPE_TEXT,但默认并没有注册到 lua 中使用。

skynet.register_protocol {
	name = "text",
	id = skynet.PTYPE_TEXT,
	pack = function(m)     return tostring(m) end,
	unpack = skynet.tostring,
}

(1)新的类别必须提供 pack 和 unpack 函数,用于消息的编码和解码。
pack 函数必须返回一个 string 或是一个 userdata 和 size 。在 Lua 脚本中,推荐你返回 string 类型,而用后一种形式需要对 skynet 底层有足够的了解(采用它多半是因为性能考虑,可以减少一些数据拷贝)。
unpack 函数接收一个 lightuserdata 和一个整数 。即上面提到的 message 和 size 。lua 无法直接处理 C 指针,所以必须使用额外的 C 库导入函数来解码。skynet.tostring 就是这样的一个函数,它将这个 C 指针和长度翻译成 lua 的 string 。
接下来你可以使用 skynet.dispatch 注册 text 类别的处理方法了。当然,直接在skynet.register_protocol 时传入 dispatch 函数也可以。
dispatch 函数会在收到每条类别对应的消息时被回调。消息先经过 unpack 函数,返回值被传入 dispatch 。每条消息的处理都工作在一个独立的 coroutine 中,看起来以多线程方式工作。但记住,在同一个 lua 虚拟机(同一个 lua 服务)中,永远不可能出现多线程并发的情况。**你的 lua 脚本不需要考虑线程安全的问题,但每次有阻塞 api 调用时,脚本都可能发生重入,这点务必小心。CriticalSection 模块可以帮助你减少并发带来的复杂性。
(3)skynet.ret(message, size) 回应一个消息。它会将 message size 对应的消息附上当前消息的 session ,及 skynet.PTYPE_RESPONSE 这个类别,发送给当前消息的来源 source 。由于某些历史原因(早期的 skynet 默认消息类别是文本,而没有经过特殊编码),这个 API 被设计成传递一个 C 指针和长度,而不是经过当前消息的 pack 函数打包。或者你也可以省略 size 而传入一个字符串。
(4)skynet.ret(skynet.pack(…))
由于 skynet 中最常用的消息类别是 lua ,这种消息是经过 skynet.pack 打包的,
(5)btw,skynet.pack(…) 返回一个 lightuserdata 和一个长度,符合 skynet.ret 的参数需求;
(6)skynet.unpack(message, size) 它可以把一个 C 指针加长度的消息解码成一组 Lua 对象。
问题:
skynet.ret 在同一个消息处理的 coroutine 中只可以被调用一次,多次调用会触发异常。有时候,你需要挂起一个请求,等将来时机满足,再回应它。而回应的时候已经在别的 coroutine 中了。
解决办法:
(7)skynet.response(skynet.pack) 获得一个闭包,以后调用这个闭包即可把回应消息发回。这里的参数 skynet.pack 是可选的,你可以传入其它打包函数,默认即是 skynet.pack。
(8)skynet.response 返回的闭包可用于延迟回应。调用它时,第一个参数通常是 true 表示是一个正常的回应,之后的参数是需要回应的数据。如果是 false ,则给请求者抛出一个异常。它的返回值表示回应的地址是否还有效。如果你仅仅想知道回应地址的有效性,那么可以在第一个参数传入 “TEST” 用于检测。
注:skynet.ret 和 skynet.response 都是非阻塞 API 。
关于消息数据指针
skynet 服务间传递的消息在底层是用 C 指针/lightuserdata 加一个数字长度来表示的。当一条消息进入 skynet 服务时,该消息会根据消息类别分发到对应的类别处理流程,(由skynet.register_protocol ) 。这个消息数据指针是由发送消息方生成的,通常是由skynet_malloc 分配的内存块。默认情况下,框架会在之后调用 skynet_free 释放这个指针。
如果你想阻止框架调用 skynet_free 可以用skynet.forward_type 取代 skynet.start 调用。和 skynet.start 不同,skynet_forwardtype 需要多传递一张表,表示哪些类的消息不需要框架调用 skynet_free 。
例如:

skynet.forward_type( { [skynet.PTYPE_LUA] = skynet.PTYPE_USER }, start_func )

1、表示 PTYPE_LUA 类的消息处理完毕后,不要调用 skynet_free 释放消息数据指针。这通常用于做消息转发。
这里由于框架默认定义了 PTYPE_LUA 的处理流程,
而 skynet.register_protocol 不准重定义这个流程,所以我们可以重定向消息类型为 PTYPE_USER 。
还有另一种情况也需要用 skynet.forward_type 阻止释放消息数据指针:
如果针对某种特别的消息,传了一个复杂对象(而不是由 skynet_malloc分配
出来的整块内存;那么就可以让框架忽略数据指针,而自己调用对象的释放函数去释放这个指针。

5.3、消息的序列化

每类消息都应该定义该类型的打包和解包函数。
当我们能确保消息仅在同一进程间流通的时候,便可以直接把 C 对象编码成一个指针。因为进程相同,所以 C 指针可以有效传递。但是,skynet 默认支持有多节点模式,消息有可能被传到另一台机器的另一个进程中。这种情况下,每条消息都必须是一块连续内存,我们就必须对消息进行序列化操作。
skynet 默认提供了一套对 lua 数据结构的序列化方案。即上一节提到的 skynet.pack 以及skynet.unpack 函数。
skynet.pack 可以将一组 lua 对象序列化为一个由 malloc 分配出来的 C 指针加一个数字长度。你需要考虑 C 指针引用的数据块何时释放的问题。当然,如果你只是将skynet.pack 填在消息处理框架里时,框架解决了这个管理问题。skynet 将 C 指针发送到其他服务,而接收方会在使用完后释放这个指针。
如果你想把这个序列化模块做它用。
api skynet.packstring
它返回一个 lua string 。而 skynet.unpack既可以处理 C 指针,也可以处理 lua string 。这个序列化库支持 string, boolean, number, lightuserdata, table 这些类型,但对 lua table 的 metatable 支持非常有限,所以尽量不要用其打包带有元方法的 lua 对象。

5.4、消息推送和远程调用

有了处理别的服务发送过来的请求的能力,势必就该有向其他服务发送消息或请求的能力。
(1)skynet.send(address, typename, …) 把一条类别为 typename 的消息发送给 address 。它会先经过事先注册的 pack 函数打包 … 的内容。
skynet.send 是一条非阻塞 API ,发送完消息后,coroutine 会继续向下运行,这期间服务不会重入。
(2)skynet.call(address, typename, …) 它会在内部生成一个唯一 session ,并向 address 提起请求,并阻塞等待对 session 的回应(可以不由 address 回应)。当消息回应后,还会通过之前注册的 unpack 函数解包。表面上看起来,就是发起了一次 RPC ,并阻塞等待回应。call 不支持超时,skynet.call 仅仅阻塞住当前的 coroutine ,而没有阻塞整个服务。在等待回应期间,服务照样可以响应其他请求。所以,尤其要注意,在 skynet.call 之前获得的服务内的状态,到返回后,很有可能改变。
还有三个 API 与之相关,但并非常规开发所需要:
(3)skynet.redirect(address,source,typename,session,…)
它和 skynet.send 功能类似,但更细节一些。它可以指定发送地址(把消息源伪装成另一个服务),指定发送的消息的 session 。注:address 和 source 都必须是数字地址,不可以是别名。skynet.redirect 不会调用 pack ,所以这里的 … 必须是一个编码过的字符串,或是 userdata 加一个长度。
(4)skynet.genid() 生成一个唯一 session 号。
(5)skynet.rawcall(address, typename,message,size)
它和 skynet.call 功能类似(也是阻塞 API)。但发送时不经过 pack 打包流程,收到回应后,也不走 unpack 流程。

5.5、服务的启动和退出

每个 skynet 服务都必须有一个启动函数。这一点和普通 Lua 脚本不同,传统的 Lua 脚本是没有专门的主函数,脚本本身即是主函数。而 skynet 服务,你必须主动调用

skynet.start(function()end)

(1)skynet.start() 注册一个函数为这个服务的启动函数。当然你还是可以在脚本中随意写一段 Lua 代码,它们会先于 start 函数执行。但是,不要在外面调用 skynet 的阻塞 API ,因为框架将无法唤醒它们。
如果你想在 skynet.start 注册的函数之前做点什么,可以调用 skynet.init(function() … end)。这通常用于 lua 库的编写。你需要编写的服务引用你的库的时候,事先调用一些 skynet 阻塞 API ,就可以用 skynet.init 把这些工作注册在 start 之前。
(2)skynet.exit() 用于退出当前的服务。skynet.exit 之后的代码都不会被运行。而且,当前服务被阻塞住的 coroutine 也会立刻中断退出。这些通常是一些 RPC 尚未收到回应。所以调用skynet.exit() 请务必小心。
(3)skynet.kill(address) 可以用来强制关闭别的服务。但强烈不推荐这样做。因为对象会在任意一条消息处理完毕后,毫无征兆的退出。所以推荐的做法是,发送一条消息,让对方自己善后以及调用 skynet.exit 。注:skynet.kill(skynet.self()) 不完全等价于 skynet.exit() ,后者更安全。
(4)skynet.newservice(name, …) 用于启动一个新的 Lua 服务。name 是脚本的名字(不用写 .lua 后缀)。只有被启动的脚本的 start 函数返回后,这个 API 才会返回启动的服务的地址,这是一个阻塞 API 。如果被启动的脚本在初始化环节抛出异常,或在初始化完成前就调用 skynet.exit 退出,`skynet.newservice` 都会抛出异常。如果被启动的脚本的 start 函数是一个永不结束的循环,那么 newservice 也会被永远阻塞住。
注意:启动参数其实是以字符串拼接的方式传递过去的。所以不要在参数中传递复杂的 Lua 对象。接收到的参数都是字符串,且字符串中不可以有空格(否则会被分割成多个参数)。目前推荐的惯例是,让你的服务响应一个启动消息。在 newservice 之后,立刻调用 skynet.call发送启动请求。
(5) skynet.launch(servicename, …) 用于启动一个 C 模块的服务。由于 skynet 主要用 lua 编写服务,所以它用的并不多。
注意:同一段 lua 脚本可以作为一个 lua 服务启动多次,同一个 C 模块也可以作为 C 服务启动多次。服务的地址是区分运行时不同服务的唯一标识。如果你想编写一个服务,在系统中只存在一份.

5.6、时钟和线程

skynet 的内部时钟精度为 0.01s秒。
(1)skynet.now() 返回 skynet 节点进程启动的时间。这个返回值的数值本身意义不大,不同节点在同一时刻取到的值也不相同。只有两次调用的差值才有意义。用来测量经过的时间。每 100 表示真实时间 1 秒。这个函数的开销小于查询系统时钟。在同一个时间片内这个值是不变的。(注意:这里的时间片表示小于skynet内部时钟周期的时间片,假如执行了比较费时的操作如超长时间的循环,或者调用了外部的阻塞调用,如os.execute(‘sleep 1’), 即使中间没有skynet的阻塞api调用,两次调用的返回值还是会不同的.)
(2)skynet.starttime() 返回 skynet 节点进程启动的 UTC 时间,以秒为单位。
(3)skynet.time() 返回以秒为单位(精度为小数点后两位)的 UTC 时间。它时间上等价于:skynet.now()/100 + skynet.starttime()
(4)skynet.sleep(ti) 将当前 coroutine 挂起 ti 个单位时间。一个单位是 1/100 秒。它是向框架注册一个定时器实现的。框架会在 ti 时间后,发送一个定时器消息来唤醒这个 coroutine 。这是一个阻塞 API 。它的返回值会告诉你是时间到了,还是被 skynet.wakeup 唤醒 (返回 “BREAK”)。
(5)skynet.yield() 相当于 skynet.sleep(0) 。交出当前服务对 CPU 的控制权。通常在你想做大量的操作,又没有机会调用阻塞 API 时,可以选择调用 yield 让系统跑的更平滑。
(6)skynet.timeout(ti, func) 让框架在 ti 个单位时间后,调用 func 这个函数。这不是一个阻塞 API ,当前 coroutine 会继续向下运行,而 func 将来会在新的 coroutine 中执行。
skynet 的定时器实现的非常高效,所以一般不用太担心性能问题。不过,如果你的服务想大量使用定时器的话,可以考虑一个更好的方法:即在一个service里,尽量只使用一个skynet.timeout,用它来触发自己的定时事件模块。这样可以减少大量从框架发送到服务的消息数量。毕竟一个服务在同一个单位时间能处理的外部消息数量是有限的。
timeout 没有取消接口,这是因为你可以简单的封装它获得取消的能力:

function cancelable_timeout(ti, func)
	local function cb()
		if func then
			func()
		end
	end
	local function cancel()
		func = nil
	end
	skynet.timeout(ti, cb)
	return cancel
end
local cancel = cancelable_timeout(ti, dosomething)
cancel()  -- cancel dosomething

(7)skynet.fork(func, …)–不需要注册定时器
从功能上,它等价于skynet.timeout(0, function() func(…) end) 但是比 timeout 高效一点。因为它并不需要向框架注册一个定时器。
(8)skynet.wait() 把当前 coroutine 挂起。
通常这个函数需要结合 skynet.wakeup 使用。
(8)skynet.wakeup(co)
唤醒一个被 skynet.sleep 或 skynet.wait 挂起的 coroutine 。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值