CIS307: Spring 97: Hints for Homework 5

Organization of the program

We now have two programs - STORE_MANAGER and RAND_PROC. These programs are invoked manually from the command line on possibly different computers. At least three copies of RAND_PROC will execute concurrently. RAND_PROC communicates with STORE_MANAGER through sockets. STORE_MANAGER maintains a table of ordered pairs in STORE, and processes serially (an unbounded number of) requests for read and update operations on STORE received through socket messages from the RAND_PROC processes, sending the response to each request in a message addressed to the requestor.

STORE_MANAGER is the server and is invoked first from the command line on a computer. It creates a datagram socket and binds it to a port chosen by the system. It then writes on the screen the port number associated with its socket. It maintains the store table and its associated mutex locks. It satisfies user requests by creating for each request a thread to carry out that request. Threads will execute concurrently with each other and with the main server thread.

RAND_PROC is the client. It has the same structure as in Homework 4, that is, it has two threads, one interacting with the user and the other interacting with the server. The client program RAND_PROC is invoked after STORE_MANAGER displays the port number assigned to its socket. Each client program is invoked from the command line with arguments specifying the host name of the computer that STORE_MANAGER is running on, and the port number displayed by STORE_MANAGER. Each client program may be invoked on the host running STORE_MANAGER or, better, on a different computer.

Given this set up, the programs may be organized into two files - store_manager.c and rand_proc.c (additional header files may be used to group together data structures and operations needed in each program). Thus STORE_MANAGER may be run, say on snowhite server, by invoking

% store_manager

at the command line at snowhite server. Thereafter RAND_PROC may be run, say on thunder, by invoking

% rand_proc hostname portnumber

at the command line on nimbus, where

  
    hostname   :  host name of the computer that STORE_MANAGER is 
                  running on, in this example, snowhite.cis.temple.edu, 

    portnumber :  the port number displayed on the screen by STORE_MANAGER

are the arguments to the program in rand_proc.c. Other copies of RAND_PROC may be run on snowhite, thunder, or any other machine of your choice by invoking rand_proc as shown above with the appropriate arguments from the command line on that computer. The files store_manager and rand_proc contain the executable code for store_manager.c and rand_proc.c respectively.

Establishing sockets for communication

RAND_PROC uses sockets to communicate with STORE_MANAGER. Each program creates a datagram socket in the Internet domain and binds an address to it to be able to receive messages through it. The address bound to a socket comprises an internet address (usually that of the host that the socket is created on) and a port number. A program typically sends messages to a socket by specifying the address associated with the socket.

STORE_MANAGER first creates an Internet domain datagram socket. It then binds a local address to the socket, getting the system to pick a port number for it. It queries the system for the port number assigned to the socket and writes the port number on the screen. Recall that the host name of the computer that STORE_MANAGER is invoked on and the port number displayed by STORE_MANAGER are made available to RAND_PROC through command line arguments. This enables them to construct the address associated with the socket created by STORE_MANAGER. The following is an outline of the steps carried out by STORE_MANAGER in establishing a socket.

  Algorithm SetUpSocketInStoreManager
  begin

   1. Create a datagram socket by invoking socket() as 
        s := socket(AF_INET,SOCK_DGRAM,0);
   
   2. Bind local address to socket as follows :   
        { sm_socket_address is assumed to be declared as a variable of type 
          struct sockaddr_in }

        sm_socket_address.sin_family := AF_INET;
        sm_socket_address.sin_addr.s_addr := INADDR_ANY; 
        { The wildcard INADDR_ANY matches all valid network addresses of  
          the computer. }

        sm_socket_address.sin_port := 0;  { Let system pick port number } 
       
        Now invoke bind() as 
          bind(s,&sm_socket_address,sizeof(sm_socket_address));
   
   3. Find the port number assigned by the system by invoking 
      getsockname() as  
        getsockname(s,&sm_socket_address,&length_of_address);

   4. Print the host's representation of the port number, 
      ntohs(sm_socket_address.sin_port);

  end SetUpSocketInStoreManager
Each client program also creates a datagram socket and binds an address to it, having the system select a port number for the socket. STORE_MANAGER gets implicitly the address of the socket in a client when it receives a message (containing a request for a read or update operation) from the client, and (one of its threads) sends its response to the socket associated with that address. The client program constructs the address of the socket in STORE_MANAGER from the host name and the port number arguments, and sends its messages requesting read and update operations to this address. Algorithm SetUpSocketInRandProc details the steps outlined above.

  Algorithm SetUpSocketInRandProc
  begin

   1. Create a datagram socket by invoking socket() as 
        s := socket(AF_INET,SOCK_DGRAM,0);
   
   2. Bind local address to socket as follows :   
        { rp_socket_address is assumed to be declared as a variable of type 
          struct sockaddr_in }

        rp_socket_address.sin_family := AF_INET;
        rp_socket_address.sin_addr.s_addr := INADDR_ANY;
        { The wildcard INADDR_ANY matches all valid network addresses of 
          the computer. }

        rp_socket_address.sin_port := 0;  { Let system pick port number } 
       
        Now invoke bind() as 
          bind(s,&rp_socket_address,sizeof(socket_address));
   
   4. Construct the address of the socket created by STORE_MANAGER by 
      assigning the appropriate values to the components of 
      sm_socket_address. sm_socket_address is assumed to be declared 
      as a variable of type struct sockaddr_in.

        sm_socket_address.sin_family := AF_INET;

        Get the IP address of the computer running STORE_MANAGER from the 
        the first command line argument : 
 
        { assumes the declaration  
                           struct hostent *host_info;
        }
          host_info := gethostbyname(argv[1]);
        
        Assign the IP address by copying individual bytes, invoking bcopy as  
          bcopy(host_info->h_addr,&sm_socket_address.sin_addr,
                host_info->h_length);

        Assign the network representation of the port number from the 
        second command line argument : 
 
          sm_socket_address.sin_port := htons(atoi(argv[2]));

      { Now sm_socket_address contains the address of the socket created 
        by STORE_MANAGER. }

  end SetUpSocketInRandProc

