1.Connection的生命周期
关于Connection的生命周期,那可以先看看Server类是如何管理Connction的。Server类是使用std::unordered_map管理Connection的智能指针的。
那假如我们不用智能指针,就直接使用栈变量呢,如这样管理unordered_map<int,Connection>。
//创建出Connection对象的函数实现
void Server::newConnection(int sockfd)
{
setNonblock(sockfd);
auto conn = std::make_shared<Connection>(loop_, sockfd);
//假如使用栈空间的话,例如:Conneecton conn(loop_,sockfd);
connections_[sockfd] = conn;
conn->setMessageCallback(messageCallback_);
conn->setCloseCallback([this](const ConnectionPtr& connection) {removeConnection(connection); });
conn->connectEstablished(); //建立连接,
}
使用栈空间的话,那上图中的第六步中erase()函数就会析构掉该Connection对象,而此时Channel的handleEvent()还正在调用,直接删除Connection对象,那么其成员channel_ 也会被析构,那就会产生core dump错误。
所以要让Connection对象生命周期长于handleEvent()函数。
那假如用普通指针呢,在上图中的第六步使用了erase()函数,那之后要想析构该Connection对象,就找不到了。
因为connections_这时已经没有保存到该元素,就不能析构了。
Connection 对象生存期要长于handleEvent() 函数,直到执行完connectDestroyed() 后才可以析构。
使用智能指针std::shared_ptr<Connection>能很好的解决这个问题。
std::enable_shared_from_this的使用。
class person:public std::enable_shared_from_this<person>
{
public:
person(){}
~person() { cout << "~person().." << endl; }
auto getptr_this() //使用这个引用计数不会+1 ,只会维持原来的
{ return this; }
auto getptr(){ return shared_from_this(); }
};
int main()
{
auto p = std::make_shared<person>();
cout << "p.use_count= " << p.use_count() << endl; //这里会打印出 1
auto t = p->shared_from_this();
//auto getthis = p->getptr_this();
cout << "hou p.use_count= " << p.use_count() << endl //这里会打印出 2
return 0;
}
若一个类person继承std::enable_shared_from_this<person>,则会为该类person提供成员函数:shared_from_this()。
看上面的例子:当person类型对象 被一个为名为p的std::shared_ptr<person>类对象管理时,调用person::shared_from_this成员函数,将会返回一个新的std::shared_ptr<Connection>对象t,t与p共享该person类型对象的所有权,即是该引用计数+1了。
void Connection::handleRead()
{
int savedErrno = 0;
auto n = inputBuffer_.readFd(fd(), &savedErrno);
if (n > 0) {
messageCallback_(shared_from_this(), &inputBuffer_); //这个是用户设置好的函数
}
//....................省略
}
在异步编程中,当我们需要有回调函数,需要把参数传递给回调函数时,而当回调函数开始执行时,原先传递过来的对象是可能存在已经析构的风险。
messageCallback_函数中使用shared_from_this()是不能解决这个原先传递过来的对象是可能存在已经析构的风险问题的。
而是解决在回调函数刚开始执行的时候,传递过来的对象还没析构,而在该回调函数执行的过程中,该connetion对象被其他的线程析构了,那这回调函数还在继续执行,这就会出现问题,所以使用shared_from_this()去使引用计数+1。若其他线程不想用这个对象,要进行析构,而这个回调函数一直在执行,让其引用计数始终不为0,那其他线程就不可能能析构这个对象。
所以是保证在该回调函数执行过程中,传递过来的对象不会被析构;而不是保证在执行回调函数之前,该对象就不会被析构。要弄清楚这个时间点。
这样也是为了可以全局统一使用Connection的智能指针。
要保证在执行回调函数之前,传递的对象是需要其他的操作。
2.保证在执行回调函数之前,传递的对象还存活
因为只有channel才能收到Epoll给它通知的事件回调,Connetion类对应的channel执行的事件回调都是Connection设置给它的。
问题来了:如果由于一些原因这个Connection对象没有了,那channel到时候还执行不执行回调?
所以,channel在这里使用了 weak_ptr弱智能指针来记录了这个Connection对象,到时候通过弱智能指针的提升来监测Connection是否存活,存在就执行相应的回调,不存在就不执行了。
Channel类添加成员变量std::weak_ptr<void> tie_和std::atomic_bool tied_,添加成员函数tie(const std::shared_ptr<void> &obj)。
void Channel::tie(const std::shared_ptr<void> &obj)
{
tie_ = obj;
tied_ = true;
}
这个函数在TCP建立新连接时使用
void TcpConnection::connectEstablished()
{
setState(kConnected);
channel_->tie(shared_from_this()); //新添加的,将此通道绑定到shared_ptr管理的所有者对象,防止在handleEvent中销毁所有者对象。
channel_->enableReading();
}
那么Channel::handleEvent()需要修改为
//channel得到poller通知以后,处理事件的
void Channel::handleEvent(Timestamp receiveTime)
{
if (tied_){//绑定过
std::shared_ptr<void> guard = tie_.lock();//弱智能指针-》强智能指针 ,若提升成功,引用计数+1
if (guard){//是否存活 ,存活才调用回调函数
handleEventWithGuard();
}
}
else { //这个else里面是用来建立连接的,因为开始建立连接的时候tied_是false,是连接建立后开始 通信tied_才为true
handleEventWithGuard();
}
}
//得到发生的具体事件, 由channel负责调用具体的回调操作
void Channel::handleEventWithGuard(Timestamp receiveTime)
{
if ((revents_ & EPOLLHUP) && !(revents_ & EPOLLIN)){
if (closeCallback_){
closeCallback_();
}
}
if (revents_ & EPOLLERR){
if (errorCallback_){
errorCallback_();
}
}
//.........省略其他事件
}
这样就可以保证在准备执行回调函数的时刻,该Connection对象时存在的;如果不存在,就不会调用回调函数。
那这样有些使用shared_from_this()的情况就不需要了嘛(不过为了安全,也是可以使用的),因为通过前面说的这点可以确保在执行回调函数的时刻,该Connection对象时存在的。
3.添加关闭回调函数和错误处理回调函数
在Connection 构造函数中添加
channel_->setCloseCallback([this]() {handleClose(); });
channel_->setErrorCallback([this]() {handleError(); });
那么在Chnannel类中也要添加setCloseCallback()等函数,也要在Chnannel::handleEvent()函数中添加对应的关闭和错误回调函数的使用。
handleError()只是调用::getsockopt(channel_->fd(), SOL_SOCKET, SO_ERROR, &optval, &optlen)去获取错误,没有其他的操作,需要有其他的操作,可以自行添加。
4.添加连接成功或关闭的通知回调函数
这个功能是给用户设置的,通过让用户设置好这个函数。在连接成功或失败或关闭的时候,可以去通知用户,去实现用户的一些操作。简单点的,例如:连接成功后,用户想打印出客户端的ip地址之类的。
在Server类中添加成员变量connectionCallback_,在Connection类中也需要添加该成员变量,这个回调函数也是和messageCallback_一样,是通过Server类调用Server::setConnectionCallback()来设置给Server::messageCallback_,接着Connection类再把Server::messageCallback_通过Connection::setConnectionCallback()设置为自己的messageCallback_。
using ConnectionCallback = std::function<void(const ConnectionPtr&)>;
ConnectionCallback connectionCallback_;
要想在连接建立成功时打印出客户端的ip地址,那Connection类就要保存该fd的ip地址,需要在Connection类添加成员变量InetAddress peerAddr_,这个比较简单,就不多说了。
需要在 Server::newConnection(int sockfd, const InetAddr& peerAddr)中添加setConnectionCallback()函数。
void Server::newConnection(int sockfd, const InetAddr& peerAddr)
{
auto conn = std::make_shared<Connection>(loop_, sockfd, localAddr,peerAddr);
connections_[sockfd] = conn;
//...........其他的省略
//绑定连接建立或断开函数
conn->setConnectionCallback(connectionCallback_);
conn->connectEstablished(); //建立连接
}
connectionCallback_的使用时刻
有两处,在建立连接时(Connection::connectEstablished()),关闭连接时(Connection::connectDestroyed())。
void Connection::connectEstablished()
{
assert(state_ == StateE::kConnecting);
setState(StateE::kConnected);
channel_->tie(shared_from_this());
channel_->enableReading();
//connectionCallback_使用的位置
connectionCallback_(shared_from_this()); //调用用户设置的连接成功或断开的回调函数
}
void Connection::connectDestroyed()
{
if (state_ == StateE::kConnected) {
setState(StateE::kDisconnected);
channel_->disableAll();
//connectionCallback_使用的位置
connectionCallback_(shared_from_this());//调用用户设置的连接成功或断开的回调函数
}
channel_->remove();
}
其实这个函数目前也没有什么用处,感觉是可以不要的,也不会影响这逻辑。所以也就放在这节来用于完善Connection。
5添加写完成通知回调函数
在Connection类和Server类中添加
using WriteCompleteCallback = std::function<void(const ConnectionPtr&)> ;
WriteCompleteCallback writeCompleteCallback_;
这个回调函数和connectionCallback_也是类似的,需要先通过Servrer类设置的。
writeCompleteCallback_的使用时刻
void Connection::send(const std::string& message)
{
if (state_ == StateE::kDisconnected)
return;
ssize_t nwrote = 0;
size_t reamining = message.size();
//如果当前channel没有写事件发生,并且发送缓冲区无待发送的数据,那就可以直接发送
if (!channel_->isWrite() && outputBuffer_.readableBytes() == 0) {
nwrote = ::write(fd(), message.data(), message.size());
if (nwrote >= 0) {
reamining = message.size() - nwrote;
if (reamining == 0) {
//这是新添加的
//表示数据已完全发送出去,通知用户写已完成
if (writeCompleteCallback_) {
writeCompleteCallback_(shared_from_this());
}
}
}
//...........其他的省略
}
}
需要理解的是,这个回调函数不是通知用户说数据已发送到客户端的,是数据已写到内核发送区,用户端是否接受到是未知的。
这个函数目前也是用处不大。
6.添加主动关闭函数
添加shutdown(),forceClose()函数
void Connection::shutdown()
{
if (state_ == StateE::kConnected) {
setState(StateE::kDisconnecting);
if (!channel_->isWrite()) {
sockets::shutdownWrite(fd());
}
}
}
void Connection::forceClose()
{
if (state_ == StateE::kConnected || state_ == StateE::kDisconnecting)
{
setState(kDisconnecting);
handleClose();
}
}
Connection::shutdown没直接关闭Tcp连接,只关闭了写端。
Connection没有提供close,而只提供shutdown(),这么做是为了收发数据的完整性。
Tcp是一个全双工协议,同一个文件描述符即可读又可写,shutdownWrite() 关闭了 “ 写”
方向的连接,保留了“ 读 ”方向。如果直接close(socketfd),那么socketfd就不能读或写了。
用shutdown而不用close的效果是,如果对方已经发送了数据,这些数据还“ 在路上 ”,
那么程序就不会漏收这些数据。换句话说,这可以在TCP这一层面解决了“当你打算关闭网络忘记的时候,如何得知对方是否发了一些数据而你还没有收到?”这一问题。
如果当前outputBuffer里面还有数据未发出的话,那也不会立刻调用shutwownWrite,而是等到数据发送完毕再执行shutdown,可以避免对方漏收数据。
7.总结
单reactor单线程
Reactor就是核心类EventLoop,就是一个循环。简单点说,其实就是一个epoll,通过epoll_wait()函数来进行dispatch。黄色的read(),send()这些就是可以看成是Connection类。
这样,基础的单Reactor的模型就形成了。这是一个重要的版本,之后的版本都是在这个基础上面添加内容的,例如需要使用多线程。
完整源代码:https://github.com/liwook/CPPServer/tree/main/code/server_v10