服务端代码踩了不少坑,其中在学习lua时还碰到坑爹的教程错误,而且skynet还没有api文档,初次看服务端源码时,会有些摸不着头脑,傻傻分不清哪部分属于skynet功能,哪部分属于自定义封装,有些时候不得不打开skynet源码查看,这时skynet的另一个优点就体现出来了,skynet核心源码并不多,要查起来也不会特别费时间,不过还是希望能有个详细的api文档,至少对初学者来说会方便不少,降低门槛。
下面开始分析范例中的服务端代码。 范例github地址:
https://github.com/cloudwu/skynet_sample
源码分为3个文件夹,分别为service、lualib、src。
其中service主要是服务端业务逻辑,lualib为基础工具封装,src为c语言服务封装。
一般阅读代码时先从main入手,跟着逻辑一步一步不断深入。
main.lua
文件路径:service/main.lua
1 | local skynet = require "skynet" |
入口函数很简洁,首先启动了控制台服务,该服务使得skynet在运行时,可以打印log以及信息到终端界面。
protoloader服务为proto协议的封装,注意这里的protoloader为自定义封装,而不是skynet核心代码里自带的那个。
首先调用skynet.uniqueservice挂起了protoloader服务,所以接下来看看protoloader是怎么运行的。
protoloader.lua
文件路径:service/protoloader.lua
1 | local skynet = require "skynet" |
主要是定义了如何加载proto协议并保存,以及获取协议的编号(slot),然后最后面调用了另一个lua文件的代码service.init方法。
service.lua
文件路径:lualib/service.lua
1 | local skynet = require "skynet" |
该代码做的主要有3个工作,
- 设定当前skynet服务debug info信息
- 设定当前skynet服务在接收到其他服务调用信息时的处理,比如上一段protoloader代码中的load函数和index函数。其他服务只要发送”函数名”或者“参数”,即可调用对应的函数。其中一个值得注意的点是,skynet中服务与服务的通信必须通过skynet.ret和skynet.retpack函数进行打包和封包处理。
- 加载其他服务,并保存在service这个table中。
- 往后所有服务的代码都会通过init函数处理info信息、注册函数、加载其他服务。
回到main.lua中,紧接着调用了service.init中设定的load函数。然后挂起了hub服务。 为了不让文章变的太过冗长,下面会省略一些具体实现逻辑,主要以梳理流程为主,只有在关键点才会有详细的解释。
hub.lua
文件路径:service/hub.lua
1 | local skynet = require "skynet" |
可以看到没有执行具体逻辑,主要以定义为主,并启动了两个服务,manager和auth。 在最开始local proxy = require “socket_proxy”中还启动了socket_proxyd服务。 不过在这边并有具体的逻辑执行,所以先不管,顺着流程继续看。
监听网络端口
回到main.lua,调用了hub服务中的open函数,开始监听网络端口。来看看该函数的具体实现。
1 | function hub.open(ip, port) |
这时如果客户端发起请求,就会将监听到的链接交给new_socket函数处理
1 | function new_socket(fd, addr) |
向proxyd服务发送链接对象
文件路径:service/socket_proxyd.lua
1 | local function subscribe(fd) |
这里创建了一个c语言编写的package服务,不必关心具体实现,只要知道它的功能主要是让当前链接成为一个单独的skynet服务即可,返回值是该服务的ID。 最后通过skynet.response函数返回服务ID给调用前的函数,也就是socket_proxy的subscribe。
处理链接数据
一路回到hub.lua的new_socket函数。又将链接提交给auth_socket函数
1 | local function auth_socket(fd) |
通过service.call启动service中保存的auth服务,调用其中的shakehand函数,并发送fd链接对象。
文件路径:service/auth.lua
1 | function auth.shakehand(fd) |
这里可以理解为和客户端一第次“握手”,调用client的dispatch函数,并发送链接。
文件路径:lualib/client.lua
1 | --消息处理 |
这个函数为消息处理最关键的函数,所有请求都通过这里进行分发,其中回调函数都保存在handler这个table中,当前范例的回调函数都在agent.lua和,auth.lua中定义。 通过代码可以看到,该函数是一个死循环,也就是说,会不断的读取客户端发送过来的信息进行处理。只有当exit变量为ture时才退出,这个exit怎么来的先不管。 接下来通过客户端发送的请求流程继续梳理。
网络请求流程
回顾下客户端的消息流程: 首先会发送signin请求,发现sign失败,用户不存在,继续发送signup请求注册用户,注册成功后再次发送signin请求,登入成功,接着会发送login请求,后面的就是正常的业务请求了。请求顺序如下: signin > signup > signin > login > other 一步一步来,首先是signin,回到上面的dispatch,通过请求信息调用名为signin回调函数。signin的回调实现在auth.lua中
文件路径:service/auth.lua
1 | -- signin登入请求回调 |
可以看到,当客户端第一次请求signin时,是返回失败FAIL的。这时又会回到client的dispatch函数,返回客户端注册失败消息后,继续读取链接信息。 当客户端接到signin失败后,会紧接着发送signup请求。 同样来到auth.lua
1 | -- signup注册账号请求回调 |
这时就回返回成功,回到dispath发送注册成功消息。 接下来客户端再次发送sigin请求就会signin成功。 要注意的是,在signin回调函数中,如果signin成功则会修改exit变量,这会导致退出client的dispatch循环。为什么这里能修改exit变量呢,原因是所有回调函数都是隐式传参的,在函数定义中使用了“冒号”,表示隐式传送self参数。在dispatch中可以看到调用回调时,都会传进c变量。pcall(f, c, args) 登入成功后就会退出dispatch的循环,那么跟踪代码退到hub中的new_socket方法,发现又调用了assign_agent函数。
1 | local function assign_agent(fd, userid) |
assign_gent则调用了service中保存的manager服务,同时调用manage服务中的assign方法,并传入链接对象和登入成功返回的userid。
文件路径:service/manager.lua
1 | function manager.assign(fd, userid) |
顺着流程走,由于客户端第一次登入,会调用new_agent方法。
1 | local function new_agent() |
这里会为每个用户启动一个agent服务。 接着until中会调用该agent服务的assign方法。
文件路径:service/agent.lua
1 | function agent.assign(fd, userid) |
做了很简单一件事,调用了new_user这个方法,这里要注意的是,使用了skynet.fork方法调用,该方法的作用是,不会阻塞线程,为什么要这么做接着往后看。
1 | local function new_user(fd) |
可以看到,这里调用了client中的dispatch,前面知道该函数是个死循环,这就是为什么之前使用了skynet.fork的原因了。 好了,这样就又进入消息处理循环了,之前客户端已经发送了signin,signup,signin,这三个请求,接下来会发送login请求,去到login请求回调。该回调定义在agent.lua中。
1 | function cli:login() |
主要就是调用client.push函数发送welcome信息,再来看看client中push函数的实现
1 | function client.push(c, t, data) |
调用socket_proxy中的write
1 | function proxy.write(fd, msg, sz) |
这里用到了skynet.send函数,第一参数是服务地址,就是之前创建的package服务。 以上就是范例中服务端代码的主要逻辑。
另外我对服务端代码进行了大部分注释。
链接: http://pan.baidu.com/s/1dFKD2oh 密码: rfc9
扩展阅读
[skynet 的一个简单范例-云风的BLOG]
(http://blog.codingnow.com/2016/06/skynet_sample.html)
skynet LuaAPI
skynet API参考
skynet github 项目