mudou网络库思想理解
Reactor与多线程
服务器构建过程中,不仅仅使用一个Reactor,而是使用多个Reactor,每个Reactor执行自己专属的任务,从而提高响应效率。
首先Reactor是一种事件驱动处理模式,其主要通过IO多路复用机制统一监听想要关心的事件,如果关心的事件有响应后,则将该响应事件分发给进程或者线程去处理,从而提高网络服务器的性能。简单可以理解为服务器利用该模式处理多路请求,然后将它们同步的分发给线程或者进程去处理。
其次构建服务器可以使用多个Reactor,从而来提高服务器对事件的响应效率。将负责连接与客户端通信监控分离开,则是一个Reactor专门负责连处理新连接的请求事件,如果发现有新的连接,则将其分发给子Reactor中去监控,另一个Reactor则主要负责监控客户端是否发送了请求或者其他事件,如果有事件触发,则将其交给线程池处理。
主线Reactor处理新连接 ---->交给子Reactor进行监控 ——>事件发生交给线程处理
最后,Reactor需要将待处理的任务交付给线程或者进程去处理,如果想要使得服务器效率高,肯定要避免线程或者进程的频繁的创建销毁,这样会占用服务器的性能。所以构建线程池与Reactor配合,有助于减少性能消耗。
任务线程池的主要作用则是分配独立的线程去执行Reactor中需要处理的任务,最后将处理好的数据交给Reactor线程,让其完成对客户端的响应。
One Loop Per One Thread
主Reactor负责获取连接,获取新连接后,将新连接分发给子Reactor进行网络通信的事件处理。子Reactor监控各个描述符下的读写等事件,当事件响应的时候分发给线程池中的线程去处理。
主要思想是将某一事件所有的操作放在一个线程中进行,即一个线程对应着一种事件的处理,而每个 Reactor都可以使用线程池让其为自己服务,从而可以并发的实现请求的响应。(每个线程与一个独立的事件循环绑定)
核心思想分析
- 事件驱动
- 每个线程都绑定一个事件循环,处理一系列非阻塞的IO事件
- 事件循环,则是通过事件队列接收事件,同事根据事件类型和回调函数执行该事件对应的操作
- 线程与事件绑定
- 线程间独立运行自己的事件循环,而不会与其他线程共享事件循环
- 这样的好处在于,避免了线程竞争,从而提高并发效率
- 非阻塞I/O
- 事件循环的过程中,I/O操作都是非阻塞的,这样一来,线程就不会因为等待I/O操作,而阻塞等待
- 实现一个线程在同一时间内,可以处理多个I/O请求,从而提高系统的吞吐量
- 任务调度与并发
- 任务被分发到不同的线程,各个线程可以独立处理自己的任务序列
- 通过该方式,可以借助负载均衡机制,确保各个线程的工作量均衡,从而防止某个线程过载。
- 编程简化
- 一个线程对应一个事件循环,所以并发程序的设计过程中,无需考虑线程同步与锁的机制
- 只需要关注如何处理事件以及回调函数即可
一个线程绑定的事件在服务器中通常都是I/O事件
- 连接事件
- 建立连接:客户端尝试连接服务器的时候,服务器的监听线程会收到一个连接建立的事件(Accept)。
- 服务器接收这个连接后,将其分配一个工作线程去处理。(在该网络库的思想中,服务器接收连接后,会将其交给子Reactor监控,子Reactor监控到事件发生后,再从线程池中调取线程去执行)
- 读事件
- 数据到达:客户端发送请求数据到达服务器的时候,服务器的线程收到一个读事件,此时监控读事件的Reactor开始响应。从线程池中调用线程去读取数据,解析HTTP请求,并根据请求的内容做出对应处理
- 写事件
- 数据可写事件:服务器准备好响应数据,并准备将该响应数据发送给客户端的时候,服务器此时会触发写事件,表示可以将数据写入到客户端的连接中。
- 关闭事件
- 连接关闭事件:客户端或者服务端关闭连接的时候,触发这个关闭事件,服务器需要清理相应的资源,确保连接正常关闭,避免该连接浪费服务器资源。
项目改进架构思路
服务器架构通过多线程和事件驱动的架构来高效的处理大量并发请求。服务器同时采用非阻塞I/O模式,避免了阻塞操作带来的性能瓶颈,从而让服务器可以高效的处理大量并发请求。
主要模块功能逻辑
Main Reactor
- 初始化
- 创建EventLoop实例
- 创建并初始化Acceptor,设置监听端口和回调函数
- 接受新连接
- Acceptor中,监听新连接请求
- 使用accept系统调用,同时调用回调函数,为新连接进行对应封装
- 分发新连接
- 主Reactor将新连接分配给Reactor,创建Channel对象管理连接
Sub Reactor
- 初始化
- 创建EventLoop对象
- 创建独立线程去运行EventLoop(一个线程一个Reactor思想)
- 处理I/O事件
- EventLoop中通过EPOLL等待关心事件发生
- 将发生的事件分配给Channel对象
- 事件处理
- Channel根据事件的类型,调用注册的回调函数
- 读事件处理:读取数据并解析HTTP请求
- 写事件处理:发送HTTP响应
Epoller逻辑
- 初始化
- 创建EPOLL实例
- 管理文件描述符
- 添加、更新和删除文件描述符
- 等待监控事件发生
- 利用epoll_wait 等待事件的发生
HttpServer逻辑
- 初始化
- 创建HttpServer实例,同时继承TcpServer的功能
- 注册HTTP请求处理函数
- 处理HTTP请求
- 解析HTTP请求,生成HttpRequsest对象
- 根据请求的路径调用相应的处理函数
- 生成并发送响应
- 调用处理函数后生成HttpResponse对象
- 将响应转化为字符串的形式发送给客户端
Main逻辑
- 注册处理函数
- 注册URL路径以及对应的处理函数
- 处理具体业务逻辑
- 根据具体的需求,处理HTTP请求并生成响应
代码架构设计
Server
#ifndef SERVER_HPP
#define SERVER_HPP
#include <vector>
#include <functional>
#include <memory>
#include <sys/epoll.h>
#include <unistd.h>
class Channel;
class EventLoop;
class Epoller {
private:
int _epollFd;
std::vector<epoll_event> _events;
public:
Epoller();
~Epoller();
void UpdateChannel(Channel* channel);
void RemoveChannel(Channel* channel);
void Poll(std::vector<Channel*>& activeChannels);
};
class Channel {
private:
EventLoop* _loop;
const int _fd;
uint32_t _events;
uint32_t _revents;
std::function<void()> _readCallback;
std::function<void()> _writeCallback;
public:
Channel(EventLoop* loop, int fd);
void SetReadCallback(const std::function<void()>& cb);
void SetWriteCallback(const std::function<void()>& cb);
void EnableReading();
void EnableWriting();
void DisableWriting();
void DisableAll();
void Remove();
void HandleEvent();
void Update();
int Fd() const;
uint32_t Events() const;
uint32_t Revents() const;
void SetRevents(uint32_t revents);
};
class EventLoop {
private:
bool _quit;
std::vector<Channel*> _activeChannels;
Epoller _poller;
public:
EventLoop();
void Loop();
void Quit();
void UpdateChannel(Channel* channel);
void RemoveChannel(Channel* channel);
};
class Acceptor {
private:
EventLoop* _loop;
int _listenFd;
std::function<void(int)> _newConnectionCallback;
public:
Acceptor(EventLoop* loop, int port);
void SetAcceptCallback(const std::function<void(int)>& cb);
void Listen();
void HandleRead();
};
class SubReactor {
private:
EventLoop _loop;
std::thread _thread;
public:
SubReactor();
void Run();
EventLoop* GetLoop();
void Join();
};
class MainReactor {
private:
EventLoop _loop;
Acceptor _acceptor;
std::vector<SubReactor*> _subReactors;
int _nextReactor;
public:
MainReactor(int port, int subReactorCount);
void NewConnection(int fd);
void Run();
void JoinSubReactors();
};
#endif // SERVER_HPP
HTTP
#ifndef HTTP_HPP
#define HTTP_HPP
#include "server.hpp"
#include <unordered_map>
#include <functional>
#include <string>
class HttpRequest {
public:
std::string _method;
std::string _path;
std::string _version;
std::unordered_map<std::string, std::string> _params;
std::unordered_map<std::string, std::string> _headers;
std::string _body;
// 解析请求
};
class HttpResponse {
public:
int _status;
std::unordered_map<std::string, std::string> _headers;
std::string _body;
void SetContent(const std::string& content, const std::string& type) {
_body = content;
_headers["Content-Type"] = type;
}
// 响应
};
class HttpServer : public TcpServer {
private:
std::unordered_map<std::string, std::function<void(const HttpRequest&, HttpResponse*)>> _handlers;
public:
HttpServer(int port);
void RegisterHandler(const std::string& path, const std::function<void(const HttpRequest&, HttpResponse*)>& handler);
void OnMessage(const PtrConnection& conn, const std::string& message);
};
#endif // HTTP_HPP
main
#include "http.hpp"
#define WWWROOT "./wwwroot/"
int main() {
HttpServer server(8888);
server.Start();
return 0;
}
细节问题梳理
Reactor、Channel|、epoll三者结合
多Reactor模型中三者结合分析
- 每个Reactor都拥有自己的epoll实例:具体也就是每个EventLoop(Reactor)都拥有自己属于自己的epoll实例来管理文件描述符和就绪事件
- 每个Channel对象与一个文件描述符和一个Reactor(EventLoop)关联:新连接到来时,主Reactor接受新连接请求后,将文件描述符分配给子Reactor,子Reactor创建一个Channel对象来管理文件描述符
- 事件循环独立运行:每个EventLoop都是独立运行,用于管理文件描述符上关心的事件
新连接创建后其文件描述符作用
- 主Reactor接收新连接后,accept会返回这个连接的文件描述符,然后将其分发给对应的子Reactor中
- 子Reactor管理连接:Channel类负责将文件描述符上的(读写)事件通知到对应的处理函数
进程与线程关系梳理/新连接文件描述符管理问题
- 首先,服务器启动后作为一个进程运行,多个Reactor则是该进程下的线程
- 主Reactor和子Reactor都是在服务器下作为线程执行
- 每个Reactor线程有自己的文件描述符,管理客户端连接(自己需要关心的特定事件)
- 新连接到来后,内核又会为新连接创建一个新的文件描述符
- 主Reactor又会将这个新获取的文件描述符分发给子Reactor中进行管理和处理
线程池和和任务队列的处理
- 主Reactor接收新连接后,将新连接分发给子Reactor中进行管理和处理
- 子Reactor处理I/O事件后,将其放入任务队列
- 线程池中的线程取出任务并执行任务
- 线程池与事件循环放在一起,子Reactor线程将任务交给线程池处理即可
请求到响应流程分析
- 服务器初始化
- 监听端口;设置线程数量(Reactor数量)注册处理函数
- 启动服务器接口
- 接收新连接
- 主Reactor监听新连接,新连接到达后通过NewConnetion方法将连接分发给从Reactor中进行管理
- 分发连接
- 主Reactor借助LoopThreadPool将新连接交给子Reactor中的EventLoop中进行处理
- 处理请求
- 子Reactor线程的EventLoop处理分配到的连接
- 读取请求数据,同时将其解析为HttpRequest对象
- 调用注册的处理函数对请求进行处理
- 生成响应
- 处理数据,根据传入的请求,生成HttpResponse对象,同时设置响应内容
- 发送响应
- 将生成的响应,转换为字符串格式,发送给客户端
- 关闭连接
- 处理请求和响应后,将连接放入到连接池中备用