HTTPServer改进思路1

Nginx源码思考项目改进

架构模式

  • 事件驱动架构(EDA)用于处理大量并发连接和IO操作
    • 优点:高效处理大量并发请求,减少线程切换和阻塞调用
    • 技术实现:直接使用EPOLL,参考Node.js的http服务器
  • 网络通信
    • 协议:HTTP2
      • 本身是改进的HTTP1.1,保持与HTTP的高兼容性(gRPC虽然可以提供更高效的传输效率,但是与项目的主要功能不符,所以暂时不用)
      • HTTP2,在快速加载静态和动态库的内容中适用,可以提高速度
      • 主要特性
        • 多路复用:在同一连接中并行处理多个请求和响应,减少了延迟。
        • 服务器推送:允许服务器未经请求即推送资源,提高页面加载速度。
        • 头部压缩:通过HPACK压缩协议减少了请求和响应头的大小。
      • 尝试使用nghttp2的库来支持HTTP2
    • gRPC:基于HTTP/2的高效RPC协议,适用于服务间通信。
    • WebSocket:用于实时通信的全双工协议。
  • 并发处理
    • 事件驱动模型
      • Reactor模式:主线程负责监听事件并分发给工作线程处理。适用于高并发、低延迟场景。(大量短请求)
      • Proactor模式:主线程负责完成事件处理,工作线程处理业务逻辑。适用于需要高并发和高吞吐量的场景。
    • 线程池
      • 使用线程池处理并发请求,避免频繁创建和销毁线程的开销。
      • 配置合理的线程池大小,避免过多或过少的线程影响性能。
    • 协程
      • 使用协程实现轻量级并发处理,提高系统资源利用率。协程相比于线程,开销更小,切换更快。
  • 资源管理
    • 内存管理
      • 使用高效的内存分配器,如tcmalloc、jemalloc,减少内存碎片和分配开销。
      • 避免内存泄漏,使用智能指针(如C++的std::shared_ptrstd::unique_ptr)管理内存。
    • 文件描述符管理
      • 合理分配和管理文件描述符,避免资源泄漏。
    • 限流
      • 实现限流策略,防止高并发请求压垮系统。可以使用漏桶算法或令牌桶算法。
  • 缓存
    • 本地缓存
      • 在服务器内存中缓存频繁访问的数据,减少数据库访问次数。
    • 分布式缓存
      • 对于需要高一致性的场景,使用一致性哈希算法管理缓存节点

HTTP请求模块阅读后思考

主要目标是在云服务器上设计一个高性能HTTP服务器,从而实现HTTP请求的高效处理。

改进方向

  • 非阻塞IO和事件驱动模型
    • 使用EPOLL以及reactor去完成高效的I多路复用机制
    • 尝试改进方向
      • EPOLL来处理并发连接
      • 使用非阻塞的IO操作,避免单个连接阻塞整个服务器
    • 内存池管理
      • 使用内存池进行内存管理,减少频繁内存分配和释放内存操作,提高内存利用率,减少内存碎片
      • 改进方向
        • 实现简单的内存管理器,预分配大内存,然后从中分配小内存
        • 请求结束后统一释放内存
    • 请求处理和解析
      • 优化存储的数据结构
    • 反向代理和负载均衡
      • 将请求转发到后端服务器,同时将响应返回给客户端。
      • 改进方向
        • 使用功能HTTP客户端库或者服务器完成HTTP请求转发
        • 尝试使用简单的轮询、加权轮询或者哈希算法实现负载均衡
    • 压缩和缓存
      • 对HTTP响应进行gzip压缩,减少传输数据量。实现缓存功能,缓存后端服务器的响应,减少后端服务器负载,提高响应速度。
      • 改进方向
        • 使用zlib库进行gzip压缩。
        • 使用哈希表或LRU缓存算法实现缓存。
    • 限流和访问控制
      • 限制客户端的请求速率,防止恶意请求或流量攻击。通过IP地址或其他条件进行访问控制,限制特定客户端的访问权限
      • 改进方向
        • 使用令牌桶或漏桶算法实现限流。
        • 使用IP白名单或黑名单进行访问控制

