【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

文章目录

一、前言

嗨,大家好,我是新发。
认识我的朋友都知道我是一名Unity游戏开发工程师,也就是我平时做的是客户端部分的开发,其实以前我是一名服务端开发工程师,后来因为工作原因,转岗做了Unity客户端开发,然后就一直干到现在。
最近,我在搞服务端的skynet框架,看看以后自己做些作品(skynet框架服务端+Unity客户端)。今天呢,我就先把skynet环境搞一下,讲讲流程,也方便想学习的同学,话不多说,我们开始吧~

二、关于Skynet

Skynet是一个轻量级的网络游戏框架,可用于许多其他领域。
建议看下云风的《Skynet设计综述》。

三、Ubuntu虚拟机

Skynet需要运行在linuxmacos系统中,我这里作为演示,就搞一个Ubuntu虚拟机好了。

1、Ubuntu系统镜像下载

下面这些地址都可以下载,大家选择一个即可。
网易开源镜像:http://mirrors.163.com/ubuntu-releases/
Ubuntu官方:http://releases.ubuntu.com/
Ubuntu中国官网:https://ubuntu.com/download/alternative-downloads
中科开源镜像:http://mirrors.ustc.edu.cn/ubuntu-releases/
阿里开源镜像:http://mirrors.aliyun.com/ubuntu-releases/
浙江大学开源镜像:http://mirrors.zju.edu.cn/ubuntu-releases/

我以Ubuntu 16.04.7版本为例,地址:http://mirrors.163.com/ubuntu-releases/16.04.7/
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
iso文件下载到本地,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

2、VirtualBox虚拟机软件

虚拟机软件大家常用的可能是VMWare,这里我强烈推荐使用VirtualBox,轻量、开源免费,非常好用。

VirtualBox是一款开源虚拟机软件。VirtualBox是由德国Innotek公司开发,由Sun Microsystems公司出品的软件,使用Qt编写,在 SunOracle收购后正式更名成 Oracle VM VirtualBox
VirtualBox号称是最强的免费虚拟机软件,它不仅具有丰富的特色,而且性能也很优异!它简单易用,可虚拟的系统包括Windows(从Windows 3.1Windows 10Windows Server 2012,所有的Windows系统都支持)、Mac OS XLinuxOpenBSDSolarisIBM OS2甚至Android等操作系统!使用者可以在VirtualBox上安装并且运行上述的这些操作系统!

2.1、VirtualBox下载

VirtualBox官网:https://www.virtualbox.org/
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
选择windows版本,点击下载,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

下载完毕,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

2.2、VirtualBox安装

运行安装,过程没有什么特别的,这里不赘述。
安装成功后打开VirtualBox
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

2.3、创建虚拟机

打开VirtualBox,点击菜单控制/新建
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
填写虚拟机名称,设置虚拟机保存路径,如下,我设置为E:\ubuntu16
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

设置内存大小,建议分配2G内存,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

创建虚拟硬盘,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

建议分配10G的虚拟硬盘空间,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
虚拟机创建完成,如下
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

3、载入Ubuntu iso镜像

点击启动虚拟机,会提示选择启动盘,点击下面的小按钮,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
点击注册,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
选择我们刚刚下载的iso系统镜像文件,打开,可以看到列表中出现了我们的镜像,选中它,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
点击启动,即可进入系统安装。

4、Ubuntu系统安装过程

点击Install Ubuntu
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
点击Continue
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
点击Install Now
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
此时会弹个提示框,点击Continue
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
时区填写China Time,然后点击Continue
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
语言默认English,点击Continue
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
接着输入账号密码,后面进入系统的时候要用到,这里提示我的密码弱(Weak password),由于只是自己学习使用,密码弱也没什么关系,点击Continue
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
接着就是耐心等待它安装,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
完成后,会提示需要重启,点击Restart Now
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
进入下面这个界面时,按一下回车键,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
输入刚刚设置的密码,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
顺利进入系统,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

四、安装必要的工具

1、安装git

