8.14

12 Universe🔗

home work!

Purpose Implement a chat program, via universe and a shared server.

Exercises

In this lab, you will build a chat program, using big-bang and universe, that connects to a single server that is shared by all students in the class. Once you have finished, you will be able to send and recieve messages from other students.

As is no surprise, writing an interesting program requires understanding the data definitions involved. For universe programming, the most important data definitions to understand are the messages you can send to the server, and the messages that the server will send to you. We will use the following definitions:

; A Client->Server#Message is a String, and represents a plain text
; message being sent by a world to all the other worlds in the universe
 
; A Server->Client#Message is one of
; - (list 'users [List-of String])
;   This represents a message from the server itself, notifying all worlds
;   about the current worlds in the universe that are chatting.
; - (list 'send String String)
;   This represent a specific message being sent by a particular user: the first
;   string is the username, and the second string is the message.

From this description, you can see that when you send messages, you just include the message, but when you receive messages, they have your full name attached. How is that done? Please design your program in the following way:

; main : String -> ...
(define (main username)
  (big-bang ...
    [name username]
    ...
    ...))

i.e., you should make your main program take, as an argument, a String that will be your username for this chat client. (We’ll assume for now that usernames are unique; we’ll ensure this below.) This should be used in the name clause of big-bang. Then when you want to run your program, in the interactions window type (main "YOURCODE") to start the program.

Developing the client

You should develop a window that looks roughly like this:

image

The current message that you are typing is in a rectangle at the bottom, and the history of chat messages appear above it, and the current users in the chat is above that. We’ll assume messages are only one line long: as soon as you press Enter, your client should send the message.

Exercise 1 Figure out an appropriate ClientWorld world state for your client. (Hint: how many pieces of information do you need to track?)

Build several examples of such world states.

Exercise 2 Develop a draw-chat-client function that renders your world state as an image.

Exercise 3 Key handling, part 1: develop a function for on-key that handles all appropriate key presses, except for the Enter key. These keystrokes should not need to send any messages to the server; why not?

Developing the server

Homework 12a does not require you to write a server; we will do that for you. This section of the lab is for you to follow along and understand how such servers might be written, to better understand how your clients interact with it.

We’ve used big-bang to write local World programs that run on our own machines, and let us create single-user interactive programs. In order to allow multiple worlds to interact, we need to connect them through a server, which we’ll whimsically call a universe.

Just like a big-bang program has a world state, and a bunch of various handlers for when a key is pressed, a mouse is clicked, a timer ticks, etc., so too a universe program has a universe state, and various handlers. The universe’s state needs to keep track of all the worlds that are connected to it, and the various handlers that we’ll be interested in are
  • on-new, for when a new world tries to join the universe,

  • on-disconnect, for when a world ends or disconnects from the universe, and

  • on-msg, for when a world sends a message that the universe needs to relay to other worlds

Our server will start off looking as follows:
; A UniverseState is a [List-of IWorld]
; and represents the current collection of worlds connected to this server
; An IWorld is the internal representation of worlds that the universe knows about.
 
; server : String -> nothing
; Starts up a universe instance, and runs it until we shut it down
(define (server name)
    (universe '() ; the universe state
              [on-new add-world] ; how to add a new world
              [on-disconnect drop-world] ; how to remove a world
              [on-msg relay] ; how to relay a message))

“All” we have to do now is define our three universe handlers.

Exercise 4 Adding new worlds: When a new world tries to join our universe, our add-world function will be called:

; add-world : UniverseState IWorld -> UniverseBundle
; This function is called with the current universe state, and the world
; that's trying to join the universe.
(define (add-world univ world)
  ...)

We have three tasks to perform here:
  1. Ensure the world has a unique name

  2. Add the world to our universe state

  3. Notify all the worlds of the usernames of all the worlds currently connected.

(define (add-world univ world)
  (if (name-isnt-unique? univ world)
      ...reject world...
      ...add and notify everyone...))

We can get the name of a world using iworld-name, a built-in function on IWorlds. So we can define

(define (name-isnt-unique? univ world)
  (member? (iworld-name world) (map iworld-name univ)))

The output of our add-world handler will be a UniverseBundle:
; A UniverseBundle is a (make-bundle UniverseState [List-of Mail] [List-of IWorld])
; A (make-bundle univ messages kickout) represents
; - the universe state we currently have,
; - the list of mail messages we want to send out, and
; - the list of worlds we want to evict from our universe.
; A Mail is a (make-mail IWorld Server->Client#Message)
; and lets us send a message to a specific IWorld.

So our completed add-world handler will be

(define (add-world univ world)
  (if (name-isnt-unique? univ world)
      (make-bundle univ '() (list world))
      (local [(define new-univ (cons world univ))
              (define all-names (map iworld-name new-univ))
              (define all-mails (map (λ(w) (make-mail w (list 'users all-names))) new-univ))]
         (make-bundle new-univ all-mails '()))))

Add sufficient comments to the code above to explain to yourselves what it means and how it accomplishes our goals.

Exercise 5 Dropping worlds: our drop-world handler is called when a world has left our universe, and so it is very similar in structure to our add-world handler:

; add-world : UniverseState IWorld -> UniverseBundle
; This function is called with the current universe state, and the world
; that's just left the universe.
(define (drop-world univ world)
  (local [(define new-univ (filter (λ(w) (not (iworld=? w  world))) univ))
          (define all-names (map iworld-name new-univ))
          (define all-mails (map (λ(w) (make-mail w (list 'users all-names))) new-univ))]
     (make-bundle new-univ all-mails '())))

Add sufficient comments again to explain what this function means and how it works.

Exercise 6 Relaying messages: our relay function is simplest of all. It simply receives a message, and forwards it along to all the connected worlds:

; relay : UniverseState IWorld Client->Server#Message  -> UniverseBundle
; This function is called with the current universe state, the world
; sending a message, and the message itself
(define (relay univ world msg-content)
  (local [(define sender (iworld-name world))
          (define msg (list 'send sender msg-content))
          (define all-mails (map (λ(w) (make-mail w msg)) univ))]
     (make-bundle univ all-mails '())))

Add sufficient comments again to explain what this function means and how it works.

Exercise 7 Enhance this function so that if a sender is somehow not in the universe, the message is blocked.

Exercise 8 The actual signature for relay is a bit more general: it allows clients to send arbitrary s-expressions rather than just our expected messages. But we don’t want to support that! Further enhance it so that if the sender’s message is not a simple string (as expected by the Client->Server#Message definition), the message is blocked and the sender is dropped from the universe.

Connecting your clients to the server

To launch your server and various worlds, you’ll use the launch-many-worlds function to launch one server and as many worlds as you like:
(launch-many-worlds (server "S")
                    (main "Ben")
                    (main "Amal")
                    ...)

You will need to add one new clause to your big-bang calls in your main function: [register LOCALHOST], which indicates that you’d like your clients to connect to a server running on your own machine.

Exercise 9 Key handling, part 2: In your key handler, we now need to handle when the user presses the Enter key (which will be represented as "\r"). To send a message, your handler should now return a ClientResponse, rather than just a ClientWorld:

; A ClientResponse is one of
; - a ClientWorld
; - a (make-package ClientWorld Client->Server#Message)
; The first option is simply whatever updated ClientWorld you currently want
; to use, without sending any messages out into the universe.
; The second option lets you compute both an updated ClientWorld and a
; message that you want to broadcast to all the available worlds in the universe.

Update your key-handler function accordingly.

Exercise 10 Receiving messages: To receive messages from the universe, you will need to add an on-receive handler to your big-bang calls, with the following signature:
; receive-message : ClientWorld Server->Client#Message -> ClientResponse
(define (receive-message world msg)
  ...)

When you receive a 'users message, update the names in the topmost box of current users in your world state. when you receive a 'send message, add the message (with its sender and its content) to the chat transcript in your world state.