【Lua杂谈】服务端架构skynet简易入门项目——create-skynet

前言

skynet通信原理与源码分析一文中,我们已经详尽地弄清楚了skynet地通信架构,为我们上手skynet提供了极大的帮助。因此本篇文章接续上文,正式上手使用skynet。

skynet入门项目:create-skynet

要做一个基于skynet的项目,首先需要一个好的模板。skynet的最佳实践并非将服务卸载skynet模块中,而是将skynet当作一个单独的库/SDK看待,自己独立在另外的目录写业务逻辑。因此,笔者在数月前简单地整合了一下skynet的boilerplate项目——create-skynet,采用这个项目搭建skynet服务端结构会较为清晰。

服务&库的约定

在create-skynet的配置中,每个服务的lua文件入口以config中luaservice项为准:

  • service/服务名.lua
  • service/服务名/main.lua
  • skynet/service/服务名.lua(默认的服务)

skynet启动时会根据服务名注册相应服务,因此自己在service下定义的服务名最好不要与skynet原有服务重名。

skynet在为每一个服务读取lua库的时候,会根据运行skynet脚本的工作目录以及config里的设置去读取,这是由config的lua_pathlua_cpath为准的。在create-skynet里,lualib的位置有:

  • service/服务名/?.lua
  • lualib/?.lua
  • luaclib/?.so
  • skynet/lualib/?.lua(默认)
  • skynet/luaclib/?.so(默认)

因此后续在服务逻辑中require时,需要注意lualib文件所在的位置。

skynet更多具体的config设置,可参考官方wiki

启动skynet服务

create-skynet实现了skynet最简单的sproto服务的例子。如果您对sproto不了解,可以参阅笔者的lua专用rpc协议sproto一文。

在create-skynet里,config的start项指定了初始的服务入口——main,因此我们只需在service/main.lua里写内容,skynet就会自动读到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- service/main.lua
local skynet = require "skynet"

skynet.start(function()
skynet.error("create-skynet start server~")
skynet.uniqueservice("proto")
local debug_console_port = skynet.getenv("debug_console_port")
if debug_console_port then
skynet.newservice("debug_console", debug_console_port)
end
skynet.newservice("db")
local watchdog = skynet.newservice("watchdog")
local watchdog_port = skynet.getenv("watchdog_port")
skynet.call(watchdog, "lua", "start", {
port = watchdog_port,
nodelay = true
})
skynet.error("Watchdog listening on", watchdog_port)
skynet.exit()
end)

在主入口service/main.lua中,通过skynet.start(callback)的形式,就可以定义该服务启动时的逻辑。首先启动了协议服务proto;其次通过skynet.getenv读取debug_console_port配置项,如果有则启动skynet内置debug_console服务;而后启动db服务,是一个意思意思的内存kv数据库;之后启动watchdog,监听配置的watchdog_port所对应的端口。

我们可以通过解构watchdog服务,从而了解skynet服务的基本样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
-- service/watchdog/main.lua
local skynet = require "skynet"

local CMD = {}
local SOCKET = {}
local gate
local agent = {}

function SOCKET.open(fd, addr)
skynet.error("New client from : " .. addr)
agent[fd] = skynet.newservice("agent")
skynet.call(agent[fd], "lua", "start", {
gate = gate, client = fd, watchdog = skynet.self()
})
end

local function close_agent(fd)
local a = agent[fd]
agent[fd] = nil
if a then
skynet.call(gate, "lua", "kick", fd)
-- disconnect never return
skynet.send(a, "lua", "disconnect")
end
end

function SOCKET.close(fd)
print("socket close",fd)
close_agent(fd)
end

function SOCKET.error(fd, msg)
print("socket error",fd, msg)
close_agent(fd)
end

function SOCKET.warning(fd, size)
-- size K bytes havn't send out in fd
print("socket warning", fd, size)
end

function SOCKET.data(fd, msg)
end

function CMD.start(conf)
skynet.call(gate, "lua", "open" , conf)
end

function CMD.close(fd)
close_agent(fd)
end

skynet.start(function()
skynet.dispatch("lua", function(session, source, cmd, subcmd, ...)
if cmd == "socket" then
local f = SOCKET[subcmd]
f(...)
-- socket api don't need return
else
local f = assert(CMD[cmd])
skynet.ret(skynet.pack(f(subcmd, ...)))
end
end)
gate = skynet.newservice("gate")
end)

watchdog服务主入口的代码中,出现了skynet.dispatch,它会为某个类型的消息注册回调函数从而进行处理。如果哪个服务调用了skynet.send/call(watchdog地址, "lua", ...),那么watchdog就会拿回调函数跟后边...的参数凑上去,执行业务逻辑了。

在skynet架构中,watchdog与内置的网关服务gate是强耦合的。我们可以看到watchdog在刚启动时也会新增gate服务,然后service/main.lua调用skynet.call(watchdog, "lua", "start", 配置)时,watchdog主入口中对应的CMD.start就会被执行,然后在gate调用的gateserver.lua中,CMD.open的开启端口监听的逻辑被执行了。之后,如果有新的连接,gate会通知watchdog新连接的fd跟地址,并且而watchdog只需要负责新增agent服务,根据socket的不同情况执行相应回调管理agent就好了。

agent服务里,只需要根据连接的fd,读取或发送数据就好,网关服务gate会帮你分包。这里agent主入口也采用官方例子中的代码,基本的套路也是一方面不断read这个fd出来的数据,识别sproto,然后调用db服务存取数据;另一方面会隔一段时间向fd写入心跳包,实现双工长连接tcp。此处便不再赘述啦~

总结

skynet的基本用法便是如此,后面还有很多挖掘点,可以查看wiki等资料深入探索~

版权声明
本文为博客HiKariのTechLab原创文章,转载请标明出处,谢谢~~~