从实例看muduo网络库各模块交互过程

Posted 看,未来

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从实例看muduo网络库各模块交互过程相关的知识,希望对你有一定的参考价值。

muduo网络库的核心代码模块

1、channel
2、Poller 和它的子类 EpollPoller
3、EventLoop
4、Thread、EventLoopThread、EventLoopThreadPool
5、Sock、Acceptor
6、Buffer
7、TcpServer、TCPConnection

至于其他还有Logger模块,就不是重点了吧。


各模块功能解释

经过我三天的研究,以及之前的源码铺垫,整理出来了第一个版本,当然后面会持续更新,预计更新到国庆节回来,那个版本应该是能看了。


Channel

根据收到的事件,调用相应的回调。

一个channel绑定一个fd
生命周期:新连接产生 -> 该连接断开。

由poller管理,从属于loop,可配置epoll监听事件。


Poller

muduo中多路事件分发器的核心模块,包含了一个 channel 数组,同时也是一个抽象基类(我只继承了epoll模块),
可以说:One loop per poller.

EpollPoller

实现了:
epoll_create:构造函数
epoll_ctl:一堆的 enable 函数
epoll_wait:poll方法。
通过epoll_wait,将有事件的channel通过参数传递给EventLoop。

此处参数:events[i].data.ptr。(经验呐!!!我觉得有这么一点,这篇就亮了!!!还不止呢。)

void EpollPoller::fillActiveChannels(int numEvents, ChannelList* activeChannels) const {

    //for(Channel* channel:activeChannels){
        //这样剧让不行了!!!

    //eventloop 即将拿到它的poller返回的所有发生事件列表
    for (int i = 0; i < numEvents; ++i) {
        Channel* channel = static_cast<Channel*>(events_[i].data.ptr);  //666666666,这一行代码,N年的功力,你接得住吗?
        channel->set_revents(events_[i].events);
        activeChannels->push_back(channel);
    }
}

void EpollPoller::update(int operation, Channel* channel) {
    epoll_event event;
    int fd = channel->fd();

    memset(&event, 0, sizeof(event));
    event.events = channel->events();
    event.data.ptr = 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);
        }
    }
}

EventLoop

事件循环,One loop per thread,per poller,many channels,per wakeupchannel.

这个 wakeupchannel 是干嘛的呢?专门用于监听唤醒 eventfd 相应的 loop,这个事件通知机制没有见过吧,反正我是第一次见,基于文件描述符的,据说比 condition 要高档一些,condition都显得有点老了,这个比较年轻。

//通过轮询的方式唤醒channel
int createEventfd() {
    //创建一个能被用户应用程序用于时间等待唤醒机制的eventfd对象
    //eventfd 单纯的使用文件描述符实现的线程间的通知机制,可以很好的融入select、poll、epoll的I/O复用机制中
    int evtfd = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
    if (evtfd < 0) {
        LOG_ERROR("Failed in eventfd%d\\n", errno);
    }
    return evtfd;
}

每一个 EventLoop,都配备有一个wakeupfd,有一个wakeup channel 专门负责处理 wakeupfd 事件。
处理办法:随便读个数据,唤醒本 loop 起来干活了。(每一个 EventLoop 都监听了 wakeupchannel 的 EPOLL_IN 事件)


loop() 开始运行后,
1、通过poll函数进行epoll_wait,获取activeChannel。
2、唤醒相应channel。
3、执行doPendingFunction 方法(这里默认有子loop。)
这里又是个巧夺天工的设计:

void EventLoop::doPendingFunctors()
{
    std::vector<Functor> functors;
    callingPendingFunctors_ = true;

    {
        std::unique_lock<std::mutex> lock(mutex_);
        functors.swap(pendingFunctors_);
    }

    for (const Functor& functor : functors)
    {
        functor();
    }
    callingPendingFunctors_ = false;
}

看这一行:functors.swap(pendingFunctors_);

这里为什么要把 pendingFunctors_ 置换出来?这个置换有意思吗?那可太有意思了。

