Lab 11: Universe

In the beginning...

In this lab, you'll be converting a chat client that only works on a single machine to interact with a chat server and multiple other clients. Download the initial chat client to get started.


How it works

The Universe teachpack allows for client/server programs-- that is, programs where a server manages multiple clients, and a client and the server communicate through messages.

To start our client, we use the big-bang form that we've been using all semester, while the server is started (by your friendly TAs) using a new universe form. A longer description of client/server programming can be found on the main Universe HelpDesk page.

Clients

Client programs look very similar to the kinds of interactive programs we've already written. There are just a few changes involved:

  1. A name clause to identify the client,
  2. A register clause to connect to a server, and
  3. New protocols for sending (packages) and receiving (messages) to/from the server.

Connecting to the server

To connect we use the clause (register String), where the String is either a hostname or an IP address. When big-bang is executed, it attempts to connect to the universe running on the given machine. If the connection fails the program (i.e., the client) will run locally without sending messages.

In addition, a client should specify a name using (name String) to identify it to other clients. In our chat program, we will use it as the nickname of each connected user.

Sending messages to the server

To send messages to the server, functions that originally returned a new World can instead (optionally) return a package. For example, the on-key clause originally took a function with the following contract:


 ;; World KeyEvent -> World
  
But now we'll treat it as:

 ;; World KeyEvent -> [Or World Package]

 ;; An [Or X Y] is one of:
 ;;  - X
 ;;  - Y
    

A package consists of two items: a new World (as before) and a message to send to the server.


 ;; A Package is: (make-package World Message)

 ;; A Message is an SExpr
    
To make a package use make-package (yup), and as with Worlds, what makes up a Message is up to the server and clients. At first, we will simply use Strings.

Receiving messages from the server

To receive messages from the server, include the on-receive clause. It takes a function with the following contract:


 ;; World Message -> [Or World Package]
  

When a message is received, the function is passed the current local World state and the message from the server. The result of the function is either just the next World state, or a Package that contains the next World state and a Message to send back to the server.


Modifying the Client

First things first... your name's not Alfred E. Neuman right? Modify your nick to something reasonable (keep it at most PG-13 please). Now look through the code, figure out where we've marked **** MODIFY HERE ****, and try to get an idea of what the code does.

Now that you have a background on the World, Universe, and the client code, we can start setting things up. Your basic chat client currently echoes what you've typed to your screen whenever you hit Enter. What we need to do is make the client connect to the server, and start sending and receiving messages.

  1. Update the handle-key client function to return a Package whenever you hit Enter. Unlike the local implementation it should not add text to the buffer, but instead just send it as a message to the server.
  2. Design the function handle-msg that can be installed with on-receive. The function should take a World (i.e., a Client) and a Message (i.e., a String) and add the message to the line buffer of the client. When you're done, add (on-receive handle-msg) to the big-bang form.
  3. Adjust the big-bang form to register with the server.

Now run it! It should now function as a little chat client. You may see other people connecting, and you should be able to send them messages. The TAs should be starting up a client up on the projector (as you read this) so you can see people connecting, sending messages, and disconnecting as they work on the lab.


Altering the protocol

Right now the server and its clients send around Strings as messages. It would make sense to make the protocol more extensible to deal with future changes, additions, etc. We do this by defining a Message to be a non-empty [Listof String]. The first String defines the type of the Message and the meaning of the rest of the strings in the list depend on the message type.

The first type of message you will handle will be "MSG". The server sends these messages in the format (list "MSG" <name> <content>), where name and content are strings, name is the name of the client who sent the message, and content is the content of the message.

When clients send a "MSG" Message to the server, they do not send a name. The server will add the appropriate client name before forwarding the message to other clients. In other words, the only Message sent from clients to the server at this point is (list "MSG" <content>).

A sample server message: (list "MSG" "Someone" "Knock knock!")
A sample client message: (list "MSG" "Who's there?")

We are also running an advanced server at the address given by the TAs. You can test your advanced client by connecting to it. It supports all the messages described below.

  1. Change the protocol/message send functions as described above (minor adjustments to contracts and functions). Make sure you handle "MSG" messages (you can discard all other types for now).
  2. Add action messages to the protocol. The user specifies actions by starting a message with "/me " (not part of the actual action). For example, if the client "vlad" types: /me watches TV and hits Enter, the message that will be displayed to all clients should be *vlad watches TV.

    Send a message of the form (list "ACTION" <action>) when the user wants to perform an action. In the example above, the message would be (list "ACTION" "watches TV").

    Hint: It will be useful to first design a string-prefix? function, that takes two strings and checks to see if the second string is a prefix of the first, and a string-remove-prefix function that removes a number of characters from the front of a string (substring is useful for both).
  3. Handle "CLIENTS" messages. When a client connects, it will receive a message from the server with information about how many clients are connected to the server and what are the names of the clients. The format is (list "CLIENTS" <number> <clients>). This information should be displayed in the newly connected client's window. Let's also make it possible for the client to obtain that information anytime, by sending (list "CLIENTS") when the user types "/clients".
  4. Update the client so that each client will have a "status". As opposed to actions or regular messages, a client is associated to a status as long as it is connected. Of course, an active client can change its status, but it will always have one. When a client first enters the chat room, its status is "Available". At any time, the client can change its status by typing: "/newstatus 'something'", where 'something' can be any String. A client can inquire about another client's status by typing "/status? clientname". If the client is not connected, the status will be "Offline".

    Use messages of the form (list "STATUS" <user>) when asking for user's status and messages of the form (list "UPDATE-STATUS" <new-status>) when updating your own status.

    Expect to receive messages of the form (list "NEW-STATUS" <user> <status>) when user updates their status or when you send the command "/status? username". You should display the messages as "clientname status: the-new-status".

Possible extensions

Now that clients have the list of connected clients and the protocol is more extensible, you can change the client in many ways. Take your pick of the exercises below, or propose a new extension to the TAs and tutors! If they like it, they'll add the required functionality to the server while you add it to the client.
  1. Add the ability to change one's nickname. The user specifies a nick change by starting a message with "/nick", followed by the new nickname. Continue using the argument to run as the original nickname, but note that this means that the client name and the displayed nickname can now be different. Use the String "NICK" for the Message type.
  2. Change the GUI to show a list of connected users and their statuses.
  3. Highlight messages that mention the user's nickname. This highlighting can be performed by changing the color of the text for the line including the user's nickname.
  4. Add private messages (messages directed to and received by a single client). The user specifies a whisper by starting a message with "/msg", followed by the nick to whom the whisper should be sent, followed by the message to be sent (all separated by spaces). Use the String "PRIVMSG" for the Message type.