賴勇浩(http://laiyonghao.com)
註:本文根據 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 密集的應用……(待續)