东阳的学习笔记
muduo 只有一种关闭连接的方式:被动关闭(p. 191)。
- 即:对方先关闭连接,本地 read(2) 返回0,触发关闭逻辑。
如果有必要也可给 TcpConnection 新增 forceClose() 成员函数,用于主动关闭连接,实现很简单,调用 handleClose() 即可。
函数调用的逻辑如下图:
- 一般来说数据的删除比新建要麻烦,TCP 连接也不例外。关闭连接的流程看上去有点 “绕”,原因是对象生命期管理的需要(p. 274)
- 其中 X 表示 TcpConnection 通常会在此析构
一、Channel 的改动
Channel class 新增了 CloseCallback 事件回调,并且断言
在事件处理期间本 Channel 不会析构
,即不会发生 p. 274 讲的出错情况。
POLLHUB
:当 socket 的另一端关闭时或读到文件结尾时。Channel::~Channel() { // 断言在事件处理期间本channel对象不会析构 assert(!eventHandling_); } void Channel::handleEvent() { +eventHandling_ = true; if (revents_ & POLLNVAL) { LOG_WARN << "Channel::handle_event() POLLNVAL"; } +if ((revents_ & POLLHUP) && !(revents_ & POLLIN)) { // 添加了 close 事件 + LOG_WARN << "Channel::handle_event() POLLHUP"; + if (closeCallback_) closeCallback_(); + } if (revents_ & (POLLERR | POLLNVAL)) { if (errorCallback_) errorCallback_(); } if (revents_ & (POLLIN | POLLPRI | POLLRDHUP)) { if (readCallback_) readCallback_(); } if (revents_ & POLLOUT) { if (writeCallback_) writeCallback_(); } +eventHandling_ = false; }
二、TcpConnection 的改动
TcpConnection class 也新增了 CloseCallback 事件回调,但:
- 这个回调是给 TcpServer 和 TcpClient 用的,用于
通知他们移除所持有的 TcpConnectionPtr
,这不是给普通用户用的,普通用户继续使用 ConnectionCallback。
+ void setCloseCallback(const CloseCallback& cb) + { closeCallback_ = cb; } // called when TcpServer accepts a new connection void connectEstablished(); // should be called only once + // called when TcpServer has removed me from its map + void connectDestroyed(); // should be called only once private: ! enum StateE { kConnecting, kConnected, kDisconnected, }; void setState(StateE s) { state_ = s; } void handleRead(); + void handleWrite(); + void handleClose(); + void handleError();
2.1 TcpConnection::handleRead()
TcpConnection::handleRead()
会检查read(2) 的返回值
,根据返回值分别调用 messageCallback_、handleClose()、handleError()。void TcpConnection::handleRead() { char buf[65536]; ssize_t n = ::read(channel_->fd(), buf, sizeof buf); + if (n > 0) { messageCallback_(shared_from_this(), buf, n); + } else if (n == 0) { + handleClose(); + } else { + handleError(); + } }
2.2 TcpConnection::handleClose()
TcpConnection::handleClose()
的主要功能是调用 closeCallback_
,这个回调绑定到TcpConnection::removeConnection()
void TcpConnection::handleClose() { loop_->assertInLoopThread(); LOG_TRACE << "TcpConnection::handleClose state = " << state_; assert(state_ == kConnected); // we don't close fd, leave it to dtor, so we can find leaks easily. channel_->disableAll(); // must be the last line closeCallback_(shared_from_this()); // 这个回调绑定到`TcpConnection::removeConnection()` }
2.3 TcpConnection::handleError()
TcpConnection::handleError() 并没有采取进一步的行动,只是在日志中输出错误消息,
这并不影响连接的正常关闭。
void TcpConnection::handleError() { int err = sockets::getSocketError(channel_->fd()); LOG_ERROR << "TcpConnection::handleError [" << name_ << "] - SO_ERROR = " << err << " " << strerror_tl(err); }
2.4 TcpConnection::connectDestoryed()
TcpConnection::connectDestoryed() 是 TcpConnection
析构前最后调用的一个成员函数
:
- 它通知用户连接已断开。其中的 &6 与 handleClose 中的 &7 重复,这是因为在某些情况下可以不经由 handleClose() ,而直接调用
connectDestoryed()
。void TcpConnection::connectDestroyed() { loop_->assertInLoopThread(); assert(state_ == kConnected); setState(kDisconnected); channel_->disableAll(); connectionCallback_(shared_from_this()); loop_->removeChannel(get_pointer(channel_)); }
三、TcpServer的改动
TcpServer 向 TcpConnection 注册 CloseCallback,用于接收连接断开的消息。
- 将连接保存到
std::map<std::string, TcpConnectionPtr> ConnectionMap
中 - 注册相应的回调函数
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
// 省略没有变化的代码
TcpConnectionPtr conn(
new TcpConnection(loop_, connName, sockfd, localAddr, peerAddr));
connections_[connName] = conn; // 将连接保存在Map中
conn->setConnectionCallback(connectionCallback_);
conn->setMessageCallback(messageCallback_);
conn->setCloseCallback(
boost::bind(&TcpServer::removeConnection, this, _1));
conn->connectEstablished();
}
3.1 TcpServer::removeConnection
通常 TcpServer 的生命期长于它建立的 TcpConnection,
因此不用担心 TcpServer 对象失效
(所以这里没用shared_from_this
)。在 muduo 中,TcpServer 的析构函数会关闭连接,因此也是安全的。
- TcpServer::removeConnection() 把 conn 从
ConnectionMap
中移除。这时 TcpConnection 已经 “奄奄一息
”。注意这里一定要用 queueInLoop() ,否则就会出现对象生命期管理的问题。
- 这里使用 bind, 让 TcpConnection 的生命期长到调用
connectDestroyed
void TcpServer::removeConnection(const TcpConnectionPtr& conn) { loop_->assertInLoopThread(); LOG_INFO << "TcpServer::removeConnection [" << name_ << "] - connection " << conn->name(); size_t n = connections_.erase(conn->name()); assert(n == 1); (void)n; loop_->queueInLoop( boost::bind(&TcpConnection::connectDestroyed, conn)); }
思考并验证:如果用户不持有 TcpConnectionPtr,那么 TcpConnection 对象究竟是在什么时候析构的??
四、EventLoop 和 Poller的改动
本节的 TcpConnection 不再是只生不灭,因此要求 EventLoop 也提供 unregister 功能。EventLoop 新增了 removeChannel() 成员函数,它会调用 Poller::removeChannel():
- 从 EventLoop 所管理的 Poller 所管理的
std::vector<struct pollfd>
中移除void Poller::removeChannel(Channel* channel) { assertInLoopThread(); LOG_TRACE << "fd = " << channel->fd(); assert(channels_.find(channel->fd()) != channels_.end()); assert(channels_[channel->fd()] == channel); assert(channel->isNoneEvent()); int idx = channel->index(); assert(0 <= idx && idx < static_cast<int>(pollfds_.size())); const struct pollfd& pfd = pollfds_[idx]; (void)pfd; assert(pfd.fd == -channel->fd()-1 && pfd.events == channel->events()); size_t n = channels_.erase(channel->fd()); assert(n == 1); (void)n; if (implicit_cast<size_t>(idx) == pollfds_.size()-1) { pollfds_.pop_back(); } else { int channelAtEnd = pollfds_.back().fd; iter_swap(pollfds_.begin()+idx, pollfds_.end()-1); if (channelAtEnd < 0) { channelAtEnd = -channelAtEnd-1; } channels_[channelAtEnd]->set_index(idx); pollfds_.pop_back(); } }
注意其中从数组 pollfds_ 中删除元素是 O(1) 复杂度,办法是将待删除的元素与最后一个元素交换,再 pollfds_.pop_back()。这需要相应地修改 Poller::updateChannel 的代码:
// update existing one
assert(channels_.find(channel->fd()) != channels_.end());
assert(channels_[channel->fd()] == channel);
int idx = channel->index();
assert(0 <= idx && idx < static_cast<int>(pollfds_.size()));
struct pollfd& pfd = pollfds_[idx];
assert(pfd.fd == channel->fd() || pfd.fd == -channel->fd()-1);
pfd.events = static_cast<short>(channel->events());
pfd.revents = 0;
if (channel->isNoneEvent()) {
// ignore this pollfd
pfd.fd = -channel->fd()-1;
}
}
}