Example Project

Basics of network programming on Linux

Linux offers a sockets api to implement network communication. The textbook unfortunately does not provide any coverage of this topic, so here is a link to an online tutorial that covers the basics of Linux socket programming.

The MiniWeb server

As our first example of a network program we are going to construct a very minimal web server. This web server will be able to serve up static content from a set of files in a local folder.

To compile and run the server, start by clicking the button at the top of these notes to download the project folder for the miniweb server. Expand the archive and then open the MiniWeb folder in Visual Studio Code. Open a terminal pand and run these commands:

gcc miniweb.c -o miniweb
./miniweb

To confirm that the web server is working correctly, open a browser on your computer and paste in this URL:

http://localhost:8888/index.html

That should give you a confirmation page to confirm that the server is up and running.

To stop the server, press the control-c key combination in the terminal pane.

More about web servers

When a browser requests some content from a web server it will construct an HTTP request and send it to the server. The request is structured as a list of headers and an optional body. The first line in the request takes the form

<Verb> <URL> <HTTP version>

The simplest type of HTTP request is the GET request, which uses GET as its <Verb>. Our simple web server will only respond to GET requests. In the case of a simple GET request the <URL> will be a path to a file. Our server will open the requested file and send it back to the client.

To send back a properly constructed response the server has to respond with a list of response headers followed by the content requested. A minimal set of response headers should look like this:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: <Length>

Here <Length> is the length of the content in bytes. After the response headers we need to provide a blank line followed by the response content.

In a case where the requested file is not found on the server the first line of the response changes to

HTTP/1.1 404 Not Found

and the body of the response changes to the server's not found page.

Setting up the sockets

The main() function of the web server program carries out the basics steps need to start listening for connections from clients.

int main() {
  // Create the socket
  int server_socket = socket(AF_INET , SOCK_STREAM , 0);
  if server_socket == -1) {
    printf("Could not create socket.\n");
    return 1;
  }

  //Prepare the sockaddr_in structure
  struct sockaddr_in server;
  server.sin_family = AF_INET;
  server.sin_addr.s_addr = INADDR_ANY;
  server.sin_port = htons( 8888 );

  // Bind to the port we want to use
  if(bind(server_socket,(struct sockaddr *)&server , sizeof(server)) < 0) {
    printf("Bind failed\n");
    return 1;
  }
  printf("Bind done\n");

  // Mark the socket as a passive socket
  listen(server_socket , 3);

  // Accept incoming connections
  printf("Waiting for incoming connections...\n");
  while(1) {
    struct sockaddr_in client;
    int new_socket , c = sizeof(struct sockaddr_in);
    new_socket = accept(server_socket, (struct sockaddr *) &client, (socklen_t*)&c);
    if(new_socket != -1)
      serveRequest(new_socket);
  }

  return 0;
}

There are four functions we need to use to establish a connection to a client.

  1. socket() creates a new server socket and returns a file descriptor that we can use to refer to the socket.
  2. bind() connects the server socket to a port that the server will listen on. This server will listen for connections on port 8888.
  3. listen() changes the socket to passive mode to prepare it to receive incoming connections from clients.
  4. accept() puts the server socket in a waiting state to wait for a client to connect to the server. When a client connects accept() will return a file descriptor for a new socket that we can use to communicate with the client.

Handling a request

The serveRequest() function handles a single request from a client.

void serveRequest(int fd) {
  // Read the request
  char buffer[1024];
  int bytesRead = read(fd,buffer,1023);
  buffer[bytesRead] = '\0';

  char method[16];
  char url[128];
  sscanf(buffer,"%s %s",method,url);
  char fileName[128];
  strcpy(fileName,"www");
  strcat(fileName,url);
  int filed = open(fileName,O_RDONLY);
  if(filed == -1) {
    // The user has a requested a file that we don't have.
    // Send them back the canned 404 error response.
    int f404 = open("404Response.txt",O_RDONLY);
    int readSize = read(f404,buffer,1023);
    close(f404);
    write(fd,buffer,readSize);
  } else {
    const char* responseStatus = "HTTP/1.1 200 OK\n";
    const char* responseOther = "Connection: close\nContent-Type: text/html\n";
    // Get the size of the file
    char len[64];
    struct stat st;
    fstat(filed,&st);
    sprintf(len,"Content-Length: %d\n\n",(int) st.st_size);
    // Send the headers
    write(fd,responseStatus,strlen(responseStatus));
    write(fd,responseOther,strlen(responseOther));
    write(fd,len,strlen(len));
    // Send the file
    while(bytesRead = read(filed,buffer,1023)) {
      write(fd,buffer,bytesRead);
    }
    close(filed);
  }
  close(fd);
}

The only line of the request that we need to read is the first line. This line contains the method and the URL for the file being requested.

When the request comes in we try to open the requested file. If we can't open it we return an HTTP 404 'Not Found' error response. Since this response will always be the same, we can prepare a text file ahead of time that stores the response and then just send the client the contents of this file. Here is the response we will send:

HTTP/1.1 404 Not Found
Connection: close
Content-Type: text/html
Content-Length: 124

<!DOCTYPE html>
<head>
    <title>File not found</title>
</head>
<body>
    <p>The requested file was not found.</p>
</body>

If we are able to open the requested file we construct the appropriate response headers and send them back to the client followed by the contents of the file. The logic for sending the file contents repeatedly reads chunks of data from the file and writes them out to the client until all of the data in the file has been sent. When we have read everything possible from the file read() will return a byte count of 0, which will cause us to exit from the read/write loop.

After responding to the client's request we go ahead and close the socket connection to the client.