How to Write a Simple HTTP Server in C++

Click the blue text
How to Write a Simple HTTP Server in C++
Follow us

Source from the Internet, please delete if infringing

This article will not involve many complex concepts and does not write difficult-to-understand template functions. The code is simple and readable. This article is dedicated to everyone who wants to write an HTTP server in C++! Experts and big shots can skip this!

Main Text

How to write a simple HTTP server? It’s very simple, you just need to return the three basic things.

  • Status Code

  • File Length

  • File Type

Status Code such as 200 (file found), 404 (file not found), I implemented these two status codes quite simply.

File Length For example, if the client requests the index.html page, how does the browser know when the received file ends? It relies on the file length.

File Type The type for HTML is HTML format, for CSS it is CSS format, images have their formats, and ZIP has its ZIP format corresponding to the file type table (https://tool.oschina.net/commons).

Return these three items to the client along with the requested file (if the requested file exists).

HTTP Workflow

This section briefly introduces the general workflow of HTTP. The client accesses your website via the URL (remember that the client actively requests the connection, while the server is passively connected). In essence, it is accessed through IP + port, but the default port for HTTP is 80. For example, if you do not have a domain name or cloud server, how can you test locally? Just enter ip:port in the browser, such as 127.0.0.1:9996, and press Enter to access your HTTP server locally. Of course, HTTP should give the client (requester) a homepage. When the client does not specify a webpage and simply types the domain name or 127.0.0.1:9996, it should provide a default homepage, which is also something we need to implement. When the client writes a request file, we check whether it exists. If it exists, we return status code 200 and the content of the requested file. If it does not exist, we return 404 directly. How do we determine that? Let’s take the simplest GET as an example; the others are similar, and interested students can research the others themselves. We use regular expressions to parse whether the request sent by the client is GET or POST or another type of request, and parse the requested file. We then use the state machine concept to check if the file exists; if it exists, we determine the file type. This is the general flow.

Http.h

#pragma once
#include <string>
#include <unordered_map>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

class TcpClient;
class Http {
    // Root directory of the file
    const std::string path_ = "./www/dxgzg_src";
    std::string filePath_; // Absolute path of the requested file
    std::string fileType_; // Type of the requested file
    std::string header_; // HTTP header response
    int fileSize_;
    int fileFd_;
    struct stat fileStat_;
    // If it's a POST request, save the message to a local file
    bool isPostMode_ = false;

public:
    Http() = default;
    void addHeader(const std::string& head);
    void Header(bool flag);
    // Add header information, only call this function when successful,
    // and return data from the file
    void processHead();
    // Add the requested file's path
    void addFilePath(const std::string& requestFile);
    // Get the file type
    void analyseFileType(const std::string& requestFile);

    bool analyseFile(const std::string& request);
    void SendFile(int clientFd, bool isRequestOk);
    bool fileIsExist();
    // User-defined callback function must handle exceptions correctly and close the socket
    void ReadCallback(TcpClient* t);
};

Http.cpp

#include "Http.h"
#include "TcpClient.h"
#include "Logger.h"

#include <regex>
#include <sys/socket.h>
#include <sys/sendfile.h>
#include <fcntl.h>
#include <unordered_map> // For test
#include <iostream>
using namespace std;

void Http::addHeader(const string& head) {
    if (!head.empty()) {
        header_ += head;
        header_ += "\r\n"; // Console.WriteLine("I'm here head!= null" + header_);
    } else {
        header_ += "\r\n"; // Console.WriteLine("I'm here head == null" + header_);
    }
}

void Http::Header(bool flag) {
    // Determine the header to send; true means 200, false means 404
    if (flag == true) {
        header_ = "HTTP/1.1 200 OK\r\n";
    } else {
        header_ = "HTTP/1.1 404 NOTFOUND\r\nContent-Length:0\r\n\r\n";
    }
}

void Http::processHead() {
    string ContentType = "Content-Type:";
    if (fileType_ == "html") {
        ContentType += "text/html";
    } else if (fileType_ == "js") {
        ContentType += "application/x-javascript";
    } else if (fileType_ == "css") {
        ContentType += "text/css";
    } else if (fileType_ == "jpg" || fileType_ == "png") {
        ContentType += "image/" + fileType_;
    } else if (fileType_ == "zip" || fileType_ == "tar") {
        ContentType += "application/" + fileType_;
    }
    addHeader(ContentType);
    // To be improved, need to open the file; filePath_ is the path of the requested file
    fileSize_ = fileStat_.st_size;
    string ContentLength = "Content-Length:" + to_string(fileSize_);
    addHeader(ContentLength);
    // Finally add an end
    addHeader(""); // Console.WriteLine("process fileContent_:");
}

void Http::addFilePath(const string& requestFile) {
    filePath_ += requestFile;
}

void Http::analyseFileType(const string& requestFile) {
    for (int i = 0; i < requestFile.size(); ++i) {
        if (requestFile[i] == '.') {
            // Get the ending of the requested file
            fileType_ = requestFile.substr(i + 1);
        }
    }
}

bool Http::fileIsExist() {
    fileFd_ = ::open(filePath_.c_str(), O_CLOEXEC | O_RDWR);
    if (fileFd_ < 0) {
        // File not found
        return false;
    }
    return true;
}

bool Http::analyseFile(const string& request) {
    // Call the header
    // In [], ^ means starting with something, what's inside [] means not
    string pattern = "^([A-Z]+) ([A-Za-z./1-9-]*)";
    regex reg(pattern);
    smatch mas;
    regex_search(request, mas, reg);
    // Index 0 represents the overall match
    if (mas.size() < 3) {
        LOG_INFO("Not a normal request");
        // Not anything directly return false
        return false;
    }
    string requestMode = mas[1];
    if (requestMode == "POST") {
        isPostMode_ = true;
        cout << "POST request!!!!!!!" << endl;
    }
    // Specific requested file
    string requestFile = mas[2];
    // If it's /, give default value
    bool flag;
    if (requestFile == "/") {
        filePath_.clear(); // Clear first
        filePath_ = path_;
        filePath_ += "/run.html";
        // File type must also be added
        fileType_ = "html";
    } else {
        filePath_.clear(); // Clear first
        filePath_ = path_;
        addFilePath(requestFile);
        // Use open function
    }
    flag = fileIsExist();
    // If the file is not found
    if (!flag) {
        LOG_INFO("File not found for the client");
        cout << filePath_ << endl;
        return false;
    }
    ::fstat(fileFd_, &fileStat_);
    // If the file does not exist, there is no need to parse type
    analyseFileType(requestFile);
    return true;
}

void Http::SendFile(int clientFd, bool isRequestOk) {
    long len = 0;
    // The header must exist.
    while (len < header_.size()) {
        len += ::send(clientFd, header_.c_str(), header_.size(), 0);
        cout << "len header" << header_ << endl;
    }
    // After sending the header, send the requested file information. If it's 404, there is none
    if (isRequestOk == true) {
        len = 0;
        int num = 0;
        int tmpLen = 0; // If it doesn't change for several times, add a num
        while (len < fileSize_) {
            // The number of files sent has been written into len
            ::sendfile(clientFd, fileFd_, (off_t*)&len, fileStat_.st_size - len);
            cout << "len sendfile" << "len:" << len << "fileSize" << fileSize_ << endl;
            if (len <= 0) {
                break;
            }
            if (tmpLen == len) {
                ++num;
                if (num > 10) {
                    break;
                }
            }
            tmpLen = len;
        }
    }
}

void Http::ReadCallback(TcpClient* t) {
    cout << "ReadCallback" << endl;
    int sockFd = t->getFd();
    char buff[1024];
    int r = ::recv(sockFd, buff, sizeof(buff), 0);
    if (r == 0) {
        t->CloseCallback();
        return;
    }
    buff[r] = '\0';
    string str = buff;
    cout << str << endl;
    // If the file is not found, respond with 404.
    bool flag = analyseFile(str);
    Header(flag);
    if (!flag) {
        SendFile(sockFd, false);
        // t->CloseCallback();
        return;
    }
    // This modifies the header file, call this first
    processHead();
    // This is the file found and sent
    SendFile(sockFd, true);
    if (isPostMode_) {
        int fd = ::open("./postLog/message.txt", O_RDWR);
        if (fd < 0) {
            LOG_ERROR("File not found");
        } else {
            // Move the file offset to the end
            ::lseek(fd, 0, SEEK_END);
            ::write(fd, str.c_str(), str.size());
            close(fd);
        }
        isPostMode_ = true;
    }
    // Close the file socket
    close(fileFd_);
    // Close the connection after sending, mainly to allow several threads to run faster
    // t->CloseCallback();
}

Without considering high concurrency, design a synchronous blocking epoll. After understanding the three essentials of HTTP, you can already write a server. My underlying socket uses a self-encapsulated network library with the Reactor model; the code files are quite numerous, so I did not include them. As long as you implement the status code, file type (that large section of if statements), and file length, you can build a simple HTTP server. You can use sendfile zero-copy to send files.

Additionally, the HTTP protocol ends with \r\n. There is also a \r\n at the end, such as 404, HTTP/1.1 404 NOTFOUND\r\nContent-Length:0\r\n\r\n, followed by another \r\n to end a segment and another \r\n.


If you are over 18 years old and find learning [C Language] too difficult? Want to try another programming language? Then I recommend you learn Python. There is a limited-time free course worth 499 yuan for Python beginners, limited to 10 spots!



▲ Scan the QR code - Get it for free



How to Write a Simple HTTP Server in C++
Click to read the original text to learn more

Leave a Comment