我们需要通过git来下载skynet,所以必须先安装git,我们先打开终端,如下:
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
在终端输入下面的命令:(注意按回车后需要输入一次密码,并且密码不会显示出来,不要怀疑你的键盘)

sudo apt-get install git

如下:【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
安装完毕后,输入下面的命令检查下git是否安装成功了,

git -v

如果输出了版本号,则说明git已经安装成功了,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

2、安装autoconf

编译skynet需要用到autoconf,在终端输入下面的命令来安装autoconf

sudo apt-get install autoconf

如下:
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
安装完毕后,输入autoconf --version按回车,如果能输出版本号,则说明安装成功了,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

3、安装gcc

编译skynet需要用到gcc,因为Ubuntu默认安装了gcc,所以我们这里就不用重复安装了,可以在终端中输入gcc --version,如果能输出版本号,则说明已经安装了gcc
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
如果提示没有gcc,则执行sudo apt-get install gcc进行安装即可。

五、下载Skynet源码

在终端中执行下面的命令,

git clone https://gitee.com/mirrors/skynet.git

如下:
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
下载完毕后,我们打开文件夹浏览器,可以在Home目录中看到多了一个skynet文件夹,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
进入skynet文件夹,就可以看到框架源码啦,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

六、编译Skynet源码

在终端中进入skynet目录,

cd skynet

如下:
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
然后执行下面的命令:

make linux

此处你可能会报错,如下:
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
这是因为过程中去gitub下载jemalloc失败了,可以多试几次,编译成功后显示的输出内容如下:
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
我们可以在skynet目录中看到生成了一个可执行文件:skynet,如下:
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

七、运行Skynet案例

在终端中进入skynet目录后,执行下面的命令,

./skynet example/config

如下,可以看到服务启动成功了,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
接下来,我们开启一个新的终端,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
运行一个客户端来测试一下,先cd skynet进入目录,
然后执行如下命令:

./3rd/lua/lua example/client.lua

如下,每隔5秒就会给服务端发送一个heartbeat心跳包,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
我们可以输入hello,服务器会回应一个world,如下:
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

八、写个Demo

想要自己写个Demo,得先知道skynet是如何工作的。

1、配置文件

运行skynet时,需要制定一个配置文件,例:

./skynet example/config

我们先看看这个config文件里面是啥,进入examples目录,打开config文件,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
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文件,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
内容如下:

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"

现在,我们把两个文件合在一起看,如下:

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"

我这里对几个重要的参数做一下说明:

参数 描述
luaservice 服务脚本路径,包括skynet框架自带的一些服务和自己写的服务
lualoader lua脚本加载器,指定skynetloader.lua
lua_path 程序加载lua脚本时,会搜索这个lua_path配置的路径
lua_cpath C语言编写的程序库(.so文件)的路径
thread 启用的工作线程数量,一般配置为CPU核心数
harbor 主从节点模式。skynet初期提供了master/slave集群模式,后来提供了更适用的cluster集群模式,建议使用cluster模式,配0
start 主服务入口
cpath C语言编写的服务模块的路径

画个图,强化记忆:
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

2、规范目录结构

上面介绍了配置文件,现在我们可以自己写一个配置文件啦,不过,实际项目中,一般会先规范一下目录结构,我们把跟目录重命名为game,新建一些子文件夹:

文件夹 说明
etc 存放配置文件
luaclib 存放一些C模块(.so文件)
lualib 存放lua模块
service 存放各服务的lua代码
skynet 存放skynet框架(存放我们刚刚下载的skynet框架源码)

start.sh为启动脚本。

最终如下:
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

3、自己写个配置文件

我们在etc目录中新建一个config.node1配置,如下:
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
因为我们把skynet框架源码丢在了skynet子目录中,所以我们配置路径的时候需要加多一层skynet,最终config.node1配置如下:

thread = 8
cpath = "./skynet/cservice/?.so"
bootstrap = "snlua bootstrap"

start = "main"
harbor = 0

lualoader = "./skynet/lualib/loader.lua"

luaservice = "./service/?.lua;" .. "./service/?/init.lua;" .. "./skynet/service/?.lua;"

