pclients.xhtml   [plain text]


<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 "&lt;%d&gt;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 "&lt;%d&gt;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 "&lt;%d&gt;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 "&lt;%d&gt;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 &gt;= 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>