In this assignment, you will write a simple group chat server program and a client program. The server will accept multiple clients and relay client messages back to the clients. Each client, on the other hand, will be a fuzzer generating messages and sending and receiving those messages to and from the server.
The group chat server accepts client connections, receives client messages, and echoes those messages back to all clients. The following are the requirements of the server.
-
The command-line interface should be able to start the server as follows.
$ ./server <port number> <# of clients>
-
The executable of your server should be named
server. -
The server should bind to port
port number. -
<# of clients>is the expected number of clients.- A client can join any time and leave any time.
- You do not need to handle cases where this number is just too big and our container cannot handle that many clients. We will not test with more than 100 clients.
-
The server should use
AF_INET. -
The server should bind to all available IP addresses for the local host.
-
The server should receive all messages from each and every client as they send them.
-
When the server receives a message from a client, it should send the message to all clients, including the sender.
-
Message ordering
- When a client sends multiple messages, all clients should receive the messages in the original sending order.
- In addition, all clients should receive all messages in the same order.
- For example, suppose we have three clients, C0, C1, and C2.
- Consider the following scenario.
- C0 sends a message, M0, which is received by the server.
- C1 then sends a message, M1, which is also received by the server.
- Lastly, C2 sends a message, M2, which is again received by the server.
- Based on the above scenario, C0, C1, and C2 should all receive M0 first, M1 next, and then M2.
- This message ordering is not a trick requirement. It is just what you expect from a group chat room.
-
Note: the simplest way to test a server is to use
telnet <IP> <port>.- Suppose your local IP address is
192.168.68.6and you run the server, which binds to port8000. telnet 192.168.68.6 8000will act as a client connected to the server.127.0.0.1is a local IP address that refers to the local machine, sotelnet 127.0.0.1 8000will also work.localhostis a host name that refers to the local machine, sotelnet localhost 8000works as well.- You can enter messages on your terminal as a standard input to
telnetand they will be sent to the server.
- Suppose your local IP address is
The following is how the server and the clients should format and interpret messages they send and receive.
- For clarity, we use two terms---a group chat message and a message.
- A group chat message refers to what we would normally call a "message" in the context of a group chat app.
- A message is a piece of data sent by a client or a server through a socket. As described below, this may or may not contain a group chat message.
- The message size should be no more than 1024 bytes.
- Both for the server side and the client side, a message should always start with a one-byte
(
uint8_t) "type."0means that it is a regular message that contains a group chat message.- We refer to this as a type
0message.
- We refer to this as a type
1means that it is a special "end of execution" message.- Two-Phase Commit Protocol below fully explains this.
- We refer to this as a type
1message.
- Both for the server and the clients, a new line character
'\n'always marks the end of a message.- This means that when you send out a message, it's best to make sure you send up to
'\n'. If you blindly send outxbytes since that is the size of your buffer, you will have a hard time making things work. The same goes for receiving a message. It's best to make sure that you receive up to'\n'and handle the message. - In addition, you need to be careful with other aspects of this due to the requirement described next.
- This means that when you send out a message, it's best to make sure you send up to
- When the server receives a type
0message from a client, the server should get the sender's address (IP and port) and include the address in the message sent to all clients.- Consider a typical group chat app. When a client sends a group chat message, everyone should know who sent it. We achieve this by having the server add the sender's address to each group chat message.
- The IP address should be of type
uint32_t. It should come after the first byte that indicates the type (0). - The port number should be of type
uint16_t. It should come after the IP address. - Note that only the server performs this address inclusion. A client does not do this.
- Also note that the address and the port number are numbers,
uint32_tanduint16_t, respectively. They are not strings. This means that those numbers can contain a byte value that is'\n', which, in our protocol, indicates the end of a message. You need to be careful with this when you send and receive messages.
- In summary, when the server receives a message from a client, it should interpret the first 1 byte
as the type. If it is
0, then the server should interpret the rest of the message as a group chat message until it reads'\n'.- If it is
1, the server should follow the description in Two-Phase Commit Protocol below.
- If it is
- Similarly, when a client receives a message from the server, it should interpret the first 1 byte
as the type. If it is
0, then the client should interpret the next 4 bytes as the original sender's IP address, the next 2 bytes as the original sender's port number, and the rest as a group chat message until it reads'\n'.- If it is
1, the client should follow the description in Two-Phase Commit Protocol below.
- If it is
The client program is a simple custom fuzzer that generates messages and sends those messages to the server. It also receives messages sent by the server. It is a custom fuzzer in the sense that it does not use a fuzzing framework such as libFuzzer discussed in A5. The following are the requirements.
-
The command-line interface should be able to start a client as follows.
$ ./client <IP address> <port number> <# of messages> <log file path>
-
The executable should be named
client. -
IPandportare the IP address and the port number of the server, respectively. -
A client should follow the above message protocol when sending and receiving messages.
-
After it starts, a client should keep generating and sending random messages.
<# of messages>should be the number of messages sent.-
The random messages should be of type
0. -
After sending
<# of messages>messages, the client should stop sending type0messages, and prepare for termination as described below in Two-Phase Commit Protocol. -
You can use
getentropy()to generate random bytes. Readman getentropyto understand how to use it. -
Since
getentropy()gives you random bytes that you may not be able to print out, use the following function to convert it to a hex string.#include <stdint.h> #include <stdio.h> #include <unistd.h> /* * `buf` should point to an array that contains random bytes to convert to a * hex string. * * `str` should point to a buffer used to return the hex string of the random * bytes. The size of the buffer should be twice the size of the random bytes * (since a byte is two characters in hex) plus one for NULL. * * `size` is the size of the `str` buffer. * * For example, * * uint8_t buf[10]; * char str[10 * 2 + 1]; * getentropy(buf, 10); * if (convert(buf, sizeof(buf), str, sizeof(str)) != 0) { * exit(EXIT_FAILURE); * } */ int convert(uint8_t *buf, ssize_t buf_size, char *str, ssize_t str_size) { if (buf == NULL || str == NULL || buf_size <= 0 || str_size < (buf_size * 2 + 1)) { return -1; } for (int i = 0; i < buf_size; i++) sprintf(str + i * 2, "%02X", buf[i]); str[buf_size * 2] = '\0'; return 0; }
-
-
A client should print out each and every type
0message it receives from the server and store them in a file named<log file path>.- Use this format:
"%-15s%-10u%s", e.g.,printf("%-15s%-10u%s", ip, port, str); - For example,
192.267.128.205 9000 9391DE3E275ADB19637D - Make sure you print out one message per line. Adjust the format string accordingly regarding the new line character.
- Use this format:
-
Note that a client should be able to send messages and receive messages concurrently.
To gracefully end the execution for both the server and the clients, the server and the clients should collectively implement a simplified two-phase commit protocol signaling the end of execution as follows.
- After sending
# of messagesmessages, each client should send a type1message to the server (as opposed to a type0message described in Message Protocol). This is to indicate that the client is done sending messages. - After the server receives a type
1message from each and every client, it should send a type1message to all clients. It should then terminate itself. - Each client should terminate after receiving a type
1message from the server.
- You need to use the same code structure as previous assignments with
src/andinclude/. - You also need to write
CMakeLists.txtthat produces two executables,serverandclient. Be very careful about file names and capitalization. If they are not exactly what we expect, it will result in a 0.
-
Our base grading does not use the debug option or sanitizers. Therefore, your code should function correctly without them.
-
You can use
server-tester.<arch>andclient-tester.<arch>underbin/to test your program. If you're using an x86-64 Docker, useserver-tester.amd64andclient-tester.amd64. If you are using an ARM64 Docker, useserver-tester.arm64andclient-tester.arm64. These executables are essentially the same as the ones we use for grading, though they may not be exactly the same. Below, we will generally refer to them asserver-testerandclient-tester. -
You should not use these executables as debuggers. They do not tell you what the problems are. They do provide some error messages but those are not intended to serve as debug messages. They mostly just provide only a simple pass/fail result.
-
When a test case does not pass, you need to construct the same scenario and debug your program to find the problems.
-
Testing your server program:
server-testeris the program that tests your server program. The following is how you run the program.$ ./server-tester <IP address> <port number> <# of clients> <# of messages> <test #>
- You need to run your server program first before running
server-tester. IP addressandport numberare the IP address and the port number of the server, respectively.<# of messages>is the number of messages sent by each client. Some tests may ignore this argument as they do not need more than one message.<# of clients>is the number of clients that start. Some tests may ignore this argument as they do not need more than one client.<test #>is the test number. The following are the test numbers and their descriptions.0[5 pts]: One client sends one type0message and receives some message back.1[10 pts]: One client sends one type0message and receives the server's response message back in the correct format (i.e., according to the protocol with 0, IP, port, and chat message).2[10 pts]: One client keeps sending type0messages and receives those messages back in the correct format and in the original sending order. We will test this with a large# of messages(e.g., 1000).3[5 pts]: One client sends one type1message and receives one type1message back from the server. The server terminates.4[10 pts]: Many clients each send one type0message and receive all the messages back in the correct format and in the same order. We will test this with a large# of clients(e.g., 100).5[15 pts]: Many clients each keep sending messages and receiving all the messages back in the correct format and in the same order. We will test this with a large# of clientsand a large# of messages(e.g., 100 clients and 100 messages each).6[5 pts]: Many clients each send one type1message and all receive one type1message back from the server. The server terminates.
- The executable prints out logs as it runs.
- You can change the log level by adding
LOG_LEVEL=<number>to the command, e.g.,LOG_LEVEL=0 ./server-tester .... - The log levels range from 0 to 3. 0 is the most verbose, 3 the least.
- The default log level is 1.
- When testing and debugging, you will want to use the log level 0 to get more information about the execution.
- Our grader uses the default log level. Thus, you need to make sure that your submission works with the default log level.
- Note that the log level 0 will print out a lot of messages. It is better to redirect it
to a file (e.g.,
LOG_LEVEL=0 ./server-tester ... > test.log 2>&1and examine the file after the execution.
- You can change the log level by adding
- You need to run your server program first before running
-
Testing your client program:
client-testeris the program that tests your client program. The following is how you run the program.$ ./client-tester <client executable path> <client log prefix> <port number> <# of clients> <# of messages> <test #>
- This is essentially a server but it also executes your client. Thus, the output of your client program will be printed out on the terminal, along with the output of the program.
<client file path>is the path to your client executable.<client log prefix>is the prefix of the log file that your client program generates. The program will append the client number to the prefix to create the log file name.port numberis the port number thatclient-testerbinds to.<# of clients>is the number of clients that you want to start. Some tests may ignore this argument as they do not need more than one client.<# of messages>is the number of messages sent by each client. Some tests may ignore this as they do not need more than one message.<test #>is the test number. The following are the test numbers and their descriptions.0[5 pts]: After a single client starts, the server receives at least one type0message and the client prints the message out correctly.1[10 pts]: After a single client starts, the server receives<# of messages>random type0messages and the client prints out all messages correctly.2[5 pts]: After a single client sends<# of messages>messages, the server receives one type1message and sends it back. The client terminates.3[15 pts]: After many clients start, the server keeps receiving type0messages and the clients print out all messages correctly.4[5 pts]: After receiving<# of messages>from each and every client, the server receives one type1message from them and sends it back. All clients terminate.
- The executable prints out logs as it runs.
- You can change the log level by adding
LOG_LEVEL=<number>to the command, e.g.,LOG_LEVEL=0 ./server-tester .... - The log levels range from 0 to 3. 0 is the most verbose, 3 the least.
- The default log level is 1.
- When testing and debugging, you will want to use the log level 0 to get more information about the execution.
- Our grader uses the default log level. Thus, you need to make sure that your submission works with the default log level.
- Note that the log level 0 will print out a lot of messages. It is better to redirect it
to a file (e.g.,
LOG_LEVEL=0 ./client-tester ... > test.log 2>&1and examine the file after the execution.
- You can change the log level by adding
-
Occasionally, and especially before you submit, make sure that you run the minimum checker, which is already installed in your Docker container (
minimum_checker). Please don't forget to run it from the top directory. As with previous assignments, it performs basic checks (e.g.,.record,.nvim, copy-and-paste, etc.). If this does not pass, you will receive a 0. -
Code that does not compile with CMake gets a 0.
-
Code that does not generate all the required executables (
clientandserver) gets a 0. -
Memory issues have a penalty of -10 pts. Use appropriate sanitizers and enable the debug option. Make sure your code works without the debug option as well. Before you submit, make sure that you remove the debug option.
-
A wrong code directory structure has a penalty of -10 pts.
-
Thread/synchronization issues have a penalty of -10 pts.
-
You should not hard-code or customize your implementation tailored to our test cases. Generally, you should consider the provided test cases as examples or specific instances of general cases.