Indian Institute of Space Science and Technology AV 341: Computer Networks Lab
Indian Institute of Space Science and Technology AV 341: Computer Networks Lab
Date: 1/04/2013 and 5/04/2013 Experiment No: 5 Objectives: To implement and study iterative and concurrent servers implementations for communication over TCP sockets. Study multi-process and multi-threaded concurrent servers. Observe the performance of the servers by measuring the round trip time for each of the servers. Documents Required: You may use the code fractions provided in this document. Additionally, you may use the past Lab documents that were distributed through the course websites in the last weeks. Use the lab computer to write the code and experiment the outcome. References: Linux Manual pages on Socket APIs. Unix Network Programming by Richard Stevens et. al. Introduction: There exist two methods to implement TCP or UDP servers in network applications: (i) iterative server and (ii) concurrent server. An iterative server serves the client connections in an iterative manner (one function such as send/receive at a time in a sequence or one socket after another in a sequence). On the other hand, a concurrent server serves the client connections almost in parallel without any particular sequence. For very lightly loaded servers, there will be no performance difference between iterative and concurrent servers. However, for moderate to high load servers, or servers with moderate to larger numbers of clients, iterative servers may not work satisfactorily.
There exist several ways to implement concurrent servers. Two important approaches are: (a) using multiple processes where a child process serves a client and (b) using multiple threads where each thread serves a clients connection. A process is an instance of a computer program that is being executed in a computer. The Operating System (OS) creates a process by running the program code, maintaining the state variables, and allocating resources such as program memory, data memory, and processor cycles. An OS typically runs several processes by switching the execution between them at such a fast rate that they appear as concurrently running. Example depictions of processes within an OS is shown in Figure 1 .
Operating System
Process-1 (Data, Program, Cycles) Process-2 (Data, Program, Cycles) Process-N (Data, Program, Cycles)
Operating System
Process-1 (Data, Program, Cycles) Thread-1 Thread-2 Process-N (Data, Program, Cycles)
Compared to a process, a thread is a lightweight process or program execution sequence that shares most of the resources of the parent process such as program memory, data memory, etc. However, the stack pointer and other very important data, necessary for the execution of the program, will be separate for each thread within a process. Simply speaking, threads are light processes that share the data variables in its process. Therefore, it becomes necessary to safeguard the data when multiple treads manipulate them. Figure 2 shows a depiction of threads within a process.
Procedure:
Step 1: An iterative TCP server
Create an iterative TCP server application for string reversal service in another machine for serving multiple client programs implemented in your lab machine as shown in the figure below.
To simulate a computing intensive processing at the server, add a delay of 2 seconds in the string reversal server. Measure the performance such as delay of service when 1, 2, or 4 clients connect to the server and seek string reversal service. Make a chart of the delay performance as shown below. Number of simultaneous clients Average time taken for iterative and concurrent servers. Averaged from atleast 4 measurements) Iterative server Multi-process Multi-threaded concurrent server concurrent server
1 2 3
Appendix A of this document shows an example implementation of Iterative server. Changes required at the server: You may want to implement the string reversal service at the server using the past lab documents. That is, you may have to add a sleep(2) function in the connection handling function in the server process. Changes required at the client: Time measurement for measuring the service time between making a request to process the string and its response. You may want to use information from past lab documents.
Step 2: Create a Multi-process concurrent server where a separate process serves each client connection.
The lab-1 experiment you carried out is an example of multi-process concurrent server. In the server code that we used in Lab 1, the function fork() is used for creating multiple processes where each new connection is served by a separate process. The changes required in the Step 1 to effect the String reversal service as well as addition of delay must be done to the Server code. Time measurement at the client code is essential. Fill the delay observations made in the table mentioned in Step 1. Appendix B of this document shows an example implementation of Iterative server. You may need to modify the code for (i) time measurement at the client side and (ii) string reversal service at the server side.
$ gcc filename.c o filename.out lpthread For the multithreaded server implementation the following new declarations are required: #include <pthread.h>
static void* str_echo(void *arg); //declaring the function
The following new functions to be used for creating threads: int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func) (void *), void *arg) //to create a new thread 1. pthread_t tid: thread identifier (unsigned int) 2. pthread_attr_t *attr: attributes such as thread priority, initial stack size, or should be run as deamon or not. Can be NULL to use default variables. 3. void*(*func)(void*): a function to be executed for the thread. The function should take a void pointer and returns a void pointer. The thread begins execution by running the function. The thread exits either by returning from the function or by explicitly calling ptherad_exit() function. 4. void * args: pointer to pass data to the function executed by the thread. It returns 0 if success and nonzero for an error.
pthread_t
tid;
int *tempsocket; . .
listen(sockfd, 5); for ( ; ; ) { /* * Wait for a connection from a client process. * This is an example of a concurrent threaded server. */ clilen = sizeof(cli_addr); newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen); if (newsockfd < 0) perror("server: accept error");
tempsocket=malloc(sizeof(int)); *tempsocket = clientsock; pthread_create(&tid, NULL, &str_echo, (void *)tempsocket); fprintf(stdout, "Client connected: %s\n", inet_ntoa(cli_addr.sin_addr));
In addition, it is essential to modify the str_echo(..) function properties. For example, modify the str_echo() function as follows:
static void* str_echo(void *arg) {
return; /* connection terminated */ else if (n < 0) perror("str_echo: readline error"); if (send(sockfd, line, n,0) != n) perror("str_echo: writen error"); } close(sockfd); }
Now, the most important point is to make sure the global data variables, that each of the multiple threads manipulate, are protected. For example if thread 1 and thread 2 are modifying a global variable then it may behave very inconsistent. You need to protect the critical data variables that multiple threads may manipulate. The two functions that can be used to safeguard threads operating on the data are: int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); The above two functions are meant for Mutually Exclusive (MUTEX) operations on variables so that two threads may not manipulate some important global variables simultaneously. An example of how to use the mutex functions is below. #include <pthread.h>
{ n = recv(sockfd, line, MAXLINE, 0); if (n == 0) return; /* connection terminated */ else if (n < 0) perror("str_echo: readline error");
// code fraction protected from other threads. // suppose if you use a global variable for string // reversal, you need to use protection/sync //mechanisms
pthread_mutex_unlock(&buffer_mutex); //unlocks the mutex variable
Other changes required in the Step 1 to effect the delay in string reversal and time measurements are required in Step 3 also. Fill the delay observations made in the table mentioned in Step 1.
Homework/Documentation: Prepare a lab notebook documenting the code and outcome for the experiment.
char *pname; /* * Example of server using TCP protocol. */ main(argc, argv) int argc; char *argv[]; { int sockfd, newsockfd, clilen, childpid; struct sockaddr_in cli_addr, serv_addr; int connCount; int socketArray[CONN_LIMIT]; pname = argv[0]; /* * Open a TCP socket (an Internet stream socket). */ if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) perror("server: can't open stream socket"); /* * Bind our local address so that the client can send to us. */ bzero((char *) &serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port
= htons(SERV_TCP_PORT);
if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) perror("server: can't bind local address"); listen(sockfd, 5); for (connCount=0; connCount < CONN_LIMIT; connCount++) { /* * Wait for connections from client processes. * This is an example of iterative server. */ clilen = sizeof(cli_addr); socketArray[connCount]=accept(sockfd,(struct sockaddr *) &cli_addr, &clilen); if (socketArray[connCount] < 0) perror("server: accept error"); } for ( ; ; ) { for (connCount=0; connCount < CONN_LIMIT; connCount++) { str_echo(socketArray[connCount]); } } // // } /* * Read a stream socket one line at a time, and write each line back * to the sender. * * Return when the connection is terminated. */ str_echo(sockfd) int sockfd; { close(sockfd); for (int connCount=0; connCount < CONN_LIMIT; connCount++) //{close(socketArray[connCount]);}
int n; char line[MAXLINE]; n = recv(sockfd, line, MAXLINE, 0); if (n == 0) return; /* connection terminated */ else if (n < 0) perror("str_echo: readline error"); if (send(sockfd, line, n,0) != n) perror("str_echo: writen error"); }
<stdio.h> <stdlib.h> <string.h> <sys/types.h> <sys/socket.h> <netinet/in.h> <arpa/inet.h> /* host addr for server
#define SERV_TCP_PORT 9877 #define SERV_HOST_ADDR "127.0.0.1" replace it with the servers IP address*/ #define MAXLINE 512
char *pname; /* * Example of client using TCP protocol. */ main(argc, argv) int argc; char *argv[]; { int sockfd; struct sockaddr_in serv_addr; // char SERVER_IP_ADDR[25]; pname = argv[0]; /*
* Fill in the structure "serv_addr" with the address of the * server that we want to connect with. */ // strcpy(SERVER_IP_ADDR,"localhost"); bzero((char *) &serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(SERV_HOST_ADDR); serv_addr.sin_port = htons(SERV_TCP_PORT); /* * Open a TCP socket (an Internet stream socket). */ if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) perror("client: can't open stream socket"); /* * Connect to the server. */ if (connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) perror("client: can't connect to server"); str_cli(stdin, sockfd); close(sockfd); exit(0); } /* * Read the contents of the FILE *fp, write each line to the * stream socket (to the server process), then read a line back from * the socket and write it to the standard output. * * Return to caller when an EOF is encountered on the input file. */ str_cli(fp, sockfd) register FILE *fp; register int sockfd; { int n; char sendline[MAXLINE], recvline[MAXLINE + 1]; printf(Enter the string to be sent to the server\n); while (fgets(sendline, MAXLINE, fp) != NULL) { n = strlen(sendline); /* do it all */
printf(Sending %d bytes to server at %s\n,n,SERV_HOST_ADDR); if (send(sockfd, sendline, n,0) != n) perror("str_cli: writen error on socket"); /* * Now read a line from the socket and write it to * our standard output. */ n = recv(sockfd, recvline, MAXLINE,0); if (n < 0) perror("str_cli: readline error"); recvline[n] = 0; /* null terminate */ printf("Received From Server: "); fputs(recvline, stdout); } if (ferror(fp)) perror("str_cli: error reading file"); }
Modification required for TCP Iterative client/Server codes: Client requires round trip time (RTT) measurement code that you may have implemented in Lab 2. Server code requires code addition, between receiving a packet from client and sending it back to the client, for (i) string reversal code and (ii) a sleep(2) function call.
main(argc, argv) int argc; char *argv[]; { int sockfd, newsockfd, clilen, childpid; struct sockaddr_in cli_addr, serv_addr; pname = argv[0]; /* * Open a TCP socket (an Internet stream socket). */ if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) perror("server: can't open stream socket"); /* * Bind our local address so that the client can send to us. */ bzero((char *) &serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(SERV_TCP_PORT); if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) perror("server: can't bind local address"); listen(sockfd, 5); for ( ; ; ) { /* * Wait for a connection from a client process. * This is an example of a concurrent server.
*/ clilen = sizeof(cli_addr); newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen); if (newsockfd < 0) perror("server: accept error"); if ( (childpid = fork()) < 0) perror("server: fork error"); else if (childpid == 0) { close(sockfd); str_echo(newsockfd); exit(0); } close(newsockfd); } } /* * Read a stream socket one line at a time, and write each line back * to the sender. * * Return when the connection is terminated. */ str_echo(sockfd) int sockfd; { int n; char line[MAXLINE]; for ( ; ; ) { n = recv(sockfd, line, MAXLINE, 0); if (n == 0) return; /* connection terminated */ else if (n < 0) perror("str_echo: readline error"); if (send(sockfd, line, n,0) != n) perror("str_echo: writen error"); } } /* child process */ /* close original socket */ /* process the request */
/* parent process */
Client code:
#include #include #include #include #include #include #include #define #define #define <stdio.h> <stdlib.h> <string.h> <sys/types.h> <sys/socket.h> <netinet/in.h> <arpa/inet.h> SERV_TCP_PORT SERV_HOST_ADDR MAXLINE 512 9877 "127.0.0.1" /* host addr for server */
char *pname; /* * Example of client using TCP protocol. */ main(argc, argv) int argc; char *argv[]; { int sockfd; struct sockaddr_in serv_addr; // char SERVER_IP_ADDR[25]; pname = argv[0]; /* * Fill in the structure "serv_addr" with the address of the * server that we want to connect with. */ // strcpy(SERVER_IP_ADDR,"localhost"); bzero((char *) &serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(SERV_HOST_ADDR); serv_addr.sin_port = htons(SERV_TCP_PORT); /* * Open a TCP socket (an Internet stream socket). */ if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) perror("client: can't open stream socket");
/* * Connect to the server. */ if (connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) perror("client: can't connect to server"); str_cli(stdin, sockfd); close(sockfd); exit(0); } /* * Read the contents of the FILE *fp, write each line to the * stream socket (to the server process), then read a line back from * the socket and write it to the standard output. * * Return to caller when an EOF is encountered on the input file. */ str_cli(fp, sockfd) register FILE *fp; register int sockfd; { int n; char sendline[MAXLINE], recvline[MAXLINE + 1]; printf(Enter the string to be sent to the server\n); while (fgets(sendline, MAXLINE, fp) != NULL) { n = strlen(sendline); printf("Sending %d bytes to server at %s\n",n,SERV_HOST_ADDR); if (send(sockfd, sendline, n,0) != n) perror("str_cli: writen error on socket"); /* * Now read a line from the socket and write it to * our standard output. */ n = recv(sockfd, recvline, MAXLINE,0); if (n < 0) perror("str_cli: readline error"); recvline[n] = 0; /* null terminate */ printf("Received From Server: "); /* do it all */