赖勇浩(http://laiyonghao.com)
(续上)
游戏(服务器)是一种 CPU 密集、I/O 密集的应用,但是因为 GIL 的原因,Python 不能充分利用多核,所以一般都采用分布式的方案,那么 CPU 方面就没有太多好讲的了,不过 I/O 方面蛮有意思,可以讲一下。这里有没有 node.js 社区的朋友?(有人举手)。这句话你熟悉吗?(幻灯片上是一句话:I/O needs to be done differently.)这句话是 node.js 的作者说的,他说 I/O 该用不同的方法来实现啦。我觉得他说得很对,……后来他也做了 node.js。这里有一个 node.js 操作 DB 的例子,DB 操作必须是有 I/O,有 I/O 就有阻塞,有阻塞就并发性较差。node.js 是这样解决的:
在操作数据库的时候,指定一个回调函数,在操作结束的时候,再由 node.js 把结果推送给你。借助 javascript 强大的闭包语法,可以写出“很漂亮”的带有回调的程序,而且看起来好像阻塞程序一样简单,又带有很高的并发性能。这就是 node.js 认为的 I/O 该有的样子。但是我不认同这句话。我认为 I/O 应该这样做,以下举个页游编程中常见的例子:
上面是玩家输入用户名、密码后按下登陆键,从账号验证到进入游戏的流程。当用户名、密码发送到专门用以登陆的 signin 服务器,signin 需要先查询数据库验证用户名、密码。在此我们只考虑用户名、密码无误的情况,signin 知道用户名、密码无误之后,就得先告知 game 服务器,game 服务器会返回一个令牌给 signin,后面客户端可以凭此令牌登陆 game 服务器开始游戏之旅。这么复杂的流程,涉及到多条进程之间的通信,也就是有许多的 I/O。这种应用使用 node.js 来写的话,可能需要写上两三个回调,个人觉得是比较麻烦的。那么我觉得 I/O 最好能够像上图的代码中那样,……但这不就是传统的阻塞式的 I/O 吗?对的!我觉得 I/O 的接口应该跟之前无二,但是底层的实现需要改变;而不是像 node.js 一样,都带一毛 callback 的尾巴。那么怎么做到这一点呢?
解决方案就是协程,协程才是未来。接下来介绍一下 gevent,gevent 能够让上图的代码运行起来。gevent 就是 libevent 加上 greenlet,简单介绍一下这两个库。libevent 提供指定的文件描述符事件发生时调用回调函数的机制,当然 timeout、signals 等也会调用回调。所以底层其实跟 node.js 是一样的,是有一个回调函数的,但是可以通过 greenlet 来封装出同步的 API,将丑陋的回调隐藏起来。greenlet 是一种 green thread……即一种用户空间线程,提供伪并发机制,所谓伪并发就是它并不能让你拥有充分利用多核 CPU 的性能,但他的好处是它的调度是虚拟机层面的,确切地说就是可以由程序员自己来进行调度。greenlet 是 Python 界对 green thread 的一种比较好的实现。
如果使用 gevent 写一个简单的 echo 服务器,大概是这样子的:
可以看到有一个 echo 函数,它处理每一个客户端连接,它读一行、写一行的方式来实现 echo 业务。大家可以看到代码还是有点长。类似 gevent 的项目还有沈崴的 eurasia,http://code.google.com/p/eurasia 。
谈完了 I/O,接下来有必要看一下协议方面,因为页游对网络协议的处理还是颇有些要求的。这方面我比较推荐 google protobuf,这是一门协议描述语言,官方支持生成 C++/java/Python 的代码,有许多第三方插件可以生成其它语言的代码。通过 protobuf 可以很方便地描述业务协议,比如登陆的时候需要有 username 和 password,以及可选的 timestamp 之类的,protobuf 能够帮助你去做序列化和反序列化的工作。protobuf 还支持声明 RPC,也就是 service,这个特性能让大家方便地实现 RPC。我们也做了一套,就是 abu.rpc,它是基于 gevent 加 protobuf 来实现的。得益于 gevent,它提供了同步的 API,即当调用 RPC 的时候,就像调用普通函数一样,等待返回就可以了,无需回调。得益于 protobuf,它是一个二进制协议,所以每个数据包都有较小的尺寸。libevent 是一个高效的异步 I/O 库,所以 abu.rpc 也很快。不过 abu.rpc 最重要的两个特点是并行管线和双向调用。所谓双向调用就是指客户端可以调用服务器端,而服务器端也可以调用客户端提供的服务;也就是说客户端也是可以有服务的,它可以在创建的连接上绑定自己的服务。所谓的并行管线是这样的:
有时候客户端会发起一个比较重量级的、比较耗时的请求,而后又发起一个较轻量的请求。如果没有并行管线的支持,那么虽然轻量级的请求很快就处理完了,但客户端也只能等到重量级的请求完成以后才能收到轻量级的请求的处理结果,如上图左。这样轻量级的请求就为别的请求所累,响应时间就变长了。如果有并行管线的机制,当轻量级的请求发过来时,经过简单的计算,马上就能够返回结果,就有更快的响应,更好的实时性。大家再来看一下使用 abu.rpc 的 echo 服务器:
可以看到代码比直接使用 gevent 还是变短了不少的,复杂的地方可能是需要声明 service 吧。
今天上午周琦(ZoomQuiet)提到使用 rabbitMQ 来解耦,的确,MQ 挺适合在应用(进程)间的,他提到的面向消息编程其实大概可以说就是发布订阅模式:我对什么东西感兴趣,到时候你就推送给我。现在大家都比较关注进程之间的 MQ,但其实在进程之内、模块之间,也是极其需要“MQ”的,所以我们实现了一个叫 message 的模块。可以订阅感兴趣的主题,当有这样的主题发布时,订阅的回调就会被调用到了。
上图中右侧就是输出。通过 context 可以对消息处理流程做简单的干涉,比如某个订阅者处理完了认为别的函数都没有必要再调用之类,那么可以使用 context 来终止它。主要是受到 falcon 语言的启发而编写的,falcon 有丰富的语言特性,比如面向消息编程这个词,我第一次见就是在 falcon 编程语言的手册里看到的。关于这个库,我之前有写过一个 slide,放在这里:http://www.slideshare.net/laiyonghao/pythonmessage010 。在我们的《天下盛境》项目中,我们将其应用于任务、邮件以及好友等子系统中。举个任务子系统的例子,比如完成任务需要杀死 5 只怪物,针对这么多玩家做轮询的话是比较麻烦的,但通过订阅“怪物死亡”的主题,就可以在合适的时机去判定任务是否已经完成了,从而达到模块与模块间比较好的解耦的效果。
最后,给大家介绍一个简单但又很难归类的库。在游戏中,需要大量处理二进制的数据,这些数据通常由不同的平台、操作系统生成或存储,比如在 32 位机上进行开发,但运营部署是在 64 位机器上,在 py2.6 的环境开发,在 py2.7 的环境部署,等。这时候往往产生一些兼容性的问题,这类问题很难查出真正的原因。比如内置函数 hash() 在 32 位机器上返回的是 32 位的有符号整数,在 64 位机器上返回 64 位的有符号数,如果两台机器需要比对哈希结果,稍加不注意就可能会出问题,解决问题分分钟需要两个晚上都很常见,因为代码没有任务地方出错,但逻辑全乱了套。因为我们在做客户端与服务器端的通信加密时也是使用 Python 去实现,所以遇到了不少类似的问题。后来我们总结出需要一系列绝对地返回 32 位带符号整数的函数,所以我们编写了这个 absolute32 程序库。它很简单,只是对标准库的几个函数进行了封装,提供了 hash/add/crc/adler 等函数。以 add 为例,它对溢出的的处理是与 C 语言一样的,而不是像 Python 那样自动转换为 long 类型。这个库的几个函数我们在 py2.6/py3.1 和 32bit/64bit ubuntu 是进行了交叉测试,可以很好的简单我们的兼容性问题。
以上就是我今天要介绍的内容,谢谢大家。
- 3楼sniffer12345昨天 22:48
- 写得真不错n我记得去年好像也看过楼主的文章吧,当时楼主说试验了用python做的gs,单机并发我没记错的话应该是1K+吧?n这文章对我来说价值更大的在于对库的使用以及对架构的把握。n对于js异步回调的写法,我记得老赵好像有写过一个库可以将异步改成同步的写法,或者jq的deferred也是类似的功能。n另外,楼主你们做的页游,是flash吗?socket还是http?http的话能用protobuf吗?base64?n另外,对于“并行管线”不大理解。。是不是针对消息队列的一个优先级调整而已?
- 2楼dkbyjh昨天 10:08
- 写的很好,很受益,在用python做后台业务逻辑, 担心效率问题,楼主能给出单机并发数据么
- 1楼laoluohenmang昨天 09:54
- 顶。n与楼上同问。