修改服务器架构

EPOLL事件驱动在HTTP服务器架构下,HTTP请求和HTTP响应分别对应EPOLL的什么状态。

  • EPOLLIN (Readable): 表示有新的数据可读。对于一个HTTP服务器,当一个新的HTTP请求到达时,socket变为可读状态,触发EPOLLIN事件。你需要监听这个事件来读取客户端发送的HTTP请求数据。

  • EPOLLOUT (Writable): 表示可以向socket写入数据。当你需要向客户端发送HTTP响应时,通常会监听这个事件。

  • EPOLLET (Edge Triggered): EPOLL的边缘触发模式。这个模式下,事件只在状态变化时通知,所以需要非阻塞I/O和循环读取/写入数据,直到数据全部处理完毕。对于高性能服务器,这种模式更高效。

事件类型和socket之间的关系分析

  • ADD (EPOLL_CTL_ADD): 当你第一次监听一个socket时,使用EPOLL_CTL_ADD事件类型,将socket添加到EPOLL实例中,并指定你要监听的事件类型(通常是EPOLLIN)。

  • MOD (EPOLL_CTL_MOD): 当你已经在监听一个socket,但想修改其监听的事件类型时,使用EPOLL_CTL_MOD事件类型。例如,当你读取完HTTP请求数据后,想监听EPOLLOUT事件以便发送响应,就可以使用EPOLL_CTL_MOD修改监听事件类型。

  • DEL (EPOLL_CTL_DEL): 当你不再需要监听某个socket时,使用EPOLL_CTL_DEL事件类型将其从EPOLL实例中删除。

HTTP请求后,EPOLL具体实现步骤分析

  • 添加socket到EPOLL实例中 : 使用epoll_ctl函数和EPOLL_CTL_ADD事件类型,将监听的socket添加到EPOLL实例中,并指定要监听EPOLLIN事件。
  • 读取请求数据: 当EPOLL检测到EPOLLIN事件时,读取HTTP请求数据。
  • 修改监听事件类型: 如果需要发送响应,可以使用EPOLL_CTL_MOD修改监听事件类型为EPOLLOUT。
  • 发送响应数据: 当EPOLL检测到EPOLLOUT事件时,发送HTTP响应数据。
cpp 复制代码
//简化EPOLL模型

#include <sys/epoll.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define MAX_EVENTS 10
#define READ_BUFFER_SIZE 1024

void handle_connection(int client_fd) {
    char buffer[READ_BUFFER_SIZE];
    int bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);

    if (bytes_read > 0) {
        buffer[bytes_read] = '\0'; // Null-terminate the string
        printf("Received request:\n%s\n", buffer);
        // Process the HTTP request and generate a response here.
        // For simplicity, we'll just send a basic HTTP response.
        const char *response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, World!";
        write(client_fd, response, strlen(response));
    } else if (bytes_read == 0) {
        // Client closed the connection
        close(client_fd);
    } else {
        // Read error
        perror("read");
        close(client_fd);
    }
}

int main() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0); // Create a listening socket
    // Bind and listen steps are omitted for brevity...

    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        perror("epoll_create1");
        exit(EXIT_FAILURE);
    }

    struct epoll_event event;
    struct epoll_event events[MAX_EVENTS];

    event.data.fd = listen_fd;
    event.events = EPOLLIN;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event) == -1) {
        perror("epoll_ctl: listen_fd");
        exit(EXIT_FAILURE);
    }

    while (1) {
        int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        for (int i = 0; i < n; i++) {
            if (events[i].events & EPOLLIN) {
                if (events[i].data.fd == listen_fd) {
                    // Handle new connection
                    int client_fd = accept(listen_fd, NULL, NULL);
                    if (client_fd == -1) {
                        perror("accept");
                        continue;
                    }

                    // Add new client socket to EPOLL instance
                    event.data.fd = client_fd;
                    event.events = EPOLLIN;
                    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
                        perror("epoll_ctl: client_fd");
                        close(client_fd);
                    }
                } else {
                    // Handle data from a connected client
                    handle_connection(events[i].data.fd);
                    // Optionally, modify the socket to listen for EPOLLOUT if needed
                }
            }
        }
    }

    close(listen_fd);
    close(epoll_fd);
    return 0;
}