lua_path = "./etc/?.lua;" .. "./lualib/?.lua;" .. "./skynet/lualib/?.lua;" .. "./skynet/lualib/?/init.lua;"

lua_cpath = "./luaclib/?.so;" .. "./skynet/luaclib/?.so"

4、主服务

上面我们配置的主服务是main

start = "main"

它会去配置的luaservice路径中查找一个main.lua脚本,

luaservice = "./service/?.lua;" .. "./service/?/init.lua;" .. "./skynet/service/?.lua;"

框架会去启动这个main服务,我们现在还没有这个main.lua脚本,现在我们就来写这个main.lua脚本吧~

进入service目录,创建main.lua脚本,代码如下:

local skynet = require "skynet"
skynet.start(function()
	skynet.error("[start main] hello world")
	
	-- TODO 启动其他服务
	
	skynet.exit()
end)

上面我们用到了skynet的三个API
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
有同学会问了,明明说三个API,我怎么列了四个,那个newservice(name, ...)没看呀!
因为main是主服务,它是由框架来启动的,所以是框架帮我们调用了newservice,如果我们想在主服务中启动其他服务,就要自己调用newservice

现在我们打开终端,进入game目录,然后执行命令:

./skynet/skynet etc/config.node1

运行效果如下,可以看到main.lua脚本被执行了,输出了[start main] hello world
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

5、写个打工服务

service文件夹中新建一个worker目录,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
进入worker目录,新建一个init.lua脚本,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

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
        local isok, err = pcall(update, frame)
        if not isok then
            skynet.error(err)
        end
        local etime = skynet.now()
        -- 保证0.2秒
        local waittime = frame * 20 - (etime - stime)
        if waittime <= 0 then
            waittime = 2
        end
        skynet.sleep(waittime)
    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)

这里对代码说明一下,timer定时器函数中,waittime代表每次循环等待的时间,由于程序有可能会卡住,我们很难保证 “每隔0.2秒调用一次update” 是精确的,update函数本身执行也需要时间,所以等待时间是0.2减去执行时间,执行时间就是etime - stime

6、在主服务中启动打工服务

我们回到主服务main.lua脚本中,添加一句skynet.newservice调用,如下:

-- main.lua脚本

local skynet = require "skynet"

skynet.start(function ()
    skynet.error("[start main] hello world")

	-- 启动打工服务,其中第二个参数和第三个参数会透传给service/worker/init.lua脚本
    local worker1 = skynet.newservice("worker", "worker", 1)

    skynet.exit()
end)

这样就启动了打工服务了,我们在game目录中执行命令

./skynet/skynet etc/config.node1

运行效果如下,可以看到启动了一个worker服务,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
有同学可能会问了,我们调用skynet.newservice时第一个参数是worker,框架怎么知道会去执行service/worker/init.lua脚本呢?
还记得我们的config.node1配置吗,里面的luaservice我们配置了"./service/?/init.lua;",如下:

-- config.node1配置

luaservice = "./service/?.lua;" .. "./service/?/init.lua;" .. "./skynet/service/?.lua;"

其中,?符号会匹配服务名,也就是说,当我们调用skynet.newservice("worker")时,框架先去检查./service/worker.lua脚本是否存在,发现不存在,于是接着检查./service/worker/init.lua脚本,发现存在,于是执行./service/worker/init.lua脚本作为worker服务,当然,如果找不到,它就会去检查./skynet/service/worker.lua是否存在了。

另外,newservice的函数原型是newservice(name, ...),我们调用skynet.newservice时可以透传一些参数给服务,比如我们上面的

-- main.lua脚本

local worker1 = skynet.newservice("worker", "worker", 1)

第二个参数和第三个参数就会透传给init.lua脚本,我们在init.lua脚本中可以取出来缓存起来,如下:

-- service/worker/init.lua脚本

-- 服务名
local worker_name = ""
-- 服务id
local worker_id = ""

local function init(name, id)
    worker_name = name
    worker_id = id
end

init(...)

7、在主服务中给打工服务发消息

