From Scratch: TCP Web-Server in C

From Scratch: TCP Web-Server in C

Serve up a web page the most common way

Goal

For this article, I want to implement a TCP protocol web server in C. Note that I'm no expert, and I'm learning while writing!

Network layers

It is not the purpose of this article to explain networking, but a brief understanding is quite helpful in getting a grasp of the web server. The internet is composed of a stack of protocols, the list below is ordered from top to bottom, the top being closest to the end-user.

  1. Application

  2. Transport

  3. Network

  4. Link

  5. Physical

The top layer is what most users will ever concern themselves with. The application layer is responsible for sending this exact web page to your browser using the HTTP(S) protocol, other protocols include e-mail (SMTP) and file transfer (FTP).

The transport layer ensures that data is transferred reliably between end users (e.g. web server and browser on separate devices). The data from the application layer is broken down into smaller components according to either the TCP or the UDP protocol.

Data from the transport layer is then linked between different networks using the IP protocol in the network layer. Data must reach the desired IP address traveling between numerous routers, and the path of travel must be as efficient as possible.

Subsequently, the link layer translates the information into a physical entity such as waves on Wi-Fi, electricity on Ethernet, light in optic cables or some other entity as specified in the physical layer.

Relevant to the scope of this exercise is the application and transport layer. We will be using the TCP (Transmission Control Protocol) protocol, as it establishes a connection between end devices before sending data in a controlled manner with regards to speed, ordering of data and error correction, features the UDP (User Datagram Protocol) is missing.

Coding it in C

I'm trying to keep the code as simple as possible, therefore the entire code file follows along with the HTML file, ready for copy and paste. The file is sectioned with comments that relate to sections in the article explaining the code.

Build and use as follows:

user@machine:~$ gcc -o server server.c
user@machine:~$ ./server 9000

Code for the server file with .c file extension.

/*======|Libraries|======*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>

/*======|Helpers|======*/
void error(const char *msg) { printf("[-] Error: %s\n", msg); perror(msg); exit(EXIT_FAILURE); }
void success(const char *msg) { printf("[+] Success: %s\n", msg); }
void http_request(const char *msg) { printf("[=] HTTP Request: \n%s\n", msg); }

int main(int argc, char **argv)
{
    /*======|Data Types|======*/
    long bytes_read, bytes_wrote;
    int server_socket, client_socket;
    char buffer_rx[256], buffer_tx[1024];
    struct sockaddr_in address;
    int address_size = sizeof(address);

    /*======|Socket Creation|======*/
    if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        error("Creating server socket");
    else success("Creating server socket");

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(atoi(argv[1]));

    /*======|Socket Binding|======*/
    if (bind(server_socket, (struct sockaddr*)&address, sizeof address) < 0)
        error("Binding server socket");
    else success("Binding server socket");

    /*======|Socket Listening|======*/
    if (listen(server_socket, 10) < 0)
        error("Listening server socket");
    else success("Listening server socket");

    while(1)
    {
        printf("_____Waiting for client connection_____\n");

        /*======|Socket Acceptance|======*/
        if ((client_socket = accept(server_socket, (struct sockaddr*)&address, (socklen_t*)&address_size)) < 0)
            error("Accepting client socket");
        else success("Accepting client socket");

        /*======|Reading Message|======*/
        bytes_read = read(client_socket, buffer_rx, sizeof buffer_rx);
        http_request(buffer_rx);

        /*======|Writing Message|======*/
        FILE *file_handle = fopen("index.html", "rb");
        fread(buffer_tx, 1, sizeof buffer_tx, file_handle);
        fclose(file_handle);

        bytes_wrote = write(client_socket, buffer_tx, strlen(buffer_tx));
        close(client_socket);
    }
    close(server_socket);
    return 0;
}

Code for the content file with .html file extension.

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


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>Hello World</h1>
    <p>This is from a C server</p>
</body>
</html>

Libraries and Helpers

Let's get the formalities out of the way. The libraries include the standard library functions, as well as TCP socket-specific types and functions. For cleanliness, I've made three helper methods for printing program status to the terminal, and exit if errors occur.

The data types used in the program include longs as byte counters when reading and writing messages, ints to serve as handles for the sockets, chars to store some strings, as well as a sockaddr_in structure containing information regarding the server socket. A socket is just a general term used for an end-point in two-way communication.

Socket Creation

The first method called is the socket, which returns an unbound file descriptor (a handle, i.e. something that relates to a specific entity). We pass AF_INET to specify the use of IPv4 addresses, SOCK_STREAM for streaming bytes in TCP style and 0 to use a default protocol. If successful, a non-negative integer is returned.

Subsequently, the sockaddr_in struct is initialized with the IPv4 addresses using AF_INET, INADDR_ANY specifies to not use any specific IP and the port number is provided as a command-line argument (conversion path of string to int to binary).

Socket Binding

The socket handle and sockaddr_in struct are then bound using the bind function, if zero is returned then it has been successful. The server is now set up!

Socket Listening

Next up the listen function marks this socket as passive, meaning it is used to accept incoming connection requests. The number 10 specifies a backlog, in the form of a maximum number of pending connections allowed.

Socket Acceptance

This server is an iterative one, meaning that one client is being handled at a time. The server opens the connection, handles the transaction, closes the connection and listens for the next client on the port.

Again, the server socket handle, as well as its information is passed as arguments to a function named accept, the return value is the newly created socket representing the client connecting to the server. A non-negative integer represents success.

Reading/Writing Message

The Linux system call function read is used to store the HTTP request from the client in a buffer that is printed to the terminal. The HTML file is opened and copied to a new buffer before being written to the client using the system call write.

The read and write functions return the number of bytes read or written, this comes in handy when regulating the stream of data, e.g. when sending a file.

When closing the connection the server loops back to accept the next client in line.

Running it!

By building the application, and running it on port 9000 it can then be accessed from a browser on IP address 127.0.0.1:9000. As seen below an HTTP request is sent from the browser.

HTTP is a communication protocol of requests and responses, by reading the request header it is clear that the client is a Firefox browser and luckily it accepts text/html. If you have taken a look at the HTML file, the top section represents the response, specifying an HTTP OK as well as the format of the incoming data. This should be done more elegantly by separating the response header and the HTML content.

lundc@lundc:~$ gcc -o server server.c
lundc@lundc:~$ ./server 9000
[+] Success: Creating server socket
[+] Success: Binding server socket
[+] Success: Listening server socket
_____Waiting for client connection_____
[+] Success: Accepting client socket
[=] HTTP Request: 
GET / HTTP/1.1
Host: 127.0.0.1:9000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
_____Waiting for client connection_____

Do it yourself!

Hopefully, it is somewhat clear how you could set up your own TCP server from scratch, but I encourage you to test it out, tweaking some values and breaking some stuff!