2.5 使用Twisted框架进行MMP服务整合
Glyph Lefkowitz,Twisted Matrix Labs
glyph@twistedmatrix.com
本文的源代码包含在所附的光盘中
|
大 |
型多人在线游戏是世界上最复杂的软件系统之一。让成千上万人在同一个空间奔跑、与同样的敌人作战并且消耗同样的游戏资源是一个相当艰巨的任务。这些游戏还必须面对一个重要挑战:在MMP虚拟世界中对外部服务进行整合。开发团队往往会忽视这部分工作并且把它们推迟到游戏运营后由运营团队来进行专门的(ad-hoc)实现。然而,由于MMP游戏逐渐面向越来越多的主流玩家,而留住订阅者所需要的资金投入也越来越高,对外部服务进行整合也就变得越来越重要了。
游戏中有一些功能明显需要由外部工具来帮助实现。游戏需要一个聊天系统,就算没有其他用途,至少可以用它来和客户进行交流从而在游戏内部提供支持。这个功能可以用第三方的即时消息系统实现。游戏开发人员还可以使用外部的报表工具(reporting tool)从关系数据库中获取数据来为营销人员和管理人员提供报表。
一旦完成了游戏核心功能的设计,开发人员往往不愿意整合这些外部功能。因为通常要为第三方整合提供机制,游戏开发人员必须在行政方面和技术方面都作出很大的努力,而出于进度方面的考虑,他们常常会放弃这个想法。
整合会带来一些好处,但是这些好处可能不值得为此付出努力。然而,如果对整个游戏生命期进行全盘考虑,就会发现整合显然是无法避免的,游戏开发人员所能采取的最佳行动就是提前为它作好准备。
譬如说,假设在某些版本的客户端软件中有一个错误(这是不可避免的),玩家可以使用某种方法来对游戏进行攻击从而使其他玩家退出游戏。如果用于发行的游戏客户端(或许对于某些授权用户来说还支持游戏管理员扩展)是访问游戏的惟一机制,那么支持人员就无法访问游戏世界,也就无法在开发人员之前改正这个错误。由于不同问题的严重程度和复杂度各不相同,开发人员可能要花费几天甚至几个星期的时间来改正它们。
游戏开发人员往往低估了加入聊天之类的简单功能所带来的复杂度。譬如说,如果游戏玩家觉得游戏中的聊天系统约束过多或功能有限,他们可能会去使用那些能更好地满足他们需要的其他外部程序。有些玩家可能
会在IRC(Internet Relay Chat)之类的聊天服务上受到其他玩家的骚扰,而服务人员会发现在处理这类抱怨时,他们也许都无法登录那些服务器,更别提获得日志文件或管理员权限了。
相对来说,整合一个关系数据库而不是开发自己特有的数据存储机制所带来的好处已经得到了广泛的认同:编写自己的数据存储层意味着需要编写所有的报表和数据获取工具,更不用说正确地实现存储管理本身就非常困难。
以上这些看上去很像一个对陈腐的面向对象软件重用思想的冗长呼吁。当我们所遇到的问题与业务领域的核心优势不一致时,与其自行编写,还不如整合那些现存的能够运行的可靠方法。即使一个团体能够良好地重用内部开发的代码,也不如直接使用外部代码来得省时省力。
2.5.1 DIY(Do It Yourself)所带来的问题
由整合和扩展带来的问题会在系统的每一层发生。创建一个稳健并且可扩展的底层架构是一个极具挑战性的问题。这和编写一个可以发布运行的简单游戏完全不同。事实上,正如极限编程(Extreme Programming)方法所指出的,过早地为可扩展性作计划是在浪费资源。要决定一个特定的扩展机制是否必须,惟一的方法就是为它找一个用例(参见第1.5节,“使用用例来描述游戏行为”)。
这并不是说游戏的开发永远都不需要可扩展性。而是说,创建一个可扩展系统的惟一方法就是让它持续运行一段很长的时间,并且让一个具有不同兴趣的庞大用户群体来使用它,从而暴露出那些真正需要扩展、改进和稳定的地方。
为特定目标创建的框架很难和不同的服务进行良好的整合,这是因为它们面向的用户有限而且开发的时间往往很短。游戏开发人员很难通过对今后开发中所需要的功能进行预期来提供意料之外的功能;错误的猜测会把本来可以用于开发重要的核心特性的时间浪费在实现那些可能毫无用处的扩展上。如果在建立底层框架时考虑不周全,往往会顾此失彼。有时,游戏开发人员所创建的底层架构可以很好地为特定的游戏模式服务,但却有可能最终成为客户服务人员的沉重负担。
最后,如果游戏开发人员从上到下地创建了一个底层架构,即使它卓然超群并且易于扩展,有哪些第三方开发人员(无论是免费的还是商业性的)能够向其中加入有用的特性呢?如果只有一小部分人知道怎样为它设计模块,那么即使是设计得最好的模块化整合软件包也是毫无用处的。
2.5.2 付费找别人来做带来的问题
现有的可以立即使用的MMP解决方案都具有相同的问题。虽然它们附带了很多工具,但是其价格都很高昂以至于第三方开发人员无法创建任何其他工具。游戏开发人员最多只能从同一个厂商那里分别购买底层架构的不同组件,但是这种模式并不常见。随着更多的游戏转向BSD或是Linux服务器并在内部使用像Python和Lua之类的开源组件,这些稳健的后端工具变得日益重要。
MMP解决方案包有时会在错误的领域提供灵活性。它们会提供一个游戏世界底层架构的完整实现,这使非常规的游戏模式不能使用;并且,它们也不会提供那些游戏开发人员没有兴趣去尝试的外部工具。
在游戏产业以外,还存在着很多其他类型的底层架构,它们被应用在通用网络、分布式计算以及数据存储中。这些架构中包含了类似于J2EE API这样的架构。从产品描述来看,它们似乎可以作为开发游戏底层架构的起点。
然而,绝大多数这类产品都是非常昂贵的中间件,它们往往定位于批量交易处理中。而游戏包含了事件驱动的动态交互。无论一个底层架构是否适合其他整合任务,如果它不能对玩家的行动做出实时反应,游戏开发人员就无法用它来实现游戏世界中的大多数基本操作。
2.5.3 问题小结
要获得一个MMP游戏能够高效运行所必需的灵活性,最重要的是使用一个满足下述条件的底层开发架构:支持MMP游戏的事件驱动本质;提供了足够的服务因而值得为此付出额外的培训费用;提供了公开的编程接口从而可以利用多个专业群体的技能来解决创建一个完整的MMP解决方案所面临的各种领域的问题。
2.5.4 构造一个解决方案
Twisted网络框架(Twisted Network Framework)就是这样一个解决方案。虽然作为一个完整的网络应用程序通用平台,它已经在几个不同的应用领域使用过,但在其设计初期,支持大型多人游戏就已经作为其设计目标之一了。作为一个开源软件,Twisted网络框架拥有一个大规模的开发人员社区。作为一个消息驱动的网络平台,它同时支持高级编程(使用Python)以及为满足更高的速度要求(speed-critical)而使用C和C++进行扩展。
Twisted可能并不适用于所有游戏。如果出于某些原因本文所描述的特定实现不适合某些特殊需要,请记住上述问题仍然需要解决,而使用和本文所描述的方案相似的方法也许是一个不错的主意。
在本文所提供的代码例子中,假定读者熟悉Python及基本的网络编程。本节将以一个假设的游戏《变形记在线》(Metamorphosis Online)[Kafka]为背景来讨论这些工具,这个游戏中的玩家是患了忧郁症的昆虫。
2.5.5 简介:通用的延迟执行机制
Twisted使用一个通用的机制来对可能阻塞(block)的行动进行包装,这是它整合网络应用程序最基本的方法之一。在很多异步网络框架中,阻塞行动主要使用回调函数来表示,也就是说,必须把符合特定接口的对象作为一个参数传入任何需要对阻塞操作进行等待的函数。对不同的阻塞操作类型来说,这些接口通常也不同,这会导致两个问题:首先,向回调函数传递额外的参数会很困难;其次,由于接口不同,如果任意两个阻塞系统需要相互通讯,就必须在它们之间编写一段特定的胶合代码,这常常会引入第3个不同的公有接口。
Twisted通过twisted.internet.defer模块来解决这些问题。所有可能等待阻塞操作的函数都会返回一个延迟操作(Deferred)类的实例。一个延迟操作对象起到了两个逻辑函数链的作用,一个用来进行错误处理,另一个用来传递结果。当一个延迟操作对象的结果变为可用时,这个结果会被传递给函数链中的第一个回调函数,然后这个回调函数的结果会被传递给第二个回调函数。如果有错误产生,错误处理回调函数就会被调用。这种方法部分是由E语言[Steigler02]中的结果(Eventually)运算符启发而来的,并且具有这个运算符所拥有的大部分预防死锁的特性。同样,因为延迟操作对象可以接受任何具有一个参数并且返回一个结果的函数,很多Python标准库函数都可以使用于Twisted网络框架中。
下面是一些使用延迟操作的例子。
# defer-examples.py
def printResult(x):
print 'result:',x
# remote method call
remoteObject.callRemote("test").addCallback(printResult)
# database interaction
dbInterface.runQuery("select * from \ random_table").addCallback(printResult)
# threadpool execution
def myMethod(a,b,c):
return a + b + c
# adding a callback
deferToThread(myMethod,1,2,3).addCallback(printResult)
# Remote method calls that invoke a database connection and wait
# until the database is done returning its query before turning
# the query into a string and then sending the string as the
# result of the remote method call.
class MyClass(Referenceable):
def remote_doIt(self):
return self.databaseConnector.runQuery( \
"select * from foo").addCallback(str)
本文已经对此作了简单的介绍,接下去深入了解一下callRemote(远程调用)是怎样工作的。
2.5.6 高层网络服务:全景代理
Twisted框架为网络应用程序开发人员提供的最强大的工具之一就是它内建的多用途远程方法调用协议(multipurpose remote method-invocation protocol)和视角代理(Perspective Broker,PB),通常被称为PB。
PB是从头开始设计的,它的不少设计目标都有助于解决整合问题,比如以下所列的几点。
· PB运行在独立的传输层连接上,它同时允许客户/服务器(未授信的、广泛分布的)以及服务器/服务器(授信的、本地集群的)通信。这可以减少系统所需要的通讯代码的总量。PB具有非常高的可定制性,并且在解码的3个层次:(反)列集(marshal)、(反)串行化(serializing)和消息路由上都提供了钩子(Hook)。这使得PB在保持很高的通用性和兼容性的同时,仍然可以对特定应用程序的需要做出迅速响应。
· PB提供了对象间的通信,而不是进程间的通信。这使得多个服务可以在一个连接上进行透明的多路复用,也使得独立的功能可以被整合到现有的服务器和客户中。
· PB使用动态的方法查询(method lookup)和方法路由(method routing)。这使客户/服务器的版本可以平滑地升级,这样在决定对遗留客户端的支持要维持多久的时候,不会再受到技术上的限制。
· PB在协议层上提供了安全稳健的认证机制。
这些特性中有很多不仅有趣,而且非常有用,最关键的是它们能够在同一网络连接上对独立开发的服务进行整合,而不必在完全不同的代码上进行耗时的整合工作。这无论是在外部(譬如说,使用从第三方厂商或是志愿者那里获得的代码)还是在内部都很有用。通过使用PB或类似的技术,如果两个开发团队工作在彼此无关的抽象功能(譬如说聊天和背囊管理)上,它们之间就可以完全独立,这使得服务端的开发过程可以并行地进行。
认证同样是这个问题的重要部分。无论所访问的服务是什么类型的,用户需要通过这个服务的认证来证明他们有权使用。
Twisted使用它的认证模式twisted.cred来统一PB中的服务。
1.twisted.cred入门
twisted.cred把认证分解为4个主要的抽象概念:
1.twisted.cred.service.Service;
2.twisted.cred.perspective.Perspective;
3.twisted.cred.authorizer.Authorizer;
4.twisted.cred.identity.Identity。
前两个特定于应用程序,后两个则特定于认证方式。
服务(Service)作为一个支配性(over-arching)抽象概念代表了所要提供的整个服务。对于任意需要在一个特定的子系统中进行全局跟踪的对象来说,服务对象所起的作用类似于管理者(Manager)设计模式[EventHelix]。一个服务对象应该包含与一个逻辑服务相关的所有状态。通常的应用程序,在最后都需要编写至少一个服务类的子类,有时候很可能会需要2个到3个这样的子类。
服务主要是对视角(Perspective)类的实例进行管理。每个服务的子类通常都会有一个与之相应的视角子类,并且服务只会对它自己实例化的视角实例进行管理。视角代表了与给定服务相关的用户状态。它把用户角色和他们在服务内部所使用的功能联系起来。每个视角都有一个名字。
用来管理认证的对象被称为授权者(Authorizer)。在通常的Twisted应用程序中并不需要开发者实现一个授权者,因为它可以使用现有的实现,也可以通过文件、数据库或是内存中的表来进行认证。根据保存认证信息的场所不同,可能需要自己实现授权者来载入或保存账户信息。
一个授权者对象本质上就是标识(Identity)实例的持久化集合。每个标识通常代表一个有权限访问游戏系统的真实用户。只有当需要实现一个新的认证方式时,系统才需要实现一个新的标识类型。Twisted提供了可以使用明文密码和口令-应答(challenge-response)认证的标识实现。它还实现了一个支持SSH的公共密码加密认证机制以支持SSH客户。一个标识不仅维护了用户必须提供的证书,还包含了这个用户通过认证后有权访问的视角实例。twisted.cred和PB之间的接口非常紧密。
这里所提供的例子将包括对内建认证机制的使用以及新服务的创建。
2.一个简单的例子:弗立茨和弗朗茨去看心理医生
在《变形记在线》游戏生涯的第一天,玩家乔和鲍勃登录到游戏中并使用他们的角色:弗立茨和弗朗茨。弗立茨和弗朗茨需要在心理医生的办公室见面以进行小组治疗。
在这个例子中,弗立茨和弗朗茨首先会通过移动电话讨论他们的约会,然后商量怎样在游戏世界中进行历险,最后准时到达办公室。
虽然游戏仿真很难分解为独立的服务,但是对玩家进行全局处理的功能(像日志、审计、消息流量控制、脏话过滤等功能)是肯定可以分开的。
这个例子将实现一个基于移动电话的简单聊天系统。在实际应用中,使用整合的twisted.words聊天包及其所附带的工具可能是一个更明智的方法。
关于代码片段
下面的每段代码示例都应该被放在单独的源文件中。这些代码片段,加上一个Twisted的安装,应该可以生成一个完整的(虽然很小)应用程序。
# cellphone.py - cell phone service
from twisted.spread.pb import Service,Perspective
class Cellphone(Perspective):
def attached(self,remoteEar,identity):
self.remoteEar = remoteEar
self.caller = None
self.talkingTo = None
return Perspective.attached(self,remoteEar,identity)
def detached(self,remoteEar,identity):
del self.remoteEar
self.caller = None
self.talkingTo = None
return Perspective.detached(self,remoteEar,identity)
def hear(self,text):
self.remoteEar.callRemote('hear',text)
def perspective_dial(self,phoneNumber):
otherPhone = \
self.service.getPerspectiveNamed(phoneNumber)
otherPhone.ring(self)
callerID = True
def ring(self,otherPhone):
self.caller = otherPhone
if self.callerID:
displayNumber = otherPhone.perspectiveName
else:
displayNumber = "000-555-1212"
self.remoteEar.callRemote('ring',displayNumber)
def perspective_pickup(self):
if self.caller:
self.caller.phoneConnected(self)
self.phoneConnected(self.caller)
self.caller = None
def phoneConnected(self,otherPhone):
self.talkingTo = otherPhone
self.remoteEar.callRemote('connected')
def perspective_talk(self,message):
if self.talkingTo:
self.talkingTo.hear(message)
class PhoneCompany(Service):
perspectiveClass = Cellphone
第一段代码提供了一个可以独立使用的移动电话服务,它不依赖于任何游戏。在这个服务中,每个玩家都被表示为一个移动电话(Cellphone)实例,用一个电话号码来作为独一无二的标识,这个号码被当作这个视角的名字。注意,那些用perspective_作为名字前缀的方法,它们可以被这个服务的网络客户直接调用。
# metamorph.py
from twisted.spread.pb import Perspective,Service
class Bug(Perspective):
angst = 0
def attached(self,remoteBugWatcher,identity):
self.remoteBugWatcher = remoteBugWatcher
self.psychologist = None
self.angst += 5 # It's hard to get up in the morning.
return Perspective.attached(self,\
remoteBugWatcher,identity)
def detached(self,remoteBugWatcher,identity):
self.remoteBugWatcher = None
if self.psychologist:
self.psychologist.leaveTherapy(self)
self.psychologist = None
return Perspective.detached(self,\
remoteBugWatcher,identity)
# Remote methods.
def perspective_goToPsychologist(self,name):
psychologist = self.service.psychologists.get(name)
if psychologist:
psychologist.joinTherapy(self)
self.psychologist = psychologist
return "You made it to group therapy."
else:
return "There's no such psychologist."
def perspective_psychoanalyze(self):
if self.psychologist:
self.psychologist.requestTherapy(self)
return "Therapy requested!"
else:
return "You're not near a therapist."
# Local methods.
def heal(self,points):
self.angst -= points
if points > 0:
feeling = "better"
else:
feeling = "worse"
self.remoteBugWatcher.callRemote(
"healed",
"You feel %s points %s. Your angst is now: %s." %
(abs(points),feeling,self.angst))
class Psychologist:
def __init__(self,name,skill,world):
self.name = name
self.skill = skill
self.group = []
world.psychologists[name] = self
def joinTherapy(self,bug):
self.group.append(bug)
def leaveTherapy(self,bug):
self.group.remove(bug)
def requestTherapy(self,bug):
for bug in self.group:
bug.heal(self.skill + len(self.group))
class BuggyWorld(Service):
perspectiveClass = Bug
def __init__(self,*args,**kw):
Service.__init__(self,*args,**kw)
self.psychologists = {}
Psychologist("Freud",-5,self)
Psychologist("Pavlov",1,self)
Psychologist("The Tick",10,self)
第二段代码定义了一个非常简单的游戏。为了展示视角之间的间接通信,游戏开发人员在视角和服务(心理医生)之上提供了另一个类。同样,那些用perspective_为前缀的方法组成了这个游戏中客户和服务器间的远程接口。
现在系统已经实现了两个完全独立的完整模块。然而,最重要的是实现它们的方式。PB可以利用它们的并行结构,这样只需要一段简短的代码就能把它们整合到一个独立的服务器中。
# fritz-franz-setup.py
from twisted.spread.pb import AuthRoot,BrokerFactory,portno
from twisted.internet.app import Application
from twisted.cred.authorizer import DefaultAuthorizer
# Import our own library
import cellphone
import metamorph
# Create root-level object and authorizer
app = Application("Metamorphosis")
auth = DefaultAuthorizer(app)
# Create our services (inside the App directory)
phoneCompany = cellphone.PhoneCompany("cellphone",app,auth)
buggyWorld = metamorph.BuggyWorld("metamorph",app,auth)
# Create Identities for Joe and Bob.
def makeAccount(userName,phoneNumber,bugName,pw):
# Create a cell phone for the player,so they can chat.
phone = phoneCompany.createPerspective(phoneNumber)
# Create a bug for the player,so they can play the game.
bug = buggyWorld.createPerspective(bugName)
# Create an identity for the player,so they can log in.
i = auth.createIdentity(userName)
i.setPassword(pw)
# add perspectives to identity we created
i.addKeyForPerspective(phone)
i.addKeyForPerspective(bug)
# Finally,commit the identity back to its authorizer.
i.save()
# Create both Bob's and Joe's accounts.
makeAccount("joe","222-303-8484","fritz","joepass")
makeAccount("bob","222-303-8485","franz","bobpass")
app.listenTCP(portno,BrokerFactory(AuthRoot(auth)))
app.run()
最后,游戏系统通过为鲍勃和乔创建账户来把这些服务联系起来。这个例子的关键在于makeAccount(创建账户)方法,它创建了与正确的视角实例相关联的标识实例。
通过修改这个文件,可以任意添加更多的服务。这个过程分为3步:
(1)实例化新的服务——在“# Create our services(创建我们的服务)”小节中有讲解;
(2)调用服务的createPerspective(创建视角)方法来创建新的视角;
(3)在相应的标识对象中加入视角的键(key),这样用户一旦通过了这个标识的认证就可以对相应的视角进行访问。
乔和鲍勃怎样才能真正地进入游戏呢?一个完整的交互式客户端足以作为另一篇文章的主题来介绍了,这里本文只提供一个简单的例子,它会登录到《变形记在线》中并且进行5秒钟的游戏。
# metaclient.py
from twisted.spread.pb import authIdentity,getObjectAt,portno
from twisted.spread.flavors import Referenceable
from twisted.internet import reactor
from twisted.internet.defer import DeferredList
class BugClient(Referenceable):
def gotPerspective(self,pref):
print 'got bug'
pref.callRemote("goToPsychologist",\
"The Tick").addCallback(self.notify)
pref.callRemote("psychoanalyze").addCallback(self.notify)
def notify(self,text):
print 'bug:',text
def remote_healed(self,healedMessage):
print 'Healed:',healedMessage
class CellClient(Referenceable):
def gotPerspective(self,pref):
print 'got cell'
# pref.callRemote()
def remote_connected(self):
print 'Cell Connected'
def remote_hear(self,text):
print 'Phone:',text
def remote_ring(self,callerID):
print "Your phone is ringing. It's a call from",\
callerID
bugClient = BugClient()
phoneClient = CellClient()
# Log-In Information
username = "bob"
password = "bobpass"
bugName = "franz"
phoneNumber = "222-303-8485"
# A little magic to get us connected...
getObjectAt("localhost",portno).addCallback( \
# challenge-response authentication
lambda r: authIdentity(r,username,password)).addCallback( \
# connecting to each perspective with 'attach' method
# of remote identity
lambda i: DeferredList([ \
i.callRemote("attach","metamorph",bugName,bugClient),\
i.callRemote("attach","cellphone",phoneNumber,\
phoneClient)]) \
# connecting perspectives to client-side objects
).addCallback(lambda l: (bugClient.gotPerspective(l[0][1]),\
phoneClient.gotPerspective(l[1][1])))
reactor.callLater(5,reactor.stop) # In five seconds,log out.
reactor.run() # Start the main loop.
客户端代码的主要责任就是为服务器上存在的每个视角对象提供客户对象,以及接收服务器发送的通知。用几行胶合代码就可以把客户端实例连接到服务器端的视角上,其中大部分与服务端胶合代码相同。
图2-22所示是对PB应用程序所需要的登录过程进行了可视化的说明。客户和服务器需要通过8个基本交互步骤来建立一个工作会话,图中用带有数字的箭头来表示。

