Architecture
We'll assume that you've read all the preceding sections of this tutorial and have just finished the "Echo" application example. As such, we don't need to do any more "mental preparation" and can skip straight to a description of the architecture.
Fundamentally, this is no different than our echo application: there is
a little more chatter that takes place between the client and server;
there's another object involved (a ChatRoom
); and we'll have
to run the server a little differently.
Here are the new features we want to support:
- login form;
- in-memory user storage;
- the ability to send global alerts to all users; and
- the ability for all users to "hear" when another user speaks in the chat room;
A general rule we can establish about our architecture is that if something has to happen for everyone, that code needs to appear on the server side, since it's the server that is keeping track of all users. If something is going to happen irrespective of other users or if browser DOM manipulation is required, then we know the client will be the recipient of the code.
As such, in the features above, the login form will be client code. The user storage, global alerts, and "hearing" will be implemented in server code for the data; updating the DOM with that data will be implemented in client code.
The user experience of this application will be the following:
- they will be presented with a login box (no password, only username);
- upon logging in, a message will be sent to all logged in users that this person has joined, they will see a message at the bottom of the chat that states their login name, and the login form will be replaced with a chat area and a text input field;
- they will type text in the input field; and
- the typed text will appear in the browser of every person who is logged in.
Building upon our previous example, our application will do the following:
- JavaScript client code will extract user input and send it to our server;
- Python code will receive messages from the client;
- Python code will process these messages;
- Python code will send messages to the all clients; and
- a template file (or
stan
code) will be used for presentation.
More Coding
Presentation
The template is very similar as it was in the previous example, with the differences being a new login box, a "logged in as" area, and some name changes:
<div xmlns:nevow="http://nevow.com/ns/nevow/0.1" xmlns:athena="http://divmod.org/ns/athena/0.7" nevow:render="liveElement"> <h2>Chatter Element</h2> <form name="chatBox"> <athena:handler event="onsubmit" handler="doSay" /> <div name="scrollArea" style="border: 1px solid gray; padding: 5; margin: 5"> </div> <div name="sendLine" style="display: none"> <input name="userMessage" /><input type="submit" value="Send" /> </div> </form> <form name="chooseBox"> <athena:handler event="onsubmit" handler="doSetUsername" /> Choose your username: <input name="username" /> <input type="submit" name="GO" value="Enter"/> </form> <div name="loggedInAs" style="display:none"><span>Logged in as </span></div> </div>
We've now got two JavaScript methods that need to be defined:
doSetUsername()
and doSay()
. We can also infer
from this template that elements will be hidden and shown after login
(note the presence of style="display:none"
in two places). With
these observations in hand, let's proceed to the JavaScript code.
Writing the Client
Referring back to our thoughts in the "Architecture" section above, we can establish that the JavaScript code needs the following:
- have the same basic boilerplate as in the "echo" example (imports, inheritance, attribute-setting in the constructor);
- implement the
doSetUsername()
anddoSay()
methods; - create a method that will send a message to all users; and
- create a method that will let everyone know when someone says something. Let's see how this is done.
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 33 34 35 36 37 38 39 40 41 42
// import Nevow.Athena Nevow.Athena.Widget.subclass(ChatThing, 'ChatterWidget').methods( function __init__(self, node) { ChatThing.ChatterWidget.upcall(self, "__init__", node); self.chooseBox = self.nodeByAttribute('name', 'chooseBox'); self.scrollArea = self.nodeByAttribute('name', 'scrollArea'); self.sendLine = self.nodeByAttribute('name', 'sendLine'); self.usernameField = self.nodeByAttribute('name', 'username'); self.userMessage = self.nodeByAttribute('name', 'userMessage'); self.loggedInAs = self.nodeByAttribute('name', 'loggedInAs'); }, function doSetUsername(self) { var username = self.usernameField.value; self.callRemote("setUsername", username).addCallback( function (result) { self.chooseBox.style.display = "none"; self.sendLine.style.display = "block"; self.loggedInAs.appendChild(document.createTextNode(username)); self.loggedInAs.style.display = "block"; }); return false; }, function doSay(self) { self.callRemote("say", self.userMessage.value); self.nodeByAttribute('name', 'userMessage').value = ""; return false; }, function displayMessage(self, message) { var newNode = document.createElement('div'); newNode.appendChild(document.createTextNode(message)); self.scrollArea.appendChild(newNode); document.body.scrollTop = document.body.scrollHeight; }, function displayUserMessage(self, avatarName, text) { var msg = avatarName+': '+text; self.displayMessage(msg); });
There is a little abstraction here:
- we need a general message-sending method (
displayMessage()
) for any message that gets sent to all users; - for user chat messages, we need something that will prepend the username so
that everyone knows who said what (
displayUserMessage()
), and once this method does its thing, it passes the adjusted message on todisplayMessage()
.
Other than that, this is very straight-forward code; it's pretty much
the same as the "Echo" tutorial. The display*()
methods
are only responsible for updating the UI, just as we would expect.
1 2 3 4 5 6 7 8
from twisted.python import util from nevow import athena import chatthing chatthingPkg = athena.AutoJSPackage(util.sibpath(chatthing.__file__, 'js'))
Writing the Server
The server code is a bit more complicated. We anticipated this above in the "Architecture" section where we noted that the Python code needs to receive, process and send messages.
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
from twisted.python.util import sibpath from nevow.loaders import xmlfile from nevow.athena import LiveElement, expose class ChatRoom(object): def __init__(self): self.chatters = [] def wall(self, message): for chatter in self.chatters: chatter.wall(message) def tellEverybody(self, who, what): for chatter in self.chatters: chatter.hear(who.username, what) def makeChatter(self): elem = ChatterElement(self) self.chatters.append(elem) return elem # element to be run with twistd chat = ChatRoom().makeChatter class ChatterElement(LiveElement): docFactory = xmlfile(sibpath(__file__, 'template.html')) jsClass = u'ChatThing.ChatterWidget' def __init__(self, room): self.room = room def setUsername(self, username): self.username = username message = ' * user '+username+' has joined the room' self.room.wall(message) setUsername = expose(setUsername) def say(self, message): self.room.tellEverybody(self, message) say = expose(say) def wall(self, message): self.callRemote('displayMessage', message) def hear(self, username, what): self.callRemote('displayUserMessage', username, what)
There is something in our "Chat" code that is not at all present in the
"Echo" application: the ChatRoom
object. We need this object for the
following functionality:
- a means of instantiating new
ChatterElement
clients; - a "singleton" instance for keeping track of all
ChatterElement
clients; - a means sending messages to all clients;
Let's look at the second two reasons first. In our "Chat" application,
a new ChatterElement
is created whenever a user connects,
so we will have potentially many of these instances. In order
for our chat server to function as designed, it will need a way to
communicate with each of these. If we create an object that can keep the
ChatterElement
es in a list, then it will be able to iterate that
list and call methods that, in turn, make remote calls to the JavaScript.
Because we need the chat room to be a singleton object, it
can only be instantiated once. But we need many instantiations of
ChatterElement
-- one for each connection, in fact. So what do
we do? Well, in this case, we make one of the methods
of ChatRoom
a factory for instantiating a
ChatterElement
. Before we return the instance, though, we
append it to the list of instances that the ChatRoom
is keeping track of.
Putting it All Together
Now that we've got all the code in front of us, we can trace out exactly what happens:
- the user loads the resource in their browser, and the template is rendered;
- after typing a message in the input box, the user hits submit;
- JavaScript client code calls to the server with the text the user submitted;
- the server gets the message and shares it with all the connected
ChatterElement
s; - each
ChatterElement
hears this message and passes it back to the JavaScript client; - the client prepends the username to the message and then updates the display with the complete message.
Keep in mind that ChatterElement
entails several duties: it
establishes a relationship with a room object, it "registers" a user (there's a
one-to-one mapping between users and ChatterElement
), it sends
messages to the browser, and it receives messages from the chat room. Being a
LiveElement
subclass, ChatterElement
is also
responsible for the view (via the document factory).
Running with twistd
One last bit of code that may seem odd is the chat
variable we define right after the ChatRoom
class. What
is this? This is how we make all this cleverness work as a twisted
plugin.
If you recall, in our "Echo" application, we ran the code with the following command:
twistd -n athena-widget --element=echothing.echobox.EchoElement
The value we pass as the --element
argument is the dotted
name of the LiveElement
object of which our "web page"
is primarily comprised: the EchoElement
object. In
our "Chat" application, we have more moving parts: not only
do we have the ChatterElement
object, but we have the
ChatRoom
object which is responsible for keeping track of
many ChatterElement
es. By defining the chat
variable, we are accomplishing the following all at once:
- providing a variable that can be accessed as a dotted name and thus
used when starting the server (
chatthing.chatterbox.chat
); - creating a singleton of
ChatRoom
(via the "magic" of Python module-level instantiations); - making use of a factory, that when called, will both return a
new
ChatterElement
instance and add itself to theChatRoom
.
Running this version of our code is a little bit different than the
"Echo" version. This is because of the ChatRoom
code we
discussed above. As such, we pass a factory as our element, like so:
cd Nevow/doc/howto/chattutorial/part01/listings twistd -n athena-widget --element=chatthing.chatterbox.chat
If you executed this against the tutorial code on your local machine, you can now visit http://localhost:8080/ and start chatting to your heart's content.
Summary
Unlike our echo application, the chat application has some real functionality and does some useful stuff: supporting user chats via browser/server two-way communications. It should be evident now how the echo application provided a basic conceptual and (partially) functional foundation upon which our chat work could be based.