为什么还要做 WebServer?
①对于缺乏项目经验的C++新手,网上可找到的详细项目资料有限,多为简单的管理系统、五子棋游戏、工具库或WebServer等。
②WebServer项目有助于整合面试所需的基础知识,如C/C++语言、操作系统(包括I/O调用和多路复用机制)、计算机网络(处理网络异常)和数据库(如注册中心数据库语句和负载均衡)。这样的项目可以让面试官围绕项目考察知识,简化面试准备(将⾯试的问题限制在⼀定的范围中)。
③WebServer作为一个高性能网络框架,可以自由扩展功能,大多数WebServer以MIME服务为主。
WebServer基础知识
编程语⾔
WebServer 对编程语⾔的宽容度较⾼,掌握基本的 C/C++ 语法即可开始做,⽽掌握语⾔的新特性则能够让项⽬更上⼀层楼。
在做 WebServer 之前,最少需要掌握以下两点(最低配置要求)
- 基本的 C/C++ 语法,毕竟 linux 的系统调⽤是⽤ C 语⾔写的,保证⾃⼰会⽤和能看懂即可。
- C++11 的特性(智能指针、function等),能够掌握 C++14/17 则更好,讲语⾔的新特性⽤到⾃⼰的项⽬中去也算是⼀个加分项,在项⽬中也可以提⾼⾃⼰对语⾔的掌握程度。
简单来说,只要你能在⼒扣上⽤ C++ 刷个⼀两百道题,就完全可以开始做项⽬了。
操作系统
- 基本的 Linux 命令,调试 WebServer ⽤,可以参考《⻦哥的 Linux 私房菜》这本书,讲得很全⾯,可以当字典使⽤。
- 常⻅的系统调⽤,主要就是 read/write AND socket 等函数,参考 CSAPP 来看即可。
计算机⽹络
- TCP 和 UDP 的连接机制及对应的函数
- 常⻅的服务器模式,单对单、多对单、多对多
- 抓包⼯具的简单使⽤,如 tcpdump
数据库
- 常⽤的 MySQL 语句,参考《MySQL 必知必会》
- 数据库的安装
参考书籍
- 游双. Linux⾼性能服务器编程[M]. 机械⼯业出版社, 2013.(涵盖所有基础)
- 陈硕. Linux多线程服务端编程:使⽤muduo C++⽹络库[M]. 电⼦⼯业出版社, 2013.
- 徐晓鑫. 后台开发:核⼼技术与应⽤实践[M]. 机械⼯业出版社, 2016.
框架梳理
在进⾏了功能测试以后,我们确认了 clone 下来的项⽬起码是能够编译运⾏并且基本功能是正确的,但是此时我们还是先不要急着去写代码(就算想写也⼤概率不知道从哪个部分下⼿),因此在进⾏⼀个较⼤的项⽬时我们⾸先需要理清楚整个项⽬的框架,整个项⽬⼤概由哪些模块组成,各个模块的功能是什么,模块之间的关系具体是怎么样的。只能你脑袋⾥对整个项⽬框架有清晰的认识,你在写代码的时候才能够明确当前部分的代码应该实现怎样的功能,给下⼀部分的代码什么样的输⼊,⽽不会”猪脑过载“,陷⼊到局部细节中去。
那么问题来了,我们应该如何进⾏项⽬框架的梳理呢?⾸先我们需要明确⼀点,WebServer 已经是⼀个⾮常成熟的项⽬了(oppo的⾯试官还问过我,这个 WebServer 都已经出书了,为什么你们还在做),它的基本项⽬框架⼤致相同(reactor和proactor),只能具体功能和⼀些模块的实现略有差别。因此,如果你在做这个项⽬之前已经看过陈硕的《Linux 多线程服务端编程》这本书的话,你就会发现 GitHub 上的 ⾼并发WebServer 只有 reactor 和模拟的 proactor 两种框架。
当然,如果你之前没看过相关的内容,对服务器的⾼并发模型也⼀⽆所知(我当时就是),也是可以继续做下去的,毕竟在实际中也不会有这么多的前置资料让你理解透彻再去进⾏,从⼀定程度上来看,我们在梳理别⼈代码的过程也可以看作对服务器并发模型的学习。我们⾸先还是先看下项⽬的⽂档,看下作者有没有提供代码的框架,有的话就简单了,对着框架图去看代码,可以迅速理清代码的模块组成,快速上⼿其他⼈的代码,然后就可以⾃⼰去写了。
⾸先以项⽬qinguoyi/TinyWebServer: Linux下C++轻量级WebServer服务器 (github.com)中的框架图为例。
从图中我们可以看到,项⽬中搭建了⼀个半同步/反应堆线程池,在其中维护了⼀个请求队列,线程池中的主线程通过 epoll 来监听 socket,并且将请求队列中的任务分配给线程池中的⼯作线程,其中⼯作线程能够处理的任务分为⽇志输出、定时器处理⾮活动连接以及处理 HTTP 请求三种。
然后再看下项⽬linyacool/WebServer: A C++ High Performance Web Server (github.com)中的框架图。
这个框架图就更加简洁明了,在 WebServer 中,许多 client 在 MainReactor 中得到了连接请求的响应,并与WebServer 建⽴具体的连接。然后通过⼀个叫 Acceptor 的模块,将具体的连接分配给到⼀些叫做 SubReactor 的模块,在 SubReactor 中对具体的连接进⾏读、编码、计算、解码和写操作(即对 client 请求的响应)。这也是muduo ⽹络库中所提出的 Multi-Reactor 并发框架,如下图(来源⻅⽔印)。
如果你对 muduo 中的并发模型感兴趣的话,可以详细看下前⽂提到的书《Linux多线程服务端编程》或者⽂章⻓⽂梳理Muduo库核⼼代码及优秀编程细节剖析陈硕muduo我在地铁站⾥吃闸机的博客-CSDN博客。
当然,在项⽬找不到相关的框架⽂档时,我们也可以从代码⼊⼿去分析。我以项⽬linyacool/WebServer: A C++High Performance Web Server (github.com)为例⼤致讲⼀下如何去看代码。
└─WebServer-master
│ build.sh
│ CMakeLists.txt
│ README.md
│ 并发模型.md
│ 测试及改进.md
│ 版本历史.md
│ 连接的维护.md
│ 遇到的困难.md
│ 项⽬⽬的.md
├─datum
├─old_version
├─WebBench
└─WebServer
│ Channel.cpp
│ Channel.h
│ CMakeLists.txt
│ config.h
│ Epoll.cpp
│ Epoll.h
│ EventLoop.cpp
│ EventLoop.h
│ EventLoopThread.cpp
│ EventLoopThread.h
│ EventLoopThreadPool.cpp
│ EventLoopThreadPool.h
│ HttpData.cpp
│ HttpData.h
│ Main.cpp
│ Makefile
│ Makefile.bak
│ Server.cpp
│ Server.h
│ ThreadPool.cpp
│ ThreadPool.h
│ Timer.cpp
│ Timer.h
│ Util.cpp
│ Util.h
├─base
│ │ AsyncLogging.cpp
│ │ AsyncLogging.h
│ │ CMakeLists.txt
│ │ Condition.h
│ │ CountDownLatch.cpp
│ │ CountDownLatch.h
│ │ CurrentThread.h
│ │ FileUtil.cpp
│ │ FileUtil.h
│ │ LogFile.cpp
│ │ LogFile.h
│ │ Logging.cpp
│ │ Logging.h
│ │ LogStream.cpp
│ │ LogStream.h
│ │ Log的设计.txt
│ │ MutexLock.h
│ │ noncopyable.h
│ │ Thread.cpp
│ │ Thread.h
│ └─tests
└─tests
我们⾸先看下程序的⼊⼝,即 main() 函数所在的位置—— Main.cpp ⽂件。
int main(int argc, char *argv[]) {
int threadNum = 4;
int port = 80;
std::string logPath = "./WebServer.log";
// 参数解析
int opt;
const char *str = "t:l:p:";
while ((opt = getopt(argc, argv, str)) != -1) {
switch (opt) {
case 't': {
threadNum = atoi(optarg);
break;
}
case 'l': {
logPath = optarg;
if (logPath.size() < 2 || optarg[0] != '/') {
printf("logPath should start with \"/\"\n");
abort();
}
break;
}
case 'p': {
port = atoi(optarg);
break;
}
default:
break;
}
}
Logger::setLogFileName(logPath);
// STL库在多线程上应⽤
#ifndef _PTHREADS
LOG << "_PTHREADS is not defined !";
#endif
EventLoop mainLoop;
Server myHTTPServer(&mainLoop, threadNum, port);
myHTTPServer.start();
mainLoop.loop();
return 0;
}
main.cpp ⽂件中可以看到,作者在运⾏服务器前先进⾏了参数配置、设置⽇志的相关参数(存储路径、⽇志等级等),然后实例化了⼀个主循环对象,初始化了 WebServer 的单例对象,最后启动 WebServer 并开启 主循环。
显然,这⾥最主要的就是两个函数 myHTTPServer.start() 和 mainLoop.loop() ,所以着重分析这两个函数的功能即可。
⾸先看下 myHTTPServer.start()
void Server::start() {
eventLoopThreadPool_->start();
// acceptChannel_->setEvents(EPOLLIN | EPOLLET | EPOLLONESHOT);
acceptChannel_->setEvents(EPOLLIN | EPOLLET);
acceptChannel_->setReadHandler(bind(&Server::handNewConn, this));
acceptChannel_->setConnHandler(bind(&Server::handThisConn, this));
loop_->addToPoller(acceptChannel_, 0);
started_ = true;
}
从函数命名就可以看出函数的主要功能主要有三部分:
- 启动线程池
- 设置了 acceptChannel_ 的 handler,分别为读回调和连接回调
- 将 acceptChannel_ 加⼊了 poller
可以看出是做了⼀下建⽴连接的⼯作。
然后再看下 mainLoop.loop()
void EventLoop::loop() {
assert(!looping_);
assert(isInLoopThread());
looping_ = true;
quit_ = false;
// LOG_TRACE << "EventLoop " << this << " start looping";
std::vector<SP_Channel> ret;
while (!quit_) {
// cout << "doing" << endl;
ret.clear();
ret = poller_->poll();
eventHandling_ = true;
for (auto& it : ret) it->handleEvents();
eventHandling_ = false;
doPendingFunctors();
poller_->handleExpired();
}
looping_ = false;
}
从函数命名就可以看出函数的主要功能主要有四部分:
- 从 poller 中取出所有活动事件
- 调⽤活动事件的回调函数
- 执⾏额外的函数
- 执⾏超时的回调函数
明显这⾥地⽅才是真正进⾏业务处理的地⽅。然后再看下 acceptChannel_ 的读回调函数 (有新连接时执⾏的函数)
void Server::handNewConn() {
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(struct sockaddr_in));
socklen_t client_addr_len = sizeof(client_addr);
int accept_fd = 0;
while ((accept_fd = accept(listenFd_, (struct sockaddr *)&client_addr,
&client_addr_len)) > 0) {
EventLoop *loop = eventLoopThreadPool_->getNextLoop();
LOG << "New connection from " << inet_ntoa(client_addr.sin_addr) << ":"
<< ntohs(client_addr.sin_port);
// 限制服务器的最⼤并发连接数
if (accept_fd >= MAXFDS) {
close(accept_fd);
continue;
}
// 设为⾮阻塞模式
if (setSocketNonBlocking(accept_fd) < 0) {
LOG << "Set non block failed!";
// perror("Set non block failed!");
return;
}
setSocketNodelay(accept_fd);
shared_ptr<HttpData> req_info(new HttpData(loop, accept_fd));
req_info->getChannel()->setHolder(req_info);
loop->queueInLoop(std::bind(&HttpData::newEvent, req_info));
}
acceptChannel_->setEvents(EPOLLIN | EPOLLET);
}
这个回调函数做了以下的⼯作:
- 通过 accept(3) 来建⽴ TCP 连接
- 从线程池中获取⼀个 eventLoop
- 将建⽴的连接放⼊ eventLoop 中
通过这样逐步分析,是不是发现,建⽴连接的⽅式与 muduo 的并发模型很像呢?
开始项目
经过⼀番摸索,我们终于画出⾃⼰的框架图,下⾯我将仿照 muduo 的框架对 WebServer 各个常⻅模块的原理进⾏讲解。
⾸先,许多 client 在访问 WebServer 时,并不是每个 client 都由⼀个服务端的线程/进程进⾏业务处理,这样在⾼并发(多个 client 同时访问服务端)的场景中,服务端的响应速度,并且服务端本身可以建⽴的线程/进程数也是有限的,这样的⽅式也很容易导致服务端的崩溃。
因此,muduo 使⽤⾮阻塞的poll/epoll(IO multiplexing 多路复⽤)轮训监听(Reactor)有⽆SOCKET 读写IO事件, 将IO事件的处理回调函数分发到线程池中,实现异步返回结果。在多线程编程模型中采⽤了 “one loop perthread + thread pool” 的形式。⼀个线程中有且仅有⼀个EventLoop(也就是说每⼀个核的线程负责循环监听⼀组⽂件描述符的集合),这个线程称之为 IO 线程。如果⻓时间没有事件发⽣,IO线程将处于空闲状态,这时可以利⽤IO线程来执⾏⼀些额外的任务(利⽤定时器任务队列来处理超时连接),这就要求⾮阻塞的 poll/epoll 能够在⽆IO事件但有任务到来时能够被唤醒。
并发框架
并发框架这部分还是建议⼤家花时间去看⼀下《Linux多线程服务端编程》的第⼋章—muduo ⽹络库的设计与实现,⾥⾯对 Reactor 模型的原理和实现都讲得⾮常清晰。在具体的实现中,最核⼼的部分就是 EventLoop 、Channel 以及 Poller 三个类,其中 EventLoop 可以看作是对业务线程的封装,⽽ Channel 可以看作是对每个已经建⽴连接的封装(即 accept(3) 返回的⽂件描述符),三者的关系⻅下图(来源⻅⽔印)。
EventLoop
class EventLoop {
public:
typedef std::function<void()> Function;
// 初始化poller, event_fd,给 event_fd 注册到 epoll 中并注册其事件处理回调
EventLoop();
~EventLoop();
// 开始事件循环 调⽤该函数的线程必须是该 EventLoop 所在线程,也就是 Loop 函数不能跨线程调⽤
void Loop();
// 停⽌ Loop
void StopLoop();
// 如果当前线程就是创建此EventLoop的线程 就调⽤callback(关闭连接 EpollDel) 否则就放⼊等待执⾏函数区
void RunInLoop(Function&& func);
// 把此函数放⼊等待执⾏函数区 如果当前是跨线程 或者正在调⽤等待的函数则唤醒
void QueueInLoop(Function&& func);
// 把fd和绑定的事件注册到epoll内核事件表
void PollerAdd(std::shared_ptr<Channel> channel, int timeout = 0);
// 在epoll内核事件表修改fd所绑定的事件
void PollerMod(std::shared_ptr<Channel> channel, int timeout = 0);
// 从epoll内核事件表中删除fd及其绑定的事件
void PollerDel(std::shared_ptr<Channel> channel);
// 只关闭连接(此时还可以把缓冲区数据写完再关闭)
void ShutDown(std::shared_ptr<Channel> channel);
bool is_in_loop_thread();
private:
// 创建eventfd 类似管道的 进程间通信⽅式
static int CreateEventfd();
void HandleRead(); // eventfd的读回调函数(因为event_fd写了数据,所以触发可读事件,从event_fd读数据)
void HandleUpdate(); // eventfd的更新事件回调函数(更新监听事件)
void WakeUp(); // 异步唤醒SubLoop的epoll_wait(向event_fd中写⼊数据)
void PerformPendingFunctions(); // 执⾏正在等待的函数(SubLoop注册EpollAdd连接套接字以及绑定事件的函数)
private:
std::shared_ptr<Poller> poller_; // io多路复⽤ 分发器
int event_fd_; // ⽤于异步唤醒 SubLoop 的 Loop 函数中的Poll(epoll_wait因为还没有注册fd会⼀直阻塞)
std::shared_ptr<Channel> wakeup_channel_; // ⽤于异步唤醒的 channel
pid_t thread_id_; // 线程id
mutable locker::MutexLock mutex_;
std::vector<Function> pending_functions_; // 正在等待处理的函数
bool is_stop_; // 是否停⽌事件循环
bool is_looping_; // 是否正在事件循环
bool is_event_handling_; // 是否正在处理事件
bool is_calling_pending_functions_; // 是否正在调⽤等待处理的函数
};
从 EventLoop 的类定义中可以看出,除了⼀些状态量以外,每个 EventLoop 持有⼀个 Poller 的智能指针(对epoll / poll 的封装),⼀个⽤于 EventLoop 之间通信的 Channel ,⾃⼰的线程 id,互斥锁以及装有等待处理函数的 vector 。很明显,EventLoop 并不直接管理各个连接的 Channel (⽂件描述符的封装),⽽是通过Poller 来进⾏的。 EventLoop 中最核⼼的函数就是 EventLoop::Loop() 。
void EventLoop::Loop() {
// 开始事件循环 调⽤该函数的线程必须是该EventLoop所在线程
assert(!is_looping_);
assert(is_in_loop_thread());
is_looping_ = true;
is_stop_ = false;
while (!is_stop_) {
// 1、epoll_wait阻塞 等待就绪事件
auto ready_channels = poller_->Poll();
is_event_handling_ = true;
// 2、处理每个就绪事件(不同channel绑定了不同的callback)
for (auto& channel : ready_channels) {
channel->HandleEvents();
}
is_event_handling_ = false;
// 3、执⾏正在等待的函数(fd注册到epoll内核事件表)
PerformPendingFunctions();
// 4、处理超时事件 到期了就从定时器⼩根堆中删除(定时器析构会EpollDel掉fd)
poller_->HandleExpire();
}
is_looping_ = false;
}
每个 EventLoop 对象都唯⼀绑定了⼀个线程,这个线程其实就在⼀直执⾏这个函数⾥⾯的 while 循环,这个while 循环的⼤致逻辑⽐较简单。就是调⽤ Poller::poll() 函数获取事件监听器上的监听结果。接下来在Loop ⾥⾯就会调⽤监听结果中每⼀个 Channel 的处理函数 HandlerEvent() 。每⼀个 Channel 的处理函数会根据 Channel 中封装的实际发⽣的事件,执⾏ Channel 中封装的各事件处理函数。(⽐如⼀个 Channel 发⽣了可读事件,可写事件,则这个 Channel 的 HandlerEvent() 就会调⽤提前注册在这个 Channel 的可读事件和可写事件处理函数,⼜⽐如另⼀个 Channel 只发⽣了可读事件,那么 HandlerEvent() 就只会调⽤提前注册在这个 Channel 中的可读事件处理函数。
从中可以看到,每个 EventLoop 实际上就做了四件事
- epoll_wait阻塞 等待就绪事件(没有注册其他fd时,可以通过event_fd来异步唤醒)
- 处理每个就绪事件
- 执⾏正在等待的函数(fd注册到epoll内核事件表)
- 处理超时事件,到期了就从定时器⼩根堆中删除
Channel
在 TCP ⽹络编程中,想要通过 IO 多路复⽤(epoll / poll)监听某个⽂件描述符,就需要把这个 fd 和该 fd 感兴趣的事件通过 epoll_ctl 注册到 IO 多路复⽤模块上。当 IO 多路复⽤模块监听到该 fd 发⽣了某个事件。事件监听器返回发⽣事件的 fd 集合(有哪些 fd 发⽣了事件)以及每个 fd 的事件集合(每个 fd 具体发⽣了什么事件)。
Channel 类则封装了⼀个 fd 和这个 fd 感兴趣事件以及 IO 多路复⽤模块监听到的每个 fd 的事件集合。同时Channel类还提供了设置该 fd 的感兴趣事件,以及将该fd及其感兴趣事件注册到事件监听器或从事件监听器上移除,以及保存了该 fd 的每种事件对应的处理函数。
每个 Channel 对象只属于⼀个 EventLoop ,即只属于⼀个 IO 线程。只负责⼀个⽂件描述符(fd)的 IO 时间分发,但不拥有这个 fd。 Channel 把不同的 IO 事件分发为不同的回调,回调⽤ C++11 的特性 function 表示。声明周期由拥有它的类负责。
class Channel {
public:
typedef std::function<void()> EventCallBack;
Channel();
explicit Channel(int fd);
~Channel();
// IO事件回调函数的调⽤接⼝
// EventLoop中调⽤Loop开始事件循环 会调⽤Poll得到就绪事件
// 然后依次调⽤此函数处理就绪事件
void HandleEvents();
void HandleRead(); // 处理读事件的回调
void HandleWrite(); // 处理写事件的回调
void HandleUpdate(); // 处理更新事件的回调
void HandleError(); // 处理错误事件的回调
int get_fd();
void set_fd(int fd);
// 返回weak_ptr所指向的shared_ptr对象
std::shared_ptr<http::HttpConnection> holder();
void set_holder(std::shared_ptr<http::HttpConnection> holder);
// 设置回调函数
void set_read_handler(EventCallBack&& read_handler);
void set_write_handler(EventCallBack&& write_handler);
void set_update_handler(EventCallBack&& update_handler);
void set_error_handler(EventCallBack&& error_handler);
void set_revents(int revents);
int& events();
void set_events(int events);
int last_events();
bool update_last_events();
private:
int fd_; // Channel的fd
int events_; // Channel正在监听的事件(或者说感兴趣的时间)
int revents_; // 返回的就绪事件
int last_events_; // 上⼀此事件(主要⽤于记录如果本次事件和上次事件⼀样 就没必要调⽤
epoll_mod)
// weak_ptr是⼀个观测者(不会增加或减少引⽤计数),同时也没有重载->,和*等运算符 所以不能直接使⽤
// 可以通过lock函数得到它的shared_ptr(对象没销毁就返回,销毁了就返回空shared_ptr)
// expired函数判断当前对象是否销毁了
std::weak_ptr<http::HttpConnection> holder_;
EventCallBack read_handler_;
EventCallBack write_handler_;
EventCallBack update_handler_;
EventCallBack error_handler_;
};
从 Channel 的类定义中可以看出,每个 Channel 持有⼀个⽂件描述符,正在监听的事件,已经发⽣的事件(由Poller 返回),以及各个事件(读、写、更新、错误)回调函数的 Function 对象。
总的来说, Channel 就是对 fd 事件的封装,包括注册它的事件以及回调。 EventLoop 通过调⽤Channel::handleEvent() 来执⾏ Channel 的读写事件。 Channel::handleEvent() 的实现也⾮常简单,就是⽐较已经发⽣的事件(由 Poller 返回),来调⽤对应的回调函数(读、写、错误)。
// IO事件的回调函数 EventLoop中调⽤Loop开始事件循环 会调⽤Poll得到就绪事件
// 然后依次调⽤此函数处理就绪事件
void Channel::HandleEvents() {
events_ = 0;
// 触发挂起事件 并且没触发可读事件
if ((revents_ & EPOLLHUP) && !(revents_ & EPOLLIN)) {
events_ = 0;
return;
}
// 触发错误事件
if (revents_ & EPOLLERR) {
HandleError();
events_ = 0;
return;
}
// 触发可读事件 | ⾼优先级可读 | 对端(客户端)关闭连接
if (revents_ & (EPOLLIN | EPOLLPRI | EPOLLRDHUP)) {
HandleRead();
}
// 触发可写事件
if (revents_ & EPOLLOUT) {
HandleWrite();
}
//处理更新监听事件(EpollMod)
HandleUpdate();
}
Channel 这个类是不是很像⽂件描述符的保姆呢 😃
Poller
Poller 类的作⽤就是负责监听⽂件描述符事件是否触发以及返回发⽣事件的⽂件描述符以及具体事件。所以⼀个Poller 对象对应⼀个 IO 多路复⽤模块。在 muduo 中,⼀个 EventLoop 对应⼀个 Poller 。
class Epoll {
public:
Epoll();
~Epoll();
void epoll_add(const sp_Channel& request);
void epoll_mod(const sp_Channel& request);
void epoll_del(const sp_Channel& request);
void poll(std::vector<sp_Channel>& req);
private:
int epollFd_;
std::vector<epoll_event> events_; // epoll_wait()返回的活动事件都放在这个数组⾥
std::unordered_map<int, sp_Channel> channelMap_;
};
Poller 的主要成员变量就三个:
- epollFd_ :就是⽤ epoll_create ⽅法返回的 epoll 句柄,这个是常识。
- events_ :存放 epoll_wait() 返回的活动事件(是⼀个结构体)
- channelMap_ :这个变量是 std::unordered_map<int, std::shared_ptr> 类型,负责记录⽂件描述符fd -> Channel 的映射,也帮忙保管所有注册在你这个 Poller 上的 Channel 。
其他函数⽆⾮就是对 Epoll_ctl(4) 和 Epoll_wait(4) 的封装。
void Epoll::poll(std::vector<sp_Channel>& req) {
int event_count =
Epoll_wait(epollFd_, &*events_.begin(), events_.size(), EPOLLWAIT_TIME);
for(int i = 0; i < event_count; ++i) {
int fd = events_[i].data.fd;
sp_Channel temp = channelMap_[fd];
temp->setRevents(events_[i].events);
req.emplace_back(std::move(temp));
}
// LOG << "Epoll finished";
}
Epoll::poll(1) 这个函数可以说是 Poller 的核⼼了,当外部调⽤ poll ⽅法的时候,该⽅法底层其实是通过epoll_wait 获取这个事件监听器上发⽣事件的 fd 及其对应发⽣的事件,我们知道每个 fd 都是由⼀个Channel封装的,通过哈希表 channelMap_ 可以根据 fd 找到封装这个 fd 的 Channel 。将 IO 多路复⽤模块监听到该 fd 发⽣的事件写进这个 Channel 中的 revents 成员变量中。然后把这个 Channel 装进 req 中。这样,当外界调⽤完poll 之后就能拿到 IO 多路复⽤模块的监听结果( std::vector<sp_Channel>& req )。
⽇志系统
服务器的⽇志系统是⼀个多⽣产者,单消费者的任务场景:多⽣产者负责把⽇志写⼊缓冲区,单消费者负责把缓冲区中数据写⼊⽂件。如果只⽤⼀个缓冲区,不光要同步各个⽣产者,还要同步⽣产者和消费者。⽽且最重要的是需要保证⽣产者与消费者的并发,也就是前端不断写⽇志到缓冲区的同时,后端可以把缓冲区写⼊⽂件。
- LOG 的实现参照了 muduo,但是⽐ muduo 要简化⼀点,⼤致的实现如上图所示,看图还是⽐较好懂的。⾸先是 Logger 类, Logger 类⾥⾯有 Impl 类,其实具体实现是 Impl 类,我也不懂muduo为何要再封装⼀层,那么我们来说说 Impl ⼲了什么,在初始化的时候 Impl 会把时间信息存到 LogStream 的缓冲区⾥,在我们实际⽤ Log 的时候,实际写⼊的缓冲区也是 LogStream ,在析构的时候 Impl 会把当前⽂件和⾏数等信息写⼊到 LogStream ,再把 LogStream ⾥的内容写到 AsyncLogging 的缓冲区中,当然这时候我们要先开启⼀个后端线程⽤于把缓冲区的信息写到⽂件⾥。
- LogStream 类,⾥⾯其实就⼀个 Buffer 缓冲区,是⽤来暂时存放我们写⼊的信息的。还有就是重载运算符,因为我们采⽤的是 C++ 的流式⻛格。AsyncLogging 类,最核⼼的部分,在多线程程序中写 Log ⽆⾮就是前端往后端写,后端往硬盘写,⾸先将LogStream 的内容写到了 AsyncLogging 缓冲区⾥,也就是前端往后端写,这个过程通过 append 函数实现,后端实现通过 threadfunc 函数,两个线程的同步和等待通过互斥锁和条件变量来实现,具体实现使⽤了双缓冲技术。
- 双缓冲技术的基本思路:准备两块 buffer,A 和 B,前端往 A 写数据,后端从 B ⾥⾯往硬盘写数据,当 A 写满后,交换 A 和 B,如此反复。使⽤两个 buffer 的好处是在新建⽇志消息的时候不必等待磁盘⽂件操作,也避免每条新⽇志消息都触发后端⽇志线程。换句话说,前端不是将⼀条条⽇志消息分别送给后端,⽽是将多条⽇志消息拼接成⼀个⼤的 buffer 传送给后端,相当于批处理,减少了线程唤醒的开销。不过实际的实现的话和这个还是有点区别,具体看代码吧。
我们⾸先看下 logStream 类的实现, logStream 类的主要作⽤是将各个类型的数据转换为 char 的形式放⼊字符
数组中(也就是前端⽇志写⼊ Buffer A 的这个过程),⽅便后端线程写⼊硬盘。
class LogStream : noncopyable {
typedef LogStream self;
public:
typedef FixedBuffer<kSmallBuffer> Buffer;
self& operator<< (bool v) {
buffer_.append(v ? "1" : "0", 1);
return *this;
}
self& operator<< (short);
self& operator<< (unsigned short);
self& operator<< (int);
self& operator<< (unsigned int);
self& operator<< (long);
self& operator<< (unsigned long);
self& operator<< (long long);
self& operator<< (unsigned long long);
self& operator<< (const void*);
self& operator<< (float v) {
*this << static_cast<double>(v);
r