« 关于分工合作 | 返回首页 | 开发笔记 (8) : 策划公式的 DSL 设计 »

开发笔记 (7) : 服务器底层框架及 RPC

很久没有写工作笔记了,如果不在这里写,我连写周报的习惯都没有。所以太长时间不写就会忘记到底做了些啥了。

这半个多月其实做了不少工作,回想起来又因为太琐碎记不太清。干脆最近这几天完成的这部分工作来写写吧。

我在 开发笔记 第四篇谈到了 agent 的处理流程。但实际操作下来还是觉得概念显得复杂。推而广之,对于不是 agent 的服务,我需要一个通用的消息处理框架。

对于每个服务器,可以看成是对一组约定的服务协议进行处理。对于协议分组,之前我有许多想法,可做下来又发现了若干问题。本来我希望定义出一个完整的 session 概念,同一个 session 下,可以分不同的步骤,每个步骤都有一个激活的协议组。协议组之间可以共享状态,同时限制并发。做下来发现,很难定义出完整的事务处理流程并描述清楚。可能需要设计一个 DSL 来解决这个问题更好一些。一开始我也是计划设置这个小语言的。可一是时间紧迫,二是经验不足,很难把 DSL 设计好。

而之前的若干项目证明,其实没有良好的事务描述机制,并不是不可用。实现一个简单的 RPC 机制,一问一答的服务提供方式也能解决问题。程序员只要用足够多经验,是可以用各种土法模拟长流程的事务处理流。只是没有严格约束,容易写出问题罢了。那么这个问题的最小化需求定义就是:可以响应发起请求人的请求,解析协议,匹配到对应的处理函数。所有请求都应该可以并发,这样就可以了。至于并发引起的问题,可以不放在这个层次解决。

我谨慎的选择了 RPC 这种工作方式。实现了一个简单的 RPC 调用。因为大多数服务用 Lua 来实现,利用 coroutine 可以工作的很好。不需要利用 callback 机制。在每条请求/回应的数据流上,都创建了独立的环境让工作串行进行。相比之前,我设计的方案是允许并发的 RPC 调用的。这个修改简化了需求定义,也简化的实现。

举例来说,如果 Client 发起登陆验证请求,那么由给这个 Client 服务的 Agent 首先获知 Client 的需求。然后它把这个请求经过加工,发送到认证服务器等待回应(代码上看起来就是一次函数调用),一直等到认证服务器响应发回结果,才继续跑下面的逻辑。所以处理 Client 登陆请求这单条处理流程上,所有的一切都仅限于串行工作。当然,Agent 同时还可以相应 Client 别的一些请求。

如果用 callback 机制来表达这种处理逻辑,那就是在发起一个 RPC 调用后,不能做任何其它事情,后续流程严格在 callback 函数中写。

每个 RPC 调用看起来是这样的:

  local salt = service.call(addr ,
     "login.salt" , { username = username }).salt
  local response = md5(username .. ":" .. password .. ":" .. salt)
  local ok = service.call(addr, "login.challenge" ,
     { username = username , response = response }).ok
  print(ok)

而 service 编写是这样的:

-- login.lua
function salt(username)
  local salt = gen_salt(username)
  return { salt = salt }
end

