skynet源码阅读及实践总结
skynet是一个云风大大开源的一款基于c+lua的框架,架子由c语言编写完成,逻辑上用lua来实现的一个框架
如果不关心框架的话,至少需要知道这框架里面有些什么东西,知道他的来龙去脉,由于工作原由断断续续的深入了解这个框架。由于日常开发没太在意底层实现原理,有时看下底层代码后当时知道了原理,之后再来看时基本忘记得差不多了。所以由此需要做一些笔记和深入的思考
1. 两个服务之间通信就必须知道消息发送方和消息接收方以及消息和消息的长度,skynet里面对发送一条消息的参数注释如下:
|
|
- address 指目标接收方的服务,服务用一个
uint32
的值来标识,也可以为一个服务取一个string
类型的名称来唯一 - type 指发送的消息的类型
- session 指消息发送方对此次发送这条消息的会话标识,默认传0,传空底层会为其生成一个
- message 指消息内容,消息内容有可能也是一个
message_ptr
|
具体可看skynet\lualib-src\lua-skynet.c ——> _send(...)
函数实现
服务与服务之间实际传递的消息结构体如下:
|
|
tips:
消息类型保存在
sz |= (size_t)type << MESSAGE_TYPE_SHIFT
拿回类型用sz >> MESSAGE_TYPE_SHIFT
长度用sz & MESSAGE_TYPE_MASK
type的值包括以下:
|
|
一直在想type的值的设置和取值为什么这样设计,也查阅了一些位操作方面的知识,还是不太知道,后问了下一老司机同事,他翻开云风大大博客对这样的设计已经给出了答案,阅读和思考后最终还是清楚了。
因为type的值是一个byte,取值范围也就是0-255,一个在服务内传递的消息的在16M(24个bit 214*210)以内,所以把type的值放在sz的最高位来保存。所以在取type值的时候sz的二进制左移MESSAGE_TYPE_SHIFT即可,
而sz的值的话只要取除去最高位再sz的二进制右移高位即MESSAGE_TYPE_MASK。
2. skynet里面是由各种lua服务来组成的,必然服务之间需要能信来往,当然也就包含阻塞发消息和非阻塞的方式
服务之间的消息流如下:
|
|
虚线代表有些是不需要B回包的,此次调用可能是阻塞或非阻塞
非阻塞调用
1234function skynet.send(addr, typename, ...)local p = proto[typename]return c.send(addr, p.id, 0 , p.pack(...))end阻塞调用
12345678function skynet.call(addr, typename, ...)local p = proto[typename]local session = c.send(addr, p.id , nil , p.pack(...))if session == nil thenerror("call to invalid address " .. skynet.address(addr))endreturn p.unpack(yield_call(addr, session))end
- 阻塞与非阻塞的send函数的第三个参数分别为nil和0,nil意思就是session是由skynet分配来分配
- 阻塞调用,如果收到阻塞调用的服务再阻塞去调其它服务,所有阻塞调用的服务的当前coroutine都会进入睡眠状态,等待回包被唤醒,进入睡眠并不影响lua_Stat进行其它的任何工作。
- 如果服务的dispatch函数没有返回值则不能用call来调用此服务,否则call的coroutine会一直阻塞中
代码实现在skynet\lualib\skynet.lua ——> raw_dispatch_message(…)函数里面,实际此函数也是每个服务收到消息后的回调函数
消息的内存管理是由发送方申请,消息接收端在回调成功后释放掉
|
|
3. 进程启动过程
|
|
skynet启动进程主要与服务相关代码如上面
每个lua服务都有自己的一个队列,并且创建后就会放到全局队列里面,有工作线程会去从全局队列里面去拿服务队列并处理完一条消息后若全局队列不为空再放到全局队列里去返回下个需要处理的部队。为空则返回自己服务的队列
- 回调函数调用
在加载lua代码的时候skynet.start(...)
函数里面注册了回调函数
|
|
4. skynet服务
4.1 服务标识
- skynet为了方便针记住某个服务,可以为每个独立的服务起一个名字,名字格式
.name
一个服务的名字可以重复设置吗?(可以)
两个服务的名字可以相同吗?(不可以,已经有了这样一个名字直接返回空)
4.1 服务名字存储及handle的存储结构和服务相关的命令映射
带着这些疑问就去翻阅代码去解决脑海里的疑点,首先从skynet.name('.myservice')
的代码跟到他调用的c函数,所有处理针对服务的一些函数都在skynet_server.c
文件里面,最后按参数找到最终处理名字的逻辑skynet_handle.c
,大体看了下简单的实现,代码说明按自己的理解整理下。
- 数据结构
|
|
- 命令映入结构1234567891011121314151617181920static struct command_func cmd_funcs[] = {{ "TIMEOUT", cmd_timeout },{ "REG", cmd_reg },{ "QUERY", cmd_query },{ "NAME", cmd_name },{ "EXIT", cmd_exit },{ "KILL", cmd_kill },{ "LAUNCH", cmd_launch },{ "GETENV", cmd_getenv },{ "SETENV", cmd_setenv },{ "STARTTIME", cmd_starttime },{ "ENDLESS", cmd_endless },{ "ABORT", cmd_abort },{ "MONITOR", cmd_monitor },{ "MQLEN", cmd_mqlen },{ "LOGON", cmd_logon },{ "LOGOFF", cmd_logoff },{ "SIGNAL", cmd_signal },{ NULL, NULL },};
4.2 服务名字代码说明下服务名set和get
- 服务名set
|
|
- 服务名get
通过服务名来找handle
|
|
服务的handle是怎么够用的?
|
|
poll
pipe命令读取
_________
| L Listen继续处理poll事件
1 | S 退出poll事件,返回SOCKET_OPEN
| B
| O
socket事件处理
2 events
accepte
connect
data
poll后再返回给指定的service去处理消息
- 接着是start这个fd,实际是记录到socket_server这个里面去
这两步就是创建一个listen的fd了,接下来只需要accepte客户端连接了
|
|
tcp包处理
收到包如何通知gateserver?
包大小?一个没没读玩又来了另一个包怎么处理?
tcp网络上实际传输的是字节流,所以我们在处理自定义的网络数据包的时候一般都是 len+data来作为一个数字包在tcp上传送,skynet框架是两个字节的长度,
skynet io是读指定长度的包再抛到上面来,处理包的完整性在lua-netpack.c
,socket的消息描述如下:
参考
socket_server.c : function forward_message
|
|
放到skyknet里面的service回调函数处理这个包,其它的不多说了,直接看lua-netpack.c : lfilter(...)
里面的源码
7. sample架子理解
把下面的关系弄清楚
client
login
gateserver
agent
client 是客户端发起登录的对象
login 是处理client登录请求,成功后告知gateserver
gateserver 类似agent的一房门。主要负责管理网络这块的事件传递到agent
agent 是消息实际处理者,也就是操作fd连接的