这篇文章用于分析muduo的TcpServer类和Acceptor类,原本打算将TcpConnection也放到这里一起聊的,但是那个太多啦,一篇文章太长会让人读的很不舒服把。
当然我用的代码是其他大神开发的flamingo的源码,这是一个基于muduo开发的即时聊天服务器
具体的地址https://blog.csdn.net/analogous_love/article/details/69481542
不废话了,进入正题吧
我有一个习惯,查看一个新class的时候,首先看private中有哪些成员,可能这样更好捋清class之间的关系把。
typedef std::map<string, TcpConnectionPtr> ConnectionMap;
private:
EventLoop* loop_; // the acceptor loop
const string hostport_;
const string name_;
std::shared_ptr<Acceptor> acceptor_; // avoid revealing Acceptor
std::shared_ptr<EventLoopThreadPool> eventLoopThreadPool_;
ConnectionCallback connectionCallback_;
MessageCallback messageCallback_;
WriteCompleteCallback writeCompleteCallback_;
ThreadInitCallback threadInitCallback_;
std::atomic<int> started_;
int nextConnId_; // always in loop thread
ConnectionMap connections_;//每个生成的链接的指针都会保存在这里
};
一个TcpServer有一个EventLoop,先简单的理解为用来接收新的连接。
(英文备注基本上是陈硕大佬自己写的备注)。
关于类之间的关系,可以看到持有一个Acceptor和一个EventLoopThreadPool的指针。
再看看TcpServer初始化的代码。
TcpServer::TcpServer(EventLoop* loop,
const InetAddress& listenAddr,
const std::string& nameArg,
Option option)
: loop_(loop),
hostport_(listenAddr.toIpPort()),
name_(nameArg),
acceptor_(new Acceptor(loop, listenAddr, option == kReusePort)),
//threadPool_(new EventLoopThreadPool(loop, name_)),
connectionCallback_(defaultConnectionCallback),
messageCallback_(defaultMessageCallback),
started_(0),
nextConnId_(1)
{
acceptor_->setNewConnectionCallback(std::bind(&TcpServer::newConnection, this, std::placeholders::_1, std::placeholders::_2));
}
TcpServer实例被创建的时候,会new一个Acceptor,并由自己对应得指针指向这个Acceptor,我们也可以从最后看出,Acceptor设置了回调函数,绑定为了TcpServer的newConnection函数,也就是说,当这一个Acceptor的newConnectionCallback_函数被调用的时候,会回调到TcpServer的newConnection函数来。
TcpServer是提供给客户代码的接口。那么构造是需要客户代码来协助实现的。
值得注意的是Option是一个枚举类型
enum Option
{
kNoReusePort,
kReusePort,
};
这里提到了端口复用的问题,端口复用可以让多个套接字绑定同一个端口,往往在于服务器主动断开连接或者产生异常的时候,因为会有一个time_wait=2MSL,具体可以设置。所以端口还会被占用。为了保证端口可用,就要使用端口复用,详细就不展开聊了
用户代码需要通过start函数来启动TcpServer,至少我是用“启动”这个词的,不知道这个词会不会有点偏差。
void TcpServer::start(int workerThreadCount/* = 4*/)
{
if (started_ == 0)
{
eventLoopThreadPool_.reset(new EventLoopThreadPool());//初始化事件循环线程池
eventLoopThreadPool_->Init(loop_, workerThreadCount);//acceptor loop
eventLoopThreadPool_->start();
//threadPool_->start(threadInitCallback_);
//assert(!acceptor_->listenning());
loop_->runInLoop(std::bind(&Acceptor::listen, acceptor_.get()));
started_ = 1;
}
}
首先,关于started_这个应该好理解吧,就是有没有启动。
注意到EventLoopThreadPool在start中才被new和init,但是Acceptor在TcpServer构造的时候就被new了,我不是很清楚为什么会分开,我个人的理解,可能是创建和初始化EventLoopThreadPool,意味着要开几个新线程吧,所以留到确定要启动Tcpserver才执行吧。
值得注意的是 loop_->runInLoop(std::bind(&Acceptor::listen, acceptor_.get()));
我们来看看runInLoop这个函数
void EventLoop::runInLoop(const Functor& cb)
{
if (isInLoopThread())//如果是本线程,则直接运行,否则加入队列,等待运行
{
cb();
}
else
{
queueInLoop(cb);
}
}
总体来说是执行里面的std::bind(&Acceptor::listen, acceptor_.get())
至于EventLoop后面会开一篇文章专门来分析。简单来说这里最终执行了listen函数,开始监听这个socket。
void TcpServer::stop()
{
if (started_ == 0)
return;
for (ConnectionMap::iterator it = connections_.begin(); it != connections_.end(); ++it)
{
TcpConnectionPtr conn = it->second;
it->second.reset();
conn->getLoop()->runInLoop(std::bind(&TcpConnection::connectDestroyed, conn));
conn.reset();
}
eventLoopThreadPool_->stop();
started_ = 0;
}
关闭TcpServer的基本操作就是:
迭代器,遍历ConnectionMap(存放所有的TcpConnection),这里首先reset了map中的TcpConnectionPtr,我不是很理解为什么要,创建一个再reset掉之前的,为什么不用it->second->getLoop()->runInLoop()
,是不是会出现内存泄露的问题,不是很理解,有待进一步查证!!!
getLoop()函数是获取当前TcpConnection对应的EventLoop,其实是在创建TcpConnection的时候,会从线程池中选一个已经存在的EventLoop给它。应该说这个TcpConnection属于这个EventLoop。
整个框架是事件驱动的,什么意思,很多实际需要执行的函数,都会被送入EventLoop中等待执行。所以你会在很多地方看到runInLoop函数,这个函数的作用是如果当前函数是属于当前这个EventLoop线程的,就立即执行,否则push到pendingFunctors_中,等待执行。具体我想到了EventLoop的时候再好好梳理一下。
当TcpConnection被销毁完后,再停掉线程池,将标志started_设为0,就算完成了stop任务了
这个应该算是比较重要的一个函数了,这个函数负责开始建立连接。
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
loop_->assertInLoopThread();
EventLoop* ioLoop = eventLoopThreadPool_->getNextLoop();
char buf[32];
snprintf(buf, sizeof buf, ":%s#%d", hostport_.c_str(), nextConnId_);
++nextConnId_;
string connName = name_ + buf;
LOGD("TcpServer::newConnection [%s] - new connection [%s] from %s", name_.c_str(), connName.c_str(), peerAddr.toIpPort().c_str());
InetAddress localAddr(sockets::getLocalAddr(sockfd));//生成一个InetAddress,有一个新的sockaddr
// FIXME poll with zero timeout to double confirm the new connection
// FIXME use make_shared if necessary
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, std::placeholders::_1)); // FIXME: unsafe
//该线程分离完io事件后,立即调用TcpConnection::connectEstablished
ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
}
我们来看看第二行EventLoop* ioLoop = eventLoopThreadPool_->getNextLoop();
这一行其实是创建一个指针,用来指向当前线程池中下一个线程的函数,(假设线程池有6个线程,那么每个连接就会被平均分配到这6个,算是负载均衡?)
接下来就是生成了一个新的InetAddress(封装了sockaddr),然后创建了新的TcpConnection,并放入map中,为TcpConnection设置回调函数,最后调用了TcpConnection::connectEstablished,由TcpConnection做收尾的工作。
接下来来分析一下Acceptor这个类,顾名思义Acceptor接收者,其实就是接收连接用的,我们看看它的成员变量。
private:
EventLoop* loop_;
Socket acceptSocket_;
Channel acceptChannel_;//接受通道
NewConnectionCallback newConnectionCallback_;
bool listenning_;
在看看它的初始化
Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport)
: loop_(loop),
acceptSocket_(sockets::createNonblockingOrDie()),//返回一个int型,fd表示socket,socket 被设置成了非阻塞
acceptChannel_(loop, acceptSocket_.fd()),//设置通道,对应的循环和fd
listenning_(false)
{
#ifndef WIN32
idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
#endif
acceptSocket_.setReuseAddr(true);
acceptSocket_.setReusePort(reuseport);
acceptSocket_.bindAddress(listenAddr);
acceptChannel_.setReadCallback(std::bind(&Acceptor::handleRead, this));//注册了回调函数,
}
主要来说就是一个通道channel和一个socket。在之前整体的分析中,我也简单分析过读事件建立连接的基本过程:epoll->Channel->Acceptor->TcpServer->TcpConnection。
成员acceptSocket_其实就是侦听socket,会被调用listen方法和accept方法。
Channel是通道,每一个TcpConnection和TcpServer所持有的Acceptor都有一个Channel连接到外部,所以都会持有一个Channel。顾名思义“通道”,通往外界的道路。
对于Acceptor比较重要的函数就是handleRead了
void Acceptor::handleRead()//要准备新建一个链接
{
loop_->assertInLoopThread();
InetAddress peerAddr;//相当于一个socket,被封装了的sockaddr_in
//FIXME loop until no more
int connfd = acceptSocket_.accept(&peerAddr);//执行socket::accept接收连接请求,并将新创建的socket的信息写入peerAddr,然后返回对应的文件描述符。
if (connfd >= 0)
{
string hostport = peerAddr.toIpPort();
LOGD("Accepts of %s", hostport.c_str());
//newConnectionCallback_实际指向TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
if (newConnectionCallback_)
{
newConnectionCallback_(connfd, peerAddr);
}
else
{
sockets::close(connfd);
}
}
else
{
LOGSYSE("in Acceptor::handleRead");
#ifndef WIN32
if (errno == EMFILE)
{
::close(idleFd_);
idleFd_ = ::accept(acceptSocket_.fd(), NULL, NULL);
::close(idleFd_);
idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
}
#endif
}
}
这个函数是用来处理对应的Channel调用回调函数时,所执行的内容。
看看这个函数int connfd = acceptSocket_.accept(&peerAddr);//返回创建的新的套接字
这个函数的作用是,用Acceptor对应的socket,去accept了新的连接请求,并将创建的新的文件描述符socket(linux下socket被当作文件)等信息,写入了peerAddr中,InetAddress是一个封装sockaddr_in的类,也可以认为它就是一个sockaddr_in,这样理解起来会比较轻松。
接着是调用newConnectionCallback_,让TcpServer去创建TcpConnections。前面多了一个判断,目的是为了加一层保障,防止TcpServer没有绑定好Callback函数。
我在想一个问题,或许我把每个功能单独写一篇文章会比按类来写更加好吧。现在写感觉很多调用到其他类的函数都没有展开来说明。以后写blog注意一下吧,muduo就算啦,接着这样写下去吧!