基于 Linux 的 web 服务器

简介

Linux 下的轻量级 Web 服务器,使用 C++11 开发,架构参考 Github 高星项目 TinyWebServer。参考链接:Tinyweb

技术要点:

  1. 使用线程池实现高并发。
  2. epoll实现IO复用,采用模拟 Proactor 并发模型。
  3. 通过状态机解析 HTTP 报文请求。
  4. 采用异步日志系统记录服务器运行状态。
  5. 使用timerfd定时器接口主动对长时间无操作的连接断开。
  6. 允许提供自签名的证书和私 钥使用 openssl 提供 https 服务。

使用 webbench 压测能够实现上万并发连接数据交换。

基本架构:

简单的分为三层:数据层、服务层、连接层。其中数据层负责与MySQL数据库建立连接,进行交互;服务层负责http报文的解析和构建;连接层负责socket监听服务以及连接操作,对于https服务,提供ctx会话环境的构建和ssl连接。

文件结构:

.
├── http 										//提供http报文解析和生成服务
│   ├── httpconn.cpp					
│   ├── httpconn.h							//集成httpRequestParser和httpResponse,并负责提供静态资源托管接口、get和post路由设置函数
│   ├── httpRequestParser.cpp
│   ├── httpRequestParser.h				//http报文解析
│   ├── httpResponse.cpp
│   ├── httpResponse.h				//http报文构建
│   └── README.md
├── log										//异步日志,来自[Ring Log](git@github.com:LeechanX/Ring-Log.git),进行了一点修改
│   ├── Makefile
│   ├── README.md
│   ├── rlog.cc
│   ├── rlog.h
│   └── rlog.o
├── make.sh
├── mysql								//数据库连接池,以及提供数据库操作函数
│   ├── connect_pool.cpp
│   └── connect_pool.h			//单例模式,使用GetConnection获取数据库连接指针,ReleaseConnection放回数据库连接池。提供ConnectRAII对象管理数据库连接,使用RAII保证数据连接会安全放回连接池。使用init函数初始化数据库连接池对象,并且保证init函数只会调用一次。
├── server.cpp
├── server.h							//连接层,负责监听socket连接,将连接放入线程池以及主动断开长时间未操作的连接,webserver类用来定义一个完整的web服务器对象。
├── threadPool						//线程池类,为连接层提供线程池,使用模板实现,便于扩展(可以提供除http外的其他服务)
│   ├── README.md
│   └── threadPool.h
└── timer							//定时器类,为连接层提供定时断连服务。
    ├── README.md
    ├── timer_fd.cpp
    └── timer_fd.h

一、线程池模板类threadPool<T>

使用模板便于扩展,之后可以添加除了http之外的其他服务。
线程同步采用了c++11提供的<mutex><condition_variable>,线程库采用了c++11的thread

技术栈

1.public继承std::enable_shared_from_this<threadPool<T>>

使用智能指针管理threadPool对象,当对象析构时,停止线程循环,执行线程回收工作。

2.智能指针shared_ptr<T>

init()函数负责初始化线程池对象,构建对应数目的线程,并关联线程函数work(),传入线程池模板类对应的this的智能指针(采用shared_from_this),在work中就能调用threadPool对象的工作队列workList进行处理。

3. 异常捕获try

对于thread线程创建增加异常捕获,若线程创建失败,则不需要把线程添加进m_threadList,整个程序也不会因此崩溃,只是实际上创建的线程会少于预期。

关键代码段

//增加异常捕获,如果当前线程创建失败,则不需要添加进m_threadList
try{
		//把当前对象的智能指针传入work函数,因为work函数必须是静态函数,而静态函数没有对象的this指针,所以使用这种折中的方法
        std::shared_ptr<std::thread> t = std::make_shared<std::thread>(work, threadPool<T>::shared_from_this());
        temp.swap(t);
}
catch(const std::system_error &e){
    //报告错误
    LOG_FATAL("Create thread eroror : %s", e.what());
    continue;
}
....
//work函数
template<typename T>
void threadPool<T>::work(std::shared_ptr<threadPool<T>> request){
	//使用threadPool对象的run方法,也就是业务循环段
    request->run();
}

二、http服务,httphttp_requesthttp_response

