前言
提起中国的lua产品,就不得不想到skynet,一款针对游戏,但又不仅限于游戏的服务端架构。skynet充分利用了lua的特性,并且在此基础上易扩展HTTP、HTTPS、WebSocket等模块,因此由skynet入手理解lua原理以及服务端架构是一个非常不错的选择。
通过skynet,我们可以构建许多小巧而高性能、高可用的应用。废话不多说,让我们一起来探索skynet架构吧~
skynet通信原理与源码分析
服务端架构中,不同子服务的通信调度是核心功能。因此,我们以单点(standalone)的skynet实例为例,由外而内,逐步剖析。
要介绍skynet的通信原理,首先要提到lua中的一个概念——lua_State。lua_State是lua的运行时(runtime),是一个原生隔离的、高性能的运行环境,若在多核并行运行lua_State,其性能一定不会差。lua作为嵌入式语言,以C为基础,可以实现操作系统粒度级的lua_State调度,因此skynet也就如同lua_State管理器一样了。
每一种业务可以看作一个service,而每一个service中,都会有一个lua_State充当执行业务逻辑的环境。举个例子,在实际开发当中,比如做一个HTTP服务的话,我们需要自己预先配置好的skynet service主入口lua文件中,写上skynet.uniqueservice("app")
启动一个独特的名为app
的服务,而后在其中的逻辑中,根据每一个HTTP连接,解析其中的数据包。并调用skynet.newservice
动态创建单独的上下文服务ctx
来处理这个请求。ctx
服务还有可能需要查询数据库中的数据,并返回结果,因此我们可能还需要通过skynet.uniqueservice("db")
预先创建数据库服务db
二次封装skynet内置mongo、mysql库的功能,然后再通过skynet.call
来与db
服务通信,获得db
服务某个函数执行的返回结果,再在ctx
服务的处理逻辑中写入HTTP Response,从而完成整个处理过程。在这一过程中,具体业务逻辑的处理都会在各个service所拥有的lua_State中运行,但调度通信的逻辑,则就是底层的活了。
因此在skynet底层中,不仅需要支持多个lua_State的运转,而且相对更有挑战性的是,如何让service之间能够相互交流。为了解决这个问题,我们可以看到,在底层中,每一个service都属于snlua
类型。snlua
除了包括自己的lua_State之外,还维护了一个称之为context的运行状态:
1 | // lua-skynet.c |
在lua业务逻辑中执行skynet.newservice
或skynet.uniqueservice
,skynet框架就会根据服务名称读取对应入口的代码执行。要让这个服务启动,入口文件的代码还需要添加skynet.start
函数:
1 | -- skynet.lua |
可以看到在启动之时,会通过c.callback
注册一个回调函数用于分发消息,其逻辑如下:
1 | // lua-skynet.c |
所以我们看到,最终处理消息的逻辑,就会注册到context的cb上。context会维护这个service专属的消息队列message_queue
,多个service的消息队列在skynet里就被存放在一个全局唯一的队列global_queue
中。
1 | // skynet_mq.c |
在skynet架构启动之际,会根据用户配置创建全局消息队列以外,还会初始化定时器、日志、socket、集群等基础模块及服务。当然在这个过程中,也会创建几个worker:
1 | // skynet_start.c |
而这些worker会做什么呢?我们查看worker线程任务的函数定义即可知晓:
1 | // skynet_start.c |
也就是说,这些worker会不断地从全局队列中取出单个消息队列,而后让这个消息队列所对应的service通过其context上注册的回调函数cb,处理相应的消息。
因此到这里,消息处理这一块的逻辑已经弄清了。如果要完成通信的闭环,还需要解决两个问题:
- 发送方service如何推送消息到目标service?(
skynet.send
,非阻塞发送消息) - 发送方如何获得目标service处理的返回值?(
skynet.call
,阻塞等待消息处理结果)
我们先来看skynet.send
。这个函数调用了底层注册的send
函数,对应了lua-skynet.c
中的lsend
函数。我们以此为起点,观察消息推送的过程:
1 | // lua-skynet.c |
通过这样的一顿操作,就可以把消息发送到指定service的消息队列里了,然后就等worker来取消息回调处理啦~
那么第二个,如何实现获取处理结果的需求呢?这个是在lua层实现的,通过目标服务调用skynet.ret
逻辑,skynet.call
就可以获取返回值。我们来观察两边的逻辑:
1 | -- skynet.lua |
可以看到,通过提供发送方与session标识信息,发送方就能够知道哪些消息是该session的返回值了。
这样一来,skynet内部的通信机制,就全部串上了!
总结
一不小心写多了点,希望能有助于各位小伙伴加深对服务端以及skynet架构的理解。如果有叙述不当的地方,恳请指正~~~
那么skynet具体要怎么用呢?这一part暂时决定在后面的系列献上~