图2-22 登录过程
(1)客户联系授权者请求登录。
(2)客户通过口令-应答会话提供一个用户名和一个密码,确保密码即使在未加密的连接上也是安全的。
(3)口令-应答过程找到一个标识对象并且进行认证。
(4)给客户一个指向标识的远程对象引用。
(5)客户使用服务名/视角名来向某个视角发出请求,并且在找到这个视角后使用标识接口中的attach(连接)方法把一个指定的“客户”对象和视角连接在一起。
(6)如果这个标识包含了一个指向服务/视角名的键,它就会去寻找相应的服务。
(7)这个服务会从它的持久化存储中载入视角并且返回它,然后调用这个被装载的视角的attached方法。
(8)这个视角被返回给客户并且与客户端对象相关联,然后客户对象就可以发起通信了。
3.其他功能
虽然《变形记在线》是一个非常简单的Twisted应用程序实例,但仍然用到了很多有用的技术。这种方法最直接最明显的好处就是,这类极其简单的客户对进行服务器负载测试来说是非常有用的。设计人员必须能够使用简化了的客户来访问游戏服务器,既可以用它来防御正式客户代码中某些不稳定的部分,也可以把它作为一种自动化机制来完成一些简单的常用任务。
Twisted的底层网络架构对游戏的开发也起到了有益的作用。通过产生一个加密的密钥,游戏开发人员可以用基于行业标准的安全套接字(Secure Socket Layer,SSL)来保护PB连接。只需要简单地把服务器对listenTCP的调用替换成listenSSL就可以了。
这个方法的另一个好处就是它具有良好的安全性。通常,游戏开发人员都想要在公开发布的客户中为游戏管理员提供某些便捷的功能,但是并不想让普通用户访问。在twisted.cred中,游戏系统可以把这些危险的功能封装到一个独立的视角中,并且永远不让玩家的标识对象访问这种类型视角的键。这么做可以减少在方法中编写访问检查的需要。由于不需要进行这些检查,也就不会忘记写上这样一个检查——缺省情况下,不安全的功能将不可用。
2.5.7 基于Web的工具
在这个日益由互联网驱动的时代,任何关于网络技术整合的讨论都不可避免地包含了对HTTP或是传统协议(Legacy Protocol)支持的讨论。Twisted包含了一个稳健灵活的Web服务器,它既可以作为一个独立的服务器使用,也可以用来在Web上发布其他Twisted对象。
Web浏览器是一种无处不在的稳健客户端。Web既可以作为方便的统计报表机制,也可以用作技术支持人员的管理前端,它还可以显示持久化的阶梯(laddering)信息以满足某些玩家自吹自擂的需要,它甚至可以用来为用户提供一个小型游戏客户。
除了Web服务器,Twisted还为基于浏览器的应用程序提供了一个模版(template)和应用程序框架(application framework)。这些工具统称为WebMVC。WebMVC使用了一种较为罕见的方式来处理在创建Web应用程序时面对的问题。很多Web专用的语言和服务器API把关系数据库作为Web和应用程序的整合点,并且假设整个应用程序是专门为Web编写的。与此相反,Twisted假设游戏开发人员想要把一些现有的应用程序组件放在网页上,因此它直接把这些对象挂接到Web上。
简单地说,WebMVC把Web看作一个符合MVC模式[Helman]的GUI工具箱而不是一个批量输出格式,它是为了与运行中的系统进行基于Web的实时交互而设计的。
为了介绍这种把现有的对象发布到Web上的方法,本文将为前面的例子整合一些简单的Web功能:显示一个所有玩家的列表。
首先,创建一个HTML模版metaweb.html来说明本文的意图。
<html>
<head>
<title>Metamorphosis Player Rankings</title>
</head> <body>
<h1>Metamorphosis Player Rankings</h1>
<table view="bugDisplay" border="1">
<tr> <th>Rank</th><th>Name</th> <th>Angst</th> </tr>
<tr rowOf="bugDisplay">
<td columnOf="bugDisplay" columnName="rank" />
<td columnOf="bugDisplay" columnName="name" />
<td columnOf="bugDisplay" columnName="angst" />
</tr>
</table>
</body>
</html>
WebMVC要求所有的模版都是格式正确(Well-formed)的XML(eXtensible Markup Language)。如果游戏开发人员已经很熟悉HTML,但还不熟悉XML,那么这不过意味着要在所有不需要关闭的标签的“>”之前加一个“/”,譬如说<HR/>和<BR/>。
这个文件只不过是一个模版。它并不包含任何显示逻辑或数据,它只是指定了输出应该是什么样子。注意,加入view、rowOf以及columnOf这样的属性,就可以把文档中的某些部分标记为被代码所用。
# metaweb.py
from twisted.web import wmvc,microdom
from twisted.python import domhelpers
class MBuggyWorld(wmvc.WModel):
def __init__(self,world,*args,**kw):
wmvc.WModel.__init__(self,world,*args,**kw)
self.world = world
class CBuggyWorld(wmvc.WController):
pass
class VBuggyWorld(wmvc.WView):
templateFile = "metaweb.html"
def factory_bugDisplay(self,request,node):
rowNode = domhelpers.locateNodes([node],"rowOf",\
"bugDisplay")[0]
node.removeChild(rowNode)
bugList = self.model.world.perspectives.values()
bugList.sort(lambda a,b: a.angst < b.angst)
rank = 0
for bug in bugList:
rank += 1
rnode = rowNode.cloneNode(1)
node.appendChild(rnode)
colNodes = domhelpers.locateNodes( \
[rnode],"columnOf","bugDisplay")
bugDict = {"name": bug.perspectiveName,
"angst": bug.angst,
"rank": rank}
for cn in colNodes:
cn.appendChild( microdom.Text( str( bugDict[ \
cn.getAttribute( "columnName") ] )))
return node
wmvc.registerViewForModel(VBuggyWorld,MBuggyWorld)
wmvc.registerControllerForModel(CBuggyWorld,MBuggyWorld)
创建了模版以后,还要创建一个表示逻辑(Presentation Logic)模块,游戏开发人员在这个模块中获取数据并使用W3C(WWW Consortium)标准的文档对象模式(Document Object Model,DOM)对其进行格式化。WebMVC中的显示逻辑大致有4个执行步骤。
1.把模版读入一棵DOM树。
2.通过深度优先搜索寻找特定的节点(那些具有view属性的)。
3.对这些节点调用以factory_[view属性]命名的方法。这些方法会修改由模版生成并经过分析的XML树中的节点。
4.处理完毕后,把这棵树以XHTML流的形式传给浏览器。
现在需要用一种方法来测试这段代码。回到前面的例子(fritz-franz-setup.py),并且修改服务端代码使得它在运行PB服务器的同时还运行一个HTTP服务器。把下面这段代码加在app.run()这一行之前。
# add-web.py
from twisted.web.server import Site
from twisted.web.static import File
from twisted.web.script import ResourceScript
f = File('.')
f.processors = { '.rpy': ResourceScript }
app.listenTCP(8080,Site(f))
这段代码创建了一个Web服务器,用来向外界提供执行路径下的文件。这段代码还指定了被作为动态内容来解释的文件类型:任何以.rpy为扩展名的文件都是一个资源脚本(ResourceScript),这意味着它是一个用来产生Web资源实例的Python文件。
最后,必须创建一个.rpy文件(metaweb.rpy),由它来指定这个Web应用程序的模式。
import metaweb,__main__
resource = metaweb.MBuggyWorld(__main__.buggyWorld)
因为fritz-franz-setup.py是这个程序的__main__模块,初始化的BuggyWorld服务可以在那里找到。
现在这个游戏已经运行了自己的Web服务器,它既是一个安全的基于文件系统的全功能Web服务器,也是游戏世界的动态窗口。
当然,游戏的企业网站也是一个问题。Twisted Web提供对Zope应用服务器[O’Brien01]的直接支持,而Zope正迅速地成为内容管理方面的行业标准。虽然企业网站的开发已经超出了本文的讨论范围,但是与Zope的整合使得游戏开发人员可以非常简单地把上面所描写的组件放到一个具有新闻发布、留言板以及一个门户网站所有功能的网站中——要做到这些只需要在一个管理界面上单击几下鼠标即可。
2.5.8 整合独立对象
面向对象编程(Object-Oriented Programming,OOP)长期被吹捧为可以解决整合所带来的问题。每个有经验的软件开发人员都应该知道,这种说法虽然具有一定的正确性,但是被那些提供软件开发工具的公司无限地夸大了。OOP那时灵时不灵的特性可以归根于两个原因:a)对于什么是面向对象以及什么不是面向对象的定义过于广泛;b)人们不愿意从制度上去区分OOP中所使用技术的好坏。
到目前为止,本文对怎样使用Twisted所提供的服务来推动与外部工具的整合进行了讨论。下面将介绍Twisted怎样提供这一系列多样的内部服务而没有受到依赖性问题的困扰。
Twisted设计中与此相关的主导思想是可分离性(Separability)。两个不同的类如果能够简单地独立实例化并且执行各自的功能,那么改变其中的一个会导致另一个发生改变的可能性就会很小。此外,新用户在能够使用一个类之前需要知道的信息应该是越少越好。
通常情况下,可分离的实例是一件好事,因此Twisted在大部分情况下避免继承,除非两个类必须被紧密地联系在一起或者基类非常简单、只提供一些公共的功能。
此外,这个方法具有很好的安全属性。当某些对象可以通过网络访问时,很重要的一点就是要仔细地分离那些敏感的操作,并且确保它们远离可能的访问源。如果我们能把大多数功能分散在不同的类中,就可以避免访问控制中的错误。
这是一个古老的概念。事实上,从某种意义上来说这正是OOP的起源,它源于仿真中的行动者(Actor)模型。由于网络和安全方面的原因,很多人仍然对行动者模型很感兴趣。这也是一个崭新的概念。各种被狂热追捧的 “组件框架”本质上可以归结于一个简单的特性:这些架构强迫对象把它们的功能分离到可以独立使用的软件包中。
twisted.python.components就有这样一个具体而微的组件框架。它主要基于Zope3组件框架。虽然提供了一些高级特性,但是其本质还是一个非常简单的系统。它几乎完全基于一个特性:适配器(Adapter)。
下面的代码片段介绍了适配器的基本使用方法。严格地说,适配器只不过是一个具有特殊要求的类:它的构造函数只带有一个参数。适配器被注册后就可以在需要某个特定接口的情况下代表另一个类,因此它至少会实现一个接口。
# component-example.py
from twisted.python.components import getAdapter,registerAdapter,Interface
# Define an interface that implements a sample method,"a".
class IA(Interface):
def a(self):
"A sample method."
# define an adapter class that implements our IA interface
class A:
__implements__ = IA
def __init__(self,original):
# keep track of the object being wrapped
self.original = original
def a(self):
# define the method required by our interface,and have it
# print 'a' then call back to the object we're adapting
print 'a',
self.original.b()
# the hapless B class doesn't know anything
# about its adapter and it defines one method which displays 'b'
class B:
def b(self):
print 'b'
# register A to adapt B for interface IA
registerAdapter(A,B,IA)
# adapt a new B instance with an A instance
adapter = getAdapter(B(),IA,None)
# call the method defined by interface IA
adapter.a()
本文已经介绍了Twisted中的组件系统是怎样对外部游戏服务进行整合的。这些例子中所涉及到的大多数“幕后”工作——尤其是关于WebMVC的例子——都是由适配器实现的。那些使它有助于开发一个独立于所发布对象的Web发布系统的特性也使它有助于其他形式的分离——譬如说,独立于游戏世界几何表示的魔法系统。
2.5.9 底层整合:协议与网络
处于Twisted底层架构最底层的是twisted.internet。Twisted中其他的部分都是在这个部分的基础上构建的。twisted.internet主要是一个基于Reactor事件-响应模式[Schmidt]的网络核心。
Twisted 的设计思想之一就是它在任何层次上(从最高层的消息抽象到网络上传输的位和字节)都是可插拔(pluggable)的。这符合Twisted框架早期对MMP的想法:我们的Twisted应用程序能够根据实际情况满足对效率和灵活性的不同需求。
前面的例子已经使用了twisted.internet API来把Web和PB服务器连上因特网。这里我们将介绍怎样编写自己的服务器。要在这个层次上对Twisted进行扩展,最简单例子就是编写一个echo服务器。
# echoserver.py
from twisted.internet.protocol import Protocol,Factory
# Class designed to manage a connection.
class Echo(Protocol):
# Method called when data is received.
def dataReceived(self,data):
# When we receive data,write it back out.
self.transport.write(data)
class EchoFactory(Factory):
# Build Protocols on incoming connections
def buildProtocol(self,address):
return Echo()
from twisted.internet import reactor
# Bind TCP Port 1234 to an EchoFactory
reactor.listenTCP(1234,EchoFactory())
# run the main loop until the user interrupts it
reactor.run()
这段简单的Python脚本是一个基于Twisted框架的全功能异步多路复用服务器。这是一个不错的基本实例,可以在此基础上创建一个能够与现存MMP底层框架中的非标准传统协议进行交互的服务端处理系统。
为这些服务器实现其他形式的客户也非常有用,它们可以用来自动执行常见任务或是进行负载测试。Twisted提供了完整的客户端支持,它被刻意设计得与服务端支持尽可能地接近。
# shoutclient.py
from twisted.internet.protocol import Protocol,ClientFactory
# Class designed to manage a connection.
class Shout(Protocol):
# a string to send the echo server
stringToShout = "Twisted is great!"
# Method called when connection established.
def connectionMade(self):
# create an empty buffer
self.buf = ""
print "Shouting:",repr(self.stringToShout)
self.transport.write(self.stringToShout)
# Method called when data is received.
def dataReceived(self,data):
# buffer any received data
self.buf += data
# if we've received the full message
# then close the connection.
if self.buf == self.stringToShout:
self.transport.loseConnection()
print "Echoed:",repr(self.stringToShout)
reactor.stop()
class ShoutClientFactory(ClientFactory):
# Build Protocols on incoming connections
def buildProtocol(self,address):
return Shout()
from twisted.internet import reactor
# connect to local port 1234 and expect an echo.
reactor.connectTCP("localhost",1234,ShoutClientFactory())
# run the main loop until the user interrupts it
reactor.run()
注意,客户端和服务端的这两个例子都以reactor.run()结束。相同的reactor对象被同时用于客户端和服务端。事实上,任何数量的客户和/或服务器可以在同一个进程中运行。
2.5.10 开发社区
Twisted框架提供了很多其他有用的协议。它目前支持超过10个RFC(Request for Comments),包括因特网邮件协议(Internet mail protocol)、异步数据库访问、域名服务器和客户端以及IRC(Internet relay chat)。
比现存的工具更能让人感兴趣的是,它未来的开发潜力。对于游戏开发人员所选择的平台而言,一个有着持续的兴趣和投入的开发者社区是很重要的,它确保了对这一平台的支持不会在一夜之间消失。同样,能够与一个现有社区合作或是从中获得支持也可以大大地减少开发一个新功能所需要的时间。
其他非开源的项目也可能涌现出像Twisted这样大型而活跃的支持社区,譬如说[Java]。而这样的社区对于一个可靠的框架来说是必不可少的。
开源开发社区通常对那些难以正确解决的、晦涩难懂的问题感兴趣,但是这并不能为任何人带来真正的竞争优势。Twisted框架还具有一个好处:Twisted框架的开发者把它作为自己要使用的工具来开发,因此他们非常注重稳定性。并且,当很多不同的组织致力于同一个项目的开发时,使用Twisted框架可以大大地加强它的稳健性。
2.5.11 总结
对于MMP游戏开发人员来说,它不仅仅是一个游戏,更是一个完整的小型底层架构。他们需要工具来进行持续的开发、营销和支持。如果开发人员所关注的只是游戏,那么提供这些工具会带来非常复杂的问题,它们往往很深奥并且不是很容易解决。
如果存在一个合适的解决方案,那就应该使用它。然而,要找到一个合适的解决方案很困难,特别是它们中的大多数缺少一些有用(如果不是必需的话)的功能。
Twisted框架对于很多游戏来说都是一个有效的解决方案。它提供了多种不同的网络访问机制,包括一个灵活的通用客户服务器协议以及Web发布。另外,在这些协议上又衍生出了像认证之类有用的抽象概念。
Twisted框架在设计时就考虑了安全性。对于其开发团队来说,保持它的安全性和稳定性至关重要,因为它要用来运行twistedmatrix.com。
Twisted框架在很多层次上都是可扩展的。如果所需要的功能还不存在,那么很可能存在创建它的钩子。一个大型多样的开发社区可以确保这一点持续存在。
很少有人会用到Twisted框架提供的所有功能,但是通过在一个Twisted服务器中提供一到两个辅助游戏服务可以很方便地对某些功能进行运用。对一个底层架构方案进行评估是非常重要的。
2.5.12 参考文献
[Caromel] Caromel,Denis,“Towards a Method of Object-Oriented Concurrent Programming,” http://citeseer.nj.nec.com/300829.html.
[Cunningham] Cunningham,Ward,et al.,“Proto Patterns,” http://www.c2.com/cgi/wiki? ProtoPatterns.
[EventHelix] Event Helix,Inc.,“Manager Design Pattern,” http://www.eventhelix.com/ Realtime Mantra/ManagerDesignPattern.htm.
[Helman] Helman,Dean,“Model-View-Controller,” http://ootips.org /mvc-pattern.html.
[Java] “The Java™ White Paper,” Sun Microsystems,http://java.sun.com/ docs/white/index. html.
[Kafka] Kafka,Franz,“The Metamorphosis,” http://www.kafka.org/transl/ english/metamorp hosis.htm.
[O'Brien01] O'Brien,Larry,“And Then Came Zope...,” http://www.sdtimes.com/cols/ webwatch _ 023.htm,February 1,2001.
[Schmidt] Schmidt,Douglas C.,“Reactor -- An Object Behavioral Pattern for Event Demultiplexing and Event Handler Dispatching,” http://www.cs.wustl.edu/ ~schmidt/PDF/reactor- siemens.pdf.
[Steigler02] Steigler,Marc,“E in a Walnut,” http://www.skyhunter.com/ marcs/ewalnut.html,2000.
[W3C] World Wide Web Consortium Document Object Model Working Group,“W3C Document Object Model,” http://www.w3.org/DOM/.
[Zadka] Moshe Zadka,“Writing Servers in Twisted”,http://twistedmatrix.com /documents/ howto/servers.