http类整合了:http报文读取:read_oncehttp报文发送:write_once()http请求报文解析和响应报文构建:process()

  • http类中的static std::map<std::string, std::string> staticSource负责静态资源的托管,
  • 负责Get请求的路由设置:static std::map<std::string, void (*) (http_request&, http_response&, MYSQL*)> registerGet;
  • 负责POST请求的路由设置:static std::map<std::string, void (*) (http_request&, http_response&, MYSQL*)> registerPost;

其中http报文解析使用了http_request类专门负责报文解析模块,http响应报文构建专门使用了http_response类负责响应报文构建。

流程:

  1. 使用init(int epollfd, int sockfd, SSL* ssl)初始化,传入epoll、socket文件描述符,以及SSL*,当SSL*不为空时,认为执行https服务,空时执行http服务。
  2. 使用http::read_oncesocket中读取http请求报文,并对读取情况进行判定,read_once返回结果是READ_STAT对象
  • enum class READ_STAT{OK, BAD, CLOSE};分别代表读取成功、失败以及对方关闭连接。
  1. 成功读取http请求报文后使用process()函数,process()函数首先对报文进行解析,解析函数返回HTTP_CODE类型对象,根据不同的解析结果生成不同的响应报文,除了“报文完整”,其他都是错误信息,对于“报文完整”的情况,会跳跳转get_request()函数,分别执行GET请求和POST的事务,并构建响应报文。
//解析报文
HTTP_CODE code = this->request_parase.parser(this->m_message);
....
//HTTP报文解析结果:报文不完整,报文完整,报文错误, 内部错误(兜底)
enum class HTTP_CODE{OPEN_REQUEST = 0, GET_REQUEST, BAD_REQUEST, INTERNAL_ERROR};

三、定时器类timefdList

timefdList类维护一个定时器列表std::list<std::shared_ptr<std::vector<long>>> m_list,该m_list维护一个智能指针,指针指向vector,vector是一个长度为2的数组,分别存储socket文件描述符和timerfd_create创建的定时器对象文件描述符

接口:

  • void add(std::shared_ptr<std::vector>):负责往定时器列表添加需要监听的定时器对象
  • void adjust(std::shared_ptr<std::vector> data, int n):调整对应计时器,超时时间后移
  • void delete_timer(std::shared_ptr<std::vector> data):删除对应的计时器。

操作流程:

  • 当创建一个新的socket连接时,同时往定时器类中添加一个定时器对象,并在epoll中注册响应的定时器对象文件描述符的读操作。
  • 每次读写socket都会更新对应socket的定时器超时时间。
  • 当定时器文件描述符可读时,并不直接关闭对应的socket,而是给一个标记,等这次epoll_wait返回的事件都处理完毕,调用timefdList的tick方法处理所有超时连接。

四、连接层,webserver类

websever负责整合所有功能,主要其监听连接,并发模型使用模拟proactor

接口:

  • bool webListen(std::string ip, int port);:监听对应的端口,成功返回true,失败返回false
  • void run(int);:开始运行,输入一个int类型参数,代表socket允许的无操作时间。
  • bool openhttps(const char* cacert, const char* key, const char* passwd);:开启https,输入ca文件,私钥文件和密码,正确初始化返回true,失败返回false。
  • void addStaticSource(const std::string url, const std::string fileName);:添加静态资源
  • void addGet(const std::string, void (func) (http_request&, http_response&, MYSQL));:添加Get请求路由
  • void addPost(const std::string, void (func) (http_request&, http_response&, MYSQL));:添加POST请求路由

操作流程:

执行首先使用webListen设置监听端口,如果开启https,则需要使用openhttps传入响应文件,addStaticSource、addGet、addPost添加静态资源和GET、POST路由。

当执行run之后,服务器主循环开始,主循环epoll_wait等待epoll注册事件发生,对于接收到的事件分为4种类型:

  1. 新连接,服务器监听到新的socket连接请求,accept接收请求,若开启了https,则建立ssl连接。往epoll注册新连接的读事件,并创建一个定时器,epoll注册定时器读事件。
  2. socket读事件:更新定时器,读取socket中的信息,根据读取到的结果选择是否把http对象放入请求队列,让线程池解析报文内容并生成响应报文。
  3. socket写事件:更新定时器,往socket写响应报文。
  4. 定时器定时事件:并不会立即处理定时事件,而是把alarm_event标记置true,等到此次epoll事件处理完毕,再调用定时器列表的tick方法统一关闭超时的socket连接。
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值