<html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Managing Clients of Perspectives</title> <link rel="author" href="http://twistedmatrix.com/users/acapnotic/" title="Kevin Turner" /> </head> <body> <h1>Managing Clients of Perspectives</h1> <h2>Overview</h2> <p>In all the <code class="API" base="twisted.spread.pb">IPerspective</code> we have shown so far, we ignored the <code>mind</code> argument and created a new <code>Avatar</code> for every connection. This is usually an easy design choice, and it works well for simple cases.</p> <p>In more complicated cases, for example an <code>Avatar</code> that represents a player object which is persistent in the game universe, we will want connections from the same player to use the same <code>Avatar</code>.</p> <p>Another thing which is necessary in more complicated scenarios is notifying a player asynchronously. While it is possible, of course, to allow a player to call <code>perspective_remoteListener(referencable)</code> that would mean both duplication of code and a higher latency in logging in, both bad.</p> <p>In previous sections all realms looked to be identical. In this one we will show the usefulness of realms in accomplishing those two objectives.</p> <h2>Managing Avatars</h2> <p>The simplest way to manage persistent avatars is to use a straight-forward caching mechanism:</p> <pre class="python"> class SimpleAvatar(pb.Avatar): greetings = 0 def __init__(self, name): self.name = name def perspective_greet(self): self.greetings += 1 return "<%d>hello %s" % (self.greetings, self.name) class CachingRealm: __implements__ = portal.IRealm, def __init__(self): self.avatars = {} def requestAvatar(self, avatarId, mind, *interfaces): if pb.IPerspective not in interfaces: raise NotImplementedError if avatarId in self.avatars: p = self.avatars[avatarId] else: p = self.avatars[avatarId] = SimpleAvatar(avatarId) return pb.IPerspective, p, lambda:None </pre> <p>This gives us a perspective which counts the number of greetings it sent its client. Implementing a caching strategy, as opposed to generating a realm with the correct avatars already in it, is usually easier. This makes adding new checkers to the portal, or adding new users to a checker database, transparent. Otherwise, careful synchronization is needed between the checker and avatar is needed (much like the synchronization between UNIX's <code>/etc/shadow</code> and <code>/etc/passwd</code>).</p> <p>Sometimes, however, an avatar will need enough per-connection state that it would be easier to generate a new avatar and cache something else. Here is an example of that:</p> <pre class="python"> class Greeter: greetings = 0 def hello(self): self.greetings += 1 return "<%d>hello" % (self.greetings, self.name) class SimpleAvatar(pb.Avatar): greetings = 0 def __init__(self, name, greeter): self.name = name self.greeter = greeter def perspective_greet(self): return self.greeter.hello()+' '+self.name class CachingRealm: __implements__ = portal.IRealm, def __init__(self): self.greeters = {} def requestAvatar(self, avatarId, mind, *interfaces): if pb.IPerspective not in interfaces: raise NotImplementedError if avatarId in self.greeters: p = self.greeters[avatarId] else: p = self.greeters[avatarId] = Greeter() return pb.IPerspective, SimpleAvatar(avatarId, p), lambda:None </pre> <p>It might seem tempting to use this pattern to have an avatar which is notified of new connections. However, the problems here are twofold: it would lead to a thin class which needs to forward all of its methods, and it would be impossible to know when disconnections occur. Luckily, there is a better pattern:</p> <pre class="python"> class SimpleAvatar(pb.Avatar): greetings = 0 connections = 0 def __init__(self, name): self.name = name def connect(self): self.connections += 1 def disconnect(self): self.connections -= 1 def perspective_greet(self): self.greetings += 1 return "<%d>hello %s" % (self.greetings, self.name) class CachingRealm: __implements__ = portal.IRealm, def __init__(self): self.avatars = {} def requestAvatar(self, avatarId, mind, *interfaces): if pb.IPerspective not in interfaces: raise NotImplementedError if avatarId in self.avatars: p = self.avatars[avatarId] else: p = self.avatars[avatarId] = SimpleAvatar(avatarId) p.connect() return pb.IPerspective, p, p.disconnect </pre> <p>It is possible to use such a pattern to define an arbitrary limit for the number of concurrent connections:</p> <pre class="python"> class SimpleAvatar(pb.Avatar): greetings = 0 connections = 0 def __init__(self, name): self.name = name def connect(self): self.connections += 1 def disconnect(self): self.connections -= 1 def perspective_greet(self): self.greetings += 1 return "<%d>hello %s" % (self.greetings, self.name) class CachingRealm: __implements__ = portal.IRealm, def __init__(self, max=1): self.avatars = {} self.max = max def requestAvatar(self, avatarId, mind, *interfaces): if pb.IPerspective not in interfaces: raise NotImplementedError if avatarId in self.avatars: p = self.avatars[avatarId] else: p = self.avatars[avatarId] = SimpleAvatar(avatarId) if p.connections >= self.max: raise ValueError("too many connections") p.connect() return pb.IPerspective, p, p.disconnect </pre> <h2>Managing Clients</h2> <p>So far, all our realms have ignored the <code>mind</code> argument. In the case of PB, the <code>mind</code> is an object supplied by the remote login method -- usually, when it passes over the wire, it becomes a <code>pb.RemoteReference</code>. This object allows sending messages to the client as soon as the connection is established and authenticated.</p> <p>Here is a simple remote-clock application which shows the usefulness of the <code>mind</code> argument:</p> <pre class="python"> class SimpleAvatar(pb.Avatar): def __init__(self, client): self.s = internet.TimerService(1, self.telltime) self.s.startService() self.client = client def telltime(self): self.client.callRemote("notifyTime", time.time()) def perspective_setperiod(self, period): self.s.stopService() self.s = internet.TimerService(period, self.telltime) self.s.startService() def logout(self): self.s.stopService() class Realm: __implements__ = portal.IRealm, def requestAvatar(self, avatarId, mind, *interfaces): if pb.IPerspective not in interfaces: raise NotImplementedError p = SimpleAvatar(mind) return pb.IPerspective, p, p.logout </pre> <p>In more complicated situations, you might want to cache the avatars and give each one a set of <q>current clients</q> or something similar.</p> </body> </html>