项目设计分析与部分改进

TcpServer.hpp

getinstance()

  • 作用:实现单例模式的方法,用于确保TcpServer类的实例在程序生命周期内只被创建一次
  • 单例模式的目的:一个类只有一个实例,并提供一个全局访问点。
  • 静态互斥锁:线程锁的Lock确保它只可以被初始化一次,所以声明为静态,并在所有的getinstance的调用中共享。
  • 双重检查是否上锁:外部检查避免了在单例实例已经创建获取锁。内部检查确保了线程安全。
  • 类外初始化:类外初始化静态成员svr
  • 私有化构造函数:构造函数私有化,防止直接实例化类

改进思路

使用C++11的静态局部变量初始化特性,简化代码的同时保证线程安全

  • std::once_flag 和**std::call_once** : 使用 std::once_flagstd::call_once 确保 TcpServer 实例只被初始化一次,并且是线程安全的。
  • 静态局部变量 : svr 作为静态局部变量,只会被初始化一次,确保单例的唯一性。
  • 构造函数私有化: 将构造函数设为私有,防止类外部直接创建实例。
  • 禁止拷贝和赋值: 删除拷贝构造函数和赋值操作符,防止复制单例实例。
cpp 复制代码
class TcpServer
{
private:
    int port;
    int listen_sock;
    // static TcpServer*svr;
    static std::unique_ptr<TcpServer> svr; 
private:
    TcpServer(int _port):port(_port),listen_sock(-1)
    {}
    TcpServer(const TcpServer&s) = delete;
    TcpServer&operator = (const TcpServer&) = delete;
public:
    //单例模式
    static TcpServer*getinstance(int port)
    {
        // static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
        // if(nullptr == svr)
        // {
        //     pthread_mutex_lock(&lock);
        //     if(nullptr == svr)
        //     {
        //         svr = new TcpServer(port);
        //         svr->InitServer();
        //     }
        //     pthread_mutex_unlock(&lock);
        // }
        // return svr;
        static std::once_flag initFlag;
        std::call_once(initFlag,[&]()
        {
            svr.reset(new TcpServer(port));
            svr->InitServer();    
        });
        return svr.get();
    }

    void InitServer()
    {
    ......
    }
};

// TcpServer*TcpServer::svr = nullptr;
std::unique_ptr<TcpServer> TcpServer::svr = nullptr;

HttpServer.hpp

InitServer()

  • 忽略SIGPIPE信号,防止写入到已经关闭的套接字时导致服务器崩溃
    • unix系统编程中,SIGPIPE信号通常与管道和套接字的写操作有关,当进程试图向一个已经关闭的管道或者套接字写入数据的时候,系统会向进程发送SIGPIPE信号。如果进程没有处理这个信号,默认行为则是终止进程。所以为了避免这种情况的发生则忽略SIGPIPE信号

Loop()

  • accept接受新连接,tsvr->sock返回服务器监听的套接字描述符,peer用于保存客户端的地址信息
  • 创建任务并加入线程池
  • Loop函数是服务器的核心,它不断监听客户端连接请求,当接收到新连接的时候,将其封装成任务并提交给线程池处理。通过线程池来提高服务器的并发处理能力,同事避免频繁创建和销毁线程的开销

整体代码实现

cpp 复制代码
#pragma once 

#include<iostream>
#include<pthread.h>
#include<signal.h>
#include"TcpServer.hpp"
#include"log.hpp"
#include"Task.hpp"
#include"ThreadPoll.hpp"

#define PORT 8888

class HttpServer
{
private:
    int port;
    bool stop; //标记服务状态
public:
    HttpServer(int _port=PORT)
    :port(_port),stop(false)
    {}

