muduo记录1:一次典型的muduo based Tcp服务器建立连接的过程
一.用户创建Server实例
用户在main()函数里初始化一个EventLoop *对象,这就是baseloop,生命周期等于即将创建的整个服务器的生命周期。
所谓EventLoop,直译事件循环,做的事情主要也是在一个while里面不停地执行epoll_wait(),处理各种各样的事件,EventLoop分为两种,一个是用户主动创建的loop,一般称为mainloop或者baseloop,只管监听和建立新连接,建立好的连接扔给ioloop处理,所以另外一种就是这个ioloop或者说subloop,专门处理已建立连接套接字的读写 关闭 错误等。
class EventLoop;
EventLoop loop;
用这个baseloop构造一个TcpServer对象
TcpServer::TcpServer(EventLoop *loop, const InetAddress &listenaddr, const std::string &namearg, Option option)
:loop_(CheckLoopNotNull(loop))
,ipPort_(listenaddr.toIpPort())
,name_(namearg)
,acceptor_(new Acceptor(loop, listenaddr,option == kReusePort))
,threadPool_(new EventLoopThreadPool(loop, name_))
,connectionCallback_()
,messageCallback_()
,nextConnId_(1)
,started_(0)
{
//当有新用户连接时,会执行TcpNreconnctionCallback
acceptor_->setNewConnectionCallback(
std::bind(&TcpServer::newConnection, this, std::placeholders::_1, std::placeholders::_2));
}
在第一步中我们已经创建了唯一的baseloop_,利用一个指向baseloop_的指针作为参数,在堆上申请空间初始化Acceptor对象acceptor_和线程池对象threadpool_,这两个都是unique_ptr<>对象。nextConnId初始化为1
紧接bind newConnection回调方法给acceptor_。
接着仅仅需要baseloop_指针和name_就可以构造出一个EventLoopThreadPool实例。
baseloop加上必需的用户定义的回调函数和监听的地址端口后,server.start()可以启动
二.TcpServer第一次启动
void TcpServer::start()
{
if (started_++ == 0) //防止一个tcpserver对象被start多次
{
threadPool_->start(threadInitCallback_); //启动底层的线程池
loop_->runInLoop(std::bind(&Acceptor::listen, acceptor_.get()));
}
}
调用server.start(),在函数里server只干两件事,那就是调用threadPool_->start(threadInitCallback_)启动线程池和启动mainloop的loop循环,也就是分别让ioloop和mainloop工作起来;
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));
//底层创建线程,绑定一个新的loop并返回该loop的地址
loops_.push_back(t->startloop());
}
//整个服务端只有一个线程在运行baseloop
if (numThreads_ == 0 && cb) {
cb(baseLoop_);
}
}
1.threadPool_->start( )會根据numThreads_(subloop个数),在堆上循环创建相应数量的线程(muduo中所有的线程都是EventLoopThread,内部封裝了Thread對象),同样用unique_ptr包装。
接下來执行這個EventLoopThread類startloop(),这个函数负责启动底层的线程(底層的綫程庫函數用到的執行函數是上層傳下去的回調,缺省一次創建一个EventLoop/subloop并且开始执行loop循环),并且返回这个EventLoopThread刚刚创建的EventLoop的地址,將其放入threadPool的loops_数组成员变量里记录下来,现在这个线程池里应该有了规定数量的工作线程和subloop,分別用一個vector來保存。
EventLoop *EventLoopThread::startloop()
{
thread_.start(); //启动底层的新线程
EventLoop *loop = nullptr;
{
std::unique_lock<std::mutex> lock(mutex_);
while (!loop_)
{
cond_.wait(lock);
}
loop = loop_;
}
return loop_;
}
2.创建完了所有的ioloop并且让他们开始loop循环后,mainloop才会调用runInLoop,执行acceptor_的listen()方法,绑定server的地址端口并且开启监听。runInLoop是EventLoop的成员函数,检查当前loop是否在创建它的线程内部,是的话立即执行,否则调用queueInLoop()把bind的函数放入ioLoop自己的一个任务队列。这个loop_ 就是主线程中的loop,判断结果一定在当前线程里面,所以会直接执行listen() 。
void EventLoop::runInLoop(Functor cb)
{
//在当前的loop线程中执行cb,直接执行
if (isInLoopThread())
{
cb();
}
//在非当前loop线程中执行cb,需要唤醒loop所在线程执行cb
else
{
queueInLoop(cb);
}
}
现在有了线程池,有了acceptor,搭配上用户自己设置的相关回调操作,主事件循环可以开启了,main调用loop.loop(),server可以接收连接请求了
三.Acceptor发生的事,需要引入Channel了
此时,acceptor_建立,并且通过内部的EventLoop成员表示为Server所有,读回调bind了handleRead(),开始监听。但是muduo是基于IO多路复用(其实就是epoll)事件驱动的。新连接,可读,可写都应该通过epoll来完成。
所以,这里的listen()开始后,新连接来了不会直接accept系统调用,而是通过另外一个类Channel来进行事件到来的时的回调和分派任务。
Channel是单独用来注册和相应事件的类
Channel是单独用来创建和相应事件的类,陈硕老师在书中提到一个channel对象并不拥有fd,它是Acceptor,Connector,EventLoop,TimerQueue,TcpConnection的成员,生命期由它们管理。
Channel起到了非常重要的串联各个模块的作用,成员变量如下
static const int kNoneEvent;
static const int kReadEvent;
static const int kWriteEvent;
EventLoop *loop_; //事件循环
const int fd_; // fd,poller监听的对象
int events_; // 注册fd感兴趣的事件
int revents_; // poller返回的具体事件
int index_; // 表示这个channel在poller中的状态:未添加过 已添加 需要删除
std::weak_ptr<void> tie_;
bool tied_;
//因为channel通道里面能够获知fd里具体发生的事件,
//所以它负责调用具体事件的回调操作
ReadEventCallback readcallback_;
EventCallback writecallback_;
EventCallback closecallback_;
EventCallback errorcallback_;
可以看到,Channel类中同样有一个EventLoop指针(怎么哪都有你),假设现在创建了一个channel的实例,我们就有了一个套接字(可能是监听套接字/也可能是已连接套接字),channel对这个套接字的关注点,这个套接字发生的事件,index_是channel在poller中的状态,后面会提到。上面三个静态常量定义了channel对fd_的态度:不关心,关心读,关心写
channel中有许多回调函数,这些函数都是从上游的acceptor_和connector来的,这些回调函数定义了对channel关注点的修改,关注事件发生时的处理方法。其实,Channel有三种,acceptChannel,connectedChannel,wakeupChannel,先说前两种
acceptor_有一个成员变量acceptChannel,顾名思义这个channel专门用来接收新连接,所以通过Acceptor类的listen()就调用了Channel类的enableReading方法,把关注点设置成可读事件。同理,TcpConnection类中也有成员变量是Channel类对象,TcpConnection表示已连接的套接字,是从acceptor那里的新连接变来的,具体怎么实现呢
通过server里面的一个回调,就是TcpServer构造时为acceptor_绑定的newConnection来完成。当Client端请求到来时,mainloop里的poller返回可读事件,于是acceptorChannel根据读事件执行handleRead()成员函数,这个成员函数里面就有server类为acceptor传入的newConnection回调。函数来到server的成员函数中,通过轮询选择线程池里的一个ioloop,建立连接产生一个connfd,用connfd,*ioloop,peeraddr构造一个TcpConnection 智能指针对象,TcpConnection对象构造时会生成一个成员变量channel,同时把对已连接套接字的读写 关闭 错误处理回调函数传给channel,之后连接期间便由这个channel来负责事件响应。用智能指针包装这个连接对象,把用户设置的相关回调传给它,最后ioloop执行runInLoop,里面的函数是TcpConnection::connectEstablished(),让channel关心读事件。
思考:这里的runInLoop会因为不在创建线程而转而queueInLoop吗?
两种情况:1.单线程,只有mainloop是没有竞态的,runInLoop立即执行回调;
2.多线程环境,那这个ioloop必然是线程池里面的一个,而当前的函数是mainloop的成员函数,所以一定不在ioloop的本来线程。runInLoop判断出来跨线程了,就把任务塞进对应的队列,然后唤醒它。
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 << "TcpServer::newConnection " << name_.c_str() << " - new Connection" << connName.c_str() << "from"
<< peerAddr.toIpPort().c_str();
sockaddr_in local;
::bzero(&local, sizeof local);
socklen_t addrlen = sizeof local;
if (::getsockname(sockfd, (sockaddr *)&local, &addrlen) < 0)
{
LOG_ERROR << "sockets::getlocalAddr";
}
//通过sockfd获取其绑定的ip地址和port信息
InetAddress localAddr(local);
//根据连接成功的sockfd,创建TcpConnetion连接对象
TcpConnectionPtr conn(new TcpConnection(ioLoop, connName, sockfd, localAddr, peerAddr));
connections_[connName] = conn;
/*下面的回调都是用户设置给TcpServer
* TcpServer =》 TcpConnection
* TcpConnection =》 Channel
* Channel =》 Poller
* Poller =》 notify channel调用回调
*/
conn->setConnectionCallback(connectionCallback_);
conn->setMessageCallback(messageCallback_);
conn->setWriteCompleteCallback(writeCompleteCallback_);
//设置了如何关闭连接的 回调 当conn执行shutdown
conn->setCloseCallback(std::bind(&TcpServer::removeConnection, this, std::placeholders::_1));
//直接调用TcpConnection ::connectEstablished
ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
}
以上就是muduo服务器初始化和建立连接的大致过程,其中涉及大量的回调操作。Channel的设计思想,包装成性质不一样的三类Channel,分别去完成不同的任务。
阶段总结:
1.EventLoop是事件循环,遵循one loop per thread。每个EventLoop内部都持有一个poller,一个wakeupChannel(wakeupChannel一直关注读事件,往里面写一个东西,假设epoll_wait工作在阻塞状态,下次poll()一定会为此返回,自然会唤醒它起来做事)EventLoop都是线程栈上的对象,线程起起来就去loop了,server开着就在loop,生命周期跟server一样。
2.EventLoop和Poller,Channel具有层级关系。EventLoop不关心poller怎么工作,不关心channel如何更新,设置好回调操作以后,一切由事件和IO多路复用驱动。channel只用管好自己的注册,更新,需要的时候通过loop指针向上请求调用EventLoop的方法,EventLoop调用poller的方法,间接地设置自己的关注点,事件到来调用相应回调使用EventLoop的方法处理事件。
这是第二次写相关的笔记,用于阶段性总结。说的不对的地方请多指教,以上
浙公网安备 33010602011771号