使用muduo库:
#include <mymuduo/TcpServer.h>
#include <mymuduo/Logger.h>
#include <string>
#include <functional>
class EchoServer
{
public:
EchoServer(EventLoop *loop,
const InetAddress& addr,
const std::string& name)
:server_(loop, addr, name)
,loop_(loop)
{
//注册回调函数
server_.setConnectionCallback(
std::bind(&EchoServer::onConnection, this, std::placeholders::_1)
);
server_.setMessageCallback(
std::bind(&EchoServer::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)
);
//设置合适的loop线程数量
server_.setThreadNum(3);
}
void start()
{
server_.start();
}
private:
//连接建立或者断开回调
void onConnection(const TcpConnectionPtr& conn)
{
if (conn->connected())
{
LOG_INFO("Connection UP : %s", conn->peerAddress().toIpPort().c_str());
}
else
{
LOG_INFO("Connection DOWN : %s", conn->peerAddress().toIpPort().c_str());
}
}
//可读事件回调
void onMessage(const TcpConnectionPtr& conn, Buffer *buf, Timestamp time)
{
std::string msg = buf->retrieveAllAsString();
conn->send(msg);
conn->shutdown();//关闭写端,底层就会相应EPOLLHUP事件,会执行closeCallback_
}
EventLoop* loop_;
TcpServer server_;
};
int main()
{
EventLoop loop;
InetAddress addr(8000);
/**
* 当定义EchoServer的时候,里面会创建TcpServer对象,相当于要创建Acceptor对象,
* Acceptor相当于要创建non-blocking的listenfd,然后create,bind
*/
EchoServer server(&loop, addr, "EchoServer-01");
/**
* 进入listen状态,创建loopthread,把listenfd打包成acceptChannel,然后把它注册在mainloop上
* mainloop主要接收新用户连接,有新用户连接了,用轮询算法选择一个subloop进行connection的分发
*/
server.start();
loop.loop();//启动mainloop的底层poller
return 0;
}
业务代码流程梳理: 使用muduo库首先定义一个EventLoop(baseLoop),一个InetAddress打包了ip+port
- 构造TcpServer对象:创建了server把loop和addr给到EchoServer的构造函数,初始化底层的TcpServer对象,相当于这里创建了TcpServer对象
- 设置两个回调
- 设置底层loop线程数量,这个数量不包括baseLoop。 这样很好的把网络代码和业务代码分离,开发者只需要关注onConnection方法(响应连接的建立和断开)和onMessage(响应读写事件)。
- 调用start启动TcpServer,启动loop
一、TcpServer构造对象都做了哪些事情?
1. 构造一个Acceptor对象,工作在mainLoop线程
创建一个non-blocking(非阻塞)的listenfd,然后打包成Channel,socket绑定本机地址,给Acceptor注册读事件回调(Acceptor只需要处理读事件即可)。此外TcpServer::start()时,Acceptor才开始监听,往mainLoop的Poller上注册listenfd
2. 构造EventLoopThreadPool对象
构造EventLoopThreadPool对象,此时还没开启loop线程
3. 设置有新连接到来时执行的回调
TcpServer设置Acceptor在有新连接到来时,需要执行的回调newConnetionCallback_,有新连接到来时需要执行TcpServer::newConnection。实际上,有新连接到来时,Acceptor对象会先执行Acceptor::handleRead,Acceptor::handleRead内部再调用newConnetionCallback_,即TcpServer::newConnection
二、TcpServer.start()
首先通过atomic_int变量控制TcpServer重复start,然后启动底层线程池、执行Acceptor的listen方法
1. threadPool_->start(threadInitCallback_)
启动底层的loop线程池
启动底层线程池就是创建loop子线程并开启loop.loop()
线程池启动时,会创建事件循环线程EventLoopThread,让该EventLoopThread调用startLoop方法
接着会让事件循环对应的Thread类线程启动
这里的thread_.start方法就会调用thread_生成时传入的回调函数,即EventLoopThread::threadFunc
EventLoopThread::threadFunc方法创建了EventLoop对象loop,loop.loop()然后启动事件循环,这就是启动了subloop
构造EventLoop的时候,会注册wakeupFd到Poller,可以让mainLoop来唤醒子线程的loop,用wakeupFd作为线程间的notify,效率高。
那EventLoop是什么时候构造的呢?
EventLoopThreadPool::start被调用时,会构造EventLoopThread对象,然而此时该事件循环线程对象EventLoop为空,并没有构造EventLoop对象,EventLoop对象是在 调用thread->startLoop()然后调用thread_.start() 时调用EventLoopThread::threadFunc方法时构造的
2. loop_->runInLoop(std::bind(&Acceptor::listen, acceptor_.get()));
执行Acceptor的listen方法
这一行代码就相当于开启fd的listen模式,并把listenfd打包成accepChannel注册在baseLoop上,即使用epoll监听。 至此,subloop全都启动了,listenfd也注册到Poller上了,已经全部准备好了.
三、用户调用loop.loop()
用户调用loop.loop(),就是启动mainLoop
四、整个服务器启动流程
- 构造Acceptor对象,创建listenfd,给Acceptor注册回调
- 构造TcpServer对象,同时包含了注册回调,设置底层线程的个数,构造EventLoopThreadPool对象,设置有新连接到来时执行的回调
- TcpServer.start开启subloop,构造EventLoop对象的同时将wakeupfd注册到Poller,能够让主线程mainloop来唤醒子线程loop
- Acceptor.listen,把listenfd注册到Poller上。
- 最后启动mainLoop
五、新连接到来服务器的处理
新连接到来时,listenfd就会有响应,执行其acceptChannel_相应的readCallback_,readCallback_就是handleRead,handleRead有调用了newConnectionCallback_,newConnectionCallback_就是TcpServer::newConnection
所以有新连接到来时,会调用TcpServer::newConnection,函数如下:
- 首先根据轮询算法选择一个subLoop,让ioLoop指针指向这个subLoop,
- 创建一个TcpConnection对象,将TcpConnection对象放入TcpServer的数据成员connections_进行存储
- 注册回调connectionCallback_、messageCallback_、writeCompleteCallback_、CloseCallback,这些回调将来由TcpConnection设置到底层的Channel里,这些回调函数都是TcpServer给TcpConnection设置的,而这些回调函数会在TcpConnection的以下函数中调用:
而上述TcpConnection的成员函数早就被当作回调函数注册在了Channel上
发生事件时,Channel会调用相应的回调函数,即调用到了TcpConnection的成员函数,而在TcpConnection的成员函数内部又调用了TcpServer给TcpConnection设置的回调函数,这些TcpServer的回调数据成员又都是用户编程时,调用set方法给TcpServer设置的
TcpServer提供用于设置回调函数的接口:
用户调用方式
4. ioLoop->runInLoop=>TcpServer::connectEstablished
如果设置过subloop的数量,这个ioLoop所属的线程一定不是执行TcpServer::newConnection的线程,因为新连接的创建一定是在mainloop中完成,所以这里的runInLoop一定会执行queueInLoop
在queueInLoop里面,这个执行mainloop的线程先把当前待执行的回调函数TcpConnection::connectEstablished放入subloop的数据成员pendingFunctors_
callingPendingFunctors_ == true表示subloop的线程正在执行pendingFunctors_里的回调函数,不需要唤醒,直接走人
callingPendingFunctors_ == false则表示subloop暂时没有客户的读写事件,正在epoll_wait阻塞等待,执行mainloop的线程则会执行wakeup方法,往每个EventLoop都有的成员wakeupFd上写数据,让epoll_wait返回发生的事件,这个subloop所在的线程就会执行pendingFunctors_里的回调了
反之如果没有设置过subloop的数量,整个Reactor模型只有一个mainloop,那直接执行回调函数TcpConnection::connectEstablished
connectEstablished方法设置TcpConnection的状态为kConnected、将Channel对象和一条TcpConnection绑定、往Poller上注册Channel的读事件,调用用户传入的on_connection方法
那为什么Channel和TcpConnection需要绑定呢?
因为EventLoop通过Poller调用epoll_wait返回后,会通过Channel调用相应的回调处理事件,而Channel的回调函数都是TcpConnection设置的
如果由于某些原因TcpConnection对象不存在了,一旦发生事件,Channel再执行这些回调也就没有意义了
所以Channel用一个数据成员weak_ptr来观察TcpConnection对象,每次Channel执行回调函数前都会用weak_ptr的lock方法检查TcpConnection对象是否存活,存活则调用回调函数,否则不调用
六、有消息到来时,服务器的处理
已连接用户的可读事件到来,相应的loop线程的poller就会调用epoll_wait返回,然后调用Channel的readCallBack_
TcpServer给Channel设置readCallBack_过程如下:
首先Channel的readCallBack_是通过Channel的成员方法setReadCallBack设置的,设置Channel的回调肯定是在TcpConnection中进行的
可以看到,TcpConnection给Channel的成员readCallBack_设置的是TcpConnection::handleRead,这个函数里面调用了messageCallback_
messageCallback_也是通过调用TcpConnection::setMessageCallback设置的,给TcpConnection设置方法,那只能是在TcpServer中进行
TcpServer调用setMessageCallback那就是用户调用的了,如下:
所以,这样一层一层设置,Channel调用readCallBack_,就是调用TcpConnection::handleRead,TcpConnection::handleRead又会调用messageCallback_,即调用用户通过TcpServer设置的自定义函数on_message
就可以拿到原始的字符串,就可以进行业务处理和响应
七、连接断开时,服务器处理
如果有异常,不管是对端关闭还是服务端主动调用shutdown断开,底层EventLoop通过Poller调用epoll_wait返回,然后调用Channel都会响应closeCallback_
显然,Channel的回调函数Channel::closeCallBack_是TcpConnection设置的,是TcpConnection::handleClose
handleClose首先设置TcpConnection状态为kDisconnected,然后把Channel对应的fd以及fd所感兴趣的事件从Poller上全部删除
然后执行用户传入的on_connection方法,再执行TcpConnection::closeCallback_,这个方法是新连接到来时,在TcpServer::newConnection设置的,即TcpConnection::removeConnection方法
最终调用到TcpServer::removeConnectionInLoop方法,先在TcpServer用于记录连接的connections_中删除当前已经断开连接的TcpConnection对象,然后获取该TcpConnection对象所属subloop,然后让对应线程执行TcpConnection::connectDestroyed
一般来说,if语句是不会满足条件的,我们在TcpConnection::handleClose中,就设置了TcpConnection对象的状态为kDisconnected,最后调用Channel::remove方法,Channel::remove会调用到EPollPoller::removeChannel
先从Poller的容器中移除断开连接的Channel,接下来这个if语句一般是不会成立的
先从Poller的容器中移除断开连接的Channel,接下来这个if语句一般是不会成立的
因为index在前面的TcpConnection::handleClose中会调用:
channel_->disableAll();
就会把index设置为kDeleted
最后就是把channel的状态设置为kNew
TcpConnection::handleClose中,用shared_ptr指向当前断开连接的TcpConnection对象,这个shared_ptr出作用域后,会自动释放对象空间