CSCI 4210 — Operating Systems
Homework 4
Network Programming and TCP/IP
To succeed in this course, do not rely on program output to show whether your code is correct. And no guesswork! Instead, consistently allocate exactly the number of bytes you need regardless of whether you use static or dynamic memory allocation.
Further, deallocate dynamically allocated memory via free() at the earliest possible point in your code. Consider using valgrind to check for errors with dynamic memory allocation and use. Also close any open file descriptors or FILE pointers as soon as you are done using them.
Another key to success in this course is to always read (and re-read!) the corresponding man pages for library functions, system calls, etc. To better understand how man pages are organized, check out the man page for man itself!
Homework specifications
In this fourth assignment, you will use C to implement a single-process multi-threaded TCP server for the Wordle word game. You will use POSIX threads to implement a TCP server that handles multiple client connections in parallel.
Specifically, your top-level main thread blocks on the accept() system call, listening on the port number specified as a command-line argument. For each connection request received by your server, create a child thread via pthread_create() to handle that specific connection.
Each child thread manages game play for one client and only for one hidden word. Both during and after game play ends, child threads update a set of global variables. Note that child threads are not joined back in to the main thread.
And remember that all threads run within one process.
Wordle game play
To learn how to play this one-player game, visit https://www.nytimes.com/games/wordle. In brief, a five-letter word is selected at random, then a player has up to six guesses to guess this hidden word. For each guess, the player sees which guessed letters are in the correct position (if any), which guessed letters are in the word in an incorrect position (if any), and which guessed letters are not in the word at all.
Note that only valid five-letter words are allowed as guesses. Therefore, if a guess is not in the given words file, it does not count as a guess. In general, expect your server to receive anything, including erroneous data.
Game play stops when the player guesses the word correctly or runs out of guesses. In either case, the server closes the TCP connection, then the corresponding child thread terminates.
Global variables and compilation
For this assignment, we are giving you a head start. The provided hw4-main.c source file contains a short main() function that initializes four global variables, then calls the wordle_server() function, which you must write in your own hw4.c source file.
Submitty will compile your hw4.c code as follows:
bash$ gcc -Wall -Werror hw4-main.c hw4.c -pthread
You are required to make use of the four global variables in the given hw4-main.c source file. To do so, declare them as external variables in your hw4.c code as follows:
The first three global variables shown above count the total number of valid guesses, the total number of games won, and the total number of games lost, respectively. These totals are accumulated across all active players during game play.
The words array is a dynamically allocated array of character strings representing all of the words actually used in game play. This array is initially set (in hw4-main.c) to be an array of size 1, with *words initialized to NULL. Similar to argv, the last entry in this array must always be NULL so that the list of words can be displayed using a simple loop, as shown below. (Refer to command-line-args.c for an example using this technique.)
Submitty test cases will check these global variables when your wordle_server() function returns. Also, your function should return either EXIT_SUCCESS or EXIT_FAILURE.
Feel free to use additional global variables in your own code. And since multiple threads will be accessing and changing these global variables, synchronization is required.
Application-layer protocol
The specifications below focus on the application-layer protocol that your server must implement to successfully communicate with multiple clients simultaneously.
Once a connection is accepted, the client sends a five-byte packet containing a guess, e.g., "ready"; the guessed word can be a mix of uppercase and lowercase letters, as case does not matter.
The server replies with a eight-byte packet that is formatted as follows:
+-----+-----+-----+-----+-----+-----+-----+-----+
SERVER REPLY: |valid| guesses | result |
|guess| remaining | |+-----+-----+-----+-----+-----+-----+-----+-----+
The valid guess field is a one-byte char value that is either 'Y' (yes) or 'N' (no).
The guesses remaining field is a two-byte short value that indicates how many guesses the client has left. Since a client starts with six guesses, this counts down from five to zero for each valid guess made.
The result field is a five-byte character string that corresponds to the client’s guess. If a guess is not valid, simply send "?????"; for a valid guess, encode the results as follows. Use an uppercase
letter to indicate a matching letter in the correct position. Use a lowercase letter to indicate a letter that is in the word but not in the correct position. And use a '-' character to indicate an incorrect letter not in the word at all.
As an example, if the hidden word is “wears,” and a client guesses “ready,” the server replies with "rEA--"; note that words may contain duplicate letters, e.g., “muddy” and “radar” and so on.
If the client sends the correct word or the number of guesses remaining is zero, the server closes the TCP connection.
Command-line arguments
There are four required command-line arguments. The first command-line argument specifies the TCP listener port number.
The second command-line argument specifies the seed value for the pseudo-random number generator— this is used to “randomly” select words in a predictable and repeatable manner via rand().
The third command-line argument specifies the name (or path) of the input file containing valid words, and the fourth command-line argument specifies the number of words in this input file. Validate the inputs as necessary. If invalid, display the following to stderr and return EXIT_FAILURE:
ERROR: Invalid argument(s)USAGE: hw4.out <listener-port> <seed> <word-filename> <num-words>
The input file should contain words delimited by newline characters. Case does not matter. Here is an example file: "ready\nheavy\nUPPER\nVaguE\n"
Signals and server termination
Since servers are typically designed to run forever without interruption, ignore signals SIGINT, SIGTERM, and SIGUSR2.
Still, we need a mechanism to shut down the server. Set up a signal handler for SIGUSR1 that grace fully shuts down your server by shutting down any running child threads, freeing up dynamically allocated memory, and returning from the wordle_server() function with EXIT_SUCCESS.
Dynamic memory allocation
As with previous homeworks, you must use calloc() to dynamically allocate memory. For the global words array, you must also use realloc() to extend the size of the array.
Do not use malloc(). And of course, be sure your program has no memory leaks.
No square brackets allowed!
As we perfect the use of pointers and pointer arithmetic, once again, you are not allowed to use square brackets anywhere in your code!
If a '[' or ']' character is detected, including within comments, that line of code will be removed before running gcc.
Program execution and required output
To illustrate via an example, you could execute your program as shown below. You will have your server process running and listening on port 8192 for incoming TCP connection requests.
Note the curly brackets '{' and '}' around the listener port number (this is to help implement the Submitty auto-grader).
bash$ ./hw4.out 8192 111 wordle-words.txt 5757MAIN: opened wordle-words.txt (5757 words)MAIN: Wordle server listening on port {8192}MAIN: rcvd incoming connection requestTHREAD 139711842105088: waiting for guessTHREAD 139711842105088: rcvd guess: stareTHREAD 139711842105088: sending reply: --ArE (5 guesses left)THREAD 139711842105088: waiting for guessTHREAD 139711842105088: rcvd guess: bradeTHREAD 139711842105088: invalid guess; sending reply: ????? (5 guesses left)THREAD 139711842105088: waiting for guessMAIN: rcvd incoming connection requestTHREAD 139711833601792: waiting for guessTHREAD 139711842105088: rcvd guess: brakeTHREAD 139711842105088: sending reply: BRA-E (4 guesses left)THREAD 139711842105088: waiting for guessTHREAD 139711842105088: rcvd guess: braceTHREAD 139711842105088: sending reply: BRACE (3 guesses left)THREAD 139711842105088: game over; word was BRACE!...MAIN: SIGUSR1 rcvd; Wordle server shutting down...
To display thread IDs, use "\%lu" and pthread_self() in your printf() calls. Note that you might see duplicate thread IDs for threads not running in parallel.
If a client closes the TCP connection, your server must detect that and display the following (and count this as a loss):
THREAD 139711833601792: client gave up; closing TCP connection...
Error handling
In general, if an error is encountered in any thread, display a meaningful error message on stderr by using either perror() or fprintf(), then abort further thread execution by calling pthread_exit().
ERROR: <error-text-here>
To submit your assignment (and also perform final testing of your code), please use Submitty.
Note that this assignment will be available on Submitty a minimum of three days before the due date. Please do not ask when Submitty will be available, as you should first perform adequate testing on your own Ubuntu platform.
That said, to make sure that your program does execute properly everywhere, including Submitty, use the techniques below.
First, make use of the DEBUG_MODE technique to make sure that Submitty does not execute any
#ifdef DEBUG_MODEprintf( "the value of q is %d\n", q );printf( "here12\n" );printf( "why is my program crashing here?!\n" );printf( "aaaaaaaaaaaaagggggggghhhh!\n" );#end if
And to compile this code in “debug” mode, use the -D flag as follows:
bash$ gcc -Wall -Werror -g -D DEBUG_MODE hw4.c -pthread
setvbuf( stdout, NULL, _IONBF, 0 );