Lab 12: Threaded Socket Server
Table of Contents
1 Preliminaries
In this lab you will complete a set of C programs to expose you to the threading library and threaded socket programming.
1.1 Lab Learning Goals
In this lab, you will learn the following topics and practice C programming skills.
- Using pthreads to create and join threads
- Passing arguments to threads and retrieving return values
- Create multi-threaded applications: factorization and socket server
1.2 Lab Setup
Run the following command
~aviv/bin/ic221-up
Change into the lab directory
cd ~/ic221/labs/12
All the material you need to complete the lab can be found in the lab directory. All material you will submit, you should place within the lab directory. Throughout this lab, we refer to the lab directory, which you should interpret as the above path.
1.3 Submission Folder
For this lab, all ubmission should be placed in the following folder:
~/ic221/labs/12/
This directory contains 4 sub-directories; examples
, timer
,
term-status
, and mini-sh
. In the examples
directory you will
find any source code in this lab document. All lab work should be
done in the remaining directories.
- Only source files found in the folder will be graded.
- Do not change the names of any source files
Finally, in the top level of the lab directory, you will find a
README
file. You must complete the README
file, and include any
additional details that might be needed to complete this lab.
1.4 Compiling your programs with clang
and make
You are not required to provide your own Makefiles for this lab.
1.5 README
In the top level of the lab directory, you will find a README
file. You must fill out the README file with your name and alpha.
Please include a short summary of each of the tasks and any other
information you want to provide to the instructor.
1.6 Test Script
You are provided a test script which prints pass/fail information
for a set of tests for your programs. Note that passing all the
tests does not mean you will receive a perfect score: other tests
will be performed on your submission. To run the test script,
execute test.sh
from the lab directory.
./test.sh
You can comment out individual tests while working on different parts of the lab. Open up the test script and place comments at the bottom where appropriate.
2 PART 1: Prime Factoring
In this part of the lab you will adapt a factor finding algorithm that is single threaded to a multi-threaded implementations. Finding the factors of numbers is critically important to computer science because primes are the foundation of many cryptographic protocols. The reason that the encryption works, that is, hard to decrypt, is because that it is difficult to factor large numbers into its constituent primes.
Conceptually, to check if a number is prime (or to find a factor of
a number), you have to see if all numbers between 2 and sqrt(n)
are a factor of n
. A simple algorithm for doing this would be as
follows:
#include <stdlib.h> #include <stdio.h> #include <math.h> char USAGE[]="./slow_factor n\n" "\n" " Find a factor the number n or print that its prime\n"; int main(int argc, char * argv[]){ long n,k,sqn; //check for right number of arguments if(argc < 2){ fprintf(stderr, "ERROR: require n\n%s",USAGE); exit(1); } //convert to a long n = atol(argv[1]); //check for rigth format if( n == 0){ fprintf(stderr, "ERROR: invalid value for n\n%s",USAGE); exit(1); } //check all values between 2 and the sqrt(n) sqn = (long) sqrt(n); for(k=2; k <= sqn; k++){ if( n % k == 0){ //find a factor and return it printf("Factor: %ld\n", k); return 0; } } //did not find a factor, it's prime! printf("Prime: %ld\n", n); return 0; }
This is a fairly simple program, it just tries all numbers between 2
and the square root of n
, prints a factor if it found one,
otherwise it declares the number prime. Here's some sample output:
#> ./slow_factor 53332388942191969 Factor: 230938063 #> ./slow_factor 230938063 Prime: 230938063
It works! But, it's not so efficient because the task is easily parallelized across multiple threads. If we time the process we find that it takes over a second to find a factor of the large number:
#> /usr/bin/time -f "Elapsed Time: %E (s)" ./slow_factor 53332388942191969 Factor: 230938063 Elapsed Time: 0:01.98 (s)
2.1 Task 1: threaded factor
For this task, change into the primes
directory, where you will
find three files:
factor.c
: multithreaded factor finder, which you will completeslow_factor.c
: single threaded factor finder for referenceMakefile
: to compile your program
Your goal is to complete the factor
program so that it meets the
following specification.
- It should create
MAX_THREAD
number of threads and divide the work amongst those threads - Work should be divided such that for a given input
n
, each thread getssqrt(n)/MAX_THREADS
number of potential factors to check. - Each thread should either return a found factor or -1 if no factor is found
- The main thread should check the running state of all threads and join them when they are no longer running to retrieve the return value
- If any factor is found, the program should exit printing that factor and if no factor is found, then the program should declare the number as prime.
- All calculations should be done using
long
types (8-bytes on 64-bit machines).
Below is some sample output:
#> ./factor 53332388942191969
Factor: 230938063
#> /usr/bin/time -f "Time: %E (s)" ./factor 53332388942191969
Factor: 230938063
Time: 0:00.76 (s)
#> /usr/bin/time -f "Time: %E (s)" ./slow_factor 53332388942191969
Factor: 230938063
Time: 0:01.97 (s)
#> ###<--- factor is much faster than slow_factor, by an order of magnitude --->###
#> { /usr/bin/time -f "Time: %E (s)" ./factor 53332388942191969 & } ; ps -L
PID LWP TTY TIME CMD
3557 3557 pts/2 00:00:03 emacs
6322 6322 pts/2 00:00:00 emacs
7700 7700 pts/2 00:00:00 git-credential-
7814 7814 pts/2 00:00:00 time
7815 7815 pts/2 00:00:00 ps
7816 7816 pts/2 00:00:00 factor
7816 7817 pts/2 00:00:00 factor
7816 7818 pts/2 00:00:00 factor
7816 7819 pts/2 00:00:00 factor
7816 7820 pts/2 00:00:00 factor
22322 22322 pts/2 00:00:01 bash
#> Factor: 230938063
Time: 0:00.55 (s)
#> ###<--- You can see that 4 threads and a main thread are running and working --->###
#> ./factor
ERROR: require n
./factor n
Find a factor for the number n using threads or print that its prime
#> ./factor adam
ERROR: invalid value for n
./factor n
Find a factor for the number n using threads or print that its prime
3 PART 2: Threaded Socket Server
When implementing a socket server that can handle multiple client connections, we run into problems with blocking. First, the server must accept an incoming connection, which blocks, and once the connection is established, reading and writing from the socket is also a blocking operation. All that blocking means that it can be challenging to service multiple client connections.
In class, we saw one way to mitigate blocking by using the
select()
system call. The relevant portion of the code is
duplicated below:
//set up a select set to select from FD_ZERO(&activefds); //add the server socket to the select set FD_SET(server_sock, &activefds); while(1){ //set current active file descriptors readfds = activefds; //Perform a selct if( select(FD_SETSIZE, &readfds, NULL, NULL, NULL) < 0){ perror("select"); exit(1); } //check for activity on all file descriptors for(i=0; i < FD_SETSIZE; i++){ //was the file descriptor i set? if(FD_ISSET(i, &readfds)){ if(i == server_sock){ //activity on server socket, incoming connection //accept incoming connections = NON BLOCKING client_sock = accept(server_sock, (struct sockaddr *) &client_saddr_in, &saddr_len); printf("Connection From: %s:%d (%d)\n", inet_ntoa(client_saddr_in.sin_addr), ntohs(client_saddr_in.sin_port), client_sock); //add to active set FD_SET(client_sock, &activefds); }else{ //otherwise client socket sent something to us client_sock = i; //get the foreign address of socket getpeername(client_sock, (struct sockaddr *) &client_saddr_in, &saddr_len); //clear response buffer memset(response, 0, BUF_SIZE); //read from client and echo back n = read(client_sock, response, BUF_SIZE); if(n == 0){ //client close socket //close client sockt close(client_sock); //deactivate this client socket FD_CLR(client_sock, &activefds); //LOG printf("Client Closed: %s:%d (%d)\n", inet_ntoa(client_saddr_in.sin_addr), ntohs(client_saddr_in.sin_port), client_sock); }else{ //client sent a message //null response for safety response[n] = '\0'; //write message back to client write(client_sock, response, n); //LOG printf("Received From: %s:%d (%d): %s", inet_ntoa(client_saddr_in.sin_addr), ntohs(client_saddr_in.sin_port), client_sock, response); } } } } }
You'll notice that you must track the current socket file
descriptors, continually select from them to check if they are
actionable before calling either accept()
or read()
. While the
code is usable, it is not that elegant and can be challenging to
write.
Instead, we'd like to use threading to solve this problem. The big idea is that we generalize the actions of the server for each client into a function. Whenever a client connects, the server accepts the connection and passing the client operations over to the handler routine. If that handler routine runs within a thread, then the server can continually just accept incoming connections in the main thread and allow worker threads to service client connections.
3.1 Task 2: threaded_server
For this task, change into the socket-server
directory, where you will
find four files:
threaded_server.c
: multithreaded, multi-client socket server, which you will completeselect_server.c
: select based multi-client socket server, for referenceecho_server.c
: single thread single-client socket server, for referenceMakefile
: to compile your program
Your goal is to complete the threaded_server
program so that it meets the
following specification.
- It should create a new thread for every client and echo back all
data sent to the server to the client, like
echo_server
andselect_server
- It should handle multiple clients simultaneously
- The server should provide meaningful output that matches
select_server
; logging incoming connections, read/writes to sockets, and connection close events.
Below is some sample output from a server with multiple connections, some open and closing:
>./threaded_server
serer sock listening: (3)
Connection From: 127.0.0.1:56523 (4)
Received From: 127.0.0.1:56523 (4): testing 1
Connection From: 127.0.0.1:56525 (5)
Received From: 127.0.0.1:56525 (5): testing 2
Connection From: 127.0.0.1:56526 (6)
Received From: 127.0.0.1:56526 (6): testing 3
Client Closed: 127.0.0.1:56526 (6)
Connection From: 127.0.0.1:56527 (7)
Received From: 127.0.0.1:56527 (7): testing 4
Client Closed: 127.0.0.1:56527 (7)
Client Closed: 127.0.0.1:56525 (5)
Client Closed: 127.0.0.1:56523 (4)
The starter code for this task is purposely light, but all relevant code is available to you. You're primary task is to determine how to properly parallelize the code for threading.