打工服务中我们定义了两个消息:start_workstop_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)
    -- 开始工作
    skynet.send(worker1, "lua", "start_work")
	-- 主服务休息2秒,注意,这里是主服务休息2秒,并不会卡住worker服务
    skynet.sleep(200)
    -- 停止工作
    skynet.send(worker1, "lua", "stop_work")
    
    skynet.exit()
end)

我们再次执行命令

./skynet/skynet etc/config.node1

运行效果如下,可以看到打工服务开始工作了,2秒赚了10块钱~
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

8、封装服务类

假设我们现在要再写一个买猫粮的服务,这个时候,可以按照上面的打工服务写一个服务。事实上,每个服务都有一些通用的变量和方法,我们可以封装一个service类,方便复用减少代码量。
我们在lualib目录中新建一个service.lua脚本,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
service.lua代码如下,

local skynet = require "skynet"
local cluster = require "skynet.cluster"

-- 封装服务类
local M = {
	-- 服务名
    name = "",
    -- 服务id
    id = 0,
    -- 退出
    exit = nil,
    -- 初始化
    init = nil,
    -- 消息响应函数表
    resp = {},
}

-- 输出堆栈
local function tracback(err)
    skynet.error(tostring(err))
    skynet.error(debug.traceback())
end

-- 消息分发
local dispatch = function (session, address, cmd, ...)
	-- 从resp表中查找是否存在消息的响应函数
    local func = M.resp[cmd]
    if not func then
        skynet.ret()
        return
    end
	-- 调用响应函数
    local ret = table.pack(xpcall(func, tracback, address, ...))
    local isok = ret[1]

    if not isok then
        skynet.ret()
        return
    end

    skynet.retpack(table.unpack(ret, 2))
end

-- 初始化
local function init()
    skynet.error(M.name .. " " .. M.id .. " init")
    skynet.dispatch("lua", dispatch)
    if M.init then
        M.init()
    end
end

-- 启动服务
function M.start(name, id, ...)
    M.name = name
    M.id = tonumber(id)
    skynet.start(init)
end

return M

9、重写打工服务

有了service类,我们写服务的时候,只需要按下面这个模板写就可以了,

local skynet = require "skynet"
local s = require "service"

s.init = function()
	-- 初始化
end

s.resp.协议1 = function(source, ...)
	-- TODO
end

s.resp.协议2 = function(source, ...)
	-- TODO
end

s.start(...)

现在,我们重新写一下打工服务,改造后代码如下,我们后面新写服务也按照这个格式来写,统一,方便维护,

local skynet = require "skynet"
local s = require "service"

s.money = 0
s.isworking = false

s.update = function(frame)
    if s.isworking then
        s.money = s.money + 1
        skynet.error(s.name .. tostring(s.id) .. ", money: " .. tostring(s.money))
    end
end

s.init = function ()
    skynet.fork(s.timer)
end

s.timer = function()
    local stime = skynet.now()
    local frame = 0
    while true do
        frame = frame + 1
        local isok, err = pcall(s.update, frame)
        if not isok then
            skynet.error(err)
        end
        local etime = skynet.now()
        local waittime = frame * 20 - (etime - stime)
        if waittime <= 0 then
            waittime = 2
        end
        skynet.sleep(waittime)
    end
end


s.resp.start_work = function(source)
    s.isworking = true
end

s.resp.stop_work = function(source)
    s.isworking = false
end

s.start(...)

10、买猫粮服务

打工挣钱,有了钱我们就可以买猫粮啦,我们来写一个买猫粮的服务脚本。
我们在service文件夹中新建一个buy文件夹,如下,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
进入buy目录,然后创建一个init.lua脚本,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
代码如下:

-- service/buy/init.lua脚本

local skynet = require "skynet"
local s = require "service"

s.cat_food_price = 5
s.cat_food_cnt = 0

s.resp.buy = function (source)
	-- 先扣费
	local left_money = skynet.call("worker1", "lua", "change_money", -s.cat_food_price)
    if left_money >= 0 then
        s.cat_food_cnt = s.cat_food_cnt + 1
        skynet.error("buy cat food ok, current cnt: " .. tostring(s.cat_food_cnt))
        return true
    end
    -- 购买失败,把钱加回去
    skynet.error("buy failed, money not enough")
    skynet.call("worker1", "lua", "change_money", s.cat_food_price)
    return false
