Web server implementation|Http server based on blocking queue thread pool|Thread control|Http protocol

Multi-threaded web server based on blocking queue producer consumer model thread pool

Code address: WebServer_GitHub_Addr

README

Summary

This experiment implements a multi-threaded Web server based on blocking queue thread pool through C++ language. The server supports sending messages through the http protocol to grab specific resources on the server across hosts. At the same time, the background of the web server uses C ++ language to call pthread.h through the native system thread, and realizes a thread pool based on blocking queue, the thread pool Support manually setting the number of threads in the thread pool. At the same time, the thread pool maintains the task queue. Every time the web server gets a request, the background server will load the specific task corresponding to the specific request into the thread pool and wait for the thread in the thread pool to call. Since in this project, each remote fetch is a short link, so theoretically, the web server can receive countless requests.

According to the requirements of the experiment, this web server can accept and parse HTTP requests, then read the files requested by HTTP from the file system of the server, and send correct response messages to the client according to whether the files exist.

In the server terminal, the server can print log information according to different levels, and print message information when receiving a request.

Execution effect

Define the ./wwwroot directory under the code as the root directory of the server resources, and define ./wwwroot/index.html as the server home page.

  • When the client requests that the resource exists, the server will return the corresponding resource.
  • When the client request path is the / root directory, the server returns the homepage by default.
  • When the client application path does not exist, the server returns ./wwwroot/error/404.html as the returned resource.

Server power on

Browser remote connection

The server is a remote server of Tencent Cloud, and the browser is a local browser. This is a cross-host test across a LAN.

The client requests resources that do not exist

When the client sends a request, the message obtained by the background server

Multiple clients initiate requests

The server theoretically supports an infinite number of short links to request

Environment preparation

System: Linux version 3.10.0-1160.11.1.el7.x86_64

Compiler version: gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)

Resource file/source file preparation directory structure

  • The MyThreadPool directory maintains the handwritten thread pool source code
  • The ./wwwroot/ directory maintains server resources, where ./wwwroot/ is the root directory accessed by the client
  • Other files are HttpServer source code

RUN

Service default port number: 8080

tips: If the binary program HttpServer does not have executable permission during operation chmod 755 HttpServer

Generate executable:make clean;make

Start the server:./HttpServer 8080

Input in the browser:ip:port to visit the server home page

Or enter:ip:port/index.html You can also visit the server home page


Enter a path that does not exist: 43:xxx:xxx:xxx:8080/a.txt

Detailed Code

Web server implementation architecture

The first layer of encapsulation: encapsulating the basic operations of the socket Sock.hpp

/* The specific interface implementation can be found in the source code */
/* Sock. hpp */
class Sock
{<!-- -->
private:
    const static int gbacklog = 20;
public:
    Sock() {<!-- -->}
    ~Sock() {<!-- -->}
  /* The internal calls of the following interfaces are the underlying calls of the socket */
    int Socket(); // create and get socket -- return value: listening socket
    void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0"); // bind
    void Listen(int sock); // set listening status
    int Accept(int listensock, std::string *ip, uint16_t *port); // accept connection -- return value: service socket
    bool Connect(int sock, const std::string & amp; server_ip, const uint16_t & amp; server_port);
};

Second layer encapsulation: encapsulation of Http server HttpServer.hpp

  • By maintaining the Task class, the actions required by the thread can be encapsulated. Through the operator()() overload of the Task class, the thread can directly execute the internally bound method.
  • By maintaining the HttpServer class, after initializing and executing HttpServer::Start(), the thread pool is started, and after the Accept is successful, the corresponding task is loaded into the thread pool. Wait for a call from a thread in the thread pool.
#ifndef __Yufc_HttpServer
#define __Yufc_HttpServer

#include "Sock.hpp"
#include <functional>
#include "MyThreadPool/threadPool.hpp"
using func_t = std::function<void(int)>;
/* task class */
struct Task
{<!-- -->
public:
    func_t func__;
    int sock__;
public:
    Task() {<!-- -->}
    Task(func_t func, int sock) : func__(func), sock__(sock) {<!-- -->}
    void operator()()
    {<!-- -->
        func__(sock__);
    }
};
/* http server class */
class HttpServer
{<!-- -->
private:
    int __listen_sock;
    uint16_t __port;
    Sock __sock;
    func_t __func;
    yufc_thread_pool::ThreadPool<Task> *__thread_pool = yufc_thread_pool::ThreadPool<Task>::getThreadPool();
public:
    HttpServer(const uint16_t & port, func_t func) : __port(port), __func(func);
    ~HttpServer();
    void Start();
};
#endif

Http server startup HttpServer.cc

After executing HttpServer.cc, main() creates a server object pointer, and calls Start() to start the server.

void HandlerHttpRequest(int sockfd);The function maintains the tasks to be performed by each thread, including the following.

  • read Http request
  • Parse Http message
  • create an http response
  • send response to client
/* Generally http must have its own web root directory */
#define ROOT "./wwwroot"
/* If the client only requests one /, it will generally return to the default home page */
#define HOME_PAGE "index.html"
void HandlerHttpRequest(int sockfd); // See the source code for specific implementation
int main(int argc, char **argv)
{<!-- -->
    if (argc != 2)
    {<!-- -->
        Usage(argv[0]);
        exit(0);
    }
    std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));
    httpserver->Start();
    return 0;
}

ulity.hpp

Provide Util class, which provides static void cutString(const std::string & amp;s, const std::string & amp;sep, std::vector *out) interface. When parsing the http message, you can cut the incoming string s according to the interval of sep, and put the result in out inside.

Usage.hpp

Provide Usage function as the manual of HttpServer.

Log.hpp

