Pharo allows the definition of a REST server in a couple of lines of code thanks to the Teapot package by zeroflag, which extends the superb HTTP client/server Zinc developed by BetaNine and was given to the community. The goal of this chapter is to make you develop, in five small classes, a client/server chat application with a graphical client. This little adventure will familiarize you with Pharo and show the ease with which Pharo lets you define a REST server. Developed in a couple of hours, TinyChat has been designed as a pedagogical application. At the end of the chapter, we propose a list of possible improvements.
TinyChat has been developed by O. Auverlot and S. Ducasse with a lot of fun.
We are going to build a chat server and one graphical client as shown in Figure 46.1.
The communication between the client and the server will be based on HTTP and REST.
In addition to the classes
TinyChat (the client), we will define three other classes:
TCMessage which represents exchanged messages (as a future exercise you could extend TinyChat to use
more structured elements such as JSON or STON (the Pharo object format),
TCMessageQueue which stores
TCConsole the graphical interface.
We can load Teapot using the Configuration Browser, which you can find in the Tools menu item of the main menu. Select Teapot and click "Install Stable". Another solution is to use the following script:
Now we are ready to start.
A message is a really simple object with a text and sender identifier.
We define the class
TCMessage in the package
The instance variables are as follows:
sender: the identifier of the sender,
text: the message text, and
separator: a character to separate the sender and the text.
We create the following accessors:
Each time an instance is created, its
initialize method is invoked.
We redefine this method to set the separator value to the string
Now we create a class method named
from:text: to instantiate a message (a class method is a method that will be executed on a class and not on an instance of this class):
yourself returns the message receiver: this way we ensure that the returned object is the new instance and not the value returned by the
text: message. This definition is equivalent to the following:
We add the method
printOn: to transform a message object into a character string.
The model we use is sender-separator-text-crlf. Example: 'john>hello !!!'.
printOn: is automatically invoked by the method
printString. This method is invoked by tools such
as the debugger or object inspector.
We also define two methods to create a message object from a plain string of the form:
'olivier>tinychat is cool'.
First we create the method
fromString: filling up the instance variables of an instance. It will be invoked like this:
TCMessage new fromString: 'olivier>tinychat is cool', then the class method
fromString: which will first create the instance.
You can test the instance method with the following expression
TCMessage new fromString: 'olivier>tinychat is cool'.
When you execute the following expression
TCMessage fromString: 'olivier>tinychat is cool' you should get a message.
We are now ready to work on the server.
For the server, we are going to define a class to manage a message queue. This is not really mandatory but it allows us to separate responsibilities.
Create the class
TCMessageQueue in the package TinyChat-Server.
messages instance variable is an ordered collection whose elements are instances
OrderedCollection is a collection which dynamically grows when elements are added to it.
We add an instance initialize method so that each new instance gets a proper messages collection.
We should be able to add, clear the list, and count the number of messages, so we define three methods:
When a client asks the server about the list of the last exchanged messages, it mentions the index of the last message it knows. The server then answers the list of messages received since this index.
The server should be able to transfer a list of messages to its client given an index.
We add the possibility to format a list of messages (given an index).
We define the method
formattedMessagesFrom: using the formatting of a single message as follows:
Note how the
streamContents: lets us manipulate a stream of characters.
The core of the server is based on the Teapot REST framework. It supports the sending and receiving of messages. In addition this server keeps a list of messages that it communicates to clients.
We create the class
TCServer in the TinyChat-Server package.
The instance variable
messagesQueue represents the list of received and sent messages.
We initialize it like this:
The instance variable
teapotServer refers to an instance of the Teapot server that we will create using the method
The HTTP routes are defined in the method
registerRoutes. Three operations are defined:
messages/count: returns to the client the number of messages received by the server,
messages/<id:IsInteger>: the server returns messages from an index, and
/message/add: the client sends a new message to the server.
Here we express that the path
message/count will execute the message
messageCount on the server itself.
<id:IsInteger> indicates that the argument should be expressed as number and that it will be converted
into an integer.
Error handling is managed in the method
registerErrorHandlers. Here we see how we can get an instance of the class
Starting the server is done in the class method
TCServer class>>startOn: that gets the TCP port as argument.
We should also offer the possibility to stop the server. The method
stop stops the teapot server and empties the message list.
Since there is a chance that you may execute the expression
TCServer startOn: multiple times, we define the class method
stopAll which stops all the servers by iterating over all the instances of the class
TCServer class>>stopAll stops each server.
Now we should define the logic of the server.
We define a method
addMessage that extracts the message from the request. It adds a newly created message (instance of class
TCMessage) to the list of messages.
messageCount gives the number of received messages.
messageFrom: gives the list of messages received by the server since a given index (specified by the client).
The messages returned to the client are a string of characters. This is definitively a point to improve - using string is a poor choice here.
Now the server is finished and we can test it. First let us begin by starting it:
Now we can verify that it is running either with a web browser (figure 46.2), or with a Zinc expression as follows:
Shell lovers can also use the curl command:
We can also add a message the following way:
Now we can concentrate on the client part of TinyChat. We decomposed the client into two classes:
TinyChatis the class that defines the connection logic (connection, send, and message reception),
TCConsoleis a class defining the user interface.
The logic of the client is:
We now define the class
TinyChat in the package
This class defines the following instance variables:
We initialize these variables in the following instance
Now, we define methods to communicate with the server. They are based on the HTTP protocol.
Two methods will format the request. One, which does not take an argument, builds the requests
/messages/count. The other has an argument used to get the message given a position.
Now that we have these low-level operations we can define the three HTTP commands of the client as follows:
Now we can create commands but we need to emit them. This is what we look at now.
We need to send the commands to the server and to get back information from the server.
We define two methods. The method
readLastMessageID returns the index of the last message received from the server.
readMissingMessages adds the last messages received from the server to the list of messages known by the client.
This method returns the number of received messages.
We are now ready to define the refresh behavior of the client via the method
It uses a light process to read the messages received from the server at a regular interval.
The delay is set to 2 seconds. (The message
fork sent to a block (a lexical closure in Pharo) executes this block in a light process). The logic of this method is to loop as long as the client does not specify to stop via the state of the
(Delay forSeconds: 2) wait suspends the execution of the process in which it is executed for a given number of seconds.
sendNewMessage: posts the message written by the client to the server.
This method is used by the method
send: that gets the text written by the user.
The string is converted into an instance of
TCMessage. The message is sent and the client updates the index of the last
message it knows, then it prints the message in the graphical interface.
We should also handle the server disconnection. We define the method
disconnect that sends a message to the client indicating that it is disconnecting and also stops
the connecting loop of the server by putting
exit to true.
Since the client should contact the server on specific ports, we define a method to
initialize the connection parameters. We define the class method
TinyChat class>>connect:port:login: so that we
can connect the following way to the server:
TinyChat connect: 'localhost' port: 8080 login: 'username'
TinyChat class>>connect:port:login: uses the method
host:port:login:. This method just updates the
url instance variable and sets the
login as specified.
Finally we define a method
start: which creates a graphical console (that we will define later), tells the server
that there is a new client, and gets the last message received by the server.
Note that a good evolution would be to decouple the model from its user interface by using notifications.
The user interface is composed of a window with a list and an input field as shown in Figure 46.1.
Note that the class
TCConsole inherits from
ComposableModel. This class is the root of the user interface logic classes.
TCConsole defines the logic of the client interface (i.e. what happens when we enter text in the input field...). Based on the information given in this class, the Spec user interface builder automatically builds the visual representation.
chat instance variable is a reference to an instance of the client model
TinyChat and requires a setter method (
input instance variables both require an accessor. This is required by the User Interface builder.
We set the title of the window by defining the method
Now we should specify the layout of the graphical elements that compose the client.
To do so we define the class method
TCConsole class>>defaultSpec. Here we need a column with a list and an input field placed right below.
We should now initialize the widgets that we will use.
initializeWidgets specifies the nature and behavior of the graphical components.
acceptBlock: defines the action to be executed then the text is entered in the input field.
Here we send it to the chat model and empty it.
Note that this method is invoked by the method
refreshMessages and that changing all the list elements when we add just one element is rather ugly but ok for now.
Finally we need to define the class method
TCConsole class>>attach: that gets the client model as argument.
This method opens the graphical elements and puts in place a mechanism that will close the connection as soon as the client closew the window.
Now you can chat with your server. The example resets the server and opens two clients.
We show that creating a REST server is really simple with Teapot. TinyChat provides a fun context to explore programming in Pharo and we hope that you like it. We designed TinyChat so that it favors extensions and exploration. Here is a list of possible extensions.
There are probably more extensions and we hope that you will have fun exploring some. The code of the project is available at http://www.smalltalkhub.com/#!/~olivierauverlot/TinyChat.