muduo库剖析(2)
项目地址:https://github.com/huerni/cgmuduo.git
muduo是一个基于非阻塞IO和事件驱动的C++网络库,采用Reactor模式和one loop per thread + thread pool线程模型。
类功能介绍
Channel.*
,Poller.*
,EventLoop.*
三类为一体,在底层负责事件循环。EventLoop包含Channel,Poller,EventLoop负责轮询访问Poller,得到激活Channel列表,使Channel自己根据自身情况调用相应回调。
Thread.*
,EventLoopThread.*
,EventLoopThreadPool.*
将线程与EventLoop绑定在一起,实现one loop per thread + thread pool线程模型。
TcpServer.*
,Acceptor.*
,TcpConnection.*
为上层Reactor模式的实现。TcpServer面向用户,可由用户自定义回调方法以及启动服务器。Acceptor也看做分发器,当有新连接到来时,用来将连接转发给子线程处理。TcpConnection在子线程处理被分来的连接的具体事件。
执行流程
- 用户创建TcpServer对象调用
start()
开启服务器,启动线程池,启动一个baseLoop(主线程),由runInLoop()
处理Acceptor(Reactor)的listen()
方法,进行监听操作; - 一旦有客户端请求连接,Accetor调用TcpServer设置的newConnectionCallback_,将请求分发给subLoop(子线程),即从线程池中取出,然后将连接丢给子线程,由TcpConnection处理,此后该连接的所有操作都由该子线程独自处理,与其他线程无关。
- TcpConnection将读,写,关闭,错误操作与回调填充至绑定的Channel,然后调用
runInLoop()
和queueInLoop()
由底层事件循环处理。 - 底层事件循环中,主线程称为baseLoop,只执行监听操作与用户回调。而子线程具体执行操作由Channel具体返回的事件所执行。由图所示,蓝色部分由三类组成,EventLoop,Channel和Poller。每个EventLoop分别与各自的线程绑定,同时将Poller和Channel连接起来,调用loop()方法将Channel的结果送入Poller执行,将Poller返回的结果各自送入Channel执行。
核心模块 |
---|
Socket |
Buffer |
Channel |
Poller与Epoller |
EventLoop与EventLoopThread |
EventLoopThreadPool |
Acceptor |
TcpConnection |
TcpServer |
Socket类
Socket类是对socket套接字的一层封装。
class Socket : noncopyable{
public:
explicit Socket(int sockfd);
~Socket();
int fd() const { return sockfd_; }
void bindAddress(const InetAddress &localaddr); // 绑定地址
void listen(); // 监听函数
int accept(InetAddress *peeraddr); // 接收客户端
void shutdownWrite(); // 关闭写端
// 设置套接字属性
void setTcpNoDelay(bool on); // TCP_NODELAY 控制是否开启Nagle算法
void setReuseAddr(bool on); // SO_REUSEADDR 地址是否可重用
void setReusePort(bool on); // SO_REUSEPORT 允许将多个socket绑定到相同的地址和端口
void setKeepAlive(bool on); // SO_KEEPALIVE 用于开启或者关闭保活探测
private:
const int sockfd_; // 套接字fd
};
Buffer类
Buffer类为自定义缓冲区类,作为读写缓冲区。
buffer分三个部分,预留空间,可读区,可写区。
使用vector<char>
数据结构作为缓冲区,因为vector长度可变,且随机下标复杂度低,比较灵活。使用两个int索引来维护读位置和写位置。
为什么使用int索引而不是迭代器或指针来维护?
一旦vector扩容,迭代器会失效。
为了解决粘包问题,添加了8字节的预留空间。当收到网络信息时,可以计算长度并写入到预留空间中。
class Buffer{
public:
static const size_t kCheapPrepend = 8; // 预留空间大小
static const size_t kInitalSize = 1024; // buffer初始大小
explicit Buffer(size_t initialSize = kInitalSize) : buffer_(kCheapPrepend+initialSize)
, readerIndex_(kCheapPrepend)
, writerIndex_(kCheapPrepend)
{}
// 可读数据大小,即缓存区内已存数据大小
size_t readableBytes() const;
// 可写数据大小,即缓冲区剩余空间
size_t writableBytes() const;
// 可读位置之前的大小(包含预留空间+已读空间)
size_t prependableBytes() const;
// 可读数据起始地址
const char* peek() const;
// 读取后调整读写指针
void retrieve(size_t len);
// 将读写指针归为初始
void retrieveAll();
// 将缓存区内所有数据转为string
std::string retrieveAllAsString();
// 将指定长度可读数据转为string
std::string retrieveAsString(size_t len);
// 确保是否有足够空间可写,如果没有,扩充空间
void ensureWriteableBytes(size_t len);
// 将len长度data写入缓存区
void append(const char* data, size_t len);
// 可写数据起始地址
char* beginWrite();
const char* beginWrite() const;
// 从fd上读数据
ssize_t readFd(int fd, int* saveErrno);
// 写入数据到fd中
ssize_t writeFd(int fd, int* saveErrno);
private:
char* begin(); // buffer起始地址
const char* begin() const;
// 扩充空间
void makeSpace(size_t len);
std::vector<char> buffer_; // 使用vector作为缓冲区
size_t readerIndex_; // 读位置
size_t writerIndex_; // 写位置
};
从fd中读数据时,需要判断两种情况:
- 缓冲区可写空间足够,所以可以将数据全部读入缓冲区
- 可写空间不足,巧妙的办法是申请一段栈内缓冲区放入额外的数据,这样只需要调用一次readv函数就能得到全部数据,减少了IO开销。然后将数据从栈内缓冲区转移到buffer中。
ssize_t Buffer::readFd(int fd, int* saveErrno) {
char extraBuffer[65536] = {0};
struct iovec vec[2];
const size_t writable = writableBytes();
vec[0].iov_base = begin() + writerIndex_;
vec[0].iov_len = writable;
vec[1].iov_base = extraBuffer;
vec[1].iov_len = sizeof extraBuffer;
const int iovcnt = (writable < sizeof extraBuffer) ? 2 : 1;
const ssize_t n = ::readv(fd, vec, iovcnt);
if(n < 0) {
*saveErrno = errno;
}
else if(n <= writable) {
writerIndex_ += n;
}
else {
writerIndex_ = buffer_.size();
// 此时会扩充空间
append(extraBuffer, n - writable);
}
return n;
}
写入数据到fd中,只需要将buffer中所有可读数据发送出去即可
ssize_t Buffer::writeFd(int fd, int* saveErrno) {
ssize_t n = ::write(fd, peek(), readableBytes());
if(n < 0) {
saveErrno = &errno;
}
return n;
}
当调用append空间不足时,会调用makeSpace扩充空间。
void makeSpace(size_t len) {
// 当可写空间+已读空间不足时,扩充vector长度
// (因为可读位置每次读取是向后走的,这样前面会有一段不可用但空白区域,类似线性队列)
if(writableBytes() + prependableBytes() < len + kCheapPrepend) {
// 可写大小只扩充到len
buffer_.resize(writerIndex_ + len);
}
// 否则不需要扩充,只要将可读数据向前移动即可。
else {
size_t readable = readableBytes();
std::copy(begin() + readerIndex_, begin() + writerIndex_, begin() + kCheapPrepend);
readerIndex_ = kCheapPrepend;
writerIndex_ = readerIndex_ + readable;
}
}
Channel类
Channel封装了socket对应的event,并设置了一系列修改event的回调函数。
muduo中,Channel只设置两种事件,即读事件和写事件,包含三种状态,分为是否可读,是否可写,是否关注事件。
epoll有自己关注的一系列事件,根据Channel所对应的event进行操作或对Channel对应的event进行修改。所以,为了方便poller对Channel进行处理,Channel必须包含更新自身状态,返回自身状态,根据自身状态处理对应的回调一系列方法。
class Channel : noncopyable {
public:
// 回调函数
using EventCallback = std::function<void()>;
using ReadEventCallback = std::function<void(Timestamp)>;
Channel(EventLoop* loop, int fd);
~Channel();
// 处理事件的接口
void HandleEvent(Timestamp receiveTime);
// 设置回调函数,有读,写,关闭,错误事件
void setReadCallback(ReadEventCallback cb);
void setWriteCallback(EventCallback cb);
void setCloseCallback(EventCallback cb);
void setErrorCallback(EventCallback cb);
// 绑定指针,将弱指针转为共享指针
// 防止channel被手动关闭时,channel还在执行回调函数
void tie(const std::shared_ptr<void>& );
// 返回socket对应的event状态
int fd() const { return fd_; }
int events() const { return events_; }
// 更新event
void set_revents(int revt) { revents_ = revt; }
// 更新fd感兴趣的事件events
void enableReading();
void disableReading();
void enableWriting();
void disableWriting();
void disableAll();
// 判断socket对应event种类
bool isNoneEvent() const;
bool isWriteEvent() const;
bool inReadEvent() const;
int index() { return index_; }
void set_index(int idx) { index_ = idx; }
// 返回所属于的loop
EventLoop* ownerLoop() { return loop_; }
// 从loop中删除event
void remove();
private:
// 通知所属的eventloop更新events
void update();
// 根据事件event类型进行相应的处理
void handleEventWithGuard(Timestamp receiveTime);
// 三种事件
static const int KNoneEvent;
static const int KReadEvent;
static const int KWriteEvent;
// muduo采用one loop per thread策略,所以一个Channel自始至终只属于一个eventLoop类,且只属于一个IO线程。
EventLoop *loop_; // 事件循环
const int fd_; // 绑定的fd,poller监听的对象
int events_; // 注册fd感兴趣的事件
int revents_; // poller返回的具体发生的事件
int index_;
std::weak_ptr<void> tie_;
bool tied_;
// 四种回调函数
// channel通道里面能够获知fd最终发生的具体的事件events,所以它负责调用具体的回调函数
ReadEventCallback readCallback_;
EventCallback writeCallback_;
EventCallback closeCallback_;
EventCallback errorCallback_;
};
当poller返回激活的Channel时,该Channel必须根据自身的状态进行对应的用户回调。
void Channel::handleEventWithGuard(Timestamp receiveTime) {
// 对方已关闭,调用关闭回调
if((revents_ & EPOLLHUP) && !(revents_ & EPOLLIN)) {
if(closeCallback_) {
closeCallback_();
}
}
// 发生异常,调用异常回调
if(revents_ & EPOLLERR) {
if(errorCallback_) {
errorCallback_();
}
}
// 读事件,调用读取回调
if(revents_ & (EPOLLIN | EPOLLPRI)) {
if(readCallback_) {
readCallback_(receiveTime);
}
}
// 写事件,调用写回调
if(revents_ & EPOLLOUT) {
if(writeCallback_) {
writeCallback_();
}
}
}
Poller类
Poller是一个虚函数,包含了IO复用方法的一些基本接口,可由IO复用方法进行继承扩展。
各种IO复用方法可查看:cg网络编程记录-IO复用
class Poller : noncopyable {
public:
using ChannelList = std::vector<Channel*>;
Poller(EventLoop* loop);
virtual ~Poller() = default;
// poller统一接口,包括操作,更新,删除,由继承实现
virtual Timestamp poll (int timeoutMs, ChannelList *activeChannels) = 0;
virtual void updateChannel(Channel* channel) = 0;
virtual void removeChannel(Channel* channel) = 0;
// 判断通道是否在该IO复用中
bool hasChannel(Channel* channel) const;
// 获取loop默认IO复用具体实现
static Poller* newDefaultPoller(EventLoop* loop);
protected:
// fd => channle实例
using ChannelMap = std::unordered_map<int, Channel*>;
// poller监视的所有channel
ChannelMap channels_;
private:
EventLoop* ownerloop_; // 所属的事件训练loop
};
Epoller类
继承自Poller类,采用epoll方式对socket进行监听。
epoll方式中,每个epoll都对应一个epollfd。首先要调用epoll_create
创建epoll,获取epollfd,后续通过epollfd对该epoll进行操作。然后可以通过cpoll_ctl
对指定的socket进行操作,如add/mod/del (muduo中由Epoller类对Channel类进行操作),添加需要监听的Channel后,调用epoll_wait
开始监听,返回被激活的Channel,然后处理事件。
class EpollPoller : public Poller {
public:
EpollPoller(EventLoop* loop);
~EpollPoller() override;
// 重写基类接口
Timestamp poll (int timeoutMs, ChannelList *activeChannels) override;
void updateChannel(Channel* channel) override;
void removeChannel(Channel* channel) override;
private:
static const int kInitEventListSize = 16;
// 填充活跃通道
void fillActiveChannels(int numEvents, ChannelList* activeChannels) const ;
// 指定更新通道
void update(int operation, Channel *channel);
using EventList = std::vector<epoll_event>;
int epollfd_;
EventList events_;
};
epoll_create
在构造函数中调用
EpollPoller::EpollPoller(EventLoop* loop)
: Poller(loop)
, epollfd_(::epoll_create1(EPOLL_CLOEXEC))
, events_(kInitEventListSize) {
if(epollfd_ < 0) {
LOG_FATAL("epoll_create error:%d \n", errno);
}
}
epoll_wait
封转在update方法中。
// 指定操作更新Channel epoll_ctl具体操作 add/mod/del
void EpollPoller::update(int operation, Channel *channel) {
int fd = channel->fd();
epoll_event event; // epoll常规操作,将所需填入event结构中。
bzero(&event, sizeof event);
event.events = channel->events(); // channel感兴趣的事件
event.data.ptr = (void*)channel;
event.data.fd = fd;
if(::epoll_ctl(epollfd_, operation, fd, &event) < 0) {
if(operation == EPOLL_CTL_DEL) {
LOG_ERROR("epoll_ctl del error: %d \n", errno);
}
else {
LOG_FATAL("epoll_ctl add/mod error: %d \n", errno);
}
}
}
Epoller类保存所有监听的Channel类,所以在更新Channel时,需要一些额外的操作。
void EpollPoller::updateChannel(Channel* channel) {
// 获取Channel在poller的状态 未监听/已监听/已删除
const int index = channel->index();
// 如果poller面对一个新的Channel或者已经删除的Channel
if(index == KNew || index == KDeleted) {
if(index == KNew) {
int fd = channel->fd();
channels_[fd] = channel;
}
channel->set_index(KAdded);
update(EPOLL_CTL_ADD, channel);
}
// Channel在poller的监听列表中
else {
int fd = channel->fd();
// 如果Channel已经对任何事件不感兴趣,则把它剔除
if(channel->isNoneEvent()) {
update(EPOLL_CTL_DEL, channel);
channel->set_index(KDeleted);
}
// 否则,直接更新它的状态
else {
update(EPOLL_CTL_MOD, channel);
}
}
}
Epoller类还应该具备手动剔除Channel功能。
void EpollPoller::removeChannel(Channel* channel) {
int fd = channel->fd();
int index = channel->index();
if(index == KAdded) {
update(EPOLL_CTL_DEL, channel);
}
channels_.erase(fd);
channel->set_index(KNew);
}
设置好以后,Epoll类调用poll开始监听,即封装epoll_wati
Timestamp EpollPoller::poll (int timeoutMs, ChannelList *activeChannels) {
// epoll_wait返回被激活的socket
int numEvents = ::epoll_wait(epollfd_, &*events_.begin(), static_cast<int>(events_.size()), timeoutMs);
int saveErrno = errno;
Timestamp now(Timestamp::now());
// 返回的新的事件填充至对应的Channel
if(numEvents > 0) {
LOG_INFO("%d events happened \n", numEvents);
fillActiveChannels(numEvents, activeChannels);
if(numEvents == events_.size()) {
events_.resize(numEvents * 2);
}
}
else if(numEvents == 0) {
LOG_INFO("%s timeout! \n", __FUNCTION__);
}
else {
if(saveErrno != EINTR) {
errno = saveErrno;
LOG_ERROR("EpollPoller::poll() err!");
}
}
return now;
}
void EpollPoller::fillActiveChannels(int numEvents, ChannelList* activeChannels) const {
// 修改被激活的Channel的状态
for(int i = 0; i<numEvents; ++i) {
Channel* channel = static_cast<Channel*>(events_[i].data.ptr);
LOG_INFO("channel: %d", channel->fd());
channel->set_revents(events_[i].events);
activeChannels->push_back(channel); // eventloop拿到了poller返回的所有事件的channel列表
}
}
最终获取所有被激活且已经修改的Channel,即activeChannels。
EventLoop类
一个EventLoop与一个线程绑定,包含Poller与Channle两大模块,EventLoop的主要作用是在运行过程中进行事件循环,调用Poller执行wait,使得激活Channel执行自己的回调事件,同时执行每个EventLoop自己需要执行的回调,比如监听socket等。
class EventLoop : noncopyable{
public:
using Functor = std::function<void()>;
EventLoop();
~EventLoop();
// 开启事件循环
void loop();
// 退出事件循环
void quit();
// 在当前loop中执行cb
void runInLoop(Functor cb);
// 把cb放入队列中,唤醒loop所在线程,执行cb
void queueInLoop(Functor cb);
// 唤醒loop所在线程
void wakeup();
// EventLoop方法 =》 Poller方法
void updateChannel(Channel* channel);
void removeChannel(Channel* channel);
bool hasChannel(Channel* channel);
// 判断EventLoop对象是否在自己线程里面
bool isInLoopThread() const { return threadId_ == CurrentThread::tid(); }
private:
// 通过接收wakeupFd消息来唤醒自己
void handleRead(); // wake up
void doPendingFunctors(); // 执行回调
using ChannelList = std::vector<Channel*>;
std::atomic_bool looping_; // 原子操作 判断loop是否在事件循环
std::atomic_bool quit_; // 标识退出loop循环
const pid_t threadId_; // loop所在线程id
Timestamp pollReturnTime_; // poller返回发生事件的channels的时间点
std::unique_ptr<Poller> poller_; // loop所使用的poller
int wakeupFd_; // eventfd 用于线程间通信,通过轮询算法唤醒subloop
std::unique_ptr<Channel> wakeupChannel_; // 将eventfd封装成channel
ChannelList activeChannels_; // poller监听返回的活跃channel集合
std::atomic_bool callingPendingFunctors_; // 判断loop是否有新的回调需要执行
std::vector<Functor> pendingFunctors_; // 待处理的回调集合
std::mutex mutex_; // 互斥锁
};
EventlLoop通过loop方法开启循环,通过quit退出循环。
void EventLoop::loop() {
looping_ = true;
quit_ = false;
while(!quit_) {
activeChannels_.clear();
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
for(Channel *channel : activeChannels_) {
// Poller监听哪些channel发生事件了,然后上报给EventLoop,通知channel处理相应的事件
channel->HandleEvent(pollReturnTime_);
}
// 执行当前EventLoop事件循环需要处理的回调操作
doPendingFunctors();
}
LOG_INFO("EventLoop %p stop looping. \n", this);
looping_ = false;
}
void EventLoop::quit() {
quit_ = true;
// 判断执行quit的线程是否是该EventLoop对应的线程,
// 如果不是,唤醒对应线程
if(!isInLoopThread()) {
wakeup();
}
}
不仅如此,loop本身也需要执行对应的回调,如监听socket操作等,通过runInLoop方法设置。在执行操作中,需要判断执行线程是否是EventLoop对应线程,如果是,直接执行,如果不是,将其加入执行队列,并唤醒。
void EventLoop::runInLoop(Functor cb) {
if(isInLoopThread()) {
cb();
}
else {
queueInLoop(cb);
}
}
void EventLoop::queueInLoop(Functor cb) {
{
std::unique_lock<std::mutex> lock(mutex_);
pendingFunctors_.emplace_back(cb);
}
if(!isInLoopThread() || callingPendingFunctors_) {
wakeup();
}
}
唤醒方法与接收唤醒方法通过wakeup与handleRead方法实现。每个EventLoop都有一个只用来唤醒操作的eventfd,称为wakeupFd_。当需要唤醒对应EventLoop时,只需要向对应的fd发送1字节数据;而当wakefd接收到1字节数据时,证明该醒来工作了。
void EventLoop::wakeup() {
uint64_t one = 1;
ssize_t n = write(wakeupFd_, &one, sizeof one);
if(n != sizeof one) {
LOG_ERROR("EventLoop::wakeup() writes %lu bytes instead of 8", n);
}
}
void EventLoop::handleRead() {
uint64_t one = 1;
ssize_t n = read(wakeupFd_, &one, sizeof one);
if(n != sizeof one) {
LOG_ERROR("EventLoop::handleRead() reads %lu bytes instead of 8", n);
}
}
EventLoopThread类
muduo的核心就是one loop per thread,所以使用EventLoopThread类将线程与loop绑定起来,启动thread的同时启动对应的loop。
class EventLoopThread : noncopyable {
public:
using ThreadInitCallBack = std::function<void(EventLoop*)>;
EventLoopThread(const ThreadInitCallBack &cb = ThreadInitCallBack(), const std::string &name = std::string());
~EventLoopThread();
// 开启线程,进入事件循环
EventLoop* startLoop();
private:
void threadFunc();
EventLoop *loop_; // 对应loop
bool exiting_; // 线程是否结束
Thread thread_; // 线程
// 互斥锁 判断线程与loop是否都创建好绑定完全
std::mutex mutex_;
std::condition_variable cond_; // 条件变量
ThreadInitCallBack callback_; //线程初始化时的回调
};
所以在stratLoop方法中,当线程启动的时候, 必须确保线程对应的loop已经创建,否则进入阻塞状态等待loop创建成功。
EventLoop* EventLoopThread::startLoop() {
thread_.start();
EventLoop *loop = nullptr;
{
std::unique_lock<std::mutex> lock(mutex_);
while (loop_ == nullptr)
{
// 当loop不存在时,等待条件变量通知
cond_.wait(lock);
}
loop = loop_;
}
return loop;
}
EventLoopThreadPool类
在多线程环境下,频繁创建销毁线程是一件费时费力的事情,所以为了提高效率,会创建线程池提前存放一些可用的线程。当需要用到线程时,从线程池的拿出线程使用,而不需要再创建线程;使用完线程后,将线程放入线程池中,避免了销毁线程的开销。
class EventLoopThreadPool : noncopyable {
public:
using ThreadInitCallBack = std::function<void(EventLoop*)>;
EventLoopThreadPool(EventLoop *baseLoop, const std::string &name);
~EventLoopThreadPool();
// 设置线程池中线程个数
void setThreadNum(int numThreads) { numThreads_ = numThreads; }
// 初始化线程池,启动线程池
void start(const ThreadInitCallBack &cb = ThreadInitCallBack());
// 获取线程池中下一个loop
EventLoop* getNextLoop();
// 返回loop队列
std::vector<EventLoop*> getAllLoops();
bool started() const { return started_; }
const std::string name() const { return name_; }
private:
// 主loop,最开始用户创建的loop,在多线程环境下,可看做reactor,用来分发线程
EventLoop *baseLoop_;
std::string name_; // 线程池名字
bool started_; // 线程池是否启动
int numThreads_; // 线程数量
int next_; // 标记下次获取loop的下标
// 线程池中的线程与loop
std::vector<std::unique_ptr<EventLoopThread>> threads_;
std::vector<EventLoop*> loops_;
};
调用start方法开启线程池
void EventLoopThreadPool::start(const ThreadInitCallBack &cb) {
started_ = true;
// 创建用户指定数量的线程
for(int i = 0; i<numThreads_; ++i) {
char buf[name_.size() + 32];
snprintf(buf, sizeof buf, "%s%d", name_.c_str(), i);
// 创建线程加入线程池中
EventLoopThread *t = new EventLoopThread(cb, buf);
threads_.push_back(std::unique_ptr<EventLoopThread>(t));
// 底层创建线程,绑定一个新的EventLoop,并返回该loop的地址
loops_.push_back(t->startLoop());
}
// 如果指定线程为0, 则为单线程环境,监听与IO等操作都由baseloop完成。
if(numThreads_ == 0 && cb) {
cb(baseLoop_);
}
}
当新连接到来,需要使用新线程时,调用getNextLoop方法获取下一个空线程使用
EventLoop* EventLoopThreadPool::getNextLoop() {
EventLoop *loop = baseLoop_;
// next_标志下一个使用的线程下标
if(!loops_.empty()) {
loop = loops_[next_];
++next_;
if(next_ >= loops_.size()) {
next_ = 0;
}
}
return loop;
}