引言:
一直都是从事客户端的开发工作,最近抽了点时间想了解一下服务器开发的相关知识,一番博客瞎逛之后,发现了一个不错的框架,云风大神的 skynet开源服务器框架,这不仅仅是针对于游戏服务器开发的框架,更是一个通用的服务器基础框架。
Skynet简介:
Skynet
主要工作是管理注册服务,并开启多线程协调服务之间的调用和通讯。
1.框架核心:
根据云风博客的描述,Skynet
的核心功能就是解决一个问题:
把一个符合规范的 C
模块,从 动态库
(so文件)中启动起来,绑定一个永不重复(即使模块退出)的数字id
做为其 handle
。模块
被称为 服务
(Service),服务间可以*发送消息。
- 每个
模块
可以向 Skynet 框架注册一个callback
函数,用来接收发给它的消息; - 每个服务都是被一个个
消息包
驱动,当没有包到来的时候,它们就会处于挂起状态
,此状态对 CPU 资源零消耗。如果需要自主逻辑,则可以利用 Skynet 系统提供的timeout消息
,定期触发。
名字服务:
Skynet 提供了名字服务
,还可以给特定的服务起一个易读的名字,而不是用 id 来指代它。id 和运行时态相关,无法保证每次启动服务,都有一致的 id ,但名字可以。
简而言之,这个框架完成的功能大概如下: Skynet
只负责把一个数据包从一个服务内发送出去,让同一进程内的另一个服务收到,调用对应的callback
函数处理。它保证,模块的初始化过程,每个独立的 callback
调用,都是 相互线程安全
的。编写服务的人不需要特别的为多线程环境考虑任何问题,专心处理发送给它的一个个数据包。
2.框架优点:
-
高低级语言配合:
Skynet
是一个融合了低级语言(C)消息框架和高级动态语言(lua)的混合体,这种结构称为hybrid framework
。选择运行高效的C
来写服务节点,也可以选择同样开发高效而且安全隔离的lua
来写上层业务。Skynet
的主要核心包括两部分:-
C语言
实现的消息循环和组件加载机制; -
lua
实现的以消息为中心的进入退出coroutine
(协程)的包装层。
-
组件化能力:
Skynet
内核(C部分)自身支持加载模块(.so
文件),你可以使用C语言去写性能有要求的服务节点,通过消息与其他节点配合。lua又是一个对C语言极为友好的动态语言,所以你可以找到很多现成的lua的C扩展,skynet/3rd
路径下可以放置你需要的各种组件,比如:CJSON
、sqlite
和OpenSSL
等。
3.单进程:
很多服务器框架在构建之初,就设想着用多进程的方式来解决高并发的问题,但是所带来的问题就是多进程不可避免的进程安全锁,这样的框架经常会因为部分代码的报错而导致死锁或者内存占用不释放等问题。很多优秀的服务器框架都是使用单进程,然后通过线程池来做消息轮询和任务执行的方式来实现的,这样能够避开锁所带来的诸多问题。
Skynet也是单进程的服务器框架,在单一进程上启动一个线程池,其中包括多个 worker
线程 、一个 socket
网络线程和一个 timer
时间线程。当创建了多个 lua服务
,每个服务都相当于Erlang中的一个 Actor
(可以简单理解为:可以并行运行的对象),每个服务都有自己的消息队列,skynet也有一个全局的消息队列,线程池中的 worker
线程会随机从消息队列中取出消息来执行直到消息队列为空。此外,每个 lua服务
中又可以通过启动多个 coroutine
(携程)来实现异步操作的目的。
Skynet下载配置:
要学习开源框架,第一步肯定是先拿来试用一下,然后再取剖析源码,接下来我们就尝试下载Skynet
框架,并尝试使用这套开源的框架来搭建一个测试服务器:
1.资源下载:
Github源码地址:cloudwu/skynet
假如当前是在Linux环境下,并已经安装有 git
工具,则可以直接使用 git
指令 git clone
来拉取Github仓库的 Skynet
最新源码:
sudo git clone https://github.com/cloudwu/skynet.git
假如执行正常,输出如下:
linsh@ubuntu:/mnt/Windows$ sudo git clone https://github.com/cloudwu/skynet.git
正克隆到 'skynet'...
remote: Counting objects: 8079, done.
remote: Compressing objects: 100% (22/22), done.
remote: Total 8079 (delta 1), reused 0 (delta 0), pack-reused 8057
接收对象中: 100% (8079/8079), 2.72 MiB | 24.00 KiB/s, done.
处理 delta 中: 100% (5442/5442), done.
检查连接... 完成。
假如还没安装git
工具,可以通过以下指令安装(我的操作系统是 Ubuntu14.04.4
):
sudo apt-get install git
2.源码目录结构:
关于源码主要目录及其作用如下:
skynet-master
--3rd //第三方代码,主要生产一些给lua用的so动态库
--example //示例工程
--lualib //lua库
--lualib-src //luaclib:给lua用的c库
--service //lua服务
--service-src //csservice:c服务
--skynet-src //skynet核心c源码主程序
--test //一些类库和接口调用的客户端用例
--HISTORY.md //版本更新日志
--LICENSE
--Makefile //编译脚本
--platform.mk //运行平台相关(支持Linux、MacOSX、freebsd操作系统)
3.源码编译:
-
工具安装:
在编译前还需要安装两个工具,不然会出现报错:-
安装
autoconf
:sudo apt-get install autoconf
否则会报如下错误:
cd 3rd/jemalloc && ./autogen.sh --with-jemalloc-prefix=je_ --disable-valgrind
autoconf
./autogen.sh: line 5: autoconf: command not found
Error 0 in autoconf
make[1]: *** [3rd/jemalloc/Makefile] Error 1
make[1]: Leaving directory `/data/skynet'
make: *** [linux] Error 2 -
安装
readline-devel
:sudo apt-get install libreadline-dev
否则会报如下错误:
lua.c:83:31: fatal error: readline/readline.h: 没有那个文件或目录
#include <readline/readline.h>
^
compilation terminated.
make[3]: *** [lua.o] 错误 1
make[3]:正在离开目录 `/application/skynet/3rd/lua'
make[2]: *** [linux] 错误 2
make[2]:正在离开目录 `/application/skynet/3rd/lua'
make[1]: *** [3rd/lua/liblua.a] 错误 2
make[1]:正在离开目录 `/application/skynet'
make: *** [linux] 错误 2
-
-
编译操作:
由于下载的是源码,需要进过编译才能运行,编译过程就是:-
进入
clone
到本地的项目目录,执行make
指令编译源码:cd skynet
sudo make linux假如编译过程正常,编译完成后如下:
linsh@ubuntu:/application/skynet$ sudo make linux
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]: 正在进入目录 `/application/skynet'
cc -g -O2 -Wall -I3rd/lua -o skynet skynet-src/skynet_main.c skynet-src/skynet_handle.c skynet-src/skynet_module.c skynet-src/skynet_mq.c skynet-src/skynet_server.c skynet-src/skynet_start.c skynet-src/skynet_timer.c skynet-src/skynet_error.c skynet-src/skynet_harbor.c skynet-src/skynet_env.c skynet-src/skynet_monitor.c skynet-src/skynet_socket.c skynet-src/socket_server.c skynet-src/malloc_hook.c skynet-src/skynet_daemon.c skynet-src/skynet_log.c 3rd/lua/liblua.a 3rd/jemalloc/lib/libjemalloc_pic.a -Iskynet-src -I3rd/jemalloc/include/jemalloc -Wl,-E -lpthread -lm -ldl -lrt
mkdir cservice
cc -g -O2 -Wall -I3rd/lua -fPIC --shared service-src/service_snlua.c -o cservice/snlua.so -Iskynet-src
cc -g -O2 -Wall -I3rd/lua -fPIC --shared service-src/service_logger.c -o cservice/logger.so -Iskynet-src
cc -g -O2 -Wall -I3rd/lua -fPIC --shared service-src/service_gate.c -o cservice/gate.so -Iskynet-src
cc -g -O2 -Wall -I3rd/lua -fPIC --shared service-src/service_harbor.c -o cservice/harbor.so -Iskynet-src
mkdir luaclib
cc -g -O2 -Wall -I3rd/lua -fPIC --shared lualib-src/lua-skynet.c lualib-src/lua-seri.c -o luaclib/skynet.so -Iskynet-src -Iservice-src -Ilualib-src
cc -g -O2 -Wall -I3rd/lua -fPIC --shared lualib-src/lua-socket.c -o luaclib/socketdriver.so -Iskynet-src -Iservice-src
cc -g -O2 -Wall -I3rd/lua -fPIC --shared -Iskynet-src lualib-src/lua-bson.c -o luaclib/bson.so -Iskynet-src
cc -g -O2 -Wall -I3rd/lua -fPIC --shared lualib-src/lua-mongo.c -o luaclib/mongo.so -Iskynet-src
cc -g -O2 -Wall -I3rd/lua -fPIC --shared -I3rd/lua-md5 3rd/lua-md5/md5.c 3rd/lua-md5/md5lib.c 3rd/lua-md5/compat-5.2.c -o luaclib/md5.so
cc -g -O2 -Wall -I3rd/lua -fPIC --shared lualib-src/lua-netpack.c -Iskynet-src -o luaclib/netpack.so
cc -g -O2 -Wall -I3rd/lua -fPIC --shared lualib-src/lua-clientsocket.c -o luaclib/clientsocket.so -lpthread
cc -g -O2 -Wall -I3rd/lua -fPIC --shared -Iskynet-src lualib-src/lua-memory.c -o luaclib/memory.so
cc -g -O2 -Wall -I3rd/lua -fPIC --shared lualib-src/lua-profile.c -o luaclib/profile.so
cc -g -O2 -Wall -I3rd/lua -fPIC --shared -Iskynet-src lualib-src/lua-multicast.c -o luaclib/multicast.so
cc -g -O2 -Wall -I3rd/lua -fPIC --shared -Iskynet-src lualib-src/lua-cluster.c -o luaclib/cluster.so
cc -g -O2 -Wall -I3rd/lua -fPIC --shared lualib-src/lua-crypt.c lualib-src/lsha1.c -o luaclib/crypt.so
cc -g -O2 -Wall -I3rd/lua -fPIC --shared -Iskynet-src lualib-src/lua-sharedata.c -o luaclib/sharedata.so
cc -g -O2 -Wall -I3rd/lua -fPIC --shared -Iskynet-src lualib-src/lua-stm.c -o luaclib/stm.so
cc -g -O2 -Wall -I3rd/lua -fPIC --shared -Ilualib-src/sproto lualib-src/sproto/sproto.c lualib-src/sproto/lsproto.c -o luaclib/sproto.so
cc -g -O2 -Wall -I3rd/lua -fPIC --shared -I3rd/lpeg 3rd/lpeg/lpcap.c 3rd/lpeg/lpcode.c 3rd/lpeg/lpprint.c 3rd/lpeg/lptree.c 3rd/lpeg/lpvm.c -o luaclib/lpeg.so
cc -g -O2 -Wall -I3rd/lua -fPIC --shared lualib-src/lua-mysqlaux.c -o luaclib/mysqlaux.so
cc -g -O2 -Wall -I3rd/lua -fPIC --shared -Iskynet-src lualib-src/lua-debugchannel.c -o luaclib/debugchannel.so
make[1]:正在离开目录 `/application/skynet'
-
4.错误集:
-
错误一:
由于系统时间设置错误,导致编译进入死循环,报错如下:make[2]: *** 警告:文件“Makefile.in”的修改时间在将来1.8e+06
解决方案:
- 假如当前系统时间是错误的,修正系统时间即可;
- 假如是文件时间戳有误,可以使用
ind ./* -exec touch {} +
修正文件的时间戳。
-
错误二:
在虚拟机的共享目录下安装skynet
,由于虚拟机共享目录不能设置软连接:ln -sf libjemalloc.so.2 lib/libjemalloc.so
ln: 无法创建符号链接"lib/libjemalloc.so": 不支持的操作
make[2]: *** [lib/libjemalloc.so] 错误 1
make[2]:正在离开目录 `/mnt/Windows/skynet/3rd/jemalloc'
make[1]: *** [3rd/jemalloc/lib/libjemalloc_pic.a] 错误 2
make[1]:正在离开目录 `/mnt/Windows/skynet'
make: *** [linux] 错误 2解决方案:
选择其他非共享的目录进行安装,例如直接复制:sudo cp -r skynet /application/
启动流程:
skynet
由一个或多个进程构成,每个进程被称为一个 skynet 节点
。接下来尝试实现 skynet 节点
的启动流程。
1.配置文件Config
上面完成了源码编译,但是运行启动指令的时候,需要传入一个 Config文件
名称作为启动参数,skynet
会读取这个 Config文件
获取一些启动的必要参数,所以在运行程序之前,还需要根据要求修改配置文件,可以参考 example/config
或直接对其进行修改:
root = "./"
thread = 8
logger = nil
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"
luaservice = root.."service/?.lua;"..root.."test/?.lua;"..root.."examples/?.lua"
lualoader = "lualib/loader.lua"
snax = root.."examples/?.lua;"..root.."test/?.lua"
cpath = root.."cservice/?.so"
不难看出,这个配置文件内存其实是一个lua代码,以 key-value
形式进行赋值,skynet
启动时读取必要配置项,其他项即便用不到也会以字符串的形式存入 env
表中,所有配置项都可通过 skynet.getenv
获取。
-
必要的配置项有:
- thread 启动多少个工作线程。通常不要将它配置超过你实际拥有的 CPU 核心数。
-
bootstrap skynet 启动的第一个服务以及其启动参数。默认配置为
snlua bootstrap
,即启动一个名为bootstrap
的 lua 服务。通常指的是service/bootstrap.lua
这段代码。 -
cpath 用 C 编写的服务模块的位置,通常指
cservice
下那些.so
文件。如果你的系统的动态库不是以.so
为后缀,需要做相应的修改。这个路径可以配置多项,以;
分割。
-
在默认的 bootstrap 代码中还会进一步用到一些配置项:
-
logger 它决定了 skynet 内建的
skynet_error
这个 C API 将信息输出到什么文件中。如果logger
配置为nil
,将输出到标准输出。你可以配置一个文件名来将信息记录在特定文件中。 -
logservice 默认为
"logger"
,你可以配置为你定制的 log 服务(比如加上时间戳等更多信息)。可以参考service_logger.c
来实现它。注:如果你希望用 lua 来编写这个服务,可以在这里填写snlua
,然后在 logger 配置具体的 lua 服务的名字。在examples
目录下,有config.userlog
这个范例可供参考。 -
logpath 配置一个路径,当你运行时为一个服务打开 log 时,这个服务所有的输入消息都会被记录在这个目录下,文件名为服务地址。
standalone
如果把这个 skynet 进程作为主进程启动(skynet 可以由分布在多台机器上的多个进程构成网络),那么需要配置standalone
这一项,表示这个进程是主节点,它需要开启一个控制中心,监听一个端口,让其它节点接入。 -
master 指定 skynet 控制中心的地址和端口,如果你配置了
standalone
项,那么这一项通常和standalone
相同。 - address 当前 skynet 节点的地址和端口,方便其它节点和它组网。注:即使你只使用一个节点,也需要开启控制中心,并额外配置这个节点的地址和端口。
-
harbor 可以是
1-255
间的任意整数。一个 skynet 网络最多支持 255 个节点。每个节点有必须有一个唯一的编号。
如果harbor
为 0 ,skynet 工作在单节点模式下。此时master
和address
以及standalone
都不必设置。 -
start 这是
bootstrap
最后一个环节将启动的 lua 服务,也就是你定制的 skynet 节点的主程序。默认为main
,即启动main.lua
这个脚本。这个 lua 服务的路径由下面的luaservice
指定。
-
logger 它决定了 skynet 内建的
-
集群服务用到的配置项:
- cluster 它决定了集群配置文件的路径。
-
lua 服务由 snlua 提供,它会查找一些配置项以加载 lua 代码:
-
lualoader 用哪一段 lua 代码加载 lua 服务。通常配置为
lualib/loader.lua
,再由这段代码解析服务名称,进一步加载 lua 代码。snlua 会将下面几个配置项取出,放在初始化好的 lua 虚拟机的全局变量中。具体可参考实现。 - SERVICE_NAME 第一个参数,通常是服务名。
-
LUA_PATH config 文件中配置的
lua_path
。 -
LUA_CPATH config 文件中配置的
lua_cpath
。 -
LUA_PRELOAD config 文件中配置的
preload
。 -
LUA_SERVICE config 文件中配置的
luaservice
。 -
luaservice lua 服务代码所在的位置。可以配置多项,以 ; 分割。 如果在创建 lua 服务时,以一个目录而不是单个文件提供,最终找到的路径还会被添加到
package.path
中。比如,在编写 lua 服务时,有时候会希望把该服务用到的库也放到同一个目录下。 -
lua_path 将添加到
package.path
中的路径,供require
调用。 -
lua_cpath 将添加到
package.cpath
中的路径,供require
调用。 -
preload 在设置完 package 中的路径后,加载 lua 服务代码前,loader 会尝试先运行一个
preload
制定的脚本,默认为空。 -
snax 用
snax
框架编写的服务的查找路径。 -
profile 默认为 true, 可以用来统计每个服务使用了多少 cpu 时间。在
DebugConsole
中可以查看。会对性能造成微弱的影响,设置为false
可以关闭这个统计。
另外,你也可以把一些配置选项配置在环境变量中。比如,你可以把 thread 配置在
SKYNET_THREAD
这个环境变量里。你可以在config
文件中写:thread=$SKYNET_THREAD
这样,在 skynet 启动时,就会用
SKYNET_THREAD
这个环境变量的值替换掉 config 中的$SKYNET_THREAD
了。 -
lualoader 用哪一段 lua 代码加载 lua 服务。通常配置为
2.启动Skynet服务:
编译完成后,查询根目录的文件列表,发现生成了一个 skynet
可执行文件:
linsh@ubuntu:/application/skynet$ ls
3rd HISTORY.md lualib platform.mk service-src test
cservice LICENSE lualib-src README.md skynet
examples luaclib Makefile service skynet-src
在skynet的根目录运行以下指令 ./skynet examples/config
启动skynet服务:
linsh@ubuntu:/application/skynet$ ./skynet examples/config
[:01000001] LAUNCH logger
[: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:34760 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 8888
[:01000009] KILL self
[:01000002] KILL self
其他资料:
- 云风对Skynet的介绍视频:
云风:基于 Actor 模式的开源框架