如果不置换,直接拿着 pendingFunctors_ 去执行,这个资源是不是要被锁住?那接下来有新事件过来要放哪里?再开个pendingFunctors_ 2号吗?

这样一置换,相当于这些事件可以并发执行了。

有意思吧。


再看这个queueInLoop 和runInLoop:

void EventLoop::runInLoop(Functor cb) {
    if (isInLoopThread()) {
        cb();
    }
    else {
        queueInLoop(std::move(cb));
    }
}

void EventLoop::queueInLoop(Functor cb) {

    {
        std::unique_lock<std::mutex> lock(mutex_);
        pendingFunctors_.push_back(std::move(cb));
    }

    if (!isInLoopThread() || callingPendingFunctors_) {
        //callingPendingFunctors_:我是在本线程,而且我还在执行回调,有啥事情赶紧的拿过来,不然一会儿loop转一圈过去又阻塞了
        wakeup();
    }
}

如果有子loop,主loop只处理 Acceptor(runInLoop),剩下的全都在queueInLoop -> wakeup 子Loop->handleRead(或者事件已就绪)->唤醒 channel。


EventLoopThread

功能:用于绑定一个 loop 和一个thread。
类成员:一个 EventLoop 指针,一个thread 对象,锁、条件变量、回调等。

one loop per thread 在此处体现:

startloop:启动底层新线程,执行回调,配置 loop 并返回,创建一个独立的 loop,并开启事件循环。


EventLoopThreadPool

事件循环线程池。
包含一个baseloop的指针,第一个EventLoopThread的vector,以及一个EventLoop的vector。

start:创建一定数量的事件循环线程,添加到 std::vector<std::unique_ptr<EventLoopThread>>。并启动这些线程,添加到std::vector<EventLoop*>中。
GetNextLoop:如果工作在多线程中,baseloop 会默认以轮询的方式分配channel给subloop。


TcpServer

负责处理新连接。
Acceptor、EventLoopThreadPool、TCPConnection。

TcpServer(EventLoop* loop,
        const InetAddress& listenAddr,
        const std::string& nameArg,
        Option option = kNoReusePort);
        
//设置底层subloop个数
void setThreadNum(int numThreads);

//开启服务器监听
void start();
TcpServer::TcpServer(EventLoop* loop,
    const InetAddress& listenAddr,
    const std::string& nameArg,
    Option option = kNoReusePort)
    :loop_(CheckLoopNotNull(loop)),
    ipport_(listenAddr.toIpPort()),
    name_(nameArg),
    acceptor_(new Accept(loop, listenAddr, option == kReusePort)),	
    //在这里对sock进行了初始化,不过还没有监听,更没有accept,关于accept在后续章节再提,快了
    //只有在 server start 之后才会listen,listen到才会去accept
    
    threadpool_(new EventLoopThreadPool(loop, name_)),
		//构建一个 EventLoopThreadPool (可以视为mainreactor)对象,
		//不过也就是构建一下,不干啥,关于EventLoopThreadPool的章节后面会提
		//start之后会创建制定数量的线程,并绑定新的loop,返回地址。
	
    connectionCallback_(),
    messageCallback_(),
    nextConnId_(1)
{
    //当有新用户连接时,会执行NewConnectionCallback
    acceptor_->setNewConnectionCallback(std::bind(&TcpServer::newConnection, this, std::placeholders::_1, std::placeholders::_2));
    //在Acceptor 的handleread方法了被调用
}

//开启服务器监听
void TcpServer::start() {
    if (started_ == 0) {  //防止被多次start
        threadpool_->start(threadInitCallback_);	//EventLoopThreadPool的start
        loop_->runInLoop(std::bind(&Accept::listen, acceptor_.get()));	//这里拿来run的loop就是mainloop
        ++started_;
    }
}