end

s.start(...)

然后我们给打工服务加多一个消息,

-- service/worker/init.lua脚本

s.resp.change_money = function(source, delta)
	s.money = s.money + delta
	return s.money
end

最后,在主服务中加上买猫粮服务的启动和消息发送,如下:

local skynet = require "skynet"

skynet.start(function ()
    skynet.error("[start main] hello world")
	-- 启动打工服务
    local worker1 = skynet.newservice("worker", "worker", 1)
    -- 启动买猫粮服务
    local buy1 = skynet.newservice("buy", "buy", 1)

	-- 开始打工
    skynet.send(worker1, "lua", "start_work")
    skynet.sleep(200)
    -- 结束打工
    skynet.send(worker1, "lua", "stop_work")

    -- 买猫粮
    skynet.send(buy1, "lua", "buy")
    
    -- 退出主服务
    skynet.exit()
end)

好啦,现在我们测试一下,运行效果如下:
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
可以看到,最后买猫粮成功啦~
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

九、补充

1、网络模块

写服务端,肯定需要涉及到网络模块,需要用到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 处理消息
			
			-- 回应客户端,把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)

如果要写一个多人聊天功能的话,只需要把消息广播出去即可,例:

-- main.lua

local skynet = require "skynet"
local socket = require "skynet.socket"

local clients = {}

local function on_connect(fd, addr)
	clients[fd] = {}
	socket.start(fd)
	while true do
		local readdata = socket.read(fd)
		if readdata then
			-- 广播
			for client_fd, _ in pairs(clients) do
				socket.write(client_fd, readdata)
			end 
		else
			-- 连接断开了
			socket.close(fd)
			clients[fd] = nil
		end
	end
end

skynet.start(function()
	local listenfd = socket.listen("0.0.0.0", 8888)
	socket.start(listenfd, on_connect)
end)

2、节点集群

我上面写的打工赚钱买猫粮,都在同一个节点中,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

实际项目中,可能会开启多个节点,两个服务如果在同一个节点中,则通过skynet.sendskynet.call来传递消息,

注:send是发送消息,不会阻塞调用方;call是阻塞调用。

【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

如果在不同的节点中,则需要使用cluster.sendcluster.call

注:send是发送消息,不会阻塞调用方;call是阻塞调用。

【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

对此,我们可以优化一下上文中的service.lua,封装sendcall方法,

-- lualib/service.lua

local cluster = require "skynet.cluster"

...

function M.call(node, srv, ...)
	local mynode = skynet.getenv("node")
	if node == mynode then	
		return skynet.call(srv, "lua", ...)
	else
		return cluster.call(node, srv, ...)
	end
end

function M.send(node, srv, ...)
	local mynode = skynet.getenv("node")
	if node == mynode then	
		return skynet.send(srv, "lua", ...)
	else
		return cluster.send(node, srv, ...)
	end
end

我们在etc/config.node1的末尾加多一行

node = "node1"

拷贝一份,重命名为config.node2,把末尾一行改为

node = "node2"

然后,我们改下main.lua脚本,让它在node1节点开启打工服务,在node2节点开启买猫粮节点,最终main.lua脚本如下:

local skynet = require "skynet"
local cluster = require "skynet.cluster"
require "skynet.manager"

skynet.start(function ()
    skynet.error("[start main] hello world")

	-- 集群配置
    cluster.reload({
        node1 = "127.0.0.1:7001",
        node2 = "127.0.0.1:7002",
    })

    local mynode = skynet.getenv("node")
    if "node1" == mynode then
    	-- 启动集群节点
        cluster.open("node1")
        -- node1节点,开启打工服务
        local worker1 = skynet.newservice("worker", "worker", 1)
        skynet.name("worker1", worker1)
        skynet.send(worker1, "lua", "start_work")
        skynet.sleep(200)
        skynet.send(worker1, "lua", "stop_work")
    elseif "node2" == mynode then
    	-- 启动集群节点
        cluster.open("node2")
        -- node2节点,开启买猫粮服务
        local buy1 = skynet.newservice("buy", "buy", 1)
        -- 请求买猫粮,买三次
        skynet.send(buy1, "lua", "buy")
        skynet.send(buy1, "lua", "buy")
        skynet.send(buy1, "lua", "buy")
    end

    skynet.exit()
end)

