前置准备
因为这里使用的linux版本的skynet,所以需要先准备linux环境,我使用的是 ubuntu 22.04 LTS 版本,是在微软商店下载的;
skynet是使用c语言编写的网络库,需要先安装编译的条件
# c语言编译工具
apt install gcc -y
# 安装构建工具
apt install make -y
安装 skynet
github地址:https://github.com/cloudwu/skynet
源码安装
下载源码, 这里指定 v1.7.0版本
git clone https://github.com/cloudwu/skynet.git
cd skynet
git checkout v1.7.0
# 下载子项目的代码
git submodule update --init --recursive
以上是使用github 进行下载,若因为网络问题无法下载,可使用国内的网站下载
# 下载skynet
git clone https://gitcode.com/cloudwu/skynet.git
cd skynet
# 切换到最新版本
git checkout v1.7.0
# 下载子项目的代码
cd 3rd
git clone https://gitcode.com/jemalloc/jemalloc.git
目录说明
- 3rd:存放第三方的代码,如Lua、jemalloc、lpeg等。
- cservice:存放内置的用c语言编写的服务,如gate、harbor、snlua等。
- examples:范例。
- luaclib:用c语言编写的程序库,如bson解析、md5解析等
- lualib:用Lua编写的程序库。
- lualib-src:luaclib目录下,库文件的源码。
- service:包含skynet内置的一些服务。用Lua编写的服务。
- service-src:cservice目录下,程序代的源码。
- skynet-src:用c写的skynet核心代码。
- test:测试代码。
编译
# 进入目录进行编译
cd skynet
make linux
报错解决
autoreconf: not found
若报错以下信息,是因为 没有安装 automake, just to do below:
make all PLAT=linux SKYNET_LIBS="-lpthread -lm -ldl -lrt" SHARED="-fPIC --shared" EXPORT="-Wl,-E" MALLOC_STATICLIB="3rd/jemalloc/lib/libjemalloc_pic.a" SKYNET_DEFINES=""
make[1]: Entering directory '/root/skynet'
cd 3rd/jemalloc && ./autogen.sh --with-jemalloc-prefix=je_ --enable-prof
autoconf
./autogen.sh: 5: autoconf: not found
Error 0 in autoconf
make[1]: *** [Makefile:44: 3rd/jemalloc/Makefile] Error 1
make[1]: Leaving directory '/root/skynet'
make: *** [platform.mk:40: linux] Error 2
解决方案
执行以下命令即可
sudo apt-get install autoconf automake libtool -y
然后继续执行 make linux
命令
启动
编译完成后会在当前目录下生成一个名为 skynet 的可执行文件, 输入./skynet ./examples/config
命令即可运行,当展示以下信息时表示已运行成功,端口号为:8080
root@PAw9033927:~/skynet# ./skynet ./examples/config
[:01000002] LAUNCH snlua bootstrap
[:01000003] LAUNCH snlua launcher
[:01000004] LAUNCH snlua cmaster
[:01000004] master listen socket 0.0.0.0:2013
[:01000005] LAUNCH snlua cslave
[:01000005] slave connect to master 127.0.0.1:2013
[:01000004] connect from 127.0.0.1:65512 4
[:01000006] LAUNCH harbor 1 16777221
[:01000004] Harbor 1 (fd=4) report 127.0.0.1:2526
[:01000005] Waiting for 0 harbors
[:01000005] Shakehand ready
[:01000007] LAUNCH snlua datacenterd
[:01000008] LAUNCH snlua service_mgr
[:01000009] LAUNCH snlua main
[:01000009] Server start
[:0100000a] LAUNCH snlua protoloader
[:0100000b] LAUNCH snlua console
[:0100000c] LAUNCH snlua debug_console 8000
[:0100000c] Start debug console at 127.0.0.1:8000
[:0100000d] LAUNCH snlua simpledb
[:0100000e] LAUNCH snlua watchdog
[:0100000f] LAUNCH snlua gate
[:0100000f] Listen on 0.0.0.0:8888
[:01000009] Watchdog listen on 0.0.0.0:8888
[:01000009] KILL self
[:01000002] KILL self
连接
执行如下命令,每隔5秒就会给服务端发送一个heartbeat心跳包,
./3rd/lua/lua examples/client.lua
如下 ,输入 hello 后会返回一个 world 字符
root@PAw9033927:~/skynet# ./3rd/lua/lua examples/client.lua
Request: 1
Request: 2
RESPONSE 1
msg Welcome to skynet, I will send heartbeat every 5 sec.
RESPONSE 2
REQUEST heartbeat
REQUEST heartbeat
hello # 发送
Request: 3
RESPONSE 3
result world # skynet返回的结果
REQUEST heartbeat
REQUEST heartbeat
写个Demo
想要自己写个Demo,得先知道skynet是如何工作的。
1、配置文件
运行skynet时,需要制定一个配置文件,例:
./skynet example/config
我们先看看这个config文件里面是啥,进入examples目录,打开config文件,内容如下
include "config.path"
-- preload = "./examples/preload.lua" -- run preload.lua before every lua service run
thread = 8
logger = nil
logpath = "."
harbor = 1
address = "127.0.0.1:2526"
master = "127.0.0.1:2013"
start = "main" -- main script
bootstrap = "snlua bootstrap" -- The service for bootstrap
standalone = "0.0.0.0:2013"
-- snax_interface_g = "snax_g"
cpath = root.."cservice/?.so"
-- daemon = "./skynet.pid"
第一行引用了config.path文件,我们打开config.path文件,内容如下:
root = "./"
luaservice = root.."service/?.lua;"..root.."test/?.lua;"..root.."examples/?.lua;"..root.."test/?/init.lua"
lualoader = root .. "lualib/loader.lua"
lua_path = root.."lualib/?.lua;"..root.."lualib/?/init.lua"
lua_cpath = root .. "luaclib/?.so"
snax = root.."examples/?.lua;"..root.."test/?.lua"
现在,我们把 config 、config.path 两个文件合在一起看,如下:
root = "./"
luaservice = root.."service/?.lua;"..root.."test/?.lua;"..root.."examples/?.lua;"..root.."test/?/init.lua"
lualoader = root .. "lualib/loader.lua"
lua_path = root.."lualib/?.lua;"..root.."lualib/?/init.lua"
lua_cpath = root .. "luaclib/?.so"
snax = root.."examples/?.lua;"..root.."test/?.lua"
-- preload = "./examples/preload.lua" -- run preload.lua before every lua service run
thread = 8
logger = nil
logpath = "."
harbor = 1
address = "127.0.0.1:2526"
master = "127.0.0.1:2013"
start = "main" -- main script
bootstrap = "snlua bootstrap" -- The service for bootstrap
standalone = "0.0.0.0:2013"
-- snax_interface_g = "snax_g"
cpath = root.."cservice/?.so"
-- daemon = "./skynet.pid"
这里对几个重要的参数做一下说明:
参数 | 描述 |
---|---|
thread |
设置工作线程数 |
luaservice |
服务脚本路径,包含 skynet 自带的服务和自己写的服务 |
lualoader |
lua脚本加载器,指的是用哪一段 lua 代码加载 lua 服务。通常配置为 lualib/loader.lua ,再由这段代码解析服务名称,进一步加载 lua 代码。snlua 会将下面几个配置项取出,放在初始化好的 lua 虚拟机的全局变量中。具体可参考实现。 |
lua_path |
程序加载lua脚本时会搜索 lua_path 路径,将其添加到 package.path 中的路径,供 require 调用。 |
lua_cpath |
用c语言编写的程序库(.so 文件)路径, 将其添加到 package.cpath 路径,供 require 调用。 |
harbor |
可选项,用于指定当前节点的节点号。节点号用于标识节点之间的通信。如果不设置该项,Skynet将自动分配节点号。skynet后期提供了更适用的 cluster 模式,建议适用cluster,配 0 |
snax |
snax模版路径 |
logger |
若将logger配置项设置为nil表示禁用日志记录功能 |
logpath |
指定日志文件的保存路径。可以是绝对路径或相对路径。如果设置为nil或空字符串,则日志文件将保存在Skynet启动目录下的./log文件夹中。 |
address |
当前 skynet 节点的地址和端口,方便其它节点和它组网。注:即使你只使用一个节点,也需要开启控制中心,并额外配置这个节点的地址和端口。 |
master |
指定 skynet 控制中心的地址和端口,如果你配置了 standalone 项,那么这一项通常和 standalone 相同。 |
start |
主服务入口, 这是 bootstrap 最后一个环节将启动的 lua 服务,也就是你定制的 skynet 节点的主程序。默认为 main ,即启动 main.lua 这个脚本。这个 lua 服务的路径由下面的 luaservice 指定。 |
bootstrap |
启动的第一个服务以及其启动参数。默认配置为 snlua bootstrap ,即启动一个名为 bootstrap 的 lua 服务。通常指的是 service/bootstrap.lua 这段代码。 |
standalone |
如果把这个 skynet 进程作为主进程启动(skynet 可以由分布在多台机器上的多个进程构成网络),那么需要配置standalone 这一项,表示这个进程是主节点,它需要开启一个控制中心,监听一个端口,让其它节点接入。 |
cpath |
用 C 编写的服务模块的位置,通常指 cservice 下那些 .so 文件。如果你的系统的动态库不是以 .so 为后缀,需要做相应的修改。这个路径可以配置多项,以 ; 分割。 |
在skynet根目录下新建一个new_example目录,在里面创建 config.node1 文件
root@PAw9033927:~/skynet# mkdir new_example
root@PAw9033927:~/skynet# ls -l
total 8388
drwxr-xr-x 1 root root 4096 Apr 30 17:44 3rd
-rw-r--r-- 1 root root 13727 Apr 30 18:19 HISTORY.md
-rw-r--r-- 1 root root 1085 Apr 30 17:44 LICENSE
-rw-r--r-- 1 root root 3805 Apr 30 17:44 Makefile
-rw-r--r-- 1 root root 1656 Apr 30 18:19 README.md
drwxr-xr-x 1 root root 4096 Apr 30 17:47 cservice
drwxr-xr-x 1 root root 4096 May 7 15:42 examples
drwxr-xr-x 1 root root 4096 Apr 30 17:47 luaclib
drwxr-xr-x 1 root root 4096 Apr 30 18:19 lualib
drwxr-xr-x 1 root root 4096 Apr 30 18:19 lualib-src
drwxr-xr-x 1 root root 4096 May 13 18:24 new_example
-rw-r--r-- 1 root root 876 Apr 30 17:44 platform.mk
drwxr-xr-x 1 root root 4096 Apr 30 18:19 service
drwxr-xr-x 1 root root 4096 Apr 30 18:19 service-src
-rwxr-xr-x 1 root root 8555832 Apr 30 17:47 skynet
drwxr-xr-x 1 root root 4096 Apr 30 18:19 skynet-src
drwxr-xr-x 1 root root 4096 Apr 30 18:19 test
root@PAw9033927:~/skynet# cd new_example
root@PAw9033927:~/skynet# touch config.node1
config.node1 内容如下
thread = 4
cpath = "./cservice/?.so"
bootstrap = "snlua bootstrap"
start = "main"
harbor = 0
lualoader = "./lualib/loader.lua"
luaservice = "./service/?.lua;" .. "./new_example/service/?/init.lua;" .. "./new_example/service/?.lua;"
lua_path = "./etc/?.lua;" .. "./lualib/?.lua;" .. "./lualib/?.lua;" .. "./lualib/?/init.lua;"
lua_cpath = "./luaclib/?.so;" .. "./luaclib/?.so"
1.1、问号 (?) 的作用
在 config.node1 配置的服务脚本路径中,luaservice
有几个 ?
匹配符,这其实是用来匹配服务名用的; 主服务入口 配置的是 main
;
# 主服务入口
start = main
# 服务脚本路径
luaservice = "./service/?.lua;" .. "./new_example/service/?/init.lua;" .. "./new_example/service/?.lua;"
根据问号匹配符, skynet 启动后会根据以下顺序执行
- 先找
./service/main.lua
文件,若文件存在就会执行 - 若找不到,就会去找
./new_example/service/main/init.lua
文件,,若文件存在就会执行 - 若找不到,就会去找
./new_example/service/main.lua
,若文件存在就会执行 - 若以上三个文件都找不大到,在启动 skynet 时会报错:
[:00000008] lua loader error : ./lualib/loader.lua:24:
cannot open ./service/main.lua: No such file or directory
cannot open ./new_example/service/main/init.lua: No such file or directory
cannot open ./new_example/service/main.lua: No such file or directory
stack traceback:
[C]: in function 'error'
./lualib/loader.lua:24: in main chunk
2、主服务
上面我们配置的主服务是main,
start = "main"
它会去配置的luaservice路径中查找一个main.lua脚本; 框架会去启动这个main服务,我们现在还没有这个main.lua脚本,现在我们就来写这个main.lua脚本,在刚刚新建的 new_example 目录下新建一个 service 目录,service 目录新建一个main.lua的文件;
root@PAw9033927:~/skynet# cd new_example
root@PAw9033927:~/skynet# mkdir service
root@PAw9033927:~/skynet# touch main.lua
main.lua 内容如下
local skynet = require "skynet"
skynet.start(function()
skynet.error("[start main] hello world skynet,I'm coming!")
-- TODO 启动其他服务
skynet.exit()
end)
3、运行
回到 skynet 根目录,执行命令 ./skynet new_example/config.node1
后就可以看到控制台输出的内容了
root@PAw9033927:~/skynet# ./skynet new_example/config.node1
[:00000002] LAUNCH snlua bootstrap
[:00000003] LAUNCH snlua launcher
[:00000004] LAUNCH snlua cdummy
[:00000005] LAUNCH harbor 0 4
[:00000006] LAUNCH snlua datacenterd
[:00000007] LAUNCH snlua service_mgr
[:00000008] LAUNCH snlua main
[:00000008] [start main] hello world skynet,I'm coming!
[:00000008] KILL self
[:00000002] KILL self
写个打工的服务
1、新建打工服务
我们将打工服务 命名为 worker
,在 new_example/service 目录下新建一个目录,命名为 worker
,进入worker
目录,新建一个init.lua 文件,
cd new_example/service
mkdir worker
cd worker
touch init.lua
init.lua 文件内容如下
-- service/worker/init.lua脚本
local skynet = require "skynet"
-- 消息响应函数表
local CMD = {}
-- 服务名
local worker_name = ""
-- 服务id
local worker_id = ""
-- 工钱
local money = 0
-- 是否在工作
local isworking = false
-- 每帧调用,一帧的时间是0.2秒
local function update(frame)
if isworking then
money = money + 1
skynet.error(worker_name .. tostring(worker_id) .. ", money: " .. tostring(money))
end
end
-- 死循环定时器,每隔0.2秒调用一次update函数
local function timer()
local stime = skynet.now()
local frame = 0
while true do
frame = frame + 1
-- pcall 是 Lua 中的一个函数,用于捕获并处理函数调用过程中的错误。它的作用是在保护模式下调用一个函数,如果函数执行过程中发生错误,pcall 将捕获错误并返回错误信息,而不是中断程序的执行。
-- 调用 update函数
local isok, err = pcall(update, frame)
if not isok then
skynet.error(err)
end
-- 睡眠0.2秒,这个sleep单位是10ms起步的,如果输入1,就是延时10ms,如果输入10就是延时100ms,如果输入100,就是延时1秒
skynet.sleep(20)
end
end
-- 初始化
local function init(name, id)
worker_name = name
worker_id = id
end
-- 开始工作
function CMD.start_work(source)
isworking = true
end
-- 停止工作
function CMD.stop_work(source)
isworking = false
end
-- 调用初始化函数,...是不定参数,会从skynet.newservice的第二个参数开始透传过来
init(...)
skynet.start(function()
-- 消息分发
skynet.dispatch("lua", function(session, source, cmd, ...)
-- 从CMD这个表中查找是否有定义响应函数,如果有,则触发响应函数
local func = CMD[cmd]
if func then
func(source, ...)
end
end)
-- 开启一个协程启动定时器,里面是个死循环,会一直执行
skynet.fork(timer)
end)
2、在主服务启动打工服务
我们回到主服务main.lua脚本中,添加一句skynet.newservice调用,如下:
-- main.lua脚本
local skynet = require "skynet"
skynet.start(function ()
skynet.error("[start main] hello world")
-- 启动打工服务,其中第二个参数和第三个参数会透传给 new_example/service/worker/init.lua 脚本
local worker1 = skynet.newservice("worker", "worker", 1)
skynet.exit()
end)
现在我们测试一下,在根目录中执行命令
./skynet ./new_example/config_node1
运行效果如下,可以看到启动了一个worker服务
root@yexindong:~/skynet# ./skynet ./new_example/config_node1
[:00000002] LAUNCH snlua bootstrap
[:00000003] LAUNCH snlua launcher
[:00000004] LAUNCH snlua cdummy
[:00000005] LAUNCH harbor 0 4
[:00000006] LAUNCH snlua datacenterd
[:00000007] LAUNCH snlua service_mgr
[:00000008] LAUNCH snlua main
[:00000008] [start main] hello world skynet,I'm coming!
[:00000009] LAUNCH snlua worker worker 1 # 这是启动的worker 服务
[:00000008] KILL self
[:00000002] KILL self
有同学可能会问了,我们调用skynet.newservice时第一个参数是worker,框架怎么知道会去执行service/worker/init.lua脚本呢?
还记得我们的config.node1配置吗,里面的luaservice我们配置了”./service/?/init.lua;”,如下:
-- config.node1配置
luaservice = "./service/?.lua;" .. "./new_example/service/?/init.lua;" .. "./new_example/service/?.lua;"
其中,? 符号会匹配服务名,也就是说,当我们调用skynet.newservice(“worker”)时,框架先去检查./service/worker.lua脚本是否存在,发现不存在,于是接着检查./new_example/service/worker/init.lua脚本,发现存在,于是执行./new_example/service/worker/init.lua脚本作为worker服务,当然,如果找不到,它就会去检查./new_example/service/worker.lua是否存在了。
另外,newservice的函数原型是newservice(name, …),我们调用skynet.newservice时可以透传一些参数给服务,比如我们上面的
-- main.lua脚本
local worker1 = skynet.newservice("worker", "worker", 1)
第一个参数是服务名,第二个参数和第三个参数就会透传给init.lua脚本,我们在init.lua脚本中可以取出来缓存起来,如下:
-- new_example/service/worker/init.lua脚本
-- 服务名
local worker_name = ""
-- 服务id
local worker_id = ""
local function init(name, id)
worker_name = name
worker_id = id
end
-- 执行init函数,... 是 skynet.newservice 透传过来第2、3个参数
init(...)
3、在主服务给打工服务发消息
打工服务中我们定义了两个消息:start_work 和 stop_work,现在我们在主服务中给打工服务发送消息,添加skynet.send调用,如下:
local skynet = require "skynet"
skynet.start(function ()
skynet.error("[start main] hello world")
-- 启动打工服务,其中第二个参数和第三个参数会透传给service/worker/init.lua脚本
local worker1 = skynet.newservice("worker", "worker", 1)
-- 开始工作 - send是发送消息,不会阻塞调用方
skynet.send(worker1, "lua", "start_work")
-- 主服务休息2秒,注意,这里是主服务休息2秒,并不会卡住worker服务,这个sleep单位是10ms起步的,如果输入1,就是延时10ms,如果输入10就是延时100ms,如果输入100,就是延时1秒
skynet.sleep(200)
-- 2秒后停止工作
skynet.send(worker1, "lua", "stop_work")
skynet.exit()
end)
在执行一下命令
./skynet ./new_example/config_node1
输出结果如下, 2秒赚了10块钱
root@yexindong:~/skynet# ./skynet ./new_example/config_node1
[:00000002] LAUNCH snlua bootstrap
[:00000003] LAUNCH snlua launcher
[:00000004] LAUNCH snlua cdummy
[:00000005] LAUNCH harbor 0 4
[:00000006] LAUNCH snlua datacenterd
[:00000007] LAUNCH snlua service_mgr
[:00000008] LAUNCH snlua main
[:00000008] [start main] hello world skynet,I'm coming!
[:00000009] LAUNCH snlua worker worker 1
[:00000009] worker1, money: 1
[:00000009] worker1, money: 2
[:00000009] worker1, money: 3
[:00000009] worker1, money: 4
[:00000009] worker1, money: 5
[:00000009] worker1, money: 6
[:00000009] worker1, money: 7
[:00000009] worker1, money: 8
[:00000009] worker1, money: 9
[:00000008] KILL self
[:00000009] worker1, money: 10
[:00000002] KILL self
4、添加网络模块
写服务端,肯定需要涉及到网络模块,需要用到skynet.socket,以下是简单的案例:
-- main.lua
local skynet = require "skynet"
local socket = require "skynet.socket"
local function on_connect(fd, addr)
socket.start(fd)
while true do
local readdata = socket.read(fd)
if readdata then
-- TODO 处理消息
print("read data:" .. readdata)
-- 回应客户端,把readdata返回给客户端
socket.write(fd, "server get data: " .. readdata)
else
-- 连接断开了
socket.close(fd)
end
end
end
skynet.start(function()
local listenfd = socket.listen("0.0.0.0", 8888)
socket.start(listenfd, on_connect)
end)
5、打工服务和网络模块的结合
刚刚的打工服务是由服务端来开启的,那么现在我们需要改一下,由用户来发起打工;
修改下main.lua的代码如下,其他代码无需修改
local skynet = require "skynet"
local socket = require "skynet.socket"
local function on_connect(fd, addr)
-- 启动打工服务,其中第二个参数和第三个参数会透传给 new_example/service/worker/init.lua脚本
local worker1 = skynet.newservice("worker", "worker", 1)
socket.start(fd)
while true do
local readdata = socket.read(fd)
if readdata then
-- TODO 处理消息
print("收到命令:" .. readdata)
-- 去掉换行符
readdata = string.gsub(readdata,"\r\n","")
readdata = string.gsub(readdata,"\n","")
-- 去掉空格
readdata = string.gsub(readdata," ","")
-- 开始工作
skynet.send(worker1, "lua", readdata)
-- 主服务休息2秒,注意,这里是主服务休息2秒,并不会卡住worker服务
skynet.sleep(200)
skynet.send(worker1, "lua", "stop_work")
-- 告诉客户端,命令执行成功了
socket.write(fd, readdata .. " order execute success!")
else
-- 连接断开了
socket.close(fd)
end
end
end
skynet.start(function()
local listenfd = socket.listen("0.0.0.0", 8888)
socket.start(listenfd, on_connect)
end)
然后,我们再次运行启动命令
./skynet ./new_example/config_node1
使用linux 的 telnet工具连接skynet,连接成功后直接输入start_work
telnet 127.0.0.1 8888
执行后结果如下
root@yexindong:~# telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
start_work
start_work order execute success!
服务端的响应如下, 可以看到服务端开始打工了,最后赚到了10块钱
root@yexindong:~/skynet# ./skynet ./new_example/config_node1
[:00000002] LAUNCH snlua bootstrap
[:00000003] LAUNCH snlua launcher
[:00000004] LAUNCH snlua cdummy
[:00000005] LAUNCH harbor 0 4
[:00000006] LAUNCH snlua datacenterd
[:00000007] LAUNCH snlua service_mgr
[:00000008] LAUNCH snlua main
[:00000002] KILL self
[:0000000a] LAUNCH snlua worker worker 1
收到命令:start_work
[:0000000a] worker1, money: 1
[:0000000a] worker1, money: 2
[:0000000a] worker1, money: 3
[:0000000a] worker1, money: 4
[:0000000a] worker1, money: 5
[:0000000a] worker1, money: 6
[:0000000a] worker1, money: 7
[:0000000a] worker1, money: 8
[:0000000a] worker1, money: 9
[:0000000a] worker1, money: 10