您的位置:首页 > 科技 > 能源 > HTTPServer改进思路1

HTTPServer改进思路1

2024/11/13 18:28:56 来源:https://blog.csdn.net/gma999/article/details/140052751  浏览:    关键词: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响应数据。
//简化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 1024void 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 stringprintf("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 connectionclose(client_fd);} else {// Read errorperror("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 connectionint client_fd = accept(listen_fd, NULL, NULL);if (client_fd == -1) {perror("accept");continue;}// Add new client socket to EPOLL instanceevent.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 clienthandle_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 作为静态局部变量,只会被初始化一次,确保单例的唯一性。
  • 构造函数私有化: 将构造函数设为私有,防止类外部直接创建实例。
  • 禁止拷贝和赋值: 删除拷贝构造函数和赋值操作符,防止复制单例实例。
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函数是服务器的核心,它不断监听客户端连接请求,当接收到新连接的时候,将其封装成任务并提交给线程池处理。通过线程池来提高服务器的并发处理能力,同事避免频繁创建和销毁线程的开销

 

整体代码实现 

#pragma once #include<iostream>
#include<pthread.h>
#include<signal.h>
#include"TcpServer.hpp"
#include"log.hpp"
#include"Task.hpp"
#include"ThreadPoll.hpp"#define PORT 8888class 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);//向线程池中传入任务zThreadPoll::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:成员函数用于重置智能指针,释放其当前持有的对象,并让其持有新的对象

 

线程池的优缺点 

优点

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

缺点

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

 单例模式改进后代码

#pragma once #include<iostream>
#include<queue>
#include<pthread.h>
#include<functional>
#include<memory>
#include<mutex>
#include<unistd.h>
#include"Task.hpp"#define NUM 6class 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();}//改进2void 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::asiolibuv
      • 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模型在高并发或者分布系统中表现出色。

总结:主要是修改思路的一些总结,后面代码的具体改进也是基于此处的一些探索,非最终版本。 

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com