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.
- Basic Knowledge
- Overview
- 2.1 System Requirement
- 2.2 Process Elements
- Implement the Code
- Summary (with javascript)
- Video Streaming Protocols
- 5.1 HTTP Live Streaming (HLS)
- 5.1.1 HLS Streaming Project
- 5.2 MJPEG Streaming
- 5.2.1 MJPEG Streaming Project
- 5.1 HTTP Live Streaming (HLS)
- Advance Topic
- Make HTML More Organized
- 7.1 Implement the Code
- 7.2 Bonus: Bingo Game
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.
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/
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. |
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.
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
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.
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;
}
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 |
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.
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
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}`));
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. |
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.
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>
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.
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
$ 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="https://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.
By using our hand made http server, we can implement some interesting project.