Building a "real" concurrent system is not easy. In this lab, we aims put you through some of that experience. You will need to make design decisions, evaluate the trade-off between performance and simplicity, face debugging, and much more. While challenging, it is plenty of fun!
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.
Below, you can find a short video of how CCHAT will look and work.
Here, some details about the demo.
Command cchat:server()
starts a chat server called shire
.
For the rest of the lab, users connect to that server.
Command cchat:gui_interface()
starts the graphical user interface of
CCHAT.
It is very poorly done, we know that. The good news are that you are not going to
implement it. We will give it to you!
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 the shire
, you saw the following on the system
tab:
* Trying to connect to shire... + Connected!
The chat server cannot have two user registered under the same name. By
default, the GUI utilizes the nickname user01
. The command /nick
<username>
changes the nickname to <username>
. The format for username
is the same as form an
Erlang's atom, i.e., it will start with a lower case
letter and followed by letters, numbers, or underscore (_
). For this lab,
we assume that nicknames can be changed only before connecting to 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 small, 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.
CCHAT will use a client-server architecture. It consists on a client part, which runs the GUI and a client process, and a server which runs the chat service (server process) and host the discussions groups. The graphic below illustrates the situation.
In the graphic above, the client process (blue circle with double lines) has the goal to be 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. 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. You have complete freedom to implement the server process as you want. Take into account that the chat server might be composed by several processes, not only the one that you see in the picture above. Remember to keep your code simple.
The protocol between the GUI and the client process is partly fixed. The reason for that is two fold. 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 that. 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 variable Server
denotes the
name of the chat server chosen by the user.
Variable Atom
gets the follows values. Atom
user_already_connected
is used when the user tried to connect but it is
already connected to the server. Atom server_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 message
disconnect
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.
Variable Atom
stores any of the following atoms. Atom user_not_connected
is returned when a user tries to disconnect from a server where he/she is not
connected to. Atom leave_channels_first
when a user tries to disconnect
from a server without leaving the chat rooms first. Lastly, atom
server_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
, where
Chatroom
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. Bare in mind that only users who has joined a chat room can write messages on it.
Variable Atom
can acquire following atoms. Atom user_already_joined
is set
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
variable Chatroom
contains the name of the channel as an string
(e.g. "#hobbits") and variable String
stores the typed string (e.g. "hello
fellow hobbits").
Variable Atom
is set as follows. Atom user_not_joined
is set when a user
tries to write a message in a channel that he/she has not joined.
Leaving a chat room
When the user types /leave
in a chat room, the GUI sends the message
{leave, Chatroom}
, where variable Chatroom
contains the name of the chat
room.
Variable Atom
is set as follows. Atom user_not_joined
is set when a user
tries to leave a channel that he/she has not joined.
Asking for the nickname
When the user writes the command /whoiam
, the GUI sends the message
whoiam
to the client process.
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. Variable Name
contains the new
chosen nickname for the user (e.g. "/nick bilbo"). Clearly, for a chat
server, it should not be the case that two users have the same nickname.
Variable Atom
is set as follows. Atom user_already_connected
when a user
tries to connect with a nick that is taken already.
Until this point, the protocol describes communications initiated by the GUI. There is only one occasion when the client process starts a communication with the GUI.
The client process sends the message {income_msg, Name, Chatroom, Msg}
when
it wishes to print out the line Name++"> "++Msg
in the chat room Chatroom
.
For that, it is used the following line of code
gen_server:call(list_to_atom(GUIName), {msg_to_GUI, Chatroom, Name++"> "++Msg})
Observe that gen_server
is not the same as genserver
(the module that it has
been shown in class). Module gen_server
is the OTP implementation of a
generic server. You should keep
using the module genserver
which is simpler, although less generic, than
gen_server
.
Variable GUIName
contains the name for the GUI process. In the module
gui.erl
you will find the following code:
GUIName = find_unique_name("gui_", ?MAX_CONNECTIONS )
Variable GUIName
is the one that you need to pass to the client process when
creating it. Do not worry, we give you the code already to handle these type of
messages. See more details in the Section code
skeleton.
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
clashes in the middle of processing a request. The GUI will display the
content of variable Reason
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.
In this lab, you are required to build the client and server processes using the generic server given at the lectures. 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 saw at the lectures if it helps you.
We will give you the following files:
Component | Files | Description |
---|---|---|
GUI | gui.erl lexgrm.erl grm.erl lex.erl |
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(cl_st, {gui}). defines the record
cl_st (client state) where field gui stores the name of the GUI.
|
Client process | client.erl |
You should implement the client process loop used to handle
the requests from the GUI. For that, you need to implement
function loop and initial_state(Nick, GUI) (the latter function
generates the initial state for the client). These functions are used
to launch the client process with genserver:start/3 when starting
the GUI.
|
Server process | server.erl |
You should implement the server process loop used to handle
the requests. For that, you need to implement
function loop and initial_state(Server) . This last function
generates the initial state for the server. Variable Server contains
the name of the server. For the purpose of the lab, it will always be
"shire" . These functions are used to launch the server process
with genserver:start/3 in the module cchat .
|
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. |
Here, you can get the code skeleton! To download it, click on the button that says "Download ZIP". After you download it, you can start perform the same demo as in the first video. Unfortunately, Bilbo and Frodo are not able to communicate. It is your task to make sure that they can!
See the next video to see how to get the code skeleton and launch the GUI.
If you do not have GNUMakefile
, you should run the following
commands in the Erlang shell.
cd("the directory where the skeleton code is"). c(lexgrm). lexgrm:start(). cover:compile_directory().
Your solution should use the genserver.erl
given in the lectures to
implement the client and server process.
In order to reduce the amount of state to carry in your solution, we recommend
to use the Erlang's register
primitive as much as possible. You might want
to check the function list_to_atom
as well.
For facilitating debugging, you might introduce the message debug
for every
process started with the function genserver:start/3
. When the process
receives such message, it returns its internal state.
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 (see below for alternatives to using make
).
There are two sets of tests, correctness and performance tests.
The video below shows how to use the test suite (includes audio):
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
We have also included two cases which test the performance of your solution by spawning large numbers of clients and channels simultaneously. To run them, use the command:
$ make -s run_perf_tests
This will run the performance tests 3 times: once using 4 cores, once with 2 cores, and once with only 1 core enabled. In each case you will see the time elapsed for the test case to complete. To test your use of concurrency, we expect that your solution will be significantly faster with more cores. The actual times do not matter much, as long as there is a noticeable improvement.
We will run these tests on the remote[n].student.chalmers.se
machines (which have 4 cores), and we suggest you do the same.
make
If your system doesn't have the make
command, you can run the test suites like so:
Correctness
$ erl +P 1000000 -eval "cover:compile_directory(), eunit:test(test_client), halt()"
Performance
The number 4
below specifies the number of cores to use. Make sure to run this command with values 4, 2, and 1:
$ erl -smp +S 4 +P 1000000 -eval "cover:compile_directory(), eunit:test([{timeout, 60, {test,test_client,many_users_one_channel}},{timeout, 60, {test,test_client,many_users_many_channels}}]),halt()"
If you don't have a colour-enabled terminal, you will see a lot of ugly colour 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.
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.