Wednesday, November 7, 2012

Web Server in C

I implemented a web server in C language using only the standard libraries and thought it would be useful for you guys if I share the code.

The server runs on Linux and includes features like handling HTTP GET request, handling content types(txt, html, jpg, zip. rar, pdf, php etc.), sending proper HTTP error codes, serving the files from a web root, change in web root in a config file, zero copy optimization using sendfile method and php file handling. A port number should be provided as a command line argument.

After the server is up and running you can request for files using a web browser like Firefox.

For an example assume port number is "9000" and if you want to request a file called "test.php" which is in the webroot, use
http://localhost:9000/test.php 

WebServer.c
/*
 * WebServer.c
 *
 *  Created on: Nov 3, 2012
 *      Author: pavithra
 *
 * A web server in C language using only the standard libraries.
 * The port number is passed as an argument.
 *
 */

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>

#define EOL "\r\n"
#define EOL_SIZE 2

typedef struct {
 char *ext;
 char *mediatype;
} extn;

//Possible media types
extn extensions[] ={
 {"gif", "image/gif" },
 {"txt", "text/plain" },
 {"jpg", "image/jpg" },
 {"jpeg","image/jpeg"},
 {"png", "image/png" },
 {"ico", "image/ico" },
 {"zip", "image/zip" },
 {"gz",  "image/gz"  },
 {"tar", "image/tar" },
 {"htm", "text/html" },
 {"html","text/html" },
 {"php", "text/html" },
 {"pdf","application/pdf"},
 {"zip","application/octet-stream"},
 {"rar","application/octet-stream"},
 {0,0} };

/*
 A helper function
 */
void error(const char *msg) {
 perror(msg);
 exit(1);
}

/*
 A helper function
 */
int get_file_size(int fd) {
 struct stat stat_struct;
 if (fstat(fd, &stat_struct) == -1)
  return (1);
 return (int) stat_struct.st_size;
}

/*
 A helper function
 */
void send_new(int fd, char *msg) {
 int len = strlen(msg);
 if (send(fd, msg, len, 0) == -1) {
  printf("Error in send\n");
 }
}

/*
 This function recieves the buffer
 until an "End of line(EOL)" byte is recieved
 */
int recv_new(int fd, char *buffer) {
 char *p = buffer; // Use of a pointer to the buffer rather than dealing with the buffer directly
 int eol_matched = 0; // Use to check whether the recieved byte is matched with the buffer byte or not
 while (recv(fd, p, 1, 0) != 0) // Start receiving 1 byte at a time
 {
  if (*p == EOL[eol_matched]) // if the byte matches with the first eol byte that is '\r'
    {
   ++eol_matched;
   if (eol_matched == EOL_SIZE) // if both the bytes matches with the EOL
   {
    *(p + 1 - EOL_SIZE) = '\0'; // End the string
    return (strlen(buffer)); // Return the bytes recieved
   }
  } else {
   eol_matched = 0;
  }
  p++; // Increment the pointer to receive next byte
 }
 return (0);
}

/*
 A helper function: Returns the
 web root location.
 */
char* webroot() {
 // open the file "conf" for reading
 FILE *in = fopen("conf", "rt");
 // read the first line from the file
 char buff[1000];
 fgets(buff, 1000, in);
 // close the stream
 fclose(in);
 char* nl_ptr = strrchr(buff, '\n');
 if (nl_ptr != NULL)
  *nl_ptr = '\0';
 return strdup(buff);
}

/*
 Handles php requests
 */
void php_cgi(char* script_path, int fd) {
 send_new(fd, "HTTP/1.1 200 OK\n Server: Web Server in C\n Connection: close\n");
 dup2(fd, STDOUT_FILENO);
 char script[500];
 strcpy(script, "SCRIPT_FILENAME=");
 strcat(script, script_path);
 putenv("GATEWAY_INTERFACE=CGI/1.1");
 putenv(script);
 putenv("QUERY_STRING=");
 putenv("REQUEST_METHOD=GET");
 putenv("REDIRECT_STATUS=true");
 putenv("SERVER_PROTOCOL=HTTP/1.1");
 putenv("REMOTE_HOST=127.0.0.1");
 execl("/usr/bin/php-cgi", "php-cgi", NULL);
}

/*
 This function parses the HTTP requests,
 arrange resource locations,
 check for supported media types,
 serves files in a web root,
 sends the HTTP error codes.
 */
