Skip to content

Fully functional web server using C/C++ from scratch without third party library

Notifications You must be signed in to change notification settings

Dungyichao/http_server

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Simple Http Web Server in Linux using C/C++ from Scratch

In this tutorial, we will demonstrate how to build a http web server from scratch without using any third party library. Please go through a good reading resource first: Medium link , or you can read from the pdf version provided in this tutorial ( pdf link ). The tutorial in the Medium post only gives you a abstract concept and simple implementation but the author doesn't finish it.

My tutorial will show you how to make a fully functional web server in no more than 200 lines of code. I also provide Node.js javascript code of building a simpler web server in the Summary section.

  1. Basic Knowledge
  2. Overview
  3. Implement the Code
  4. Summary (with javascript)
  5. Video Streaming Protocols
  6. Advance Topic
  7. Make HTML More Organized

1. Basic Knowledge

In the internet world, there is always a server who can serve multiple clients. For example, Google, Netflix, Facebook... and so on are servers. People like us are client and we can use web browser (Chrome, Edge, Opera, Firefox....) to communicate with servers.

You can make a web server at your home and use your own laptop to access the server through LAN which stands for Local Area Network (having a Wi-Fi router can create a LAN at your home). However, if your html file includes some resources in WAN (Wide Area Network), then you need to be able to access the internet for displaying your html correctly.

What you need is at least one PC or laptop (acts as server) running in Linux ( Ubuntu or Debian) which should connect to the router. You can use cell phone as a client.

2. Overview

