注:本文根据 2011 年 12 月 4 日我在上海 PyCon China 上的报告的录像整理而来,有较多口语,但废话不多。
原录像:http://e.gensee.com/v_3df867_14
(前面约四分半钟的暖场与自我介绍,略)今天的主题演讲其实受到两个人的很大的启发,一个是洪强宁洪教授,他在 2010 的时候做过一个叫《Python 于 web2.0 网站的应用》,大家可以看到我今天这个标题就是直接从他那里借鉴过来的,连微创新都没有。这个 slide 是很好的,所以我有给出网址,大家可以去看一下。然后还有沈崴,沈崴在 2010 年的时候也写了一个 slide,就是《Python 编程艺术》。这个 slide 对我的 Python 代码的风格影响非常大,他主要是讲到了应用 Python 时应该遵循怎样的编程哲学,也就是说怎样更加 Pythonic。这两个 slide 我曾经在珠三角技术沙龙的某次活动上说是我在 2010 年遇到的最好的两个幻灯片。
今天我以我经历的项目为蓝本,向大家介绍一下 webgame 服务器端开发这一块的技术和工具。大部分内容都是我在过去或现在的项目中使用的,有小部分内容是我以后才会在项目中用的,大家可以只当我是在做一个介绍,不要当我在做一个推广。如果你选择了这些技术,我不负责任,风险自担。所以这是一个源于项目、高于项目的(分享),因为我在做项目的过程当中会做一些思考和实验,然后这些思考和实验的结果就反映在这个演讲里面。
(开始项目介绍)嗯,这是刚才跟大家提到过的《天下盛境》,它是一个横版卷轴的动作类网页游戏,可以看到它的地图是从左到右移动的,可以在主城里交友、跟网友互动,或是在副本打怪。它现在 host 在 0505u.com 这个运营平台,服务器端完全使用 Python 开发,没有一行 C 代码,从去年(2010 年)的 8 月份开发到现在(2011 年 12 月初)。这是我最近做的一个项目,因为演讲主题会讲到前做的一些东西,所以再介绍另一个项目——web 版的棋牌。它其实就是 QQ 游戏的一个仿制品,在我自己的网站上有提供给大家访问,它的服务器端也是完全用 Python 来开发的,时间大概是用了两三个月的时间来开发,然后后面有一些零星的维护。它是一个半成品,不是一个可以商用的成品。
接下来从库,也就是 library 的角度来介绍一下相关的开发。首先,对于 Python 在项目中的“位置”大家是怎么看的呢?游戏里面,经常用到脚本语言,比如最常用的一个脚本语言 LUA,一般来说网游会用 C++ 写一个 host,由它调用多个 LUA 脚本来完成一个项目(的业务逻辑)。这就是 LUA 项目常见的(脚本)存在形式,而 Python 的话稍有不同,Python 本身就是主体来的,有一些 C/C++ 写的扩展来解决某些特定的问题,也有一些用 Python 来写的业务逻辑,通常 Python 的网游就是这样的一个结构(见下图)。
大型的 Python 项目大家看看是不是这样的:首先有一个入口的主文件 main.py,然后有几个业务逻辑的文件(file1.py、file2.py),然后同层有一些自制或公司用的库(lib1/lib2/lib3),大家的项目是否都是这样的结构?
这样做是不对的。但是我之前经历的几个项目、包括我之前看到过的几个项目,都是这样做。然后我认为,大中型的 Python 项目的结构应该是这样的:
它就是一个入口文件,然后写了一些业务逻辑(file1.py/file2.py),就是这样。大家可能会觉得有一些奇怪,那不是更小了吗?是的,其实我主张 lib 要放在 site-packages 里面,也就是做的时候,库就是库,你要分开,不要跟业务逻辑混在一起。但是大家觉得有必要搞那么复杂吗?用刚才的方式也赚到钱了呀。项目也上线了。其实呢,这么做是有一些好处的,而且最重要的是它不复杂。
其实就只是写一个 setup.py,就是用 distutils 写一个 setup.py 然后你再把它打包、安装过去就可以了。setup.py 有两个比较关键的地方,第一个就是如何避免手写 setup.py,第二个就是怎么建立命名空间包。所谓命名空间包就就是类似这种先有一个 abu 的前缀,后面才是 rpc 的包名,这样我们就可以建立专门用来做数据库的 abu.db,包括我们自己的业务逻辑,比如 abu.qipai。这个在 zope 项目是比较常见的。为了构建这样的结构,我给大家介绍一个东西——paster。因为它已经放上 pypi 所以大家可以使用 pip/easy_install 来安装它。它提供了创建项目、安装、测试、部署和运行的全栈式的支持。
大家可以帮看一下它的帮助,它后面可以加很多命令,比如创建项目、运行项目,还有产生配置文件之类的。有很多项目都使用它来构建自己的功能,比如说像 pylons、turbogears、zopeskel 等。如果要通过 paster 来运行服务的话,像今天大妈(ZoomQuiet)说到 supervisor 是吧,其实是我比较鄙视的一个东西来的,它不好用,可以试一下这个,另外我也比较推崇 start-stop-daemon,无论如何,我觉得不需要再手写守护进程,没有必要。通过 pastedeploy 可以把自己的应用以守护进程的方式或其它方式运行起来。以上是对 paster 的简单介绍,接下来看一下它的基本用法。
要创建一个应用或一个库,首先要有一个模板。可以通过(create 子命令的 --list-template 参数来查看当前环境可用的模板,比如在这里有一安装就有的 basic_package,然后可以用 -t 参数指定模板,后续可跟项目名,即可创建(项目)包。paster 会询问一些问题,比如版本,只需要填入或采用默认值即可,等询问完成,就获得了 setup.py 和相应的目录结构及相关文件了,马上即可使用,无需手写,因为 setup.py 里的 setup 函数调用有许多参数,而且这些参数还支持多种形式,要了解清楚也是非常困难的事情(,所以能不手写就不手写吧)。以上讲述的是如何避免手写 setup.py,接下来聊一下如何创建命名空间包。创建命名空间包,可以先通过 pypi 安装 pbp.skels。pbp.skels 带有许多模板,可以加速创建命名空间包之类的应用,节省宝贵时间。安装以后,可以看到多了一个 pbp_package:
然后可以通过 -t 参数指定使用这个模板来创建带点的命名空间包了。通过命名空间,可以有效地把自己的代码与别人的代码分隔开来。接下来是一个深入的主题,我不在此展开,有兴趣的朋友可以去读一下这篇 wiki(http://lucasmanual.com/mywiki/PythonPaste),它讲述的是怎么样针对 paster 编写自己的横板、扩展它的命令,因为 paster 甚至可以让你自己添加扩展自己的命令。
(再回到 setup.py 上来),通过它可以做到项目生命周期的全系列支持。比如在开发时使用 develop 子命名,可以避免每一次改动都要 install 一次。还可以用 test 进行测试,bdist/sdist 打发布包,register 在 pypi server 注册,用 upload 把发布包上传到 pypi server 等。
所以通过这些工具的支持,大家可以很方便地把代码以库的形式分隔开来,放到库应该在的地方,而不是跟业务逻辑代码混在一起,这也有利于在产生服务器上部署代码。甚至可以自建 cheese shop,也就是 pypi server,可以建立一个公司内部使用 cheese shop,就能方便同事使用你的项目。特别是像我前东家网易这样的大公司,有时候想推广一些东西给同事用,同事说我很难用上你的东西啊,比如要穿越内网隔离之类的很麻烦,那就可以通过自建 cheese shop 来解决。
(还有一个最佳实践就是)每一次开发软件包的时候,都应该有一个干净的、纯洁的环境。virtualenv 可以帮助大家建立一个纯净的环境。在项目发布的时候,不要使用系统的那个 python 环境,而是应该针对每一个项目建立相应的 virtualenv 的目录,用 virtualenv 里的 python 来运行它。去年洪教授已经在它的幻灯片里介绍过了,大家可以找来看一下,他的幻灯片写的非常详尽、严谨,而且把一个 pythoner 应该要了解的东西他基本都有介绍到,我从他那里学习到很多。
接下来讲一下插件,首先,插件跟库有什么不同呢?为了讲好这个话题,我曾特意搜索了一下,结果看到 stackoverflow 上有一个很好的解释(http://stackoverflow.com/a/2792342):插件扩展了大性应用的能力;而库则是一系列的子程序或 class 来帮助你的开发。所以库和插件是两个有较大区别的概念,所以我今天是分开来介绍的。(举个生活中的例子),伞就是一个插件,当手握一把伞的时候,人就有了“防雨”的能力了。(回到软件开发中),我们以棋牌项目为例,如图,大家可以看到有多种游戏在其中:
有斗地主、五子棋和象棋等,这些可以看作是(软件的)“功能”。接下来转到后台看一下,可能会稍有不同的感觉:
这个棋牌的项目是每一张桌子是一条独立的进程来运行的,比如我和另一个人下棋,那们就会有一条单独的进程为服务我们。进程以 desk/main.py 作为入口点,它后面可以跟不同的参数,如果跟的是 xiangqi,那么它就会加载象棋业务逻辑的插件,成为一个象棋服务器;参数是 doudizhu,则会加载斗地主的插件,成为一个斗地主服务器。这就给开发业务逻辑程序员提供了很好的扩展性,而且可以匹配权限隔离,也可以通过封装降低业务逻辑的开发技术,可以让相对初级的程序去开发业务逻辑,甚至干脆外包也不会暴露太多细节。以这个棋牌项目为例,开发业务逻辑(游戏玩法)的人不需要了解网络编辑,不需要了解数据库,不需要了解多线程,因为它开发的时候不需要调用原始的网络、数据库、多线程的接口,因为它们都是 host 提供的功能,他们只是实现 host 定义好的接口(或协议)。这其中也是通过 setuptools 来做的:
大家看这个 doudizhu 目录,它里面有个 setup.py,还有一个很重要的是 game_impl.py 文件。在 game_impl.py 文件中,它实现了业务逻辑,也就是实现了 host 定义的接口。接下来我们先看看 setup.py 文件,大家可以看到其中最重的是这两行:可以看到定义了一个 qipaionweb.games 的节,节里面有个叫 doudizhu 的配置项,它的值是 doudizhu.game_impl:GameImpl。这个值是一个 class,它也可以是一个 function。
接下来讲一下如何加载插件,大家可以看以下代码:
大家可以看到 get_game_impl_class 函数接受一个 game_name 参数,这个 game_name 参数其实就是前面讲到的命令行传入的参数,比如 xiangqi、doudizhu 之类的。然后通过 pkg_resources.load_entry_point 把 setup.py 里通过 entry_points 参数定义的内容取出来,在这里也就是 class GameImpl 啦,所以接下来把它实例化后就可用了。在 host 中,我们除却定义接口,还做了一些业务封装和通用功能,比如与上一级进程(也就是房间进程通信),比如每一个小游戏都有的踢人,统一管理业务相关的计时器等。游戏的实现首先是根据定义实现接口,也就是实现业务逻辑。业务程序员不接触网络、不接触数据库、面对的也是单线程的编程环境,可以大大地降低开发难度。对于插件的话题,大家还可以参考一下 trac 的组件架构,喜欢 OOD 的朋友应该能从中借鉴不少东西,我自己也从中学习到许多,比如它的组件管理器、组件和扩展点,以及接口的声明等。trac 的架构与我刚才讲的稍有不同,我讲的是一个很简单的版本,因为棋牌并不需要像 trac 项目那么高的扩展性。
这是 trac 文档里的两张图,可以看到 trac.core 里面有一个组件管理器,它对应很多个组件,而组件就有很多个扩展点,每一个扩展点都会实现某一个接口,大概就是这样的架构。它的 wiki 里面有详细的文档,大家可以在线访问。插件机制不只有这一种,甚至可以自己设计和实现,比如今天上午演讲的 limodou 写的 ulipad 里面就自己实现了一套插件机制。它是一套借助 mixin 来实现的插件机制。
游戏(服务器)是一件 CPU 密集、I/O 密集的应用……(待续)