muduo库TcpConnection类
参考
长文梳理Muduo库核心代码及优秀编程细节剖析
接着之前的[muduo网络库]——muduo库Buffer类,我们接下来继续看muduo库中的TcpConnection类。
一 TcpConnection
1.1 概述
TcpConnection是整个网络库的核心, 封装一次TCP连接,以及控制该TCP连接的方法(连接建立,关闭和销毁),以及该连接发生个各种事件(读、写、错误、连接)对应的处理函数,以及这个Tcp连接的服务端和客户端的套接字地址信息,但是注意它不能发起连接
1.2 主要功能
1. 连接管理
- 建立连接:在
mainLoop
中的Acceptor
接收到一个新连接请求的时候,回调TcpServer
中的新连接处理函数,在其中选择一个subLoop
并由此构造出一个TcpConnection
对象,后者的构造函数中设置好本连接的相关信息,mainLoop
就会将这个新Connection
放到subLoop
中运行。在连接建立之后,会紧接着向EPollPoller
注册感兴趣的事件,并设置连接状态。 - 断开连接:当需要断开连接时,
TcpConnection
会关闭底层的socket文件描述符
,并触发相关的回调函数。此外,它还会处理连接关闭时的清理工作,如取消事件注册、释放资源等。
2. 数据传输
- 接收数据:当底层socket有数据可读时,事件循环会通知关联的
Channel
对象。TcpConnection
会读取数据并调用注册的MessageCallback回调函数来处理接收到的数据。 - 发送数据:应用程序可以通过调用
TcpConnection
的发送接口(send
和sendInLoop
)来发送数据。这些数据会被缓存在TcpConnection
对象内部缓冲区Buffer
中,并通过底层socket逐步发送出去。当所有数据都发送完成时,TcpConnection
会调用注册的writeCompleteCallback
回调函数来通知应用程序
3. 状态通知
- 连接状态变化:当连接状态发生变化时(如建立、断开),
TcpConnection
会触发注册的connectionCallback
回调函数来通知应用程序。这样,应用程序可以根据连接状态的变化来执行相应的逻辑,如重新连接、关闭会话等
1.3 重要成员变量
EventLoop* loop_
:该Tcp连接的Channel注册到对应的sub EventLoop。const string name_
:客户端的名字StateE state_
: 客户端的状态,对应的有一个枚举类型,分别对应着已经断开连接,正在连接,已经连接,正在断开连接。
enum StateE { kDisconnected, kConnecting, kConnected, kDisconnecting };
bool reading_
:连接是否正在监听读事件std::unique_ptr<Socket> socket_
:连接套接字, 用于对连接进行底层操作std::unique_ptr<Channel> channel_
:通道, 用于绑定要监听的事件const InetAddress localAddr_
:本地IP地址const InetAddress peerAddr_
:对端IP地址connectionCallback_
,messageCallback_
,writeCompleteCallback_
,highWaterMarkCallback_
,closeCallback_
:对应的连接建立/关闭后的处理函数,收到消息后的处理函数,消息发送完后的处理函数,高水位回调,连接关闭后的处理函数。size_t highWaterMark_
:因为发送数据,应用写得快,内核发送数据慢,需要把待发送的数据写入缓冲区,且设置了水位回调,防止发送太快Buffer inputBuffer_
,Buffer outputBuffer_
:输入输出缓冲区,在输出缓冲区是用于暂存那些暂时发送不出去的待发送数据。因为Tcp发送缓冲区是有大小限制的,假如达到了高水位线,就没办法把发送的数据通过send()直接拷贝到Tcp发送缓冲区,而是暂存在这个outputBuffer_中,等TCP发送缓冲区有空间了,触发可写事件了,再把outputBuffer_中的数据拷贝到Tcp发送缓冲区中。
1.4 重要成员函数
- 构造函数,设置读/写/关闭/错误的回调函数
TcpConnection::TcpConnection(EventLoop *loop,
const string &nameArg,
int sockfd,
const InetAddress &localAddr,
const InetAddress &peerAddr)
: loop_(CHECK_NOTNULL(loop)),
name_(nameArg),
state_(kConnecting),
reading_(true),
socket_(new Socket(sockfd)),
channel_(new Channel(loop, sockfd)),
localAddr_(localAddr),
peerAddr_(peerAddr),
highWaterMark_(64 * 1024 * 1024)
{
channel_->setReadCallback(
std::bind(&TcpConnection::handleRead, this, _1));
channel_->setWriteCallback(
std::bind(&TcpConnection::handleWrite, this));
channel_->setCloseCallback(
std::bind(&TcpConnection::handleClose, this));
channel_->setErrorCallback(
std::bind(&TcpConnection::handleError, this));
LOG_DEBUG << "TcpConnection::ctor[" << name_ << "] at " << this
<< " fd=" << sockfd;
//开启Tcp/Ip层的心跳包检测
socket_->setKeepAlive(true);
}
- 一系列的获取loop_,name_,地址,状态的函数
EventLoop* getLoop() const { return loop_; }
const string& name() const { return name_; }
const InetAddress& localAddress() const { return localAddr_; }
const InetAddress& peerAddress() const { return peerAddr_; }
bool connected() const { return state_ == kConnected; }
bool disconnected() const { return state_ == kDisconnected; }
newConnection()
,生成一次Tcp连接对象,该对象封装了该Tcp连接即将分发给每一个subEentLoop
这里的subEentLoop是由线程池threadPool_中取得的。
void TcpServer::newConnection(int sockfd, const InetAddress &peerAddr)
{
loop_->assertInLoopThread();
EventLoop *ioLoop = threadPool_->getNextLoop();
char buf[64];
snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);
++nextConnId_;
string connName = name_ + buf;
LOG_INFO << "TcpServer::newConnection [" << name_
<< "] - new connection [" << connName
<< "] from " << peerAddr.toIpPort();
InetAddress localAddr(sockets::getLocalAddr(sockfd));
// FIXME poll with zero timeout to double confirm the new connection
// FIXME use make_shared if necessary
// 生成一次Tcp连接对象,该对象封装了该Tcp连接即将分发给每一个subEentLoop,即ioLoop,
// 以及这条Tcp连接的名字(不重要)
// 以及已连接的客户端fd,还有本服务器的端口IP信息,即localAddr
TcpConnectionPtr conn(new TcpConnection(ioLoop,
connName,
sockfd,
localAddr,
peerAddr));
connections_[connName] = conn;
// 将用户自定义的连接处理函数,可读事件处理函数,写事件处理函数以及muduo定义的连接关闭事件处理函数封装经conn对象,这样如果发生了以上事件,
// 那就可以从这个conn对象中拿出这些函数来执行
conn->setConnectionCallback(connectionCallback_);
conn->setMessageCallback(messageCallback_);
conn->setWriteCompleteCallback(writeCompleteCallback_);
conn->setCloseCallback(
std::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe
// 在subEventLoop线程中执行TcpConnection::connectEstablished,
ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
}
- 发送数据
void TcpConnection::send(const std::string &buf) //直接引用buffer
{
if(state_ == kConnected)
{
if(loop_->isInLoopThread())
{
//string.c_str是Borland封装的String类中的一个函数,它返回当前字符串的首字符地址。
sendInLoop(buf.c_str(),buf.size());
}
else
{
loop_->runInLoop(std::bind(&TcpConnection::sendInLoop
, this
, buf.c_str()
, buf.size()
));
}
}
}
void TcpConnection::sendInLoop(const void* data, size_t len)
{
ssize_t nwrote = 0;
size_t remaining = len; //未发送的数据
bool faultError = false; //记录是否产生错误
//之前调用过connection的shutdown 不能在发送了
if(state_ == kDisconnected)
{
LOG_ERROR("disconnected,give up writing!");
return ;
}
//channel 第一次开始写数据,且缓冲区没有待发送数据
if(!channel_->isWriting() && outputBuffer_.readableBytes() == 0)
{
nwrote = ::write(channel_->fd(),data,len);
if(nwrote >= 0)
{
remaining = len - nwrote;
if(remaining == 0 && writeCompleteCallback_)
{
loop_->queueInLoop(
std::bind(writeCompleteCallback_,shared_from_this()));
}
}
else
{
nwrote = 0;
if(errno != EWOULDBLOCK) //用于非阻塞模式,不需要重新读或者写
{
LOG_ERROR("TcpConnection::sendInLoop");
if(errno == EPIPE || errno == ECONNRESET) //SIGPIPE RESET
{
faultError = true;
}
}
}
}
if(!faultError && remaining > 0)
{
//目前发送缓冲区剩余的待发送数据的长度
size_t oldlen = outputBuffer_.readableBytes();
if(oldlen + remaining >= highWaterMark_
&& oldlen < highWaterMark_
&& highWaterMark_)
{
loop_->queueInLoop(
std::bind(highWaterMarkCallback_,shared_from_this(),oldlen + remaining)
);
}
outputBuffer_.append((char*)data + nwrote,remaining);
if(!channel_->isWriting())
{
channel_->enableWriting(); //注册channel写事件,否则poller不会向channel通知epollout
}
}
}
- 发送数据:要发送的数据长度时
len
,如果在loop_
当前的线程里面,就调用sendInLoop
,sendLoop
内部实际上时调用了系统的write
,如果一次性发送完了,就设置writeCompleteCallback_
,表明不要再给Channel设置EPOLLOUT事件了 - 如果没有写完,先计算以下
oldlen
目前发送缓冲区剩余的待发送的长度。满足:if(oldlen + remaining >= highWaterMark_ && oldlen < highWaterMark_&& highWaterMark_)
就会触发高水位回调
高水位回调:当缓冲区中的数据量达到高水位线时,触发回调,通知应用停止发送数据,避免缓冲区溢出。 - 不满足以上的话,直接写入
outputBuffer_
- 剩余的数据保存再缓冲区,要给
Channel
注册到epollout
事件(切记,一定要注册channel的写事件,否则poller不会向channel通知epollout),这样poller发现tcp发送缓冲区有空间,会通知相应的socket-channel调用相应的writeCallback()
回调方法,也就是调用TcpConnection::handleWrite
,把发送缓冲区中数据全部发送出去
- 重中之重
handleRead()
负责处理Tcp数据连接的可读事件,它会将客户端发来的数据拷贝到用户缓冲区inputBuffer_
,然然后再调用connectionCallback_
保存的连接建立后的处理函数
void TcpConnection::handleRead(Timestamp receiveTime)
{
loop_->assertInLoopThread();
int savedErrno = 0;
ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);
if (n > 0)
{
messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
}
else if (n == 0)
{
handleClose();
}
else
{
errno = savedErrno;
LOG_SYSERR << "TcpConnection::handleRead";
handleError();
}
}
- 首先调用Buffer中的readFd从该TCP接受缓冲区中将数据读取来并放入buffer,接着判断是否有可读事件发生
- n>0,证明有可读事件发生,调用用户设置的回调函数,
shared_from_this()
获取了当前TcpConnection对象的智能指针 - n=0,说明客户端断开了,调用连接关闭后的处理函数,调用connectionCallback_保存的连接建立后的处理函数;
- n<0 出错了,调用错误处理回调;
handleWrite()
负责处理Tcp连接的可写事件
void TcpConnection::handleWrite()
{
if(channel_->isWriting())
{
int savedErrno = 0;
ssize_t n = outputBuffer_.writeFd(channel_->fd(),&savedErrno);
if(n > 0)
{
outputBuffer_.retrieve(n); //处理了n个
if(outputBuffer_.readableBytes() == 0) //发送完成
{
channel_->disableWriting(); //不可写了
if(writeCompleteCallback_)
{
//唤醒loop对应的thread线程,执行回调
loop_->queueInLoop(
std::bind(writeCompleteCallback_,shared_from_this())
);
}
if(state_ == kDisconnecting)
{
shutdownInLoop();// 在当前loop中删除TcpConnection
}
}
}
else
{
LOG_ERROR("TcpConnection::handleWrite");
}
}
else
{
LOG_ERROR("TcpConnection fd=%d is down, no more writing \n",channel_->fd());
}
}
- 如果可写,通过fd发送数据,直到发送完成
- 设置不可写,如果writeCompleteCallback_,唤醒loop对应的thread线程,执行回调
- 当前TCP正在断开连接,调用shutdownInLoop,在当前loop中删除TcpConnection
- 处理Tcp连接关闭的事件
handleClose()
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();
TcpConnectionPtr guardThis(shared_from_this());
connectionCallback_(guardThis); // 执行连接关闭的回调
// must be the last line
closeCallback_(guardThis); // 关闭连接的回调 TcpServer => TcpServer::removeConnection
}
关闭之前首先设置状态为关闭连接,然后将这个TcpConnect对象中的Channel从事件监听器中移除,然后调用connectionCallback_和closeCallback_保存回调函数。
-shutdown()
关闭连接
void TcpConnection::shutdown()
{
// FIXME: use compare and swap
if (state_ == kConnected)
{
setState(kDisconnecting);
// FIXME: shared_from_this()?
loop_->runInLoop(std::bind(&TcpConnection::shutdownInLoop, this));
}
}
void TcpConnection::shutdownInLoop()
{
loop_->assertInLoopThread();
if (!channel_->isWriting())//说明当前outputBuffer中的数据已经全部发送完成
{
// we are not writing
socket_->shutdownWrite();// 关闭写端
}
}
注意:这里的断开连接只是关闭了写端,并没有使用close(),陈硕老师原话:muduo TcpConnection没有提供close(), 而只提供shutdown(), 这么做是为了收发数据的完整性。TCP是一个全双工协议, 同一个文件描述符既可读又可写,shutdownWrite()关闭了“写”方向的连接, 保留了“读”方向, 这称为TCP half-close。 如果直接close(socket_fd), 那么socket_fd就不能读或写了。用shutdown而不用close的效果是, 如果对方已经发送了数据, 这些数据还“在路上”, 那么muduo不会漏收这些数据。 换句话说, muduo在TCP这一层面解决了“当你打算关闭网络连接的时候, 如何得知对方是否发了一些数据而你还没有收到? ”这一问题。 当然, 这个问题也可以在上面的协议层解决, 双方商量好不再互发数据, 就可以直接断开连接。也就是说muduo把“主动关闭连接”这件事情分成两步来做, 如果要主动关闭连接, 它会先关本地“写”端, 等对方关闭之后, 再关本地“读”端。
另外如果当前outputbuffer里面还有数据尚未发出的话,Muduo也不会立刻调用shutwownWrite
,而是等到数据发送完毕再shutdown
,可以避免对方漏收数据
在提供公共接口的类中Buffer,InetAddress,EventLoop,EventLoopThread,TcpConnection,TcpClient,TcpServer,总共7个类里面,TcpConnection
的生命期依靠shared_ptr
管理(即用户和库共同控制) 。 Buffer
的生命期由TcpConnection
控制。 其余类的生命期由用户控制。muduo只有一种关闭连接的方式: 被动关闭(见此处) 。 即对方先关闭连接, 本地read(返回0, 触发关闭逻辑
关闭连接的事情相当重要。一般来讲数据的删除比新建要复杂, TCP连接也不例外。 关闭连接的流程看上去有点“绕”, 根本原因是对对象生命期管理的需要。
当对方断开TCP连接时,这个IO事件会触发Channel::handleEvent()调用,后者会回调用户提供的CloseCallback,而用户在onclose()中有可能会析构Channel对象, 这就造成了灾难。 等
于说Channel::handleEvent()
执行到一半的时候, 其所属的Channel对象本身被销毁了。
muduo的解决办法是提供Channel::tie(const boost::shared_ptr<void>&)
这个函数, 用于延长某些对象的生命期, 使之长过Channel::handleEvent()
函数。 这也是muduo TcpConnection
采用
shared_ptr
管理对象生命期的原因之一。函数调用的流程见下图, 其中的“X”表示TcpConnection
通常会在此时析构。
我们重点分析TcpConnection的生命周期,即TcpConnection的引用计数问题
- 首先连接到来创建
TcpConnection
,并存入容器,引用次数+1 ,此时总数:1 - 客户端断开连接,在
Channel
的handleEvent
函数中会将Channel中的TcpConnection
弱指针提升,引用计数+1,总数:2 - 触发
TcpConnection::handleRead
,可读字节为0,进而触发HandleClose
,HandleClose函数中栈上的TcpConnectionPtr guardThis会继续计数+1,总数:3 - 触发handleclose的回调函数,在
TcpServer::removeConnection
结束后(回归主线程队列),释放HandleClose的栈指针,以及Channel里提升的指针引用计数-2 总数:1 - 主线程执行回调
removeConnectionInLoop
,在函数内部将tcpconnection从TcpServer中保存连接容器中erase掉。但在removeConnectionInLoop结尾用conn为参数构造了bind。引用计数不变 总数:1 - 回归次线程处理connectDestroyed事件,结束完释放参数传递的最后一个shard_ptr,释放TcpConnection。引用计数-1 总数:0
// 这个函数是在TcpServer::newConnection中选中的subEentLoop()所绑定的线程中执行的
void TcpConnection::connectEstablished()
{
loop_->assertInLoopThread();
// 设置状态为已连接
assert(state_ == kConnecting);
setState(kConnected);
// tie实际上是一个弱指针,延长TcpConnection的生命周期
channel_->tie(shared_from_this());
// 这里的Channel封装的是刚建立好的客户端fd,
// 所以这里是想要客户端fd及可读事件注册到这个subEventLoop的事件监听器上
channel_->enableReading();
// 调用用户提供的连接事件处理函数
connectionCallback_(shared_from_this());
}
二 TCP连接总结
至此,我们介绍了Channel,EPollPoller,EventLoop,Acceptor,Socket,Connection,Buffer,TcpConnection八大类,我们来总结一次完整的tcp通信。
这是一开始给出的muduo使用例子
#include <muduo/net/TcpServer.h>
#include <muduo/base/Logging.h>
#include <boost/bind.hpp>
#include <muduo/net/EventLoop.h>
// 使用muduo开发回显服务器
class EchoServer
{
public:
EchoServer(muduo::net::EventLoop *loop,
const muduo::net::InetAddress &listenAddr);
void start();
private:
// 用户定义的连接事件处理函数:
void onConnection(const muduo::net::TcpConnectionPtr &conn);
// 用户定义的可读事件处理函数:
void onMessage(const muduo::net::TcpConnectionPtr &conn,
muduo::net::Buffer *buf,
muduo::Timestamp time);
muduo::net::TcpServer server_;
};
EchoServer::EchoServer(muduo::net::EventLoop *loop,
const muduo::net::InetAddress &listenAddr)
: server_(loop, listenAddr, "EchoServer")
{
server_.setConnectionCallback(
boost::bind(&EchoServer::onConnection, this, _1));
server_.setMessageCallback(
boost::bind(&EchoServer::onMessage, this, _1, _2, _3));
//设置sub reactor数量
server_.setThreadNum(3);
}
void EchoServer::start()
{
server_.start();
}
void EchoServer::onConnection(const muduo::net::TcpConnectionPtr &conn)
{
LOG_INFO << "EchoServer - " << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
}
void EchoServer::onMessage(const muduo::net::TcpConnectionPtr &conn,
muduo::net::Buffer *buf,
muduo::Timestamp time)
{
// 接收到所有的消息,然后回显
muduo::string msg(buf->retrieveAllAsString());
LOG_INFO << conn->name() << " echo " << msg.size() << " bytes, "
<< "data received at " << time.toString();
conn->send(msg);
}
int main()
{
LOG_INFO << "pid = " << getpid();
// 这个EventLoop就是main EventLoop,(主reactor)即负责循环事件监听处理新用户连接事件的事件循环器。
muduo::net::EventLoop loop;
// InetAddress其实是对socket编程中的sockaddr_in进行封装,使其变为更友好简单的接口而已。
muduo::net::InetAddress listenAddr(8888);
// 调用TCPserver
EchoServer server(&loop, listenAddr);
// 启动TcpServer服务器
server.start();
// 执行EventLoop::loop()函数
loop.loop();
}
对于用户:
- 建立事件循环(主reactor反应器):
EventLoop loop
; - 创建服务器,即TcpServer类对象:
TcpServer server
; - 向TcpServer注册各类事件的用户自定义的处理函数:
setConnectionCallback()
,setMessageCallback()
; - 启动server:server.start();
- 开启事件循环loop.loop();
2.1 建立连接
2.1.1 逻辑图
这是借用我在地铁站里吃闸机的图,画的太清晰了,自己画的流程图没有他好,就直接借用他画的图
当我们调用TcpServer建立连接时,需要用户传入自定义的连接事件处理函数和用户自定义的可读事件处理函数,通过setConnectionCallback()
,setMessageCallback()
传入。上图明显分为两部分,第一部分是用户自定义的连接处理函数,第二部分是Tcpserver的构造函数,
-
用户自定义的连接处理函数通过
setConnectionCallback()
,称为成员变量connectionCallback_
,该变量在TcpServer::newConnection()
中被调用。newConnection中负责将新的Tcp连接分发个subEventLoop,并调用connectEstablished
建立新的Channel,同时将一个弱指针tie_绑定TcpConnection,延长延长TcpConnection的生命周期。 -
TcpServer::TcpServer()
构造函数:在构造函数中创建Acceptor类,同时把上面已传入成员变量的newConnection()
通过Acceptor
的setNewConnectionCallback()
传入,(Acceptor的作用:创建套接字socket,接受新客户端连接并分发连接给SubReactor),在Acceptor的构造函数中床设置可读事件的回调函数handleRead(),每当有新的客户端请求连接时,就会调用由Tcpserver传入的回调函数newConnection()
。
至此,TcpServer对象创建完毕,用户调用TcpServer::start( )方法,开启TcpServer。
void TcpServer::start()
{
if (started_.getAndSet(1) == 0)
{
threadPool_->start(threadInitCallback_);
assert(!acceptor_->listening());
loop_->runInLoop(
std::bind(&Acceptor::listen, get_pointer(acceptor_)));
}
}
void Acceptor::listen()
{
loop_->assertInLoopThread();
listening_ = true;
acceptSocket_.listen();
acceptChannel_.enableReading();
}
这里实际上调用了底层socket的listen()
,监听服务器套接字,以及将acceptChannel_
注册到main EventLoop
的事件监听器上区监听他的可读事件(新用户连接事件)。
接着用户调用loop.loop( )
,即调用了EventLoop::loop( )
函数,该函数就会循环的获取事件监听器的监听结果,并且根据监听结果调用注册在事件监听器上的Channel对象的事件处理函数。