int connection(int fd) {
 char request[500], resource[500], *ptr;
 int fd1, length;
 if (recv_new(fd, request) == 0) {
  printf("Recieve Failed\n");
 }
 printf("%s\n", request);
 // Check for a valid browser request
 ptr = strstr(request, " HTTP/");
 if (ptr == NULL) {
  printf("NOT HTTP !\n");
 } else {
  *ptr = 0;
  ptr = NULL;

  if (strncmp(request, "GET ", 4) == 0) {
   ptr = request + 4;
  }
  if (ptr == NULL) {
   printf("Unknown Request ! \n");
  } else {
   if (ptr[strlen(ptr) - 1] == '/') {
    strcat(ptr, "index.html");
   }
   strcpy(resource, webroot());
   strcat(resource, ptr);
   char* s = strchr(ptr, '.');
   int i;
   for (i = 0; extensions[i].ext != NULL; i++) {
    if (strcmp(s + 1, extensions[i].ext) == 0) {
     fd1 = open(resource, O_RDONLY, 0);
     printf("Opening \"%s\"\n", resource);
     if (fd1 == -1) {
      printf("404 File not found Error\n");
      send_new(fd, "HTTP/1.1 404 Not Found\r\n");
      send_new(fd, "Server : Web Server in C\r\n\r\n");
      send_new(fd, "<html><head><title>404 Not Found</head></title>");
      send_new(fd, "<body><p>404 Not Found: The requested resource could not be found!</p></body></html>");
      //Handling php requests
     } else if (strcmp(extensions[i].ext, "php") == 0) {
      php_cgi(resource, fd);
      sleep(1);
      close(fd);
      exit(1);
     } else {
      printf("200 OK, Content-Type: %s\n\n",
        extensions[i].mediatype);
      send_new(fd, "HTTP/1.1 200 OK\r\n");
      send_new(fd, "Server : Web Server in C\r\n\r\n");
      if (ptr == request + 4) // if it is a GET request
        {
       if ((length = get_file_size(fd1)) == -1)
        printf("Error in getting size !\n");
       size_t total_bytes_sent = 0;
       ssize_t bytes_sent;
       while (total_bytes_sent < length) {
        //Zero copy optimization
        if ((bytes_sent = sendfile(fd, fd1, 0,
          length - total_bytes_sent)) <= 0) {
         if (errno == EINTR || errno == EAGAIN) {
          continue;
         }
         perror("sendfile");
         return -1;
        }
        total_bytes_sent += bytes_sent;
       }

      }
     }
     break;
    }
    int size = sizeof(extensions) / sizeof(extensions[0]);
    if (i == size - 2) {
     printf("415 Unsupported Media Type\n");
     send_new(fd, "HTTP/1.1 415 Unsupported Media Type\r\n");
     send_new(fd, "Server : Web Server in C\r\n\r\n");
     send_new(fd, "<html><head><title>415 Unsupported Media Type</head></title>");
     send_new(fd, "<body><p>415 Unsupported Media Type!</p></body></html>");
    }
   }

   close(fd);
  }
 }
 shutdown(fd, SHUT_RDWR);
}

int main(int argc, char *argv[]) {
 int sockfd, newsockfd, portno, pid;
 socklen_t clilen;
 struct sockaddr_in serv_addr, cli_addr;

 if (argc < 2) {
  fprintf(stderr, "ERROR, no port provided\n");
  exit(1);
 }
 sockfd = socket(AF_INET, SOCK_STREAM, 0);
 if (sockfd < 0)
  error("ERROR opening socket");
 bzero((char *) &serv_addr, sizeof(serv_addr));
 portno = atoi(argv[1]);
 serv_addr.sin_family = AF_INET;
 serv_addr.sin_addr.s_addr = INADDR_ANY;
 serv_addr.sin_port = htons(portno);
 if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
  error("ERROR on binding");
 listen(sockfd, 5);
 clilen = sizeof(cli_addr);
 /*
  Server runs forever, forking off a separate
  process for each connection.
  */
 while (1) {
  newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
  if (newsockfd < 0)
   error("ERROR on accept");
  pid = fork();
  if (pid < 0)
   error("ERROR on fork");
  if (pid == 0) {
   close(sockfd);
   connection(newsockfd);
   exit(0);
  } else
   close(newsockfd);
 } /* end of while */
 close(sockfd);
 return 0; /* we never get here */
}
Make sure you have installed php and there's exist a "conf" file consisted of the webroot before running this.

Enjoy!

Monday, November 5, 2012

NS-2 Simulation

Installing NS2 on Ubuntu 11.04
sudo apt-get install ns2 nam
Here I have used NS-2 to simulate a network with the topology as shown below.