    void InitServer()
    {
        //避免写入时,server崩溃,忽略SIGPIPE信号
        signal(SIGPIPE,SIG_IGN);
        LOG(INFO,"server initialized");
    }

    //启动服务
    void Loop()
    {
        TcpServer*tsvr = TcpServer::getinstance(port);
        LOG(INFO,"Loop begin")
        while(!stop)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sock = accept(tsvr->Sock(),(struct sockaddr*)&peer,&len);
            if(sock<0)
            {
                continue;
            }
            LOG(INFO,"Get a new link");

            //构建任务
            Task task(sock);
            //向线程池中传入任务z
            ThreadPoll::getinstance()->PushTask(task);
        }
    }
    ~HttpServer()
    {}
};

ThreadPoll.hpp

成员变量分析

  • num: 线程池中线程的数量。
  • stop: 标记线程池是否停止。
  • task_queue: 存储待处理任务的队列。
  • lock: 保护任务队列的互斥量。
  • cond: 用于线程间同步的条件变量。

单例模式设计

  • single_instance:静态指针,指向唯一的线程池实例
  • getinstance():使用双重检查锁定模式确保线程池实例在多线程环境下安全初始化

任务管理模块

  • pushTask:将任务添加到任务队列,并唤醒一个线程
  • popTask:从任务队列中取出任务

InitThreadPoll()

线程池初始化,创建num个线程,并启动它们执行ThreadRoutine函数。线程池层创建多个工作线程,每个线程在后台等待并处理任务队列中的任务。通过线程池提高服务器的并发处理能力,避免了频繁创建和销毁线程的开销。

ThreadRoutine(void* args)

每个线程在任务队列为空时等待任务,一旦有任务则取出并进行处理。无限循环的从任务队列中获取任务进行处理,通过加锁确保访问任务队列是线程安全的,while循环检查任务队列是否为空,等待任务队列的到来,防止虚假唤醒。

改进思路

  • **添加停止机制:**添加stopAll方法,安全的停止所有线程
  • **条件变量广播:**使用pthread_cond_broadcast唤醒所有线程,以便在停止线程池的时候所有线程都能够及时退出
  • 改进线程池的单例模式: 使用C++11中std::call_once和std::once_flag确保线程池实例的线程安全初始化。同时使用std::unique_ptr管理单例实例的生命周期
    • **线程安全:**std::call_once和std::once_flag确保初始化操作线程安全
    • **自动资源管理:**使用std::unique_ptr管理单例实例,确保资源在程序结束的时候自动释放,避免内存泄漏

ThreadPoll单例模式实现过程分析

  • std::call_once(initInstanceFlag, { ... }); 确保 lambda 表达式只会被调用一次。
  • single_instance.reset(new ThreadPoll()); 创建一个新的 ThreadPoll 实例,并将其指针管理权交给 single_instance
  • single_instance->InitThreadPoll(); 初始化线程池。
  • return single_instance.get(); 返回 ThreadPoll 实例的指针。

当其他线程同时调用getinstance的时候,由于std::call_once的保证,lambda表达式只会在第一个线程调用时执行一次,后续的调用都不会重复执行对象的创建和初始化,所有的线程都会返回同一个ThreadPoll实例
std::call_once函数

  • C++11引入的一个标准库函数,用于确保某个操作只被执行一次,即使有多个线程同时进行该操作。与std::once_flag配合使用,能够方便的实现线程安全的初始化。
  • 参数分析
    • flag : 一个 std::once_flag 对象,用来保证所调用的函数只执行一次。
    • f: 要执行的函数,可以是函数指针、函数对象或 lambda 表达式。
    • args... : 要传递给函数 f 的参数。

single_instance.reset(new ThreadPoll())

  • sigle_instance 是一个unique_ptr对象,而reset是unique_ptr的成员函数
  • **reset:**成员函数用于重置智能指针,释放其当前持有的对象,并让其持有新的对象

线程池的优缺点

