Twisted Documentation: Managing Clients of Perspectives

# Managing Clients of Perspectives

## Overview

In all the `IPerspective` uses we have shown so far, we ignored the `mind` argument and created a new `Avatar` for every connection. This is usually an easy design choice, and it works well for simple cases.

In more complicated cases, for example an `Avatar` that represents a player object which is persistent in the game universe, we will want connections from the same player to use the same `Avatar`.

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 `perspective_remoteListener(referencable)` that would mean both duplication of code and a higher latency in logging in, both bad.

In previous sections all realms looked to be identical. In this one we will show the usefulness of realms in accomplishing those two objectives.

## Managing Avatars

The simplest way to manage persistent avatars is to use a straight-forward caching mechanism:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    from zope.interface import implements

    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

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 `/etc/shadow` and `/etc/passwd`).

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:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    from zope.interface import implements

    class Greeter:
        greetings = 0
        def hello(self):
            self.greetings += 1
            return "<%d>hello" % (self.greetings, self.name)

    class SimpleAvatar(pb.Avatar):
        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

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:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    from zope.interface import implements

    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

It is possible to use such a pattern to define an arbitrary limit for the number of concurrent connections:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    from zope.interface import implements

    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

## Managing Clients

So far, all our realms have ignored the `mind` argument. In the case of PB, the `mind` is an object supplied by the remote login method -- usually, when it passes over the wire, it becomes a `pb.RemoteReference`. This object allows sending messages to the client as soon as the connection is established and authenticated.

Here is a simple remote-clock application which shows the usefulness of the `mind` argument:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    from zope.interface import implements

    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

In more complicated situations, you might want to cache the avatars and give each one a set of current clients or something similar.

Index

Version: 11.1.0