Lab 3: CCHAT
Deadlines
-
See labs page
-
To pass this lab you have to give a short demo of your submission to the TAs on 1 March 2017; to do that, you have to sign up here by 26 February 2017, indicating your group number in Fire, to select a time slot. Please try to split evenly among the available slots.
Lab description
Building a “real” concurrent system is not easy. In this lab, we aim to put you through some of that experience. You will need to make design decisions, evaluate trade-offs between performance, simplicity, and ease of debugging, and much more. While challenging, it is plenty of fun!
Programming language
This lab assignment must be developed in Erlang.
How to install Erlang/OTP in your computer.
Background
Nowadays, messaging systems are very popular (Whatsapp, Snapchat, Facebook chat, etc.). They provide great connectivity to users as well as different features like stickers, videos clips, etc. In this lab, you will build a simple (but still quite real) text-based messaging system called CCHAT (the first C is from Chalmers). CCHAT is very much inspired by IRC, an old but still valid standard designed for group discussions. For simplicity, and reasons of time, your implementation of CCHAT is not going to use IRC’s protocol nor low-level TCP/IP communication. Instead, it will leverage Erlang’s processes and message passing features.
Overview
Below, you can find a short video of how CCHAT should look and work.
Some details about the demo:
-
Command
cchat:server()
starts a chat server calledshire
. For the rest of the lab, users connect to that server. -
Command
cchat:client()
opens a new client window. The GUI is basic, but the good news is that you do not have to implement it yourselves. -
The user interface has a
System
tab. This tab has the purpose of showing the activities carried out by different domains as well as reporting errors. For instance, when connecting to theshire
, you saw the following on the system tab:* Trying to connect to shire... + Connected!
-
The chat server cannot have two users connected using the same name. By default, the GUI utilizes the nickname
user01
. The command/nick <username>
changes the nickname to<username>
. The format forusername
is the same as for an Erlang atom, i.e., it will start with a lower case letter and followed by letters, numbers, or underscore (_
). For this lab, you only need to handle changing the client’s nickname when disconnected from the server. -
In order to chat,
bilbo
joined to the discussion group (or channel)#hobbits
with the command/join #hobbits
. By convention, all the channels’ names start with#
. -
Then,
frodo
joined to the same channel. After a short chat,frodo
left the channel using the command/leave
. Then, he disconnected from the server by issuing the command/disconnect
. Similarly,bilbo
executed the same commands and finally closed the application.
Architecture
CCHAT uses a client/server architecture. It consists of a client part, which runs the GUI and a client process, and a server, which runs the chat service (server process) and hosts the discussions groups. This picture illustrates the situation.
The client process (blue circle with double lines) has the goal of building a bridge between the GUI and the server process (the other blue circle with double lines).
-
Location of the client and server processes
The picture suggests that the client and server processes might be located in different machines. Nevertheless, and for simplicity, we will consider that all the processes are located on the same local machine. If you get the implementation of CCHAT right, you will be able to easily adapted to run in a distributed environment (see Location transparency & distribution in Lecture 8). This is one of the great aspects of Erlang!
-
The GUI and the client process
The GUI will interact with the client process through a specific protocol (described below). You do not need to implement the GUI, we will give it to you. Instead, you need to provide a implementation of the client process. Your implementation should know how to interact with the GUI, i.e., it should follow the GUI protocol (to be explained later on).
-
The server process
The server handles the requests coming from the clients. The protocol being used between a client and the server is up to you. Take into account that the chat server might be composed by several processes, not only the one that you see in the picture above. You have to think of the parts of your code where having more processes would increase the concurrency of your application. Remember to keep your code simple.
The GUI and the client process
The protocol
The protocol between the GUI and the client process is partly fixed. The reason for that is twofold. Firstly, by following the protocol, you will be able to use the GUI without knowing its internal implementation details. Secondly, and more importantly, we will test your code assuming that your client process follow the protocol. If you do not follow it, your code will not pass the tests (see the test section below) and your submission will be immediately rejected.
The protocol scheme is as follows.
The GUI sends a message Message
requesting some operations. Then, the Client either replies with the atom ok
or {error, Atom, Text}
. Atom ok
indicates that the operation succeeded. Tuple {error, Atom, Text}
denotes that something went wrong while processing the request. These errors are not fatal and the GUI can recover from them. Variable Text
contains the text to be reported in the System
tab of the GUI. You are free to choose the value of Text
. However, you should strictly use the values for Atom
as described by the protocol.
-
Connecting to the server
When the user writes the command
/connect Server
, the GUI sends the message{connect, Server}
to the client process, where variableServer
denotes the name of the chat server chosen by the user.Atom
user_already_connected
is used when the user tried to connect but it is already connected to the server. Atomserver_not_reached
is returned when the server process cannot be reached for any reason. -
Disconnecting from the server
When the user writes the command
/disconnect
, the GUI sends the messagedisconnect
to the client process. It is possible to disconnect from a server only if the user has left all the chat rooms (i.e. channels) first.Atom
user_not_connected
is returned when a user tries to disconnect from a server where the user is not connected to. Atomleave_channels_first
is returned when a user tries to disconnect from a server without leaving the chat rooms first. Lastly, atomserver_not_reached
is used when the server process cannot be reached for any reason. -
Joining a chat room
To join a chat room, users write the command
/join Chatroom
, whereChatroom
is a string starting with “#”, e.g., “#hobbits”. The GUI sends the message{join, Chatroom}
.Internally, if the chat room does not exists in the server side, the server process will create it. We assume that, once created, chat rooms are never destroyed, i.e., they will always exist as long as the server runs. Bear in mind that only users who have joined a chat room can write messages on it.
Atom
user_already_joined
is return when a user tries to join to the same channel again. -
Writing messages in a chat room
When the user is in a chat room and writes an string (not started with
/
), the GUI sends the message{msg_from_GUI, Chatroom, String}
where variableChatroom
contains the name of the channel as an string (e.g. “#hobbits”) and variableString
stores the typed string (e.g. “hello fellow hobbits”).Atom
user_not_joined
is returned when a user tries to write a message in a channel that the user has not joined. -
Leaving a chat room
When the user types
/leave
in a chat room, the GUI sends the message{leave, Chatroom}
, where variableChatroom
contains the name of the chat room.Atom
user_not_joined
is returned when a user tries to leave a channel that the user has not joined. -
Asking for the nickname
When the user writes the command
/whoami
, the GUI sends the messagewhoami
to the client process. The client should respond with the nick as a string (instead of the atomok
). There are no errors to report in this case. -
Changing the nickname
When the user writes the command
/nick Name
, the GUI sends the message{nick, Name}
to the client process. VariableName
contains the new chosen nickname for the user.To make things easier, you only need to support changing nick when the user is disconnected. Return atom
user_already_connected
if the user attempts to change nick when connected.Optional: If you wish to implement changing the nick when connected, you should return atom
nick_taken
when trying to change to nick that is already taken.
Until this point, the protocol describes communications initiated by the GUI. There is only one occasion when the client process starts communication with the GUI: when something is written to a channel, the client needs to tell the GUI to display the new text. The client process sends the message {msg_to_GUI, Chatroom, Msg}
when it wishes to print out the line Msg
in the chat room Chatroom
.
For that, we use the following line of code (which you do not need to change):
gen_server:call(list_to_atom(GUIName), {msg_to_GUI, Chatroom, Name ++ "> " ++ Msg})
Name
is the nick of the author, Msg
is the message and GUIName
contains the name for the GUI process, which is generated in the gui.erl
module and passed onto your client in client:initial_state/2
.
Observe that gen_server
is not the same as genserver
(the module that has been shown in class). Module gen_server
is the OTP implementation of a generic server.
Error handling
-
Fatal errors
The client process has the chance to respond to the GUI with the tuple
{‘EXIT’, Reason}
. This tuple indicates to the GUI that something went very wrong on the client side, e.g. when the server process crashes in the middle of processing a request. The GUI will display the content of variableReason
and exit. You might see this behavior during development but it is clear that it should not appear in your submission when we will test it. -
Other errors
When developing your solution, you might find yourself wondering “what if this fails?”, “what should process A or B do then?”, “should I modify my code to catch this error?”, etc. If you want to take this road, you will go down the rabbit hole! Try to focus on making your solution work as described by the protocol above. For instance, you might decide to only report those errors described above, and making your solution crash if some other error occurs.
Assignment
Your assignment is to implement the chat server described here, based on the skeleton available in the code skeleton section.
Successful solution requires performing these steps (and more):
-
Implement all the
not_implemented
parts in theclient.erl
file -
Implement all the needed functionality in the server loop in
server.erl
.Note that you may need to keep some data (state) both on the server and on the clients, and also create extra processes.
-
Make sure that the code passes all the test cases
-
As a requirement, your code should use the provided
genserver
for all communication between the server and the clients -
Submission: You should submit your code based on the skeleton code and whatever other files are needed for your solution to work. In addition, you should comment your code so that graders can easily review your submission. Please do not submit compressed archives. Just upload the individual source files which you have worked in.
Code skeleton
In this lab, you are required to build the client and server processes based on the structure in the skeleton code. The main reason for that is to make it easier to have your lab up and running. You could use any Erlang code that we discussed during the lectures if it helps you.
It is up to you to decide the protocol between the client and the server!
We will give you the following files. Do not edit the ones highlighted in red.
-
GUI:
gui.erl
,lexgrm.erl
,grm.yrl
,lex.xrl
These files contain the implementation of the GUI. Do not modify them.
-
Testing:
test_client.erl
,dummy_gui.erl
Files used for testing. Do not modify them.
-
Record definitions:
defs.hrl
This file contain record definitions. The state of the client, server, or any other entity that you wish to add should represent its state as a record. We provide you with some incomplete definitions already. For instance,
-record(client_st, {gui}).
defines the recordclient_st
(client state) where fieldgui
stores the name of the GUI. -
Client process:
client.erl
The exported function
client:handle/2
handles each different kind of request, returning a tuple of the response and the updated state. You also need to implementinitial_state(Nick, GUIName)
which generates the initial state for the client. -
Server process:
server.erl
For the server you need to implement, analogous to the client code, the functions
server:handle/2
for handling messages andinitial_state(ServerName)
for generating the initial state. -
CCHAT:
cchat.erl
This is the top level module. It is used to launch the server and several clients with their respective GUIs. Do not modify this file.
-
Generic Server:
genserver.erl
This file contains functions for spawning and running Erlang processes as servers and implementing synchronous message passing. It is used internally, so do not modify this file… but you might want to use its functions yourselves!
Download material for the lab
To get started, you need the skeleton code package.
First compilation
After you have downloaded the skeleton code, compile everything with the command:
make all
You can then open the Erlang shell (erl
) and start a client with cchat:start()
. You should be able to start the server and open up chat windows, but Bilbo and Frodo will not able to communicate because most of the functions are not implemented. It is your task to make sure that they can!
Tips and tricks
-
Think carefully about how you want to implement your server and how channels should be managed. Remember this is a course on concurrent programming — you should introduce concurrency wherever it is suitable.
-
The server process will be registered (using Erlang’s
register
BIF) to an atom of the same name. So when the server name is shire, you can send messages directly to the atomshire
without knowing the server’s process ID. You might also want to check the functionlist_to_atom
. -
From the Erlang shell, use the function
cover:compile_directory/0
to re-compile all Erlang files in the current directory.
Test cases
All unit tests are contained in the file test_client.erl
. Tests are carried out using EUnit. We have created entries in the Makefile
to make life easier for you.
There are positive and negative tests which check that your solution follows the requirements and protocol as specified above. To run these tests, execute the following:
make -s run_tests
If your submission fails any of the tests, it is a good indication that your submission will be rejected. However even if you pass all of the tests, that is not a guarantee that your submission will be accepted.
Performance and concurrency tests
In addition to the standard set of tests, we provide performance tests, which test your use of concurrency. There are three kinds of performance tests:
-
many_users_one_channel_one_message
creates many users that join the same channel. Then one of the users sends a single message to the channel. Sending of the message should be quick enough for the send operation not to time out. -
many_users_one_channel_many_messages
creates many users that join the same channel. Then each of the users sends a message to the channel. Sending of all messages should be quick enough for the send operations not to time out. This test has a higher chance of provoking deadlocks than the other tests. -
many_users_many_channels
creates many users and many channels. Each user joins two of the channels, and sends a message to one of them. The test fails if any of the send operations times out. Sending a message to one channel should not affect the performance of other channels in a negative way.
When the tests are run, artificial delays are introduced to receiving of some messages. The purpose of the delays is to evaluate the concurrency characteristics of your solutions.
To run the performance tests, use:
make -s run_perf_tests
The tests are not run as part of the run_tests
target.
Passing or failing the performance tests is only an approximate indication of whether your submission will be accepted or rejected, due to the unpredictability of execution timing.
Turning off color codes
If you don’t have a color-enabled terminal, you will see a lot of ugly color codes in your test output. You can disable these by commenting out the colour
function in test_client.erl
and replacing it with:
colour(Num,S) -> S.