优点

  • 提高并发性能
    • 减少线程创建和销毁的内存开销:线程池中的线程是在初始化的时候创建的,而不是等待每次任务到来的时候才创建的,从而减少频繁创建和销毁线程所带来的系统开销。
    • 提高资源利用效率:通过限制最大线程数量,从而避免系统资源被过度消耗,从而防止因为创建过度线程导致的性能下降。
  • 简化编程
    • 代码逻辑清晰:任务处理逻辑与线程管理逻辑分离,从而让代码更便于理解和维护。
    • 任务提交简单:利用线程池技术,只需要将任务提交给线程池,线程池就会自己调度和执行任务,不需要程序员管理线程的声明周期。
  • 线程复用
    • 提高线程利用效率:线程池中的线程是可以重复进行使用,提高线程的使用效率以及系统的响应速度。
  • 负载均衡
    • 均衡的分配任务:线程池可以自动均匀的将任务分配给线程,从而实现负载均衡,避免个别线程过载
  • 减少上下文切换

缺点

  • 实现复杂,不方便进行调优
  • 资源竞争
    • 同步开销:多线程环境下,多个线程竞争访问共享资源从而带来同步开销,从而影响性能
    • 死锁风险:如果对同步和锁不当的管理,会造成死锁
  • 增加内存占用
    • 提前创建线程会增加内存开销:如果处理的任务量过多,线程的创建会占用更多的内存。
  • 响应时间不确定

单例模式改进后代码

cpp 复制代码
#pragma once 

#include<iostream>
#include<queue>
#include<pthread.h>
#include<functional>
#include<memory>
#include<mutex>
#include<unistd.h>
#include"Task.hpp"

#define NUM 6

class ThreadPoll
{
    private:
        int num;//线程池中线程个数
        bool stop;//错误处理标志
        std::queue<Task> task_queue;//任务队列
        pthread_mutex_t lock;//互斥量
        pthread_cond_t cond;//条件变量

        ThreadPoll(int _num = NUM):num(_num),stop(false)
        {
            pthread_mutex_init(&lock,nullptr);
            pthread_cond_init(&cond,nullptr);
        }
        ~ThreadPoll()
        {
            pthread_mutex_destroy(&lock);
            pthread_cond_destroy(&cond);
        } 
        
        //改进1
        // ThreadPoll(const ThreadPoll&){};
        ThreadPoll(const ThreadPoll&) = delete;
        ThreadPoll&operator = (const ThreadPoll&) = delete;

        static std::unique_ptr<ThreadPoll>single_instance;
        static std::once_flag initInstanceFlag;

    public:

        //线程池单例设计
        static ThreadPoll*getinstance()
        {
            // static pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;
            // if(single_instance == nullptr)
            // {
            //     pthread_mutex_lock(&_mutex);
            //     if(single_instance == nullptr)
            //     {
            //         //类内创建线程池并完成初始化
            //         single_instance = new ThreadPoll();
            //         single_instance->InitThreadPoll();
            //     }
            //     pthread_mutex_unlock(&_mutex);
            // }
            // return single_instance;

            //改进3,使用C++11对单例模式进行升级
            std::call_once(initInstanceFlag,[](){
                single_instance.reset(new ThreadPoll());
                single_instance->InitThreadPoll();
            });
            return single_instance.get();
        }

        bool InitThreadPoll()
        {
            for(int i =0;i<num;i++)
            {
                pthread_t tid;
                //线程从线程池中拿取任务执行,需要通过this指针
                if(pthread_create(&tid,nullptr,ThreadRoutine,this)!=0)
                {
                    LOG(FATAL,"create thread pool error ");
                    return false;
                }
            }
            LOG(INFO,"create thread pool success ");
            return true;
        }

        void PushTask(const Task&task)
        {
            Lock();
            task_queue.push(task);//将任务放到任务队列中
            Unlock();
            ThreadWakeup();
        }

        //改进2
        void StopAll()
        {
            Lock();
            stop = true;
            //唤醒所有线程
            pthread_cond_broadcast(&cond);
            Unlock();
        }