We are going to implement code for a http server on Ubuntu Desktop. Please follow the Visual Studio Code official website to create a project ( link ). Download the web content (Demo website: https://npcasc2020.firebaseapp.com/) which provide to you in this tutorial folder src ( link ). Unzip the content and put all the content into your code project. It will be like the following image. (Notice that we do all the coding and compiling on Ubuntu Desktop)

Copy paste the code provided in this tutorial ( link ) into the helloworld.cpp file. Compile the code and execute it.

The local ip address of my web server is 172.16.216.205, Subnet Mask is 255.255.0.0, the default gateway should be the ip address of your router , in our case is 172.16.216.6. Modify these number to fit in your case. If everything is working properly, now you can type in 172.16.216.205:8080 in the browser on your laptop or cellphone (which should connect to Wi-Fi router at your home). What you see in the browser should be the same as the following animation.

I made this website (hosted on Google Firebase ) for the activity in our company (Nan Ya Plastics Corp. America which HQ in Taiwan) to celebrate 2020 Chinese New Year. The template is from https://startbootstrap.com/themes/agency/

2.1 System Requirement

Role Requirement
Web Server Linux OS such as Ubuntu or Debian.
C\C++ development environment: Visual Studio Code or Geany. (Raspberry pi maybe not a good idea, it can serve the website but it would hang in the middle of transfering large image.)
Client Whatever OS (Windows, IOS, Android, Ubuntu) which is able to access web browser is good. You can use your cell phone as well.

2.2 Process Elements

The following image is basically what we are going to implement in the code. We obmit some initialization part which was mentioned in the Meduim article ( link ), however, you can still find it in our code.

The story is, the server keep listening any message it received, then we need to analyze what the useful information in the message by parsing it. The useful information we care about is the file name (with path) and file extension. The server then open the file according to the path and put the content of the file into a reply-message which we will later send to the client. Before sending the reply-message, we should first tell the client what kind of file content type we are going to send, maybe image file (.jpg, .png, ...) or txt file (.html, .doc, ...) and so on (refer to https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types), then we can send the reply-message (content of file) to the client.

3. Implement the Code

The overall code can be viewed from the following link: https://github.com/Dungyichao/http_server/blob/master/src/helloworld.cpp

After running the code, and then enter the ip address and port number in the web browser, you will see the following animation in your terminal

3.1 Code Structure

We keep looping through the following code in sequence, namely 1 --> 2 --> 3 --> 4 (a) --> 5 --> 1 --> 2 --> 3 --> 4 (d) --> 5..... We only focus on number 3 and number 4 and the reply function as well.

3.2 Parse the Request from the Client

Let's take a look at what the very first request information the client sends to you

At first glance, it contains useless information (maybe not true for Hacker), how about we look at the other request information?

OK ! I think you nail it. The information between GET and HTTP/1.1. That is the file path which the client requires to display the website correctly on it's browser.

The parse function just retrieves the path and file extension (such as .jpg .html .css....) from a bunch of information.

char* parse(char line[], const char symbol[])
{
    char *message;
    char * token = strtok(line, symbol);
    int current = 0;

    while( token != NULL ) {
      
      token = strtok(NULL, " ");
      if(current == 0){
          message = token;
          return message;
      }
      current = current + 1;
   }
   return message;
}

3.3 Classify the Request

In section 2.2 Process Element we mention that we need to tell the client what kind of content we are going to send in. The classification is just a bunch of if else logic detemination according to the file extension from the parsed information (section 3.2). I just list the partial code in the following to give you some concept.

if(strlen(parse_string) <= 1){
           //case that the parse_string = "/"  --> Send index.html file
           //write(new_socket , httpHeader , strlen(httpHeader));
           char path_head[500] = ".";
           strcat(path_head, "/index.html");
           strcat(copy_head, "Content-Type: text/html\r\n\r\n");
           send_message(new_socket, path_head, copy_head);
}
else if ((parse_ext[0] == 'j' && parse_ext[1] == 'p' && parse_ext[2] == 'g') || 
(parse_ext[0] == 'J' && parse_ext[1] == 'P' && parse_ext[2] == 'G'))
{
           //send image to client
           char path_head[500] = ".";
           strcat(path_head, parse_string);
           strcat(copy_head, "Content-Type: image/jpeg\r\n\r\n");
           send_message(new_socket, path_head, copy_head);
}
else if (parse_ext[strlen(parse_ext)-2] == 'j' && parse_ext[strlen(parse_ext)-1] == 's')
{
           //javascript
           char path_head[500] = ".";
           strcat(path_head, parse_string);
           strcat(copy_head, "Content-Type: text/javascript\r\n\r\n");
           send_message(new_socket, path_head, copy_head);
}
else if (parse_ext[strlen(parse_ext)-3] == 'c' && parse_ext[strlen(parse_ext)-2] == 's' 
&& parse_ext[strlen(parse_ext)-1] == 's')
{
           //css
           char path_head[500] = ".";
           strcat(path_head, parse_string);
           strcat(copy_head, "Content-Type: text/css\r\n\r\n");
           send_message(new_socket, path_head, copy_head);
}
else if (parse_ext[0] == 'i' && parse_ext[1] == 'c' && parse_ext[2] == 'o')
{
           //https://www.cisco.com/c/en/us/support/docs/security/web-security-appliance/117995-qna-wsa-00.html
           char path_head[500] = ".";
           strcat(path_head, "/img/favicon.png");
           strcat(copy_head, "Content-Type: image/vnd.microsoft.icon\r\n\r\n");
           send_message(new_socket, path_head, copy_head);
}

I know you are still wondering the very first request information I mentioned in section 3.2 which contains / such a useless information. Actually, it does give us a hint to send it our web page, namely index.html. The client will receive the html file looks like the following

The client's web browser will read line by line and do whatever the html file tells it. When it reads until line 14 (in above image), the client will send request to the server to ask for vendor/fontawesome-free/css/all.min.css which is a css file. Server than parse the request, and then classify the request.

There are multiple file extension we need to take good care of, the following link shows you a list of file extension: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types . We need to first notify the client what kind of content we are going to send so that the client can receive the content accordingly. The notification message looks like the following:

HTTP/1.1 200 Ok\r\n
Content-Type: text/html\r\n\r\n

You need to replace the text/html with the proper MIME Type according to the file extension.

While writing this tutorial, a special file extension request from the client /favicon.ico, however, I couldn't find out the file in my website at all (I also look into all html, css, js files). It turns out that every browser will automatically request for /favicon.ico which is merely an icon for displaying on the browser tab shown in the following. So what you need is just reply a .ico or .png file to the client.

Here we list out some common file extension and their MIME Type.

File Extension MIME Type
.css text/css
.html text/html
.ico image/vnd.microsoft.icon
.jpg image/jpeg
.js text/javascript
.json application/json
.ttf font/ttf
.txt text/plain
.woff font/woff
.xml text/xml
.mp3 audio/mpeg
.mpeg video/mpeg
.m3u8 application/vnd.apple.mpegurl
.ts video/mp2t

3.4 Reply to the Client

The following function first send notification message to the client and let it knows what kind of content we are going to send (section 3.3). We then open the file using open and retrieve information of the file (not the content) using fstat and store in stat object. Lastly, we read the file content and send the content using sendfile. Because some file might be too large to send in one message, thus, we need to send the content pices by pices (size = block_size).

int send_message(int fd, char image_path[], char head[]){

    struct stat stat_buf;  /* hold information about input file */

    write(fd, head, strlen(head));

    int fdimg = open(image_path, O_RDONLY);
     
    fstat(fdimg, &stat_buf);
    int img_total_size = stat_buf.st_size;
    int block_size = stat_buf.st_blksize;

    int sent_size;

    while(img_total_size > 0){
        if(img_total_size < block_size){
            sent_size = sendfile(fd, fdimg, NULL, img_total_size);            
        }
        else{
            sent_size = sendfile(fd, fdimg, NULL, block_size);
        }       
        printf("%d \n", sent_size);
        img_total_size = img_total_size - sent_size;
    }
    close(fdimg);
}

You might not familiar with the above command, so the following link may help you.

Term Web Link
stat http://man7.org/linux/man-pages/man2/stat.2.html
sendfile http://man7.org/linux/man-pages/man2/sendfile.2.html
http://www.tldp.org/LDP/LG/issue91/tranter.html
fstat https://linux.die.net/man/2/fstat
open http://man7.org/linux/man-pages/man2/open.2.html

3.5 Create Child Process to Handle Clients

In a real world server, we are not going to reply all connected client with only one process. Our server program will not have good performance when multiple clients connecting to us at once. So we will create child process whenever new client connected. Please read the tutorial link (PDF) - Enhancements to the server code part.

We modifiy a little bit code from the tutorial link. Please see the following. We only shows the while loop part.

while (1)
 {
   newsockfd = accept(server_fd,
               (struct sockaddr *) &cli_addr, &clilen);
   if (newsockfd < 0)
     error("ERROR on accept");
   pid = fork();
   if (pid < 0){
     error("ERROR on fork");
     exit(EXIT_FAILURE);  //We add this part
    }
   if (pid == 0)
   {
     //close(server_fd);    //We omint this part because it would cause error
     ......................................
     This part is parsing the message from client, read path file, write file back to client
     ...
     ....
     .....
     ......................................
     close(new_socket);
     exit(0);
   }
   else{
            printf(">>>>>>>>>>Parent create child with pid: %d <<<<<<<<<", pid);
            close(new_socket);
   }
 } /* end o

4. Summary

This is a simple, experimental but functional Ubuntu web server. Some error protection method not included. Any advise are welcome. I also want to implment a webcam server sending real-time streaming.

Is there a simple way? Yes, you can use Node.js which is a JavaScript runtime environment where you can build a simple web server in about 60 lines of code. (Youtube Node.js Crash Course: https://www.youtube.com/watch?v=fBNz5xF-Kx4)

const http = require('http');
const path = require('path');
const fs = require('fs');

const server = http.createServer((req, res) => {
    let filePath = path.join(__dirname, req.url === '/' ? 'index.html' : req.url);
    let file_extname = path.extname(filePath);
    let contentType = 'text/html';

    switch(file_extname){
        case '.js':
            contentType = 'text/javascript';
            break;
        case '.css':
            contentType = 'text/css';
            break;
        case '.json':
            contentType = 'application/json';
            break;
        case '.png':
            contentType = 'image/png';
            break;
        case '.JPG':
            contentType = 'image/jpg';
            break;
        case '.ico':
            filePath = path.join(__dirname,'favicon.png');
            contentType = 'image/png';
            break;
        case '.ttf':
            contentType = 'font/ttf';
            break;
        case '.woff':
            contentType = 'font/woff';
            break;
        case '.woff2':
            contentType = 'font/woff2';
            break;
    }
    // Read File
    fs.readFile(filePath, (err, content) => {
        if(err){
            if(err.code == 'ENOENT'){
                console.log('Page not found');
            }
            else{
                res.writeHead(500);
                res.end('Server Error: ${err.code}');
            }
        }
        else{
            res.writeHead(200, {'Content-Type': contentType});
            res.end(content);    
        }
    });
});

const PORT = process.env.PORT || 8080;

server.listen(PORT, () => console.log(`Server is running and port is ${PORT}`));

5. Video Streaming Protocols

The following are some most common streaming protocols and most widely used in current time. However, we will only focus on the HLS.

Protocols Detail
Real-Time Messaging Protocol (RTMP) Today it’s mostly used for ingesting live streams. In plain terms, when you set up your encoder to send your video feed to your video hosting platform, that video will reach the CDN via the RTMP protocol. However, that content eventually reaches the end viewer in another protocol – usually HLS streaming protocol.
Real-Time Streaming Protocol (RTSP) It is a good choice for streaming use cases such as IP camera feeds (e.g. security cameras), IoT devices (e.g. laptop-controlled drone), and mobile SDKs.
HTTP Live Streaming (HLS) HLS is the most widely-used protocol today, and it’s robust. Currently, the only downside of HLS is that latency can be relatively high.

5.1 HTTP Live Streaming (HLS)

Reference link:
https://www.cloudflare.com/learning/video/what-is-http-live-streaming/
https://www.dacast.com/blog/hls-streaming-protocol/

Streaming is a way of delivering visual and audio media to users over the Internet. It works by continually sending the media file to a user's device a little bit at a time instead of all at once. With streaming over HTTP, the standard request-response pattern does not apply. The connection between client and server remains open for the duration of the stream, and the server pushes video data to the client so that the client does not have to request every segment of video data. HLS use TCP (more reliable) rather than UDP (more faster) as trasport protocols.

First, the HLS protocol chops up MP4 video content into short (10-second) chunks with the .ts file extension (MPEG2 Transport Stream). Next, an HTTP server stores those streams, and HTTP delivers these short clips to viewers on their devices. Some software server creates an M3U8 playlist file (e.g. manifest file) that serves as an index for the video chunks. Some .m3u8 and .ts information can be found in the following link link1(PDF), link2

HLS is compatible with a wide range of devices and firewalls. However, latency (or lag time) tends to be in the 15-30 second range with HLS live streams.

5.1.1 HLS Streaming Project

There are two way to do this project. Please go to the following link1(PDF) and follow the instructions.

Rapivid can output segment video files in local folder, however, in option 1, if not using Nginx, when stdout pipe into GStreamer to generate streaming files, it’s .ts file keep growing which never split into segment. It only generate .m3u8 playlist file when you stop the process. It requires to go through Nginx with rtmp sink to generate proper segment .ts files with playlist .m3u8. So we change to the option 2, which use ffmpeg to generate proper segment .ts files with playlist .m3u8. Finally, we can use our handmade http server to send out the .m3u8 and .ts files from local folder to the client browser for streaming. We shows the steps for option 2 below.

First we create the bash file

$ sudo nano /usr/local/bin/ffmpeg-rpi-stream

Place the following into /usr/local/bin/ffmpeg-rpi-stream. Make it executable. Make sure your http server can access the video location (in the base option). Best way is to put handmade http server, index.html, and these .m3u8, .ts file into same location.

#!/bin/bash
# /usr/local/bin/ffmpeg-rpi-stream
base="/home/pi/Desktop/http/video"     
cd $base

raspivid -ih -t 0 -b 2097152 -w 1280 -h 720 -fps 30 -n -o - | \
ffmpeg -y \
   -use_wallclock_as_timestamps 1 \   #fix error: ffmpeg timestamps are unset in a packet for stream0. 
    -i - \
    -c:v copy \
    -map 0 \
    -f ssegment \
    -segment_time 1 \
    -segment_wrap 4 \
    -segment_format mpegts \
    -segment_list "$base/s.m3u8" \
    -segment_list_size 1 \
    -segment_list_flags live \
    -segment_list_type m3u8 \
    "$base/s_%08d.ts"

The following table shows how your configuration would affect the streaming latency time in our project. (This table is based on our Raspberry Pi, camera and LAN speed)

Segment_time Segment_wrap Segment_List_Size Latency
1 2 ~ 20 1 3s ~ 5s
1 4 2 5s
1 20 2 6s
1 20 5 ~ 10 9s
2 4 1 3s ~ 4s
4 4 1 7s ~ 8s
4 4 2 11s

Segment_time means how long the .ts file (video length). Segment_wrap means how many .ts file will be kept. Segment_List_Size means how many .ts records will be kept in the .m3u8 which will affact client playback. Segment_wrap should be larger or equal to Segment_List_Size.

Make it executable:

$sudo chmod +x /usr/local/bin/ffmpeg-rpi-stream

We will create a systemd unit file to manage the gstreamer process. We'll configure it to automatically restart the stream if it goes down.

$sudo nano /lib/systemd/system/ffmpeg-rpi-stream.service

Place the following into /lib/systemd/system/ffmpeg-rpi-stream.service

[Unit]
Description=RPI FFMpeg RTMP Source

[Service]
Type=simple
ExecStart=/usr/local/bin/ffmpeg-rpi-stream
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

Enable and start the service:

$sudo systemctl daemon-reload
$sudo systemctl enable ffmpeg-rpi-stream
$sudo systemctl start ffmpeg-rpi-stream

Now, you can use browser to watch the stream of the raspberry pi camera. If want to stop streaming camera service

$ ps aux

Find the PID (raspivid task)

$sudo kill -9 <pid>

5.2 MJPEG

HLS is not a good idea for real-time streaming robot project. The latency is not acceptable if you try to remote control your robot. Therefore, we need someting more real time. MJPEG should be able to meet our requirement. MJPEG is mearly a series of JPEG files. A great open source project using C language called streamEye(link, backup), and an online tutorial using Python language (link, PDF) are a good starting point. The following project is based on these two online source.

5.2.1 MJPEG Streaming Project

Let's take a look at the system structure of StreamEye. Python will be used to capture JPEG image file, and then output to StreamEye.o for further processing, and then act as a http server waiting client to connect and reply with a series of JPEG data.

I modify the Python, and C code from StreamEye and make it more simpler (less user option, less function) so that we can have a better understanding of the concept.

Please go to Project folder (link) and download rasp_test.py and test001.c and put in whatever your project folder in rasperrypi. Use Geany to open test001.c, click on Set Build Command (reference) and input like the following

Click on build. After successfully building the c code, open command prompt, cd to your project folder, enter the following command
$ python rasp_test.py | ./test001

Notice that, in test001.c, we've defined the port to 8084, so you can now open web broswer on other PC (in the same network as raspberrypi) and enter address. In my case, my raspberrypi IP is 172.16.216.206, so I put 172.16.216.206:8084 in my web browser to see the stream.

In above GIF, the raspberrypi is connected to WIFI while my PC is wired connect to network hub.

If you have other web server servering an index.html which web page contain the MJPEG streaming, you can put the following html tag inside the index.html.

<img src="http://172.16.216.206:8084/stream.mjpg" width="320" height="240">

In my case, I run the web server and streaming server on the same raspberry pi (same ip, but different port). Both program run at the same time. There is no CROS problem because they are in the same domain.

I made a document of my troubleshooting process and answer in this (link) talking about SIGPIPE, EPIPE, errno, nonblocking Socket, pthread, realloc(), pointer arithmetic, MJPEG parsing.

6. Advance Topic

By using our hand made http server, we can implement some interesting project.

6.1 Web Remote Control Robot

https://github.com/Dungyichao/Web-Remote-Control-Robot

7. Make HTML More Organized

Recently, I made a new Bingo game in the website for our company's Chinese New Year, and the code is getting bigger and larger. I can no longer maintain all the code in just one index.html or one javascript file. The idea is to spread all components into different HTML files, and import these html files into the main index.html. From the following picture, you can see I organize HTML, javascript, images, css files into its folders.

7.1 Implement the Code

You can see the source code in this link (link). The rough idea can be shown in the following code

This is where Javascript function will load child HTML files into this main index.html.

This is where child HTML files content will be loaded and displayed. Because the browser will read line by line, so the order in the following will be reflected on the website. Actually, those div tag just act like a place holder, the browser will literally insert all the loaded HTML code into these place holder (reference by the id). However, I cannot move the Navigation bar away to separate HTML file. I do not know the reason and cannot find any solution.

This is where the browser will load all Javascript files

This is just a regular html code where I remove from original index.html and paste the content into separate html file. The following is just one of the example. You can find the rest in the zip file

After doing all of this, our web content displayed the same information, but our index.html is more cleaner and more easy to manage.

7.2 Bonus: Bingo Game

This year 2024, I added another Bingo game which allow user to input their bingo, submit to firebase. The firebase administrator will input the called number online. Use can refresh the browser and see each user's name and their number of lines.

Player Input Demo https://github.com/Dungyichao/http_server/assets/25232370/62d29331-899e-49ab-95ad-195f5b63c431

Bingo_user_input.mp4

This is player input page

This is player score

Algorithm to calculate Bingo Game match lines. You can find the code implementation in the src/Web_Content_2.zip js folder Refresh_Bingo_Score.js.

1. create an empty 1-D array which is the same number as player bingo input (m x m)
2. check player's input, if match, insert 1 into array, otherwise, insert 0 into array
3. Check row by row, column by column, two diagonal. If line added up to m, player matched line plus one
4. Continue to check next player