I have written tcl scripts to create the above topology and to show,
  1. When two TCP flows compete for the bandwidth they fairly share the bandwidth
  2. When a TCP flow and a UDP flow compete for the bandwidth there is no fair sharing
Also I have plotted the throughput at the receivers against time using xgraph.

Installing xgraph on Ubuntu 11.04
sudo apt-get install xgraph
To start the simulation
ns XXX.tcl
How two TCP flows compete for the bandwidth

tcp_tcp.tcl

This script simulates two TCP flows; one between n0 and n4 and one between n1 and n5. Also the bandwidth of L2 has selected in such a way that it becomes the bottleneck link and the "record" procedure has used to measure the throughput at the receivers. .
#Create a simulator object
set ns [new Simulator]

#Define different colors for data flows (for NAM)
$ns color 1 Blue
$ns color 2 Red

#Open the trace files outX.tr for Xgraph and out.nam for nam
set f0 [open out_tcp0.tr w]
set f1 [open out_tcp1.tr w]

#Open the NAM trace file
set nf [open out.nam w]
$ns namtrace-all $nf

#Define a 'finish' procedure
proc finish {} {
 global ns nf f0 f1
 $ns flush-trace
#Close the NAM trace file
 close $nf
#Close the output files
 close $f0
 close $f1
#Execute xgraph to display the results
 exec xgraph out_tcp0.tr out_tcp1.tr -geometry 600x400 &
#Execute NAM on the trace file
 exec nam out.nam &
 exit 0
}

#Create five nodes
set n0 [$ns node]
set n1 [$ns node]
set n2 [$ns node]
set n3 [$ns node]
set n4 [$ns node]
set n5 [$ns node]

#Create links between the nodes
$ns duplex-link $n0 $n2 2Mb 10ms DropTail
$ns duplex-link $n1 $n2 2Mb 10ms DropTail
$ns duplex-link $n2 $n3 1.7Mb 20ms DropTail
$ns duplex-link $n3 $n4 2Mb 10ms DropTail
$ns duplex-link $n3 $n5 2Mb 10ms DropTail

#Set Queue Size of link (n2-n3) to 20
$ns queue-limit $n2 $n3 20

#Give node position (for NAM)
$ns duplex-link-op $n0 $n2 orient right-down
$ns duplex-link-op $n1 $n2 orient right-up
$ns duplex-link-op $n2 $n3 orient right
$ns duplex-link-op $n3 $n4 orient right-up
$ns duplex-link-op $n3 $n5 orient right-down

#record procedure
proc record {} {
 global sink sink1 f0 f1
#Get an instance of the simulator
 set ns [Simulator instance]

#Set the time after which the procedure should be called again
 set time 0.5

#How many bytes have been received by the traffic sinks?
 set bw0 [$sink set bytes_]
 set bw1 [$sink1 set bytes_]

#Get the current time
 set now [$ns now]

#Calculate the bandwidth (in MBit/s) and write it to the files
 puts $f0 "$now [expr $bw0/$time*8/1000000]"
 puts $f1 "$now [expr $bw1/$time*8/1000000]"

#Reset the bytes_ values on the traffic sinks
 $sink set bytes_ 0
 $sink1 set bytes_ 0

#Re-schedule the procedure
 $ns at [expr $now+$time] "record"
}

#Setup a TCP connection
set tcp [new Agent/TCP]
$tcp set class_ 2
$ns attach-agent $n0 $tcp
set sink [new Agent/TCPSink]
$ns attach-agent $n4 $sink
$ns connect $tcp $sink
$tcp set fid_ 1

#Setup a FTP over TCP connection
set ftp [new Application/FTP]
$ftp attach-agent $tcp
$ftp set type_ FTP

#Setup a TCP connection
set tcp1 [new Agent/TCP]
$tcp1 set class_ 2
$ns attach-agent $n1 $tcp1
set sink1 [new Agent/TCPSink]
$ns attach-agent $n5 $sink1
$ns connect $tcp1 $sink1
$tcp1 set fid_ 2

#Setup a FTP over TCP connection
set ftp1 [new Application/FTP]
$ftp1 attach-agent $tcp1
$ftp1 set type_ FTP

#Start logging the received bandwidth
$ns at 0.0 "record"

#Schedule events for the FTP agents
$ns at 0.1 "$ftp start"
$ns at 0.8 "$ftp1 start"
$ns at 4.0 "$ftp1 stop"
$ns at 4.8 "$ftp stop"

#Call the finish procedure after 5 seconds of simulation time
$ns at 5.0 "finish"

