文章目录 API查阅 一个独立的 Skynet 服务(Actor) 消息头部的类型(ptype)字段。 register_protocol的作用 预定义常量 自定义数字(类型) 协程 常见函数解析 常见的字符串处理( Lua 标准字符串库的函数) 小知识点 lua的正则模式 /(pattern) lua虚拟机(服务 require "skynet.socket"的理解 常见问题讨论 关于socket的迷惑点 skynet.ret(skynet.pack(f(...))) 的意义 skynet.register 只适合单实例服务吗?多个实例(如房间)怎么找? API查阅 Skynet Wiki 几乎列出了全部接口,每个都有用法说明 源码注释:在 skynet/lualib/skynet.lua 里,大部分核心函数的注释非常清晰。 一个独立的 Skynet 服务(Actor) 一个 Lua 文件 + skynet.start + 由 skynet.newservice 加载 = 一个独立的 Skynet 服务(Actor) 新文件套上"标准模式"壳就变成了一个服务: local skynet= require"skynet" -- ... 引入其他模块 -- 命令处理表 local CMD= { } function CMD. xxx ( ... ) -- 处理 lua 消息 end -- 消息回调处理客户端连接等 -- ... skynet. start ( function ( ) -- 注册 lua 消息分发 skynet. dispatch ( "lua" , function ( session, source, cmd, ... ) local f= CMD[ cmd] if fthen skynet. ret ( skynet. pack ( f ( ... ) ) ) end end ) -- 初始化逻辑(比如启动监听) -- 一般用 skynet.timeout(0, function() ... end) 延迟执行 end ) 消息头部的类型(ptype)字段。 Skynet 在消息传递时,会在消息头部附加一个类型(ptype)字段。Skynet 设计上将消息类型作为框架级的大类 ,而“具体干什么”由自定义的命令字段区分。常见的类型有: “lua”:Lua 服务之间互相发送的消息(最常用的) “client”:来自客户端连接的消息 “system”:系统内部消息(如退出、错误通知) 自定义数字类型:通过 skynet.register_protocol 定义,比如给id为skynet.PTYPE_CLIENT的数字指定一个name register_protocol的作用 把一个数字和一个好记的字符串绑定,之后就可以用字符串来表示消息类型。 register_protocol 里 id 和 name 的关系就是:id:这个类型的底层数字标识(可以是预定义常量,也可以是自定义数字) name:你给这个数字起的别名,之后用字符串来指代它 例子: // 例子:注册后 Skynet 内部维护了"client" → 数字 PTYPE_CLIENT 的映射 skynet. register_protocol { // 给它起了个叫"client" 的字符串名字。注册完之后,你就可以用"client" 来代替数字id。 name= "client" , // skynet. PTYPE_CLIENT 是 Skynet 已经定义好的一个数字常量,表示“客户端消息”这个类型。// 它是预设的几个标准类型之一(像 PTYPE_LUA、PTYPE_SYSTEM 一样)。 id= skynet. PTYPE_CLIENT, } 预定义常量 常见的预定义常量还有:(冒号后面对应的是大概的用途) skynet.PTYPE_LUA:Lua 服务间消息(你用的 “lua” 背后就是这个) skynet.PTYPE_CLIENT:来自客户端连接的消息 skynet.PTYPE_SYSTEM:系统消息(服务退出通知等) skynet.PTYPE_RESPONSE:内部用于 skynet.call 的回应 自定义数字(类型) 我们都知道Skynet 在消息头部用 1 个字节(0~255)来标记消息类型。所以底层传输时,类型一定是数字。 为了让你写代码方便,Skynet 提供了两个东西:预定义的常量 :比如 skynet.PTYPE_LUA、skynet.PTYPE_CLIENT、skynet.PTYPE_SYSTEM,它们其实都是数字(分别对应 0、3、1 等值,不同版本可能略有差异,但你不需要记数字)。skynet.register_protocol :允许你为某个数字类型起一个人类可读的名字(字符串),并且也可以通过这个名字来引用它。 自定义数字(类型)就是你自己创建一个全新的消息类型编号(在 0~255 之间,不能与系统已有类型冲突)。 假设你想区分“好友私聊消息”和“世界频道消息”,可以这样: -- 自己找个没被占用的数字,比如 10 和 11 skynet. register_protocol { name= "private_chat" , id= 10 , } skynet. register_protocol { name= "world_chat" , id= 11 , } // 之后用 skynet. send ( addr, "private_chat" , ... ) 发消息。// 接收方也用 skynet. dispatch ( "private_chat" , func) 来处理。// “自定义数字(类型)”的含义:你自己为某个整数id类型分配一个名字。协程 skynet.fork(func, …) - 协程作用 :启动一个新的协程去执行一个函数,这样即使函数里阻塞了(比如 skynet.call),也不影响当前业务流程。什么时候要fork? :如果不放在协程里,就会导致出现阻塞卡住,无法处理其他消息或新数据的情况时。记忆方法 :像“开个小线程去做一件可能花时间的事”。 常见函数解析 pcall(func, …) - 安全调用作用 :protected call,安全调用一个函数,即使函数内部出错也不会让整个服务崩溃,而是返回错误信息。为什么有时用 pcall 包住 skynet.call ?:skynet.call 可能会报错时,用 pcall 可以兜底,防止 服务 挂掉。记忆方法 :“开启保护罩去调用(防自爆的保护罩,里面炸了也影响不了外面的)”。 skynet.self() - 我是谁作用 :返回当前服务自己的地址(一个数字)。用在哪 :发给某个服务时告诉对方“我是哪个服务”,便于对方发消息回来。 skynet.register(“.luaFileName”)把当前服务注册成一个全局名字,带 . 前缀代表是系统级别名。 这样就能用 skynet.queryservice(“luaFileName”) 找到它。 注意:注册名里的 . 前缀在查询时不需要带,所以 register(“.luaFileName”) 对应 queryservice(“luaFileName”)。 常见的字符串处理( Lua 标准字符串库的函数) 迭代器(match) for linein string. gmatch ( data, "[^\r\n]+" ) do // - string. gmatch ( s, pattern) 返回一个迭代器,能不断从字符串中找出所有匹配 pattern 的部分// - 这里的[ ^ \r\n] + 是正则模式,意思是:一个或多个(正数个)不是回车( \r) 或换行( \n) 的字符// - 所以整句话就是:把接收到的数据按行切割,去掉换行符,得到每行干净的命令替换函数(sub) player_name= string. gsub ( msg, "%s+" , "" ) // - string. gsub ( s, pattern, replacement) 是替换函数,把所有匹配 pattern 的地方替换成 replacement// - % s+ 表示一个或多个空白字符(空格、tab、换行等)// - 替换成"" 就是删除所有空白// - 所以这句话就是:把输入的字符串中可能混入的空格和换行全部清掉,只留纯文本小知识点 # 是长度操作符 用在表上获取数组部分的元素个数。 用在字符串上获取字符数。 … 是字符串拼接符 可以把任意基础类型拼成字符串,数字会自动转换。 Lua 中没有 + 拼接字符串,必须用 … lua的正则模式 /(pattern) Lua 的正则模式(Lua 称为 pattern),和常规正则有一点不同,但逻辑一致。 for linein string. gmatch ( data, "[^\r\n]+" ) do […] 叫做字符集,表示匹配括号里的任意一个字符。 [^…] 表示匹配不在这个集合里的任意一个字符。 \r 是回车,\n 是换行。 所以 [^\r\n] 的意思就是:任意一个不是回车、也不是换行的字符。 后面的 + 表示匹配一个或多个这样的字符。 string.gmatch(data, “[^\r\n]+”) 的效果就是:从接收到的报文里把每一行(去掉了结尾的回车换行)提取出来。 player_name= string. gsub ( msg, "%s+" , "" ) %s 是 Lua 模式里空白字符的简写(空格、tab、换行等),类似 正则里的 \s。 所以 %s+ 就是一连串的空白。 string.gsub(msg, “%s+”, “”) 就是把玩家输入的名字中可能混入的连续空白(空格、换行、tab)全部清空,只剩干净的名字。 lua虚拟机(服务 就用agent.lua 来举例: agent 有多个实例,但每个实例都是一个独立世界,不会冲突。 为什么不会冲突?:因为每次用 skynet.newservice(“agent”) 时,Skynet 都会创建一个全新的独立的 Lua 虚拟机(服务) 。 也就是说每个新服务都会完整执行一次 agent.lua 文件 ,也就是每个 agent 都有自己独立的变量/回调/等等。 举个更具体的例子:在 agent.lua 里写 socket.on_data = function(data) … end,这个赋值是发生在这个服务自己的环境里,不会影响到其他 agent 服务。 就好比每个房间都有一台电视,你把自己房间的电视换成新频道,邻居房间的电视根本不受影响。——每个 agent 只服务自己的那个玩家,互不干扰。 // 整个流程的实例化过程:// Player1 连接 → logind 创建 agent1 → agent1. lua 全新执行,内部 fd= fd1// Player2 连接 → logind 创建 agent2 → agent2. lua 全新执行,内部 fd= fd2require "skynet.socket"的理解 socket 是你代码里 require “skynet.socket” 得到的模块,它提供网络 I/O 相关的函数。 它不是服务,而是一个功能库,类似于 Skynet 给我们的一把网络工具。 每个服务中 require “skynet.socket” 得到的都是同一个底层机制的引用,但使用时只影响本服务内部的事件回调,不会干扰其他服务。 因此,可以把它看作每个服务有独立的 socket 事件注册,但底层是共享的。 常见问题讨论 关于socket的迷惑点 socket 不是全局单例吗?为什么多个 agent 赋值 on_data 不冲突? 核心原因 :每个 Skynet 服务是独立的 Lua 虚拟机,拥有完全隔离的全局环境。总结 :socket 模块在每个服务里都有一份独立的副本,on_data 回调是服务级别隔离的。具体的 :require “skynet.socket” 时,虽然加载的是同一段代码,但这段代码会在当前服务的 Lua 状态机里执行,并返回一个模块表。 这个表是当前服务私有的,与其他服务中的 socket 表不是同一个对象。 即使底层网络实现是进程内共享的,但 socket.on_data 的赋值只绑定到当前服务的回调槽位。 底层在收到数据后,会根据 fd 所属的服务 ID,把数据丢给对应服务的 on_data 函数 (虽然是socket点出来的,但是是对应服务的,可以这么理解)。 所以整个过程就像:你给本服务(agent)自己(实例)的电话本(socket 表/模块副本表)上写了“客户(fd-x)来电请转接给我”的回调。 其他服务有自己的电话本,他们怎么写都不会影响你的。 agent服务实例 ~ socket模块表(on_data) ~ 客户端(fd-x),即1个客户端->1个agentInst.socket.on_data 。 skynet.ret(skynet.pack(f(…))) 的意义 f(…) 返回了值。skynet.pack(true) 打包成二进制响应,skynet.ret 把它发回给 skynet.call 的调用方。 f(…) 没有明确返回值函数最后一句不是 return,或 return 后面无值。 Lua 中函数返回值会变成 nil。skynet.pack(nil) 打包为携带空值的响应,同样正常发回。 发送方用的是 skynet.send(异步),没有人在等待应答。skynet.ret 仍然会执行,但 Skynet 底层发现这个消息来自 send(没有请求 session),就会自动丢弃应答包,不会造成阻塞或错误。 因此,无论有没有返回值,无论发送方是 send 还是 call,这个标准的 CMD 分发模板都是安全的。这也是为什么几乎每个 Skynet 服务都会雷打不动地写这几行。 skynet.register 只适合单实例服务吗?多个实例(如房间)怎么找? skynet.register 注册的名字是全局唯一的,同名会出错,所以只适合那些只有一个实例的服务(比如我们的 matchd)。 对于动态创建的多实例服务(如 roomd),绝不会用 register,而是直接把地址传递出去。我们的流程就是:matchd 创建房间:room_addr = skynet.newservice(“roomd”)。 matchd 把 room_addr 发给两个 Agent:skynet.send(p1.agent, “lua”, “battle_start”, room_addr, p2.name)。 Agent 保存这个 room_addr,后续通信就直接发给这个地址。 这种方式不需要全局名字,效率高且天然隔离,是 Skynet 里多实例通信的标准做法。