//当有新链接来的时候,acceptor会调用这个回调
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr) {
    //根据轮询算法,选择一个subloop,唤醒subloop
    EventLoop* ioloop = threadpool_->GetNextLoop();
    char buf[64] = { 0 };
    snprintf(buf, sizeof buf, "-%s#%d", ipport_.c_str(), nextConnId_);
    ++nextConnId_;
    std::string connName = name_ + buf;

    LOG_INFO("TcpConnnection::newConnection [%s] -new connection [%s] from %s \\n",
        name_.c_str(), connName.c_str(), peerAddr.toIpPort().c_str());

    //通过sockfd获取其本机IP
    sockaddr_in local;
    ::bzero(&local, sizeof local);
    socklen_t addrlen = sizeof local;

    if (::getSockname(sockfd, (sockaddr*)&local, &addrlen) < 0) {
				//pass,日志打印,写漏了
    }
    InetAddress localAddr(::getLocalAddr(sockfd));

    //根据连接成功的fd,创建TCPConnection连接对象
		//一个连接对应一个 TCPConnectionptr 管理
		//关于TCPConnection的事情也是接下来展开
    TcpConnectionptr conn(new TcpConnection(ioloop, connName, sockfd, localAddr, peerAddr));
    connections_[connName] = conn;

    conn->setConnectionCallback(connectionCallback_);
    conn->setMessageCallback(messageCallback_);
    conn->setWriteCompleteCallback(writeCompleteCallback_);
    conn->setCloseCallback(std::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe
    
    ioloop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
    //把当前connfd封装成channel分发给subloop

}

//设置底层subloop个数
void TcpServer::setThreadNum(int numThreads) {
    threadpool_->setThreadNum(numThreads);
}

TcpConnection

一个conn对应一个fd。

创建channel、绑定读、写、关闭、错误回调。


从实际应用出发

直接看这段代码:

int main(int argc, char **argv){

    EventLoop loop;
    InetAddress addr(ip, port);
    ChatServer server(&loop,addr,"ChatServer");

    server.start();
    loop.loop();
    return 0;
}

这里面先初始化了一个 loop 的对象,这是一个baseloop,设置wakeup回调类型,以及时间发生后回调操作,监听wakeup channel 的EPOLLIN事件。

然后是 ChatServer server(&loop,addr,“ChatServer”);
1、构建Acceptor,执行到bind之后,为acceptchannel设置 ReadCallback 回调,绑定了监听套接字,在回调函数中有 accept 和 NewConnectionCallBack 回调。

2、构建EventLoopThreadPool,啥也不干。

3、设置 newConnectionCallBack。

4、server.start(); 开启服务监听,将Acceptor::listen 函数绑定在 loop上,开启listen,配置channel enableReading。
EventLoopThreadPool.start 创建事件循环线程,并运行起来。
将线程和事件循环绑定起来。

5、loop.loop() 启动主loop。


再看这段代码:

void ChatServer::onMessage(const TcpConnectionPtr &conn, Buffer *buff, Timestamp time){
    string buf = buff->retrieveAllAsString();

    json js = json::parse(buf);

    //通过msgid获取业务回调,进行网络模块和任务模块之间的解耦合
    auto msgHandler = ChatService::instance()->getHandle(js["msgid"].get<int>());
    
    //回调消息绑定好的事件处理器,执行相应的业务处理
    msgHandler(conn,js,time);

    //成功解耦
}

1、调用Buffer模块转码数据(这个模块的设计也很nice,可惜我还没把握住)。
2、调用JSON解析数据。
3、conn->send

在当前线程:sendInLoop;
不在当前线程:channel-> runInLoop->queueInLoop,(为啥不直接调用呢。。)


且先总结到此处,夜以深了,我该去想我该想的人了,哎、

以上是关于从实例看muduo网络库各模块交互过程的主要内容,如果未能解决你的问题,请参考以下文章

C++搭建集群聊天室:muduo网络库

C++搭建集群聊天室:muduo网络库

muduo2.0源码阅读记录

C++搭建集群聊天室:安装muduo网络库

C++搭建集群聊天室:安装muduo网络库

缩略muduo网络库事件分发器poller