介绍
WebServer采用I/O复用技术Epoll与线程池实现的多线程Reactor高并发模型,下面则来介绍Socket、Epoll、线程池与Reactor技术,及在项目中的实现方式。
Socket
先建立TCP服务端通信,得到监听的文件描述符,再去等待客户端的连接,以此来建立双方的通信,以下是Socket通信流程:
Socket接口使用与实现方式(为简化代码,未对异常处理):
#include <sys/socket.h>
#include <netinet/in.h>
listenFd_ = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(port_);
int optval = 1;
/* 端口复用 */
ret = setsockopt(listenFd_, SOL_SOCKET, SO_REUSEADDR, (const void*)&optval, sizeof(int));
ret = bind(listenFd_, (struct sockaddr *)&addr, sizeof(addr));
ret = listen(listenFd_, 6);
Epoll
使用I/O复用技术Epoll以同时监听到多个文件描述符,提高程序I/O的并发量,而为什么不用Select与Poll,可见一文读懂社长的TinyWebServer的IO复用方式这一部分。
Linux下Epoll接口使用与实现方式:
以下为epoller.h:
#ifndef EPOLLER_H
#define EPOLLER_H
#include <sys/epoll.h> //epoll_ctl()
#include <fcntl.h> // fcntl()
#include <unistd.h> // close()
#include <assert.h> // close()
#include <vector>
#include <errno.h>
class Epoller {
public:
explicit Epoller(int maxEvent = 1024);
~Epoller();
bool AddFd(int fd, uint32_t events);
bool ModFd(int fd, uint32_t events);
bool DelFd(int fd);
int Wait(int timeoutMs = -1);
int GetEventFd(size_t i) const;
uint32_t GetEvents(size_t i) const;
private:
int epollFd_; // epoll_create()创建一个epoll对象,返回值就是epollFd
std::vector<struct epoll_event> events_; // 检测到的事件的集合
};
#endif //EPOLLER_H
以下为epoller.cpp:
#include "epoller.h"
// 创建epoll对象 epoll_create(512)
Epoller::Epoller(int maxEvent):epollFd_(epoll_create(512)), events_(maxEvent){
assert(epollFd_ >= 0 && events_.size() > 0);
}
Epoller::~Epoller() {
close(epollFd_);
}
// 添加文件描述符到epoll中进行管理
bool Epoller::AddFd(int fd, uint32_t events) {
if(fd < 0) return false;
epoll_event ev = {0};
ev.data.fd = fd;
ev.events = events;
return 0 == epoll_ctl(epollFd_, EPOLL_CTL_ADD, fd, &ev);
}
// 修改
bool Epoller::ModFd(int fd, uint32_t events) {
if(fd < 0) return false;
epoll_event ev = {0};
ev.data.fd = fd;
ev.events = events;
return 0 == epoll_ctl(epollFd_, EPOLL_CTL_MOD, fd, &ev); // 此时修改了事件的模式如EPOLLOUT
}
// 删除
bool Epoller::DelFd(int fd) {
if(fd < 0) return false;
epoll_event ev = {0};
return 0 == epoll_ctl(epollFd_, EPOLL_CTL_DEL, fd, &ev);
}
// 调用epoll_wait()进行事件检测
int Epoller::Wait(int timeoutMs) {
return epoll_wait(epollFd_, &events_[0], static_cast<int>(events_.size()), timeoutMs); // events == &events[0]
}
// 获取产生事件的文件描述符
int Epoller::GetEventFd(size_t i) const {
assert(i < events_.size() && i >= 0);
return events_[i].data.fd;
}
// 获取事件
uint32_t Epoller::GetEvents(size_t i) const {
assert(i < events_.size() && i >= 0);
return events_[i].events;
}
Epoll实例的创建是发生在WebServer的构造函数中,之后会设置监听与通信文件描述符为ET模式,ET模式如下有解释,使用epoll_wait去监听事件,再循环处理每个事件。
ET与IT: Epoll的工作模式有两种,分别是ET模式和IT模式,代表边沿触发和水平触发,边沿触发指委托内核检测读数据,当读缓冲区有数据时,Epoll检测到了会给用户通知,这时用户需一次性将数据全部读取完,因为下次Epoll检测时不会再通知,而水平触发则如果用户不读数据或者只读了一部分数据,下次Epoll检测时仍会触发,由此可看出ET是一种高速的工作方式,IT是一种缺省的工作方式,而项目中采取的是ET模式。
以下为webserver.cpp的循环事件处理:
int eventCnt = epoller_->Wait(timeMS);
for(int i = 0; i < eventCnt; i++) {
int fd = epoller_->GetEventFd(i);
uint32_t events = epoller_->GetEvents(i);
if(fd == listenFd_) {
DealListen_();
}
else if(events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) {
assert(users_.count(fd) > 0);
CloseConn_(&users_[fd]);
}
else if(events & EPOLLIN) {
assert(users_.count(fd) > 0);
DealRead_(&users_[fd]);
}
else if(events & EPOLLOUT) {
assert(users_.count(fd) > 0);
DealWrite_(&users_[fd]);
}
else {
LOG_ERROR("Unexpected event");
}
}
线程池
ThreadPool的创建是发生在WebServer的构造函数中,并指定工作线程的线程数,在ThreadPool的构造函数中,循环创建工作线程并执行task()函数用来处理任务队列中的读写事件,为了避免过度占用cpu,采用条件变量的方式唤醒或阻塞工作线程,而任务队列则需要加锁,因为它被所有线程所共享,线程也会被设置为脱离状态即detach状态,以至于线程运行结束时,资源会被系统自动回收,无需再对线程join操作。
使用线程池而不是使用多线程即来一个请求创建一个线程,执行完又将该线程销毁,是为了避免由于线程频繁的创建和销毁所带来的性能消耗,而为了任务的并发执行,将任务交给线程池即可。
线程池中线程的数量大小:最佳线程数 = cpu核数 * 当前cpu利用率 * (1 + cpu等待时间 / cpu处理时间)
以下是threadpool.h
#ifndef THREADPOOL_H
#define THREADPOOL_H
#include <mutex>
#include <condition_variable>
#include <queue>
#include <thread>
#include <functional>
class ThreadPool {
public:
explicit ThreadPool(size_t threadCount = 8): pool_(std::make_shared<Pool>()) { // explicit防止构造函数进行隐式类型转换
assert(threadCount > 0);
// 创建threadCount个子线程
for(size_t i = 0; i < threadCount; i++) {
std::thread([pool = pool_] {
std::unique_lock<std::mutex> locker(pool->mtx);
while(true) {
if(!pool->tasks.empty()) {
// 从任务队列中取第一个任务
auto task = std::move(pool->tasks.front());
// 移除掉队列中第一个元素
pool->tasks.pop();
locker.unlock();
task();
locker.lock(); // 这里是对工作队列加锁
}
else if(pool->isClosed) break;
else pool->cond.wait(locker); // 如果队列为空,等待
}
}).detach();// 线程分离
}
}
ThreadPool() = default;
ThreadPool(ThreadPool&&) = default;
~ThreadPool() {
if(static_cast<bool>(pool_)) {
{
std::lock_guard<std::mutex> locker(pool_->mtx);
pool_->isClosed = true;
}
pool_->cond.notify_all();
}
}
template<class F>
void AddTask(F&& task) {
{
std::lock_guard<std::mutex> locker(pool_->mtx);
pool_->tasks.emplace(std::forward<F>(task));
}
pool_->cond.notify_one(); // 唤醒一个等待的线程
}
private:
// 结构体
struct Pool {
std::mutex mtx; // 互斥锁
std::condition_variable cond; // 条件变量
bool isClosed; // 是否关闭
std::queue<std::function<void()>> tasks; // 队列(保存的是任务)
};
std::shared_ptr<Pool> pool_; // 池子
};
#endif //THREADPOOL_H
并发模型
Reactor:
主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有就立即将该事件通知给工作线程(逻辑单元),将socket可读可写事件放入请求队列。除此之外,主线程不做任何其它实质性的工作。读写数据,接收新的连接,以及处理客户请求均在工作线程中完成。
Reactor、Proactor以及模拟Proactor的区别可见一文读懂社长的TinyWebServer的事件模式这一部分。
该项目的并发模型则采用Reactor模型,但不同的是主线程不仅监听文件描述符是否有事件发生,还负责接收新的客户连接,epoll_wait()用来同时监听多个文件描述符,如果是连接请求则会封装成一个HttpConnection对象,并与服务器建立连接,如果是读写事件,采用线程池的方式唤醒其中的工作线程去处理读写事件。