Python 于 webgame 的应用(下)

赖勇浩(http://laiyonghao.com)

(续上)

游戏(服务器)是一种CPU密集、I/O密集的应用,但是因为GIL的原因,Python不能充分利用多核,所以一般都采用分布式的方案,那么CPU方面就没有太多好讲的了,不过I/O方面蛮有意思,可以讲一下。这里有没有node.js社区的朋友?(有人举手)。这句话你熟悉吗?(幻灯片上是一句话:I/Oneedstobedonedifferently.)这句话是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的尾巴。那么怎么做到这一点呢?

解决方案就是协程,协程才是未来。接下来介绍一下geventgevent能够让上图的代码运行起来。gevent就是libevent加上greenlet,简单介绍一下这两个库。libevent提供指定的文件描述符事件发生时调用回调函数的机制,当然timeoutsignals等也会调用回调。所以底层其实跟node.js是一样的,是有一个回调函数的,但是可以通过greenlet来封装出同步的API,将丑陋的回调隐藏起来。greenlet是一种greenthread……即一种用户空间线程,提供伪并发机制,所谓伪并发就是它并不能让你拥有充分利用多核CPU的性能,但他的好处是它的调度是虚拟机层面的,确切地说就是可以由程序员自己来进行调度。greenletPython界对greenthread的一种比较好的实现。

如果使用gevent写一个简单的echo服务器,大概是这样子的:


可以看到有一个echo函数,它处理每一个客户端连接,它读一行、写一行的方式来实现echo业务。大家可以看到代码还是有点长。类似gevent的项目还有沈崴的eurasiahttp://code.google.com/p/eurasia

谈完了I/O,接下来有必要看一下协议方面,因为页游对网络协议的处理还是颇有些要求的。这方面我比较推荐googleprotobuf,这是一门协议描述语言,官方支持生成C++/java/Python的代码,有许多第三方插件可以生成其它语言的代码。通过protobuf可以很方便地描述业务协议,比如登陆的时候需要有usernamepassword,以及可选的timestamp之类的,protobuf能够帮助你去做序列化和反序列化的工作。protobuf还支持声明RPC,也就是service,这个特性能让大家方便地实现RPC。我们也做了一套,就是abu.rpc,它是基于geventprotobuf来实现的。得益于gevent,它提供了同步的API,即当调用RPC的时候,就像调用普通函数一样,等待返回就可以了,无需回调。得益于protobuf,它是一个二进制协议,所以每个数据包都有较小的尺寸。libevent是一个高效的异步I/O库,所以abu.rpc也很快。不过abu.rpc最重要的两个特点是并行管线和双向调用。所谓双向调用就是指客户端可以调用服务器端,而服务器端也可以调用客户端提供的服务;也就是说客户端也是可以有服务的,它可以在创建的连接上绑定自己的服务。所谓的并行管线是这样的:


有时候客户端会发起一个比较重量级的、比较耗时的请求,而后又发起一个较轻量的请求。如果没有并行管线的支持,那么虽然轻量级的请求很快就处理完了,但客户端也只能等到重量级的请求完成以后才能收到轻量级的请求的处理结果,如上图左。这样轻量级的请求就为别的请求所累,响应时间就变长了。如果有并行管线的机制,当轻量级的请求发过来时,经过简单的计算,马上就能够返回结果,就有更快的响应,更好的实时性。大家再来看一下使用abu.rpcecho服务器:


可以看到代码比直接使用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.132bit/64bitubuntu是进行了交叉测试,可以很好的简单我们的兼容性问题。

以上就是我今天要介绍的内容,谢谢大家。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值