$23.99
n this problem we'll use the `Lwt` concurrency library; you can find
the documentation [here](https://ocsigen.org/lwt/2.5.0/api/). To
interact with `Lwt` in `utop`, you'll need to `#require
"lwt.simple-top" ;;` (or add this line to your ocamlinit file) and to
build binaries using `Lwt`, you'll need to use `ocamlfind`. See [here](lwt_intro.md)
for the short introduction to using and building programs with `lwt`.
We're going to build a _server_ that allows people to (text) chat with
each other over the internet. The protocol will be similar to
[Internet Relay Chat (IRC)](https://en.wikipedia.org/wiki/Internet_Relay_Chat)
with a little less functionality. Since this is a class homework,
we'll use the command-line program `telnet` for a _client_ instead of
developing a fancier interface. The file `chatServer.ml` contains a
skeleton for the server we'll develop, and the code to run `chatServer` in in `srvmain.ml`.
Right now, it's pretty boring: if you build and run it using:
```
% ocamlfind ocamlc -package lwt -package lwt.unix -linkpkg -o server str.cma chatServer.ml srvmain.ml
% ./server
```
it will listen for and accept network connections, but won't do anything with the user's
input, or write anything at all.
Ultimately we want to be able to support interactions like the
following:
On the server:
```
hoppernj@atlas (/home/hoppernj/csci2041/repo-hoppernj/hw6) % ocamlfind ocamlc -package lwt -package lwt.unix -linkpkg -o server str.cma chatServer.ml srvmain.ml
hoppernj@atlas (/home/hoppernj/csci2041/repo-hoppernj/hw6) % ./server
```
On Alice's machine:
```
alice@remote06 (/home/alice) % telnet atlas.cselabs.umn.edu 16384
Trying 128.101.38.193...
Connected to atlas.cselabs.umn.edu.
Escape character is '^]'.
Enter initial nick:
alice
bob: <joined
carol: <joined
hi everyone!
bob: hi alice.
/n whiterabbit
/l
whiterabbit
carol
bob
bob: ooh nice.
carol: i'm outta here.
carol: <left chat
bob: rude.
well it's only an example.
/q
Connection closed by foreign host.
```
On Bob's machine:
```
bob@remote08 (/home/bob) % telnet atlas.cselabs.umn.edu 16384
Trying 128.101.38.193...
Connected to atlas.cselabs.umn.edu.
Escape character is '^]'.
Enter initial nick:
bob
carol: <joined
alice: hi everyone!
hi alice.
alice: <changed nick to whiterabbit
ooh nice.
carol: i'm outta here.
carol: <left chat
rude.
whiterabbit: well it's only an example.
whiterabbit: <left chat
/q
Connection closed by foreign host.
```
On Carol's machine:
```
carol@atlas (/home/carol) % telnet atlas.cselabs.umn.edu 16384
Trying 128.101.38.193...
Connected to atlas.cselabs.umn.edu.
Escape character is '^]'.
Enter initial nick:
carol
alice: hi everyone!
bob: hi alice.
/l
carol
bob
alice
alice: <changed nick to whiterabbit
bob: ooh nice.
i'm outta here.
/q
Connection closed by foreign host.
```
### `login_handler`
When a user connects to the chat server, srvmain.ml calls the function
`ChatServer.chat_handler` with a pair of `channel` values: the first (`inp`) is
an `input_channel` and the second (`outp`) is an `output_channel`.
The `main_loop` helper function inside the module uses the `Lwt`
*bind* operator `=` to read a line of input from `inp` (the user's
network connection) and call `handle_input` with this line when it is
available; whenever the actions initiated by `handle_input` complete,
`=` is used to call `main_loop` again. However, before
`chat_handler` invokes `main_loop`, a few set-up operations should
take place. Find the line `let _ = ()` in `chat_handler` and replace it
with a call to `login_handler` which will do the following:
+ Print a welcome message to `outp` (using one of the `Lwt_io.print`
functions) asking the user for a "nick"(name) they will use in the chat
session. (It's a technical term, honest!)
+ Read the user's nickname from `inp` using `Lwt_io.read_line`. Assign
the new value to the name `nick`.
+ The `ChatServer` module maintains a reference to an associative list, `sessions`
mapping each user's nicknames to her output channel (so that when a
message is sent to other users, she doesn't see her input twice)
Once we know the user's nickname, update `sessions` to include the
new user and output channel pair. (Since it represents shared state, your code should take measures to ensure that `sessions` is not modified by two threads simultaneously)
+ Finally, announce that the user has joined the chat (to any other
users) by calling `broadcast` with the user's nickname and the
message `"<joined"`. (We'll fill in the `broadcast` function next.)
### `broadcast`
Whenever a user enters a message, joins the chat, or leaves the chat,
we'll want to tell the other participants in the chat using the
`broadcast` function. We could step through the `sessions` list in
order, sending the message to each user once the previous message has
been sent, but this might take a considerable amount of time -- imagine
if one or more users is overseas or using dial-up internet.
Fortunately, `Lwt` provides the useful function `Lwt_list.iter_p`,
which takes a function, applies this function to every time in the
list in parallel "threads", and then waits for all of the threads to
finish before passing `()` to the next thread. Change `broadcast` to
use `Lwt_list.iter_p` to write a line of the form `sender: message` to
every user but the sender in the `!sessions` associative list. (The
function passed to `iter_p` should execute `Lwt.return ()` when the
session associated with the `sender` nick is encountered)
At this point we're starting to get a useful server: try building and
running `server`, and in separate terminals (but the same
machine) try `telnet localhost 16384`, entering distinct nicks. You
should be able to carry out a lively conversation with yourself.
(Recruit a friend if this feels too eccentric) One more tip: you can
end a telnet session on the client side by typing ctrl-] and then
entering q at the telnet prompt.
### Modify `handle_input` and associated helpers
The actual IRC protocol allows a wide variety of actions by users;
we'll support three: q(uit), n(ew nickname), and l(ist users). Modify
`handle_input` so that it checks to see if the input starts with one
of the following commands:
+ The `quit` command, `/q`: quitting should cause the user's thread to
close the input and output channels, then remove the user from the
`sessions` list. Note that `main_loop` is bound to the
`handle_input` thread, so you'll need to use a nonlocal control flow
mechanism here. (Hint: the main body of `chat_handler` calls the
`handle_error` function with any exceptions that are thrown in the
`main_loop` thread.)
+ The `newnick` command, `/n`: allows a user to change her `nick` to
the string following ``/n ``. You should add code that calls
`change_nn` with the correct inputs and modify `change_nn` to update
the session list, notify the other users, and update the `nick`
reference from `chat_handler`.
+ The list users command, `/l`: lists all users connected to the
server. Hint: the `Lwt_list.iter_s` function provides a convenient way to
iterate over the `!sessions` list and perform some action for each
`(nickname,channel)` pair.
(You might find the library functions `Str.string_after` and
`Str.string_before` useful for this portion of the assignment.)
### Do something fun!
You're almost done with the semester, it's time to do something for fun. Using
your completed `chatServer.ml` and `srvmain.ml` as a guide, create two
additional files: `funSrv.ml` and `funSrvMain.ml`. Add an
interesting/fun/useful piece of functionality to the server in `funSrv.ml`, and
include a comment at the top explaining what libraries your code needs (so we
can build and test it out) and what it does. (A few ideas: add timestamps to
messages and color-code different users' messages; add support for private
messages from one user to another; add connection logging on the server; support
multiple chat rooms; support registered nicknames...)
## All done!
Don't forget to commit all of your changes to the various files edited
for this homework, and push them to (the `hw6` directory in) your individual class repository
on UMN github:
+ `chatServer.ml`
+ `funSrv.ml`
+ `funSrvMain.ml`
You can always check that your changes have pushed
correctly by visiting the repository page on github.umn.edu.