IC221: Systems Programming (SP14)


Home Policy Calendar Syllabus Resources Piazza

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.

  1. Using pthreads to create and join threads
  2. Passing arguments to threads and retrieving return values
  3. 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 complete
  • slow_factor.c : single threaded factor finder for reference
  • Makefile : to compile your program

Your goal is to complete the factor program so that it meets the following specification.

  1. It should create MAX_THREAD number of threads and divide the work amongst those threads
  2. Work should be divided such that for a given input n, each thread gets sqrt(n)/MAX_THREADS number of potential factors to check.
  3. Each thread should either return a found factor or -1 if no factor is found
  4. 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
  5. 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.
  6. 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 complete
  • select_server.c : select based multi-client socket server, for reference
  • echo_server.c : single thread single-client socket server, for reference
  • Makefile : to compile your program

Your goal is to complete the threaded_server program so that it meets the following specification.

  1. It should create a new thread for every client and echo back all data sent to the server to the client, like echo_server and select_server
  2. It should handle multiple clients simultaneously
  3. 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.