Provide void logMessage(int level, const char *format, ...) function, responsible for printing server log information.

Log levels are:

DEBUG debugging

NORMAL Normal operation

WARNING warning – process continues to run

ERROR non-fatal error – process continues

FATAL fatal error – process terminated

Thread pool implementation architecture

thread.hpp

  • Simple encapsulation of native threads
typedef void*(*func_t_)(void*); // function pointer
class ThreadData
{<!-- -->
public:
    void* __args;
    std::string __name;
};
class Thread
{<!-- -->
private:
    std::string __name; // thread name
    pthread_t __tid; // thread tid
    func_t_ __func; // function to be called by the thread
    ThreadData __tdata; // thread data
public:
    Thread(int num, func_t_ callback, void* args);
  // num-custom thread number callback-task to be executed by the thread args-callback parameter
    ~Thread();
    void start();
    void join();
    std::string name(); // return thread name
};

lockGuard.hpp

  • Encapsulate mutex with RAII lock encapsulation style
//Encapsulate a lock
class Mutex
{<!-- -->
private:
    pthread_mutex_t *__pmtx;
public:
    Mutex(pthread_mutex_t *mtx)
        :__pmtx(mtx){<!-- -->}
    ~Mutex()
    {<!-- -->}
public:
    void lock() // lock
    {<!-- -->
        pthread_mutex_lock(__pmtx);
    }
    void unlock() // unlock
    {<!-- -->
        pthread_mutex_unlock(__pmtx);
    }
};
class lockGuard
{<!-- -->
public:
    lockGuard(pthread_mutex_t *mtx)
        :__mtx(mtx)
    {<!-- -->
        __mtx. lock();
    }
    ~lockGuard()
    {<!-- -->
        __mtx. unlock();
    }
private:
    Mutex __mtx;
};

threadPool.hpp

Overall structure of thread pool

#define _DEBUG_MODE_ false
const int g_thread_num = 3; // default 3 threads
/* The specific implementation can be seen in the source code */
namespace yufc_thread_pool
{<!-- -->
    template <class T>
    class ThreadPool
    {<!-- -->
    private:
        std::vector<Thread *> __threads; // threads
        int __num; // number of threads
        std::queue<T> __task_queue; // task queue
        pthread_mutex_t __lock; // mutex
        pthread_cond_t __cond; // condition variable
        static ThreadPool<T> *thread_ptr; // singleton mode
        static pthread_mutex_t __mutexForPool; // protect getThreadPool on mutex
    private:
        //The structure is private -- let the thread pool become a singleton mode
        ThreadPool(int thread_num = g_thread_num);
        ThreadPool(const ThreadPool<T> & amp; other) = delete;
        const ThreadPool<T> & amp; operator=(const ThreadPool<T> & amp; other) = delete;
    public:
        ~ThreadPool();
    public:
        static ThreadPool<T>* getThreadPool(int num = g_thread_num); // Lazy mode--get thread pool
        void run(); // start thread pool
        void pushTask(const T & amp;task); // Add tasks to the thread pool
        static void *routine(void *args); // what the thread has to do
    public:
        // A batch is required, and the interface for external members to access internal properties is provided to the static routine, otherwise the routine cannot be locked
        // The following interfaces are not locked, because we believe that when these functions are called, they are called in a safe context
        //Because these functions have been locked before calling, after calling, lockGuard will automatically unlock
        pthread_mutex_t *getMutex();
        void waitCond(); // wait for the condition variable to be ready
        bool isEmpty(); // Determine whether the task queue is empty
        T getTask(); // get a task
    };
    template <typename T>
    ThreadPool<T> *ThreadPool<T>::thread_ptr = nullptr;
    template <typename T>
    pthread_mutex_t ThreadPool<T>::__mutexForPool = PTHREAD_MUTEX_INITIALIZER;
    //static/global can be initialized like this, this lock is used to protect getThreadPool
}

Some specific details of the implementation

Http message analysis

Because all we receive are http requests, we can obtain client information by parsing the http request message.

The following is a complete http message

GET /index.html HTTP/1.1
Host: 43.xxx.xxx.xxx:8080
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml + xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3; q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

Because we need to extract the resources that the customer needs to access, we need to extract the resource path entered by the customer.

The second field in the first line of the http message indicates the path where the client needs to extract resources.

At the same time, the interval between each line of the http message is a special string \r\
.

Therefore, in the HandlerHttpRequest function in HttpServer.cc, we first separate each line in the http message, and extract the second string of the first line Come out, you can get the path of the resource.

Thread pool in singleton mode

In this experiment, we hope that a process can only generate one thread pool, so we set the thread pool to singleton mode. This is the singleton pattern in a lazy way.

**Lazy:** Construct the object the first time it is needed.

**Hungry man:**Construct the object before main() is executed.

  • Make the constructor private

  • Get the thread pool by static ThreadPool* getThreadPool(int num = g_thread_num)

    In this interface, if the execution flow is the first execution of the function, the function will construct a thread pool object and return its pointer. If the execution flow is not the first time the function is executed, the function will return this, which is itself.

In addition, in order to ensure thread safety when the thread pool is running, mutex locks are added to multiple operations in the thread pool to protect the operations.

Experimental results analysis and thinking

Improve section:

  • Implement multiple execution streams and support multiple clients to connect.
  • Encapsulate the thread pool so that the server can respond to http requests in parallel

Deficiencies:

  • The message returned to the client is a static web page, and the function of supporting mutual interaction between multiple clients has not been implemented yet
  • The response message is relatively rough and can be further beautified.
  • The server can be further optimized, and the server process can be set as a daemon process, allowing it to execute and provide services for a long time.
  • No simulated packet loss