function challenge(username , response)
    local salt = gen_salt(username)
    local password = service.call("authdb",
       "query" , { username = username }).password
    local result = md5(username .. ":" .. password .. ":" .. salt
    return { ok = (result == response) }
end

大家需要约定中间的协议,采用 protobuf 格式描述:

package login;

message salt {
  optional string username = 1;
  message response {
    optional bool salt = 1;
  }
}

message challenge {
  optional string username = 1;
  optional string response = 2;
  message response {
    optional bool ok = 1;
  }
}

btw, 我没有用 protobuf 的 serivce 特性。觉得不太方便,而且也没有太多必要引入过多特性。

当然这只是 RPC 范例代码,不设计认证协议的具体设计。实际应用中,校验过程会比这个更完备一些。

在 login 服务中,收到用户来的请求,会去向 authdb 服务索取密码信息。这个 RPC 调用是阻塞的,直到 authdb 返回结果。但是,login 服务并不被阻塞,可以接受其它请求。每个不同请求的回应对应关系,是在低一个层次上的 session id 来区分。这些隐藏在框架实现中了。

设计细节今天先不列了,也没太多难度。

最后,我对 RPC 的使用是谨慎的。必须留意 RPC 会带来的问题。这些在《Unix 编程艺术》7.3.2 有论述,我再说多少次也超不出这个范畴。


做这套东西,我和蜗牛是有一些分工的。服务器数据流框架并不是我来实现。蜗牛用 erlang 做的。

这些东西我们折腾了好久,这里面有我的很多工作失误。最终,我觉得应该把工作严格划分开,需要有一个清晰的模块需求定义。

本来是初步定好了,每个服务工作在一个独立的 Lua State 中(并不一定在独立进程/线程中),包括为每个 Client 服务的 Agent 。一开始是在 Lua 的层次来划分工作的。蜗牛写了一部分的 Lua 驱动代码。我觉得是我在分工问题上的错误。这个需求很难稳定下来。后来,我整理了一下思路,决定定义明确的 C 接口,然后我来做 Lua 封装。

服务器的大框架主要解决玩家的接入问题,各个服务间的数据流向问题,服务的启动和停止等等。我设想,每个服务都有单一的输入输出流,只面对框架。实现上可能框架并不是在做复制转发,有可能直接对接两个服务的输入输出。尤其是两个服务可能是在同一个线程中,以 coroutine 的形式工作。这个优化空间可以留下来。

我在做后面的工作之前,先写了一份文档,描述需求:

  • 向 skynet 发送一条指令, 并立刻获得一个结果. 目前提供以下三条指令:
    • 注册自己到 skynet , 可以给出一个 well-known 的名字 (name), 也可以不给出. skynet 返回一个 unique 名字 (uid)
    • 向 skynet 查询系统时间
    • 设置 timer , skynet 会在 timeout 时回调
  • 向 skynet 发送数据, 由 skynet 决定把数据送达何处.
  • 注册一个 callback , 当 skynet 有消息送达的时候触发.

注:skynet 是框架的开发代号。

然后是接口的 C 定义

void * skynet_context(void); void skynet_exit(void * context); void skynet_step(void * ud); const char * skynet_command(void * context, const char * cmd , const char * parm); void skynet_send(void * context, const char * addr , const char * msg, size_t sz); typedef void (*skynet_cb)(void *context, void *ud, const char * uid , const char * msg, size_t sz); void skynet_callback(void * context, skynet_cb cb);

注: command 我选取了最简单的字符串风格。是因为考虑到字符串的知识依赖最小,很容易 binding 到任何语言中。也最容易实现网络传输。不同 command 的协议定义也写了文档,包括文本协议的定义。这里就不一一列出了。

有了文档之后,我开始编写 skynet 的黑盒。基本上就是一个本地单一进程,用来调度同一进程下的不同服务。这个很好写,大约两三小时,不到 300 行 C 代码就搞定了。虽然限制很多,性能也很低下,但可以实现以上的接口。

之后的代码就不再是假盒子了。是把这几个 C 接口 binding 到 lua 里,然后在 lua 层面写真实的应用。工作量比较大,但也好做。一旦蜗牛那边真正的 skynet 工作正确,整合工作只是替换前面几个接口的实现库而已。


Blog 写这种系列, 不太方便整理。 这点还是 wiki 比较好。我把这个系列整理出一个列表了

ps. 这段时间还做了许多琐碎的工作,包括帮客户端解决一些问题。中间闲了两天,想玩一下 LaTeX ,结果就写了这个:Lua 源码欣赏 。结果工作一忙就没时间写下去了。估计这个半拉子工程会这么太监掉的。

Comments

7
https://github.com/bketelsen/skynet
@steve 重新写一个.
这个验证码插件是我自己写的,只有一行 perl 代码。就是判断输入是不是 '7' 如果答案一直不变,那只能说明SPAM的是所有的WP的系统,而不是针对你。假如有专门针对你的呢?如何处理?
ps. 这段时间还做了许多琐碎的工作,包括帮客户端解决一些问题。中间闲了两天,想玩一下 LaTeX ,结果就写了这个:Lua 源码欣赏 。结果工作一忙就没时间写下去了。估计这个半拉子工程会这么太监掉的。 =================== 这个看了一下,很受启发,希望云风能继续写完哈~~
能把那个LaTeX文档里的链接的框框去掉么..看着怪怪的~
那个LaTeX文档,能把链接的框框去掉么..在Foxit里面看着怪怪的..
同顶,云风大哥的《Lua源码赏析》确实很不错!Fans们有福了。
Lua 源码欣赏写的深入浅出,非常不错,希望坚持下去
我现在对网游服务器架构难点的理解就是在保证“高性能”前提下,做到游戏逻辑编写最容易。这个对于架构者不仅仅是技术上的挑战,至少有一半依赖于对业务的熟悉。 其他如稳定性撒的是对服务器的基本要求。 当然对于“高性能”这个永恒主题,不同的游戏有不同的追求,不同的游戏有不同的挑战。
用什么语言不重要,每个人的智商和思维习惯都不同,只要有自信能达到目标就好,目标包括可维护可扩展,方便和其它程序的交互,以及足够的性能.
用erlang=失败。
都是你俩在谈论, 我写过服务程序,但是, 涉及到并发量这么高,连接请求这么多的应用,还没接触过。 读起来有点吃力。 不过,感觉云风的工作日志非常非常具有学习和参考意义,有待以后会过头来读。
牛B。 ====== 反复去看了您的几篇开发笔记;还是不能理解skynet是如何解决IPC 服务定位的问题。 貌似erlang做了些关键的事情;作为一个不懂erlang的人,决定默默的去学习erlang。
只要能解决问题,就采用最简单的设计。 这个验证码插件是我自己写的,只有一行 perl 代码。就是判断输入是不是 '7' 。 结果它很管用。从后台 log 看,拦截了几万条 spam 。
嗯,是偶先入为主了;不好意思。 PS:“为了验证您是人类”,这个问题好像不会变?
原文我已经写了. 既然是 C 接口,就有可能是直接对接的。 框架理论是可以作到一个 send 调用直接到对方的 callback 的。 这个属于优化。只是看起来是通过了框架。 另外,原文我完全没有提到依赖网络连接层,是你的先入为主的观念吧? 事实上,我们的实现的大多数情况是和网络连接层无关的。
@Cloud intranet环境,应该可以视为稳定吧? “需求明确,文档化接口”这点我是非常赞同的。 但是否有必要让Service都依赖于同一个网络连接层? Service A/B之间为何不能直接通讯?而需要经过XXNet做中转? 如果说,服务“仅仅是 lua 的 coroutine ,所以需要调度。包括 core 的管理。”;那么,我能否将这样的调度、管理理解成为一个单机,或者说单进程内的程序架构解决方案? 这个层面我觉得应该是独立的问题。 ===== 而IPC,不同进程、物理机器上的不同服务如何通讯,我觉得是另一个层面的问题。而这个层面,是我原来公司才用XXnet去解决的;刚刚,我就直接认为同样也是skynet所要解决的问题。 我个人认为这个问题归根到底就是一个服务定位的问题。 同样是考虑优化,上万个客户端与上万个服务通讯,所有包均经过一个XXnet中转,效率应该会比客户端通过某种机制定位到服务,然后直接通讯效率低一些。 当然,客户端如果是经过外网间接进来,中转是必须的。但如果后端的上万个服务需要相互通讯呢?
@Wuvist 当然你可理解为,我刚才说的这些只是性能问题,属于优化范畴。在 internet 上也可以把这些实现出来,而不必再自己做。 但正因为以后需要这种优化(可能会跑上万个服务,比如每个 client 都有对应的处理服务),更应该把需求明确,文档化接口。变成一个独立层次。
@Wuvist 主要解决的是服务的生存监控,启动。 另外服务不一定跑在独立进程上,甚至不跑在独立线程上。可能仅仅是 lua 的 coroutine ,所以需要调度。包括 core 的管理。 还有 IPC 问题。可能 IPC 的 P 不是系统的 process 。 还有 timer 的驱动,时间控制。 另外,internet 是个不稳定的网络。还是有区别的。
呵呵,看起来跟偶原来公司的架构非常类似,XXnet, XXAgent。 XXnet对应skynet,它负责处理各种客户端、服务的连接,负责包转发,它本身是分布式的。 我个人对这样的架构感觉有点奇特。它本身不就是个internet么?按照我的理解,主要是解决service定位的问题;但这,不就是DNS做的事情么?

Post a comment

非这个主题相关的留言请到:留言本