实现逻辑分析
针对于EventLoop的设计还是严格遵循其核心思想one loop per thread思想,也就是说一个线程只可以拥有一个EventLoop实例,那么为什么这样实现?主要有以下两点原因
EventLoop每次执行的时候,都会检查当前线程是否已经创建了EventLoop实例
EventLoop::EventLoop()
: looping_(false),
threadId_(CurrentThread::tid()) // 获取当前线程ID
{
LOG_TRACE << "EventLoop created " << this << " in thread " << threadId_;
if (t_loopInThisThread) // 检查当前线程是否已有 EventLoop 实例
{
LOG_FATAL << "Another EventLoop " << t_loopInThisThread
<< " exists in this thread " << threadId_;
}
else
{
t_loopInThisThread = this; // 绑定当前线程的 EventLoop 实例
}
}
EventLoop内部提供了线程安全检查接口
void EventLoop::assertInLoopThread()
{
if (!isInLoopThread())
{
abortNotInLoopThread(); // 终止程序
}
}
bool EventLoop::isInLoopThread() const
{
return threadId_ == CurrentThread::tid(); // 检查当前线程ID是否与EventLoop线程一致
}
EventLoop的生命周期跟随其线程相同,线程退出其也会跟着退出,它会析构之间进行检查
EventLoop::~EventLoop()
{
assert(!looping_); // 事件循环必须已经停止
t_loopInThisThread = NULL; // 清空当前线程的EventLoop指针
}
EventLoop::loop()是时间循环的核心,负责监听文件描述符等操作(代码对源码进行了缩减)
void EventLoop::loop()
{
assert(!looping_);
assertInLoopThread(); // 检查是否在EventLoop线程中调用
looping_ = true;
while (!quit_) // 事件循环,直到 quit_ 被设置为 true
{
poller_->poll(kPollTimeMs, &activeEvents); // 轮询I/O事件
for (EventList::iterator it = activeEvents.begin();
it != activeEvents.end(); ++it)
{
(*it)->handleEvent(); // 处理事件
}
doPendingFunctors(); // 执行待处理的任务
}
looping_ = false;
}
总结EventLoop的设计原则
- 每个线程只拥有一个EventLoop实例
- EventLoop的所有操作都是与其绑定线程去执行
- 内部设计的有线程安全检查机制,确保线程正确性和安全性
线程池优化思路
多进程优化思路
服务器中采用I/O线程管理客户端连接(一个I/O线程也就是一个Reactor)
I/O线程在mudou的作用分析
每一个EventLoop对象都会绑定到一个线程,这个线程也就是I/O线程,然后EventLoop负责管理和处理该线程中所有的I/O事件
I/O线程的理解
I/O线程的核心功能就是等待并处理I/O事件,而不阻塞其他任务的执行,利用该设计允许服务器在处理大量的I/O操作的时候可以保持高效快速的效应,不需要为每一个连接创建单独的线程
因为I/O线程非常适合高并发场景,所以其应用主要在于以下几个方面
核心功能分析
Poller功能分析
内部机制分析
void Poller::fillActiveChannels(int numEvents, ChannelList* activeChannels) const
{
for (PollFdList::const_iterator pfd = pollfds_.begin(); pfd != pollfds_.end() && numEvents > 0; ++pfd)
{
if (pfd->revents > 0)
{
--numEvents;
ChannelMap::const_iterator ch = channels_.find(pfd->fd);
Channel* channel = ch->second;
channel->set_revents(pfd->revents); // 设置事件
activeChannels->push_back(channel); // 添加到活跃的Channel列表中
}
}
}
梳理Channel与Poller类之间交互逻辑
核心成员作用分析
Poller::updateChannel()
void Poller::updateChannel(Channel* channel)
{
assertInLoopThread(); // 确保在正确的线程中调用
if (channel->index() < 0) // index() < 0 表示这是一个新的 Channel
{
// 构造 pollfd 结构体
struct pollfd pfd;
pfd.fd = channel->fd(); // 设置文件描述符
pfd.events = static_cast(channel->events()); // 设置感兴趣的事件类型
pfd.revents = 0; // 初始化为 0,表示暂时没有事件发生
pollfds_.push_back(pfd); // 将新的 pollfd 添加到 pollfds_ 中
int idx = static_cast(pollfds_.size()) - 1; // 记录该 pollfd 在 pollfds_ 中的索引
channel->set_index(idx); // 设置 Channel 的索引
channels_[pfd.fd] = channel; // 在 channels_ 映射表中保存该 Channel
}
else // 更新已有的 Channel
{
int idx = channel->index(); // 获取 Channel 在 pollfds_ 中的索引
struct pollfd& pfd = pollfds_[idx]; // 根据索引找到对应的 pollfd
pfd.fd = channel->fd(); // 更新文件描述符
pfd.events = static_cast(channel->events()); // 更新感兴趣的事件类型
pfd.revents = 0; // 重置事件
if (channel->isNoneEvent()) // 如果 Channel 不关心任何事件
{
pfd.fd = -1; // 将 fd 设置为 -1,表示不关注该文件描述符
}
}
}
核心函数执行过程分析 EventLoop::loop()
void EventLoop::loop()
{
assert(!looping_); // 确保当前没有在循环中
assertInLoopThread(); // 确保在正确的线程中调用
looping_ = true; // 标记开始循环
quit_ = false; // 退出标志初始化为false
while (!quit_) // 当退出标志为false时继续循环
{
activeChannels_.clear(); // 清空活跃的Channel列表
poller_->poll(kPollTimeMs, &activeChannels_); // 调用Poller,获取活跃事件
for (ChannelList::iterator it = activeChannels_.begin();
it != activeChannels_.end(); ++it)
{
(*it)->handleEvent(); // 逐个处理Channel上的事件
}
}
looping_ = false; // 事件循环结束,标记停止
}
EVentLoop::quit函数的设计
该函数就是用户通过调用该方法结束事件循环的方法,也就是将quit_变量设置为true;但是其内部具有延迟处理机制,也就是该调用执行后,不会立即退出EVentLoop,而是会等待EventLoop中所有的活动完成后再进行退出
void EventLoop::quit()
{
quit_ = true;
// 如果EventLoop不在当前线程中,可能需要唤醒
// 比如通过eventfd或者其他方法唤醒正在poll()中的线程
}
定时器类的设计
数据结构的设计与选择
定时器类中选择了两种数据结构类型,其一是set容器,主要用于按照时间顺序来保存Timer;其二Timer是以pair
EventLoop中增加定时器相关接口
TimerId EventLoop::runAt(const Timestamp& time, const TimerCallback& cb)
{
return timerQueue_->addTimer(cb, time, 0.0); // 第三个参数为 0.0,表示这是一次性定时器
}
TimerId EventLoop::runAfter(double delay, const TimerCallback& cb)
{
Timestamp time(addTime(Timestamp::now(), delay)); // 计算延迟后的时间
return runAt(time, cb); // 在计算出的时间点运行回调
}
TimerId EventLoop::runEvery(double interval, const TimerCallback& cb)
{
Timestamp time(addTime(Timestamp::now(), interval)); // 计算首次触发的时间
return timerQueue_->addTimer(cb, time, interval); // 第三个参数为 interval,表示周期性定时器
}
跨线程安全性问题
EventLoop的定时器接口是允许跨线程调用的,也就是用户可以从不同的线程去调用这些定时器的函数,不需要担心线程安全问题。mudou库在此的解决办法则是通过runInLoop()函数,将定时器任务的操作转移到IO线程中执行,而不是加锁。
简单来说起流程,首先runInLoop函数会检查当前线程是否是EventLoop所属于的IO线程,如果是则直接执行任务,如果不是,则将任务添加到一个任务队列中,然后由IO线程执行
分析从EventLoop中的事件循环如何处理定时任务
该函数作用:允许程序在运行的时候,在当前事件循环线程中安全的执行某个用户提供的回调函数
执行逻辑
void EventLoop::runInLoop(const Functor& cb)
{
if (isInLoopThread()) {
cb(); // 如果是在IO线程中,直接执行回调函数
} else {
queueInLoop(cb); // 否则,将回调函数放入队列,稍后在IO线程中执行
}
}
根据主从Reactor模型理解该处执行逻辑,例如两个线程分别运行一个Reactor,A线程向B线程发送了一个任务,为了保证线程安全,A线程是无法直接操作B线程的资源的(需要将它们两个的资源进行开,从而避免线程争夺临界资源的问题),而是通过runInLoop()函数将任务交给B线程来处理
跨线程的任务队列
如果回调函数不可以立即执行,比如主线程中的runInLoop(),任务会被加入一个等待队列中等待执行,为了保证线程安全,这个任务需要进行加锁保护
void EventLoop::queueInLoop(const Functor& cb)
{
{
MutexLockGuard lock(mutex_);
pendingFunctors_.push_back(cb); // 将任务加入队列
}
if (!isInLoopThread() || callingPendingFunctors_) {
wakeup(); // 如果当前线程不是IO线程,或者正处于执行其他任务,唤醒IO线程
}
}
weakup()唤醒机制
该处逻辑可以简单的理解为一个人通过一种特定的方式唤醒另一个人起来干活
wakeup()的作用就是通过向IO线程发出信号,例如通过eventfd机制发出信号,从而打断poll()的阻塞,让IO线程可以立刻处理新加入的任务,这也是一种跨线程通信的经典方法
通过该机制可以实现即使IO线程处于阻塞状态,也可以被快速唤醒,处理主线程提交的任务
void EventLoop::wakeup()
{
uint64_t one = 1;
ssize_t n = ::write(wakeupFd_, &one, sizeof(one)); // 向wakeupFd写数据,唤醒IO线程
}
类似于双缓冲区机制的doPendingFunctors()函数的执行逻辑
该函数的执行逻辑就是执行所有已经加入到任务队列中的任务,其底层逻辑不是通过遍历一个一个的执行任务,而是通过任务队列与局部变量进行交换,从而实现缩短临界区的长度,避免阻塞其他线程向队列中添加新任务
void EventLoop::doPendingFunctors()
{
std::vector functors;
callingPendingFunctors_ = true;
{
MutexLockGuard lock(mutex_);
functors.swap(pendingFunctors_); // 将任务队列与局部变量交换
}
for (const Functor& functor : functors) {
functor(); // 执行每一个任务
}
callingPendingFunctors_ = false;
}
跨线程的唤醒机制分析
modou库中提供这个类的作用就是允许我们在任意线程中创建并运行的EVentLoop,该类是一个封装了EventLoop和线程管理的类,不需要担心线程的管理细节
class EventLoopThread {
public:
EventLoopThread();
~EventLoopThread();
EventLoop* startLoop();
private:
void threadFunc();
EventLoop* loop_;
Thread thread_;
MutexLock mutex_;
Condition cond_;
};
Accept主要就是用于监听新连接并通知上层应用层,该类主要的作用就是用来管理服务端的监听套接字,并在新客户端连接超时时进行相应的处理
主要功能
使用简单示例
void newConnection(int sockfd, const muduo::InetAddress& peerAddr) {
printf("New connection from %s\n", peerAddr.toHostPort().c_str());
::write(sockfd, "Hello, World!\n", 14); // 向新连接发送一条信息
muduo::sockets::close(sockfd); // 关闭连接
}
int main() {
muduo::InetAddress listenAddr(9981); // 监听9981端口
muduo::EventLoop loop;
muduo::Acceptor acceptor(&loop, listenAddr);
acceptor.setNewConnectionCallback(newConnection); // 设置新的连接回调
acceptor.listen(); // 开始监听
loop.loop(); // 事件循环开始
}
处理新连接过程分析
TcpServer类的主要作用分析
作用分析
状态管理
数据传输
连接对象主要就是依靠Channel来监听和处理套接字上的读写事件,同时通过handleRead()函数来处理接收到的数据,当read()返回0的时候就是表示连接断开了
数据处理则主要是在连接建立后,通过连接回调机制将接收到的数据,传递给回调函数进行处理
TcpServer与TcpConnection之间的关联
监听连接状态 -- 检测到断开 -- 执行关闭操作 -- 通知应用程序 -- 清理资源
Channel类处理关闭连接回调的思路
关闭回调调用的场景则是客户端断开连接后,服务器需要知道该连接已经关闭并且释放相应的资源,其中会涉及到断开连接的情况主要有:套接字文件描述符被关闭了、连接管理器中的对象被移除了,触发了用户定义的回调函数机制
总体实现的逻辑
void Channel::handleEvent() {
eventHandling_ = true;
if ((revents_ & POLLHUP) && !(revents_ & POLLIN)) {
LOG_WARN << "Channel::handleEvent() POLLHUP";
if (closeCallback_) closeCallback_();
}
// 其他事件处理
eventHandling_ = false;
}
TcpConnection类中关闭连接处理逻辑
主要通过两个函数执行关闭逻辑,handleRead():当连接关闭的时候,read()返回0,触发handClose()函数 ;如果还有数据可读的时候,则调用上层消息回调处理逻辑
void TcpConnection::handleRead() {
char buf[65536];
ssize_t n = ::read(channel_->fd(), buf, sizeof buf);
if (n > 0) {
messageCallback_(shared_from_this(), buf, n);
} else if (n == 0) {
handleClose(); // 连接关闭,调用 handleClose
} else {
handleError(); // 错误处理
}
}
handClose()函数:主要就是先禁用事件监听,然后通过回调函数将连接移除出去,最后调用回调释放资源
void TcpConnection::handleClose() {
loop_->assertInLoopThread();
LOG_TRACE << "TcpConnection::handleClose state = " << state_;
assert(state_ == kConnected);
channel_->disableAll(); // 禁用所有事件监听
closeCallback_(shared_from_this()); // 调用 closeCallback 通知 TcpServer 移除连接
}
TcpServer类在创建爱连接的时候,就设置了关闭连接的回调函数
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr) {
loop_->assertInLoopThread();
char buf[64];
snprintf(buf, sizeof buf, "#%d", nextConnId_);
++nextConnId_;
string connName = name_ + buf;
LOG_INFO << "TcpServer::newConnection [" << name_
<< "] - new connection [" << connName
<< "] from " << peerAddr.toIpPort();
InetAddress localAddr(sockets::getLocalAddr(sockfd));
TcpConnectionPtr conn(new TcpConnection(loop_, connName, sockfd, localAddr, peerAddr));
connections_[connName] = conn; // 将新建的连接加入 ConnectionMap
conn->setConnectionCallback(connectionCallback_);
conn->setMessageCallback(messageCallback_);
conn->setCloseCallback(
std::bind(&TcpServer::removeConnection, this, _1)); // 设置关闭回调
conn->connectEstablished(); // 设置连接已建立
}
removeConnection移除连接咯几,首先将其从连接管理的哈希表中移除,确保不会处理该连接的事件,最后清理该连接即可
void TcpServer::removeConnection(const TcpConnectionPtr& conn) {
loop_->assertInLoopThread();
LOG_INFO << "TcpServer::removeConnection [" << conn->name() << "]";
connections_.erase(conn->name()); // 从 ConnectionMap 中移除连接
loop_->queueInLoop(
std::bind(&TcpConnection::connectDestroyed, conn)); // 清理连接
}
Poller中removeChannel移除Channel对象实现逻辑
该函数的目标就是高效的移除监听事件,通过维护一个pollfds_数组和channels_哈希表,从而实现一个高效率移除操作,该种方式适用于大规模的并发操作上,可以减轻系统负担,提高服务器的响应速度
void Poller::removeChannel(Channel* channel) {
assertInLoopThread();
LOG_TRACE << "fd = " << channel->fd();
assert(channels_.find(channel->fd()) != channels_.end());
assert(channels_[channel->fd()] == channel);
assert(channel->isNoneEvent()); // 确保Channel已经没有事件要监听
int idx = channel->index();
assert(0 <= idx && idx < static_cast(pollfds_.size()));
const struct pollfd& pfd = pollfds_[idx];
assert(pfd.fd == channel->fd() || pfd.fd == -channel->fd()-1);
size_t n = channels_.erase(channel->fd()); // 从channels_表中移除
assert(n == 1);
if (implicit_cast(idx) == pollfds_.size()-1) {
pollfds_.pop_back(); // 如果是最后一个元素,直接移除
} else {
int channelAtEnd = pollfds_.back().fd; // 用最后一个元素覆盖当前元素
iter_swap(pollfds_.begin()+idx, pollfds_.end()-1);
if (channelAtEnd < 0) {
channelAtEnd = -channelAtEnd-1;
}
channels_[channelAtEnd]->set_index(idx);
pollfds_.pop_back(); // 移除最后一个元素
}
}
Buffer作用理解
主要就是负责从网络中接收数据,用于确保完成收到的发送端发来的数据,Buffer可以理解成一个本子,可以帮助记下还没有完全收到的消息,等收到完整的消息后,再将其一起处理即可。
Buffer的主要功能就是高效的管理网络中的I/O数据,非阻塞机制则是用来缓存收到的数据。因为再非阻塞情况下,数据并不是一次性达到完成,而是不确定什么时候数据可以完全到达,所以需要设计一个缓冲区来存放这些临时数据,这也就是Buffer的租用
核心执行逻辑
TcpConnection使用Buffer作为输入缓冲区
首先是在连接类中,新增了Buffer作为成员变量,专门用于存储接收到的数据,也就是先把接收到的数据放入缓冲区中,然后找到合适的时机再对其进行统一的处理
其次TcpConnection::handleRead()则是主要负责从网络中读取数据的关键,在此也是通过Buffer接收缓存数据
void TcpConnection::handleRead(Timestamp receiveTime) {
int savedErrno = 0;
ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);
if (n > 0) {
messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
} else if (n == 0) {
handleClose();
} else {
errno = savedErrno;
LOG_SYSERR << "TcpConnection::handleRead";
handleError();
}
}
OnMesssage()函数则是从连接读取接收到的内容,也就是读取不是直接从连接中读取,而是直接从缓冲区中读取
void onMessage(const muduo::TcpConnectionPtr& conn,
muduo::Buffer* buf,
muduo::Timestamp receiveTime) {
printf("onMessage(): received %zd bytes from connection [%s] at %s\n",
buf->readableBytes(),
conn->name().c_str(),
receiveTime.toFormattedString().c_str());
printf("onMessage(): [%s]\n", buf->retrieveAsString().c_str());
}
Buffer使用的优点
详细的分析Buffer::readFd()函数的作用
ssize_t Buffer::readFd(int fd, int* savedErrno) {
char extrabuf[65536]; // 临时缓冲区,用于接收大于 Buffer 剩余空间的数据
struct iovec vec[2]; // 使用 scatter/gather IO,允许一次 read 调用写入两个缓冲区
const size_t writable = writableBytes(); // 获取 Buffer 中可写的字节数
// vec[0] 用来存储 Buffer 中的可写空间
vec[0].iov_base = begin() + writerIndex_;
vec[0].iov_len = writable;
// vec[1] 用来存储超出 Buffer 可写空间的数据
vec[1].iov_base = extrabuf;
vec[1].iov_len = sizeof(extrabuf);
const ssize_t n = readv(fd, vec, 2); // 一次性从 fd 中读取数据,写入两个缓冲区
if (n < 0) {
*savedErrno = errno; // 保存读取错误信息
} else if (implicit_cast(n) <= writable) {
writerIndex_ += n; // 数据全写入 Buffer,无需使用 extrabuf
} else {
writerIndex_ = buffer_.size(); // Buffer 写满了,剩下的数据写入 extrabuf
append(extrabuf, n - writable); // 将 extrabuf 中的数据追加到 Buffer
}
return n; // 返回读取的字节数
}
重点分析
发送数据逻辑分析
代码逻辑事例
class TcpConnection {
public:
void send(const std::string& message) {
// 检查是否在 I/O 线程
if (loop_->isInLoopThread()) {
sendInLoop(message);
} else {
loop_->runInLoop([this, message]() { sendInLoop(message); });
}
}
private:
void sendInLoop(const std::string& message) {
ssize_t nwrote = 0;
if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0) {
// 尝试直接发送数据
nwrote = ::write(channel_->fd(), message.data(), message.size());
if (nwrote >= 0) {
if (implicit_cast(nwrote) < message.size()) {
// 剩余数据写入缓冲区
outputBuffer_.append(message.data() + nwrote, message.size() - nwrote);
if (!channel_->isWriting()) {
// 开启写事件监听
channel_->enableWriting();
}
}
} else if (errno != EWOULDBLOCK) {
// 处理错误
handleError();
}
} else {
// 如果缓冲区已有数据,将当前数据加入缓冲区
outputBuffer_.append(message);
if (!channel_->isWriting()) {
// 开启写事件监听
channel_->enableWriting();
}
}
}
void handleWrite() {
// socket 可写时调用此函数
ssize_t n = ::write(channel_->fd(), outputBuffer_.peek(), outputBuffer_.readableBytes());
if (n > 0) {
outputBuffer_.retrieve(n);
if (outputBuffer_.readableBytes() == 0) {
// 停止写事件监听
channel_->disableWriting();
if (state_ == kDisconnecting) {
shutdownInLoop();
}
}
} else {
// 处理写入错误
handleError();
}
}
// 缓冲区
Buffer outputBuffer_;
EventLoop* loop_;
Channel* channel_;
enum StateE { kConnected, kDisconnecting };
StateE state_;
};
TcpConnection 连接状态转变分析
mudou库设计这个类的主要作用就是发起TCP连接,然后创建socket,发起连接请求,处理连接失败的重试,然后确保连接的建立
主要功能分析
需要注意的重点
参考文章
- modou源码
- 《Linux多线程服务器编程:使用muduo C++ 网络库》 陈硕
- 《UNIX 网络编程1》
- Boost.Asio