        //判断线程是否退出
        bool IsStop()
        {
            return stop;
        }

        bool TaskQueueIsEmpty()
        {
            return task_queue.size()==0?true:false;
        }

        void Lock()
        {
            pthread_mutex_lock(&lock);
        }

        void Unlock()
        {
            pthread_mutex_unlock(&lock);
        }

        void ThreadWait()
        {
            //使用条件变量,条件变量满足时唤醒线程继续执行任务
             pthread_cond_wait(&cond,&lock);
        }

        void ThreadWakeup()
        {
            pthread_cond_signal(&cond);
        }

        static void *ThreadRoutine(void*args)
        {
            ThreadPoll*tp = (ThreadPoll*)args;
            
            while(true)
            {
                Task t;
                tp->Lock();
                //如果任务队列中没有任务,则让线程进行休眠;防止伪唤醒的出现,使用while循环
                while (tp->TaskQueueIsEmpty() && !tp->IsStop())
                {
                   tp->ThreadWait();
                }
                if(tp->IsStop())
                {
                    tp->Unlock();
                    break;
                }
                tp->PopTask(t);
                tp->Unlock();
                t.ProcessOn();
            }
            return nullptr;
        }  
        void PopTask(Task&task)
        {
            task = task_queue.front();
            task_queue.pop();
        }


};

std::unique_ptr<ThreadPoll> ThreadPoll::single_instance;
std::once_flag ThreadPoll::initInstanceFlag;

当前线程池的替换方案

  • 异步编程
    • C++的boost::asio 和 libuv
      • boost.Asio是Boost库的一部分,主要提供了异步IO的操作,同时支持TCP、UDP等协议
      • libuv 是一个多平台的支持异步 I/O 的 C 库,最初用于 Node.js。它支持事件循环和异步 I/O 操作,使得构建高性能的网络应用程序变得更加容易
      • 优点:高效处理IO操作,减少线程切换带来的开销
      • 缺点:编程模型复杂,需要管理异步操作的生命周期
    • 协程
      • 协程是轻量化的用户态线程,可以在单个线程中实现类似于多线程的并发操作
      • 优点:简化异步编程模型,代码可读性高,性能开销小
      • 缺点:需要现代编译器的支持
    • Reactor/Proactor模型
      • 事件驱动的并发模型,boost::asio就是基于Reactor模型实现的
      • 优点:高效处理并发IO请求
      • 缺点:需要理解和实现事件驱动机制,编程模型复杂
    • Actor模型
      • 并发编程模型,通过消息传递进行通信,常见的包括Erlang和Akka
      • 优点:支持并发和分布式,模型清晰
      • 缺点:代码改动大
    • IO密集型应用 适合异步编程或者Reactor模型;CPU密集型任务 则使用线程池;携程和Actor模型在高并发或者分布系统中表现出色。
      总结:主要是修改思路的一些总结,后面代码的具体改进也是基于此处的一些探索,非最终版本。
相关推荐
我言秋日胜春朝★35 分钟前
【Linux】进程地址空间
linux·运维·服务器
风尚云网43 分钟前
风尚云网前端学习:一个简易前端新手友好的HTML5页面布局与样式设计
前端·css·学习·html·html5·风尚云网
繁依Fanyi1 小时前
简易安卓句分器实现
java·服务器·开发语言·算法·eclipse
C-cat.1 小时前
Linux|环境变量
linux·运维·服务器
m51271 小时前
LinuxC语言
java·服务器·前端
运维-大白同学2 小时前
将django+vue项目发布部署到服务器
服务器·vue.js·django
烦躁的大鼻嘎2 小时前
【Linux】深入理解GCC/G++编译流程及库文件管理
linux·运维·服务器
乐大师2 小时前
Deepin登录后提示“解锁登陆密钥环里的密码不匹配”
运维·服务器
ac.char2 小时前
在 Ubuntu 上安装 Yarn 环境
linux·运维·服务器·ubuntu
敲上瘾2 小时前
操作系统的理解
linux·运维·服务器·c++·大模型·操作系统·aigc