muduo网络库学习:Tcp建立连接与断开连接
通常服务器在处理客户端连接请求时,为了不阻塞在accept函数上,会将监听套接字注册到io复用函数中,当客户端请求连接时,监听套接字变为可读,随后在回调函数调用accept接收客户端连接。muduo将这一部分封装成了Acceptor
类,用于执行接收客户端请求的任务。
Acceptor
对TCP socket, bind, listen, accept的封装 , 将sockfd以Channel的形式注册到EventLoop的Poller中.主要负责监听连接请求,调用listen() 接口时,通过Channel::enableReading() 把socket的描述符加到poller(I/O复用器)中。当有新连接到达时,先调用系统函数accept,再回调函数 newConnectionCallback_ 让TcpServer去创建连接。
void setNewConnectionCallback(const NewConnectionCallback& cb): 由服务器TcpServer设置的回调函数,在接收完客户端请求后执行,用于创建TcpConnection
void listen(): 用listen函数,转为监听套接字,同时将监听套接字添加到Poller中
void handleRead():回调函数,当有客户端请求连接时执行(监听套接字可读)
构造函数
Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport) : loop_(loop), acceptSocket_(sockets::createNonblockingOrDie(listenAddr.family())), acceptChannel_(loop, acceptSocket_.fd()), listenning_(false), idleFd_(::open("/dev/null", O_RDONLY | O_CLOEXEC))
void Acceptor::handleRead():
当有客户端尝试连接服务器时,监听套接字变为可读,epoll_wait/poll返回, EventLoop处理激活队列中的Channel,调用对应的回调函数, 监听套接字的Channel的回调函数是handleRead(),用于接收客户端请求,如果设置了回调函数,那么就调用,参数是客户端套接字和地址/端口,否则就关闭连接。这个回调函数是TcpServer中的newConnection,用于创建一个TcpConnection连接.
当有新连接到达时,先调用系统函数accept,再回调函数 newConnectionCallback_ 让TcpServer去创建连接。
一个不好理解的变量是 idleFd_
,它是一个文件描述符,这里是打开"/dev/null"
文件后返回的描述符,用于解决服务器端描述符耗尽的情况。原理如下:
当服务器端文件描述符耗尽,当客户端再次请求连接,服务器端由于没有可用文件描述符会返回-1,同时errno为EMFILE,意为描述符到达hard limit,无可用描述符,此时服务器端accept函数在获取一个空闲文件描述符时就已经失败,还没有从内核tcp连接队列中取出tcp连接这会导致监听套接字一直可读,因为tcp连接队列中一直有客户端的连接请求. 所以服务器在启动时打开一个空闲描述符/dev/null(文件描述符),先站着'坑‘,当出现上面情况accept返回-1时,服务器暂时关闭idleFd_让出'坑',此时就会多出一个空闲描述符,然后再次调用 accept接收客户端请求,然后close接收后的客户端套接字,优雅的告诉客户端关闭连接,然后再将'坑'占上
TcpConnection
TcpConnection类主要负责封装一次TCP连接,向Channel类注册回调函数(可读、可写、可关闭、错误处理),将来当Channel类上的事件发生时,调用相应的回调函数进行数据收发或者错误处理。
TcpConnection的定义主要都是写set*函数,成员变量比较多,但是重要的是以下几个
- 事件驱动循环loop_
- 用于tcp通信的socket_
- 用于监听sockfd的channel_
- 输入输出缓冲区inputBuffer_/outputBuffer_
- 由TcpServer提供的各种回调函数
connectionCallback_: 连接建立后/关闭后的回调函数,通常是由用户提供给TcpServer,然后TcpServer提供给TcpConnection。
messageCallback_:当tcp连接有消息通信时执行的回调函数,也是由用户提供
closeCallback_:tcp连接关闭时调用的回调函数,由TcpServer设置,用于TcpServer将这个要关闭的TcpConnection从保存着所有TcpConnection的map中删除,这个回调函数和TcpConnection自己的handleClose不同,后者是提供给Channel的,函数中会使用到closeCallback_
TcpConnection::handleRead,TcpConnection::handleWrite,TcpConnection::handleClose,TcpConnection::handleError
TcpConnection回调函数的设置对应了Channel的hanleEvent函数中根据不同激活原因调用不同回调函数(handleEvent调用handleEventWithGuard)。 另外,Channel::handleEvent中的tie_是对TcpConnection的弱引用,因为回调函数都是TcpConnection的,所以在调用之前需要确保TcpConnection没有被销毁,所以将tie_提升为shared_ptr判断TcpConnection是否还存在,之后再调用TcpConnection的一系列回调函数。
TcpServer
构造函数
TcpServer::TcpServer(EventLoop* loop, const InetAddress& listenAddr, const string& nameArg, Option option) : loop_(CHECK_NOTNULL(loop)), ipPort_(listenAddr.toIpPort()), name_(nameArg), acceptor_(new Acceptor(loop, listenAddr, option == kReusePort)), threadPool_(new EventLoopThreadPool(loop, name_)), connectionCallback_(defaultConnectionCallback), messageCallback_(defaultMessageCallback), nextConnId_(1) { acceptor_->setNewConnectionCallback( std::bind(&TcpServer::newConnection, this, _1, _2)); }
当TcpServer创建完TcpConnection后,会设置各种回调函数,然后调用TcpConnection的connectEstablished函数,主要用于将Channel添加到Poller中,同时调用用户提供的连接建立成功后的回调函数 。
TcpServer创建并设置TcpConnection
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
TcpServer会将用户提供的所有回调函数都传给TcpConnection,然后执行TcpConnection的connectEstablished函数,这个函数的执行要放到它所属的那个事件驱动循环线程做,不要阻塞TcpServer线程(这个地方不是为了线程安全性考虑,因为TcpConnection本身就是在TcpServer线程创建的,暴露给TcpServer线程很正常,而且TcpServer中也记录着所有创建的TcpConnection,这里的主要目的是不阻塞TcpServer线程,让它继续监听客户端请求)
创建TcpConnection
TcpConnectionPtr conn(new TcpConnection(ioLoop,
connName,
sockfd,
localAddr,
peerAddr));
/* 添加到所有tcp连接的map中,键是tcp连接独特的名字(服务器名+客户端<地址,端口>) */
Tcp连接建立的流程
1.服务器调用socket,bind,listen开启监听套接字监听客户端请求
2.客户端调用socket,connect连接到服务器
3.第一次握手客户端发送SYN请求分节(数据序列号)
4.服务器接收SYN后保存在本地然后发送自己的SYN分节(数据序列号)和ACK确认分节告知客户端已收到,同时开启第二次握手
5.客户端接收到服务器的SYN分节和ACK确认分节后保存在本地然后发送ACK确认分节告知服务器已收到
此时第二次握手完成,客户端connect返回。此时,tcp连接已经建立完成,客户端tcp状态转为ESTABLISHED,而在服务器端,新建的连接保存在内核tcp连接的队列中,此时服务器端监听套接字变为可读,等待服务器调用accept函数取出这个连接
6.服务器接收到客户端发来的ACK确认分节,服务器端调用accept尝试找到一个空闲的文件描述符,然后
从内核tcp连接队列中取出第一个tcp连接,分配这个文件描述符用于这个tcp连接。此时服务器端tcp转为ESTABLISHED,三次握手完成,tcp连接建立。
Muduo Tcp建立过程
- 创建服务器(TcpServer)时,创建Acceptor,设置接收到客户端请求后执行的回调函数
acceptor_->setNewConnectionCallback(
std::bind(&TcpServer::newConnection, this, _1, _2)); - Acceptor创建监听套接字,将监听套接字绑定到一个Channel中,设置可读回调函数为Acceptor的handleRead(这个回调函数是TcpServer::newConnection,用于创建一个TcpConnection连接)
- 服务器启动,调用Acceptor的listen函数创建监听套接字,同时将Channel添加到Poller中
- 有客户端请求连接,监听套接字可读,Channel被激活,调用可读回调函数(handleRead)
- 回调函数接收客户端请求,获得客户端套接字和地址,调用TcpServer提供的回调函数(TcpServer::newConnection)
- TcpServer的回调函数(TcpServer::newConnection)中创建TcpConnection代表这个tcp连接,设置tcp连接各种回调函数(由用户提供给TcpServer)
- TcpServer让tcp连接所属线程调用TcpConnection的connectEstablished
- TcpConnection::connectEstablished开启对客户端套接字的Channel的可读监听,然后调用用户提供的回调函数(连接建立后/关闭后的回调函数,通常是由用户提供给TcpServer,然后TcpServer提供给TcpConnection)
TcpServer::newConnection
TcpServer创建完TcpConnection --> 设置各种回调函数(第6步设置)
conn->setConnectionCallback(connectionCallback_);(第8步调用回调函数)
conn->setMessageCallback(messageCallback_);
conn->setWriteCompleteCallback(writeCompleteCallback_);
调用TcpConnection的connectEstablished函数
主要用于将Channel添加到Poller中,同时调用用户提供的连接建立成功后的回调函数 。
连接建立后,调用TcpConnection连接建立成功的回调函数,这个函数会调用用户提供的回调函数
1. 新建的TcpConnection所在事件循环是在事件循环线程池中的某个线程
2. 所以TcpConnection也就属于它所在的事件驱动循环所在的那个线程
3. 调用TcpConnection的函数时也就应该在自己所在线程调用
4. 所以需要调用runInLoop在自己的那个事件驱动循环所在线程调用这个函数
Muduo的Tcp断开连接
muduo只有一种关闭连接的方式:被动关闭。即对方先关闭连接,本地read(2)返回0,触发关闭逻辑。如果有必要也可以给TcpConnection新增forceClose()成员函数,用于主动关闭连接,实现很简单,调用handleClose()即可。函数调用的流程如下图,其中的“X”表示TcpConnection通常会在此时析构。
当客户端主动关闭(调用close)时,服务器端对应的Channel被激活,激活原因为EPOLLHUP,表示连接已关闭,此时会调用TcpConnection的回调函数handleClose,在这个函数中,TcpConnection处理执行各种关闭动作,包括
- 将Channel从Poller中移除
- 调用TcpServer提供的关闭回调函数,将自己从TcpServer的tcp连接map中移除
- 调用客户提供的关闭回调函数(如果有的话)
void TcpConnection::handleClose() { loop_->assertInLoopThread(); LOG_TRACE << "fd = " << channel_->fd() << " state = " << stateToString(); assert(state_ == kConnected || state_ == kDisconnecting); // we don't close fd, leave it to dtor, so we can find leaks easily. setState(kDisconnected); channel_->disableAll(); /* 此时当前的TcpConnection的引用计数为2,一个是guardThis,另一个在TcpServer的connections_中 */ TcpConnectionPtr guardThis(shared_from_this()); connectionCallback_(guardThis); // must be the last line /* * closeCallback返回后,TcpServer的connections_(tcp连接map)已经将TcpConnection删除,引用计数变为1 * 此时如果函数返回,guardThis也会被销毁,引用计数变为0,这个TcpConnection就会被销毁 * 所以在TcpServer::removeConnectionInLoop使用bind将TcpConnection生命期延长,引用计数加一,变为2 * 就算guardThis销毁,引用计数仍然有1个 * 等到调用完connectDestroyed后,bind绑定的TcpConnection也会被销毁,引用计数为0,TcpConnection析构 */ closeCallback_(guardThis); }
断开连接,函数执行顺序为: EventLoop::loop
->Poller::poll
->Channel::handleEvent
->TcpConnection::handleClose
->TcpServer::removeConnection
TcpConnection::handleClose 执行过程
1、--》TcpConnection::connectionCallback_--》TcpServer::connectionCallback_--》TcpServer::defaultConnectionCallback
2、--》TcpConnection::closeCallback_--》TcpServer::removeConnection
TcpConnection::handleClose()中connectionCallback_
是由用户提供的,连接建立/关闭时调用。主要调用TcpServer::closeCallback_
函数。这个函数主要就存在线程不安全的问题,原因就是此时的线程是TcpConnection所在线程 .
此时就将TcpServer暴露给其他线程,导致线程不安全的问题,为了减轻线程不安全带来的危险,尽量将线程不安全的函数缩短,muduo中使用runInLoop
直接将要调用的函数放到自己线程执行,转换到线程安全,所以这部分只有这一条语句是线程不安全的
void TcpServer::removeConnection(const TcpConnectionPtr& conn) { // FIXME: unsafe /* * 在TcpConnection所在的事件驱动循环所在的线程执行删除工作 * 因为需要操作TcpServer::connections_,就需要传TcpServer的this指针到TcpConnection所在线程 * 会导致将TcpServer暴露给TcpConnection线程,也不具有线程安全性 * * TcpConnection所在线程:在创建时从事件驱动循环线程池中选择的某个事件驱动循环线程 * TcpServer所在线程:事件驱动循环线程池所在线程,不在线程池中 * * 1.调用这个函数的线程是TcpConnection所在线程,因为它被激活,然后调用回调函数,都是在自己线程执行的 * 2.而removeConnection的调用者TcpServer的this指针如今在TcpConnection所在线程 * 3.如果这个线程把this指针delele了,或者改了什么东西,那么TcpServer所在线程就会出错 * 4.所以不安全 * * 为什么不在TcpServer所在线程执行以满足线程安全性(TcpConnection就是由TcpServer所在线程创建的) * 1.只有TcpConnection自己知道自己什么时候需要关闭,TcpServer哪里会知道 * 2.一旦需要关闭,就必定需要将自己从TcpServer的connections_中移除,还是暴露了TcpServer * 3.这里仅仅让一条语句变为线程不安全的,然后直接用TcpServer所在线程调用删除操作转为线程安全 */ loop_->runInLoop(std::bind(&TcpServer::removeConnectionInLoop, this, conn)); }
removeConnectionInLoop
函数如下
void TcpServer::removeConnectionInLoop(const TcpConnectionPtr& conn) { loop_->assertInLoopThread(); LOG_INFO << "TcpServer::removeConnectionInLoop [" << name_ << "] - connection " << conn->name(); size_t n = connections_.erase(conn->name()); (void)n; assert(n == 1); EventLoop* ioLoop = conn->getLoop(); /* * 为什么不能用runInLoop, why? */ /* * std::bind绑定函数指针,注意是值绑定,也就是说conn会复制一份到bind上 * 这就会延长TcpConnection生命期,否则 * 1.此时对于TcpConnection的引用计数为2,参数一个,connections_中一个 * 2.connections_删除掉TcpConnection后,引用计数为1 * 3.removeConnectionInLoop返回,上层函数handleClose返回,引用计数为0,会被析构 * 4.bind会值绑定,conn复制一份,TcpConnection引用计数加1,就不会导致TcpConnection被析构 */ ioLoop->queueInLoop( std::bind(&TcpConnection::connectDestroyed, conn)); }
比较重要的地方是TcpConnection生命期的问题,注释中也有提及。因为muduo中对象使用智能指针shared_ptr存储的,所以只有当shard_ptr的引用计数为0时才会析构它保存的对象。对于TcpConnection而言,它的引用计数在
- 建立之初保存在TcpServer的connections_中,这个connections_是一个map,键是字符串类型,值就是shared_ptr<TcpConnection>类型,所以建立之初,TcpConnection对象的引用计数为1
- 创建TcpConnection后,TcpConnection为内部用于监听文件描述符sockfd的Channel传递保存着自己指针的shared_ptr,但是Channel中的智能指针是以weak_ptr的形式存在的(tie_),不增加引用计数,所以此时仍然是1
- 在TcpConnection处于连接创建的过程中未有shared_ptr的创建和销毁,所以仍然是1
- 客户端主动关闭连接后,服务器端的TcpConnection在handleClose函数中又创建了一个shared_ptr引用自身的局部变量,此时引用计数加一,变为2
- 紧接着调用TcpServer::removeConnectionInLoop函数,因为传入的参数是引用类型,不存在赋值,所以引用计数仍为2
- 在removeConnectionInLoop函数中,TcpServer将TcpConnection从自己的connections_(保存所有tcp连接的map)中删除,此时引用计数减一,变为1
- TcpServer通过std::bind绑定函数指针,将TcpConnection::connectDestroyed函数和TcpConnection对象绑定并放到EventLoop中等待调用,因为std::bind只能是赋值操作,所以引用计数加一,变为2
- 返回到handleClose函数中,函数返回,局部变量销毁,引用计数减一,变为1
- EventLoop从poll中返回,执行TcpConnection::connectDestroyed函数,做最后的清理工作,函数返回,绑定到这个函数上的TcpConnection对象指针也跟着销毁,引用计数减一,变为0
- 开始调用TcpConnection析构函数,TcpConnection销毁
所以如果在第7步不使用std::bind增加TcpConnection生命期的话,TcpConnection可能在handleClose函数返回后就销毁了,根本不能执行TcpConnection::connectDestroyed函数
Linux多线程服务端编程:使用muduo C++网络库
https://blog.csdn.net/sinat_35261315/article/details/78343266