#Run the simulation
$ns run

Graph


Above graph visualization clearly shows how two TCP flows fairly share the bandwidth.

How a TCP flow and a UDP flow compete for the bandwidth

tcp_udp.tcl

This script simulates one TCP flow (between n0 and n4) and one UDP flow (between n1 and n5). Again the bandwidth of L2 has selected in such a way that it becomes the bottleneck link and the "record" procedure has used to measure the throughput at the receivers.
#Create a simulator object
set ns [new Simulator]

#Define different colors for data flows (for NAM)
$ns color 1 Blue
$ns color 2 Red

#Open the trace files outX.tr for Xgraph and out.nam for nam
set f0 [open out_tcp.tr w]
set f1 [open out_udp.tr w]

#Open the NAM trace file
set nf [open out_udptcp.nam w]
$ns namtrace-all $nf

#Define a 'finish' procedure
proc finish {} {
 global ns nf f0 f1
 $ns flush-trace
#Close the NAM trace file
 close $nf
#Close the output files
 close $f0
 close $f1
#Execute xgraph to display the results
 exec xgraph out_tcp.tr out_udp.tr -geometry 600x400 &
#Execute NAM on the trace file
 exec nam out_udptcp.nam &
 exit 0
}

#Create five nodes
set n0 [$ns node]
set n1 [$ns node]
set n2 [$ns node]
set n3 [$ns node]
set n4 [$ns node]
set n5 [$ns node]

#Create links between the nodes
$ns duplex-link $n0 $n2 2Mb 10ms DropTail
$ns duplex-link $n1 $n2 2Mb 10ms DropTail
$ns duplex-link $n2 $n3 1.7Mb 20ms DropTail
$ns duplex-link $n3 $n4 2Mb 10ms DropTail
$ns duplex-link $n3 $n5 2Mb 10ms DropTail

#Set Queue Size of link (n2-n3) to 20
$ns queue-limit $n2 $n3 20

#Give node position (for NAM)
$ns duplex-link-op $n0 $n2 orient right-down
$ns duplex-link-op $n1 $n2 orient right-up
$ns duplex-link-op $n2 $n3 orient right
$ns duplex-link-op $n3 $n4 orient right-up
$ns duplex-link-op $n3 $n5 orient right-down

#record procedure
proc record {} {
 global sink sink1 f0 f1
#Get an instance of the simulator
 set ns [Simulator instance]

#Set the time after which the procedure should be called again
 set time 0.5

#How many bytes have been received by the traffic sinks?
 set bw0 [$sink set bytes_]
 set bw1 [$sink1 set bytes_]

#Get the current time
 set now [$ns now]

#Calculate the bandwidth (in MBit/s) and write it to the files
 puts $f0 "$now [expr $bw0/$time*8/1000000]"
 puts $f1 "$now [expr $bw1/$time*8/1000000]"

#Reset the bytes_ values on the traffic sinks
 $sink set bytes_ 0
 $sink1 set bytes_ 0

#Re-schedule the procedure
 $ns at [expr $now+$time] "record"
}

#Setup a TCP connection
set tcp [new Agent/TCP]
$tcp set class_ 2
$ns attach-agent $n0 $tcp
set sink [new Agent/TCPSink]
$ns attach-agent $n4 $sink
$ns connect $tcp $sink
$tcp set fid_ 1

#Setup a FTP over TCP connection
set ftp [new Application/FTP]
$ftp attach-agent $tcp
$ftp set type_ FTP

#Setup a UDP connection
set udp [new Agent/UDP]
$ns attach-agent $n1 $udp
set sink1 [new Agent/LossMonitor]
$ns attach-agent $n5 $sink1
$ns connect $udp $sink1
$udp set fid_ 2

#Setup a CBR over UDP connection
set cbr [new Application/Traffic/CBR]
$cbr attach-agent $udp
$cbr set type_ CBR
$cbr set packet_size_ 1000
$cbr set rate_ 2mb
$cbr set random_ false

#Start logging the received bandwidth
$ns at 0.0 "record"

#Schedule events for the CBR and FTP agents
$ns at 0.1 "$cbr start"
$ns at 0.8 "$ftp start"
$ns at 4.0 "$ftp stop"
$ns at 4.8 "$cbr stop"

#Call the finish procedure after 5 seconds of simulation time
$ns at 5.0 "finish"

#Run the simulation
$ns run
Graph


Above graph visualization clearly shows, when it comes to a TCP flow and a UDP flow there is no fair sharing the bandwidth, UDP gets the most of it.