Performing I/O through sockets

The client program RAND_PROC constructs the address of the socket created by STORE_MANAGER from the name of the machine STORE_MANAGER runs on and the port number associated with the socket they receive as arguments. It can then send messages to STORE_MANAGER by specifying this address for the destination. However, the addresses of the sockets created by the client programs are not made available to STORE_MANAGER explicitly. This is because STORE_MANAGER being a server does not INITIATE the exchange of messages with the clients but sends a message to a client in response to a request received from it, and with the proper choice of communication primitives the receiver of a message can obtain the address of the socket the message originated from.

Following this scheme, RAND_PROC invokes the function sendto() to send a message to STORE_MANAGER, and STORE_MANAGER invokes recvfrom() to receive messages from the clients. The function recvfrom() returns to STORE_MANAGER the address of the socket associated with the sender (through one of its parameters) and STORE_MANAGER sends its response to the request specifying this address for the destination. The functions sendto() and recvfrom() may be used in the transmission of messages from STORE_MANAGER to the clients as well.

The details of sending and receiving messages should be hidden in the functions STORE_READ() and STORE_UPDATE() invoked by the client programs. The intent is to avoid burdening the user with the details of the implementation and present an interface that specifies only the semantics of the operation.

Examples using Sockets

Here is a trivial client and server, here is another client and server, and here is one more client and server.

Using Threads in The Store Manager

The STORE_MANAGER has to define and initialize one pthread_mutex for each entry in the store. These pthread_mutexes must be initialized by the main thread of the STORE_MANAGER before they can be used by the various threads.

The STORE_MANAGER has also to keep a count of the number of created threads that are currently active. We need to make sure that we never have more than two threads active. This is a job for a counting semaphore. Here is a possible implementation for such an object.


/* countmonitor.c  -- It implements a monitor that protects a variable 
 *                    count initialized to N that should never be negative.
 *                    It is essentially a counting semaphore.
 */

#include  <pthread.h>

typedef struct {
  int count;
  pthread_mutex_t mutex;
  pthread_cond_t condition;
} countMonitor;


/* Operations on countmonitor */

void initCountMonitor (countMonitor * p, int n ) {
  if(pthread_mutex_init(&(p->mutex), NULL) != 0) {
    perror("pthread_mutex_init");
    exit(1);}
  if(pthread_cond_init(&(p->condition), NULL) 
     != 0) {
    perror("pthread_cond_init");
    exit(1);}
  p->count = n;
}

void DownCountMonitor (countMonitor * p){
  pthread_mutex_lock(&(p->mutex));
  while (p->count <= 0) 
    pthread_cond_wait(&(p->condition), &(p->mutex));
  (p->count)--;
  pthread_mutex_unlock(&(p->mutex));
}

void UpCountMonitor (countMonitor * p){
  pthread_mutex_lock(&(p->mutex));
  (p->count)++;
  pthread_cond_signal(&(p->condition));
  pthread_mutex_unlock(&(p->mutex));
}

Another issue to be aware of is that threads when they exit they do not necessarily return the resources they are currently using. In order that they do so it is advised that once a thread is started, as its first action it marks itself as ready to give up resources upon termination. This is done with the command

   pthread_detach(pthread_self());

Format of messages exchanged through sockets

STORE_MANAGER retrieves messages one at a time from its socket, carries out on STORE the operation requested in the message, and sends its response to the socket associated with the requester. Since the update operation requires one more operand than the read operation, the structure of a message requesting an update operation would be different from that of one asking for a read operation. Since datagram sockets preserve message boundaries in the data transmitted through them, STORE_MANAGER would be able to retrieve messages one at a time while reading from its socket. However, a message can be assigned to a variable of the appropriate type directly upon retrieval only if STORE_MANAGER can predict in advance the type of operation on STORE that would be specified in the message. Since these operations are chosen at random by RAND_PROC, STORE_MANAGER would not be able to anticipate them, and would have to adopt a somewhat cumbersome procedure involving an examination of the length of the message before assigning the message to a variable of the appropriate type. We may alleviate the task by arranging to use messages of the same length (incurring some loss in effective bandwidth) while requesting both read and update operations. A message specifying a read operation is padded at the end with enough spaces to make it as long as one asking for an update operation before being sent, and STORE_MANAGER ignores the dummy characters while parsing the message. This makes for a simpler design, accompanied, however, by a reduction in communication bandwidth.

The above discussion applies to messages originating from STORE_MANAGER as well. Response messages for all cases except that of an invalid read operation have the same length, but the latter requires fewer characters. All cases may, however, be handled in a uniform manner by getting STORE_MANAGER to put in some padding in the messages containing the response to invalid read operations.