Part V: Distributed Applications

14 Chat Application

A chat system permits participants on arbitrary machines on the internet to engage in a real-time text-based discussion. New individual can join or leave the chat forum at any time. This scenario is intended to be realistic, which means that the chat system must be reasonably robust in the face of network failures, as well as machine and process crashes.

In this tutorial application, we will not set out to solve all problems that may be associated with distributed applications; rather, we will demonstrate how simple it is to realize a fully distributed application with reasonable robustness properties.

14.1 Chat Server

The server creates a port NewsPort and makes it available through a ticket. The ticket, as usual, is saved into a file which clients normally will load through a url. When a client wants to participate in the discussion forum, it needs not only NewsPort in order to post messages, but also the stream of messages that results from all posts to NewsPort, in order to display these messages to the user. The server could hand out the stream of all messages from the creation of NewsPort, but it seems more desirable to only hand out a stream that has only the messages posted after the client's request to connect to the discussion.

When a client wants to connect to the chat forum, it obtains NewsPort by means of the ticket that the server made available at some url, and it posts a message of the form connect(Messages), where Messages is a new variable. The server then binds the variable to the stream of messages following the connect(...) message.

<Chat Server>=
   Application(getCmdArgs) Connection(gate) Pickle(save)
   Args = {Application.getCmdArgs
           record(ticketfile(single type:string optional:false))}
   local Ticket in 
      {New Connection.gate init(NewsPort Ticket) _}
      { Ticket Args.ticketfile}
   {List.forAllTail { $ NewsPort}
    proc {$ H|T}
       case H of connect(Messages) then Messages=T else skip end 

The server (source in chat-server.oz) can be compiled as follows:

ozc -x chat-server.oz

and invoked as follows:

chat-server --ticketfile FILE

14.2 Chat Client

The client consists of 2 agents: (1) a user interface agent and (2) a message stream processor.

<Chat Client>=
   Application(getCmdArgs) Pickle(load) Connection(take)
   Viewer(chatWindow) at 'chat-gui.ozf' 
   Args = {Application.getCmdArgs
           record(url(single type:string optional:false)
                  name(single type:string optional:false)
   NewsPort={Connection.take {Pickle.load Args.url}}
<Chat Client: obtain and process message stream> 
<Chat Client: create user interface agent> 
<Chat Client: process message stream> 

The client obtains the stream of messages from the server by sending a connect(...) message. It then forwards every message on that stream to its internal SelfPort. The user interface will also direct messages to this internal port.

<Chat Client: obtain and process message stream>=
   {ForAll {Port.send NewsPort connect($)}
    proc {$ Msg} {Port.send SelfPort Msg} end}

When creating the user interface, we supply it with the internal SelfPort so that it may also post internal messages. In this simplistic implementation, the user interface simply posts messages of the form say(String) to request that this String be posted to the global chat message stream.

<Chat Client: create user interface agent>=
Chat = {New Viewer.chatWindow init(SelfPort)}

Finally, here is where we process all messages on the internal stream. A msg(FROM TEXT) message is formatted and shown in the chat window. A say(TEXT) message is transformed into msg(NAME TEXT), where NAME identifies the user, and posted to the global chat stream; actually TEXT is additionally converted into the more compact byte string representation for more efficient transmission.

<Chat Client: process message stream>=
{ForAll { $ SelfPort}
 proc {$ Msg}
    case Msg of msg(FROM TEXT) then 
       {Chat show(FROM#':\t'#TEXT)}
    elseof say(TEXT) then 
       {Port.send NewsPort msg(NAME {ByteString.make TEXT})}
    else skip end 

The client (source in chat-client.oz) can be compiled as follows:

ozc -x chat-client.oz

and invoked as follows:

chat-client --name USER --url URL

14.3 Graphical User Interface

The user interface is always what requires the most code. We won't go through the details here (but see the Window Programming Tutorial for extensive information), but merely point out that the @entry widget is asked to respond to a Return keypress, by invoking the post method. The latter posts a say(Text) message to the internal port, where Text is the text of the entry as typed by the user. This text is then deleted and the entry can be reused to compose and submit another message.

<Chat GUI>=
   Tk Application(exit:Exit)
   class ChatWindow from Tk.toplevel 
      attr canvas y:0 vscroll hscroll tag:0 selfPort entry quit
      meth init(SelfPort)
         selfPort := SelfPort
         canvas := {New Tk.canvas
                    tkInit(parent:self bg:ivory width:400 height:300)}
         vscroll := {New Tk.scrollbar tkInit(parent:self orient:v)}
         hscroll := {New Tk.scrollbar tkInit(parent:self orient:h)}
         entry   := {New Tk.entry     tkInit(parent:self)}
         quit    := {New Tk.button    tkInit(parent:self text:'Quit' 
                                             action:proc{$} {Exit 0} end)}
         {Tk.addYScrollbar @canvas @vscroll}
         {Tk.addXScrollbar @canvas @hscroll}
         {@canvas tk(configure scrollregion:q(0 0 200 0))}
         {@entry tkBind(event:'<KeyPress-Return>' 
                        action:proc {$} {self post} end)}
         {Tk.batch [grid(row:0 column:0 @canvas sticky:ns)
                    grid(row:1 column:0 @entry sticky:ew)
                    grid(row:0 column:1 @vscroll sticky:ns)
                    grid(row:2 column:0 @hscroll sticky:ew)
                    grid(row:3 column:0 @quit sticky:w)
                    grid(columnconfigure self 0 weight:1)
                    grid(rowconfigure self 0 weight:1)]}
      meth show(TEXT)
         {@canvas tk(create text 0 @y text:TEXT anchor:nw tags:@tag)}
            [X1 Y1 X2 Y2] = {@canvas tkReturnListInt(bbox all $)}
            {@canvas tk(configure scrollregion:q(X1 Y1 X2 Y2))}
      meth post 
         {Port.send @selfPort say({@entry tkReturn(get $)})}
         {@entry tk(delete 0 'end')}

Denys Duchier, Leif Kornstaedt and Christian Schulte
Version 1.4.0 (20080702)