最后,我们改下buy/init.lua脚本,把skynet.call改成s.call,如下:

local skynet = require "skynet"
local s = require "service"

s.cat_food_price = 5
s.cat_food_cnt = 0

s.resp.buy = function (source)

    local left_money = s.call("node1", "worker1", "change_money", -s.cat_food_price)

    if left_money >= 0 then
        s.cat_food_cnt = s.cat_food_cnt + 1
        skynet.error("buy cat food ok, current cnt: " .. tostring(s.cat_food_cnt))
        return true
    end
    skynet.error("buy cat food failed, money not enough")
    s.call("node1", "worker1", "change_money", s.cat_food_price)
    return false
end

s.start(...)

好了,现在我们先开启节点1,在终端执行命令

./skynet/skynet etc/config.node1

可以看到节点1开启了打工服务,赚了10块钱

【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
现在我们开启节点2,在终端执行命令

./skynet/skynet etc/config.node2

因为猫粮价格是5块钱一包,所以只能买两次,执行结果如下,可以看到第三次买猫粮失败,因为钱不够了,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

3、数据库模块

3.1、安装MySQL

在终端执行命令

sudo apt-get install mysql-server

执行过程中会弹出框让你输入MySQLroot账号的密码,如下,需要输入两次,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
安装完毕后,执行mysql --version,如果输出版本号,则说明安装成功了,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

3.2、启动MySQL

在终端执行命令

service mysql start

此时会弹出一个框,注意此处输入Ubuntu的开机密码,而不是MySQLroot账号密码哦,点击Authenticate
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
启动成功后,我们可以通过下面的命令看是否有mysql的进程,

ps -axj |grep mysql

可以看到有mysql进程,说明MySQL服务已经成功启动了,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

3.3、关闭MySQL

在终端执行命令

service mysql stop

此时会弹出一个框,此处输入Ubuntu的开机密码,点击Authenticate
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
执行完毕后,同理,我们可以通过下面的命令查看是否已经没有mysql进程了,

ps -axj |grep mysql
3.4、登录MySQL

在终端执行命令

mysql -h127.0.0.1 -uroot -p你的密码

如下,登录成功,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
现在我们可以愉快地使用mysql啦,执行show databases;,查看所有的数据库
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

3.5、在skynet中操作数据库

我们先在终端手动创建一个数据库,

create database test_db;

如下,
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
选择test_db
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
接着再创建一个表,

create table users (
	id int not null auto_increment,
	name varchar(30) not null,
	primary key (id));

如下:
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)
好了,现在我们在skynet中来操作数据库吧,例:

local skynet = require "skynet"
local mysql = require "skynet.db.mysql"


skynet.start(function ()
    skynet.error("[start main] hello world")

    local db = mysql.connect(
        {
            host = "127.0.0.1",
            port = 3306,
            database = "test_db",
            user = "root",
            password = "123456",
            max_packet_size = 1024 * 1024,
            on_connect = nil
        }
    )
	-- 插入数据
    local res = db:query("insert into users(name) values (\'linxinfa\')")
    -- 查询数据
    res = db:query('select * from users')
    for i, v in pairs(res) do
        print(i, " " .. v.id .. " " .. v.name)
    end

    skynet.exit()
end)

我们执行两次,结果如下,可以看到,数据正常写入到数据库中了,成功~
【游戏开发实战】手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

十、完毕

好了,就先写这么多吧,关于skynet还有很多很多内容,本文只是一个入门,希望可以帮助到新手同学~
我是林新发:https://blog.csdn.net/linxinfa
原创不易,若转载请注明出处,感谢大家~
喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信~

上一篇:五子棋Lua实现


下一篇:用于测试练习的docker-compose web项目框架