TcpConnection类可谓是muduo最核心也是最复杂的类,它的头文件和源文件一共有450多行,是muduo最大的类。
TcpConnection是muduo里唯一默认使用shared_ptr来管理的class,也是唯一继承enable_shared_from_this的类,这源于其模糊的生命期。
经过前文对TcpServer类的讲解以及上图的展示,我们知道:
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
... ...
//2.在ioLoop上,创建一个TcpConnection对象
//构造函数的定义请向下看
TcpConnectionPtr conn(new TcpConnection(ioLoop,
connName,
sockfd,
localAddr,
peerAddr));
//3.将这个对象加入到ConnectionMap哈希表中
connections_[connName] = conn;
//4.使用TcpServer类的成员变量,给TcpConnection的成员变量赋值
conn->setConnectionCallback(connectionCallback_); //defaultConnectionCallback
conn->setMessageCallback(messageCallback_); //defaultMessageCallback
conn->setWriteCompleteCallback(writeCompleteCallback_);
conn->setCloseCallback(std::bind(&TcpServer::removeConnection, this, _1));
//5.调用conn->connectEstablished()
ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
}
//TcpConnection构造函数
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)), //sockfd连接的对方是[内核中的发送/接收缓冲区]
channel_(new Channel(loop, sockfd)), //channel_
localAddr_(localAddr),
peerAddr_(peerAddr),
highWaterMark_(64*1024*1024)
{
//当通道POLLIN事件到来的时候
channel_->setReadCallback(
std::bind(&TcpConnection::handleRead, this, _1));
//当通道POLLOUT事件到来的时候
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;
socket_->setKeepAlive(true);
}
void TcpConnection::connectEstablished()
{
loop_->assertInLoopThread();
assert(state_ == kConnecting);
setState(kConnected);
channel_->tie(shared_from_this());
channel_->enableReading(); //关注sockfd的读事件
//回调connectionCallback_
// connectionCallback_的默认值为defaultConnectionCallback
connectionCallback_(shared_from_this());
}
上面就是新的client连接到TcpServer后,所做的一些工作,主要是创建TcpConnection对象用于连接,并对TcpConnection进行初始化。
EventLoop* loop_; //需要一个唯一eventloop
const string name_; //name 为了标识一个链接,用于log
StateE state_; // FIXME: use atomic variable
bool reading_;
//we don't expose those classes to client.
std::unique_ptr<Socket> socket_; //sockfd 是该连接的文件描述符
std::unique_ptr<Channel> channel_; //用sockfe创建的channel_
const InetAddress localAddr_;
const InetAddress peerAddr_;
ConnectionCallback connectionCallback_;
MessageCallback messageCallback_;
WriteCompleteCallback writeCompleteCallback_; //
HighWaterMarkCallback highWaterMarkCallback_; //高水位线
CloseCallback closeCallback_;
size_t highWaterMark_;//当outputBuffer_的数据超过时后执行对应的操作
Buffer inputBuffer_; //应用层接收缓冲区
Buffer outputBuffer_; //应用层发送缓冲区
boost::any context_; //表示连接对象可以绑定一个未知类型的上下文对象
(1)将conn与fp绑定
FILE* fp = ::fopen(g_file, "rb"); //打开文件,返回指针fp
conn->setContext(fp); //将conn与fp绑定在一起,那么通过conn就能获取fp指针,通过这种方法,就不需要通过map容器来管理这种关系
(2)获得conn对应的fp
FILE* fp = boost::any_cast<FILE*>(conn->getContext());
实际上,即便用户将数据发送给内核中的接收缓冲区过程十分复杂,但是muduo库已经帮我们完成了该操作
那么,用户只需要调用conn->send(___)函数就相当于将用户代码中的buf发送给了内核中的接收缓冲区!
注:send函数是线程安全的,可以跨线程调用
send接口根据发送的数据类型重载了3种版本,见下:
void TcpConnection::send(const void* data, int len);
void TcpConnection::send(const StringPiece& message);
void TcpConnection::send(Buffer* buf);
send代码的实现最终都调用了sendInLoop函数,sendInLoop的整体的思想:①将用户空间的数据data,发送给发送缓冲区TcpConnection::outputBuffer_ ②outputBuffer_中有数据了,就关注POLLOUT事件channel_->enableWriting();
一旦内核缓冲区有空闲位置,就将触发POLLOUT事件进而回调TcpConnection::handleWrite函数将outputBuffer_中的数据拷贝到内核缓冲区
sendInLoop伪代码
void TcpConnection::sendInLoop(const void* data, size_t len)
{
size_t remaining = len; //待发送的数据、剩余待发送的数据
if(通道没有关注POLLOUT事件 &&outputBuffer_中没数据)
就将数据直接发送给内核缓冲区
remaining = len - 已经发送给内核缓冲区中的字节数nwrote
if(数据全部发送给了内核中的缓冲区,即remaining == 0)
回调writeCompleteCallback_
else{
if (!faultError && remaining > 0)
{
将数据写入outputBuffer_之前,需要判断写入remaining之后,是否高于高水位线higWaterMark_
如果超过higWaterMark_,回调highWaterMarkCallback_
}
将remaining个字节的数据写入到outputBuffer_
此时outputBuffer_中已经有数据了,就关注POLLOUT事件
/*如果内核接收缓冲区有空闲空间,就会触发POLLOUT事件,进而
回调TcpConnection::handleWrite()*/
}
}
sendInLoop代码
//先将data指向的数据,发送len个字节给应用层的发送缓冲区outputBuffer_
//再激活channel_的POLLOUT事件
void TcpConnection::sendInLoop(const void* data, size_t len)
{
loop_->assertInLoopThread();
ssize_t nwrote = 0;
size_t remaining = len;
bool faultError = false;
if (state_ == kDisconnected) //状态是没连接
{
LOG_WARN << "disconnected, give up writing";
return;
}
//if(通道没有关注POLLOUT事件 && 发送缓冲区没有数据)
// ==> 直接write到内核中的缓冲区
if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)
{
//直接write到内核中的缓冲区,写入的字节数为nwrote
nwrote = sockets::write(channel_->fd(), data, len);
if (nwrote >= 0)
{
remaining = len - nwrote; //remaining表示剩余要写的数据
//remaining == 0,即数据全部写完了 ==> 回调writeCompleteCallback_
if (remaining == 0 && writeCompleteCallback_)
{
loop_->queueInLoop(std::bind(writeCompleteCallback_, shared_from_this()));
}
}
else // nwrote < 0 写入失败
{
nwrote = 0;
if (errno != EWOULDBLOCK)
{
LOG_SYSERR << "TcpConnection::sendInLoop";
if (errno == EPIPE || errno == ECONNRESET) // FIXME: any others?
{
faultError = true;
}
}
}
}
assert(remaining <= len);
//综合:向outputBuffer_中再写remaining个字节的数据
//case 1:因为内核缓冲区满了,导致向内核缓冲区只发送了nwrote个字节,还剩下remaining个数据没发送
// 将剩下的数据存放入outputBuffer_
//case 2:if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)不成立,表示已经关注了POLLOUT事件 || outputBuffer_中有数据
// 则应当将新来的数据添加到outputBuffer_中
if (!faultError && remaining > 0)
{
size_t oldLen = outputBuffer_.readableBytes(); //获得当前outputBuffer_中的数据大小
//将数据写入outputBuffer_之前,需要判断写入remaining之后,是否高于高水位线
//如果超过higWaterMark_,回调highWaterMarkCallback_
if (oldLen + remaining >= highWaterMark_
&& oldLen < highWaterMark_
&& highWaterMarkCallback_)
{
loop_->queueInLoop(std::bind(highWaterMarkCallback_, shared_from_this(), oldLen + remaining));
}
//将remaining个字节的数据写入到outputBuffer_
outputBuffer_.append(static_cast<const char*>(data)+nwrote, remaining);
if (!channel_->isWriting()) //如果没有关注POLLOUT事件
{
//1.就关注POLLOUT事件,即说明outputBuffer_中已经有数据了
channel_->enableWriting();
//2.一旦内核缓冲区有空闲位置,就将触发POLLOUT事件进而回
//调TcpConnection::handleWrite函数将outputBuffer_中的
//数据拷贝到内核缓冲区
}
}
}
TcpConnection::handleWrite实现细节分析:①尽可能的将outputBuffer_中的数据全部发送到内核缓冲区;②如果全部发送,则取消关注POLLOUT事件,如果连接状态是kDisconnecting,就关闭连接;③如果没全部发送,即使连接状态是kDisconnecting,也不能立即关闭连接,要等待下一次POLLOUT事件触发
//outputBuffer_有数据了&&内核接收缓冲区有空闲,才触发POLLOUT事件
//触发POLLOUT事件将回调handleWrite()函数
void TcpConnection::handleWrite()
{
loop_->assertInLoopThread();
if (channel_->isWriting()) //如果关注了POLLOUT事件
{
//就将outputBuffer_中的所有的数据写入内核缓冲区中
ssize_t n = sockets::write(channel_->fd(),
outputBuffer_.peek(),
outputBuffer_.readableBytes());
if (n > 0)
{
outputBuffer_.retrieve(n);
//if outputBuffer_中的数据全部写完
if (outputBuffer_.readableBytes() == 0)
{
channel_->disableWriting(); //就取消关注POLLOUT事件,以免出现busy loop
//回调writeCompleteCallback_函数
if (writeCompleteCallback_)
{
loop_->queueInLoop(std::bind(writeCompleteCallback_, shared_from_this()));
}
//发送缓冲区已经清空&&连接状态是kDisconnecting,就关闭连接
if (state_ == kDisconnecting)
{
//因为上面已经取消关注POLLOUT事件,所以,shutdownWrite()会执行成功
shutdownInLoop();
}
}
}
else //如果outputBuffer_中的数据没有全部写完
{
LOG_SYSERR << "TcpConnection::handleWrite";
// 即使连接状态是kDisconnecting,因为剩下的数据还要继续向对方写
// 所以也不能立即关闭写,即不能立即调用shutdownInLoop()
// if (state_ == kDisconnecting)
// {
// shutdownInLoop();
// }
}
}
else
{
LOG_TRACE << "Connection fd = " << channel_->fd()
<< " is down, no more writing";
}
}
一旦POLLIN事件到来,就会回调handleRead函数,如果数据接收成功将回调注册的messageCallback_函数,详细请看伪代码:
伪代码
void TcpConnection::handleRead(Timestamp receiveTime)
{
将内核发送缓冲区的数据读取数据,存放到inputBuffer_的writeable中
if(读取数据成功)
messageCallback_(shared_from_this(), &inputBuffer_, receiveTime); //回调messageCallback_
else if(对方关闭)
handleClose(); //与client断开连接
else
handleError(); //LOG_ERROR打印错误日志
}
实现代码
void TcpConnection::handleRead(Timestamp receiveTime)
{
loop_->assertInLoopThread();
int savedErrno = 0;
//从内核中读取数据,存放到inputBuffer_中
ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);
if (n > 0) //readFd读取到数据,则回调messageCallback_
{
messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
}
else if (n == 0) //readFd==0,表示对方已经关闭,则关闭连接
{
handleClose();
}
else
{
errno = savedErrno;
LOG_SYSERR << "TcpConnection::handleRead";
handleError();
}
}
默认注册的回调函数messageCallback_,见下:
void muduo::net::defaultMessageCallback(const TcpConnectionPtr&,Buffer* buf,Timestamp)
{
buf->retrieveAll(); //清空Buffer
}
通过修改messageCallback_,实现回射服务器,代码见下:
void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp time)
{
//取出readable中所有的数据,并转换成string类型,再返回msg
string msg(buf->retrieveAllAsString());
LOG_TRACE << conn->name() << " recv " << msg.size() << " bytes at " << time.toString(); //打印
//根据msg的内容,执行相应的动作
if (msg == "exit\n")
{
conn->send("bye\n");
conn->shutdown();
}
if (msg == "quit\n")
{
loop_->quit();
}
conn->send(msg); //将msg回射回去
}
//半关闭,关闭本端的写
void shutdown(); // NOT thread safe, no simultaneous calling
//强制关闭链接,其实就是调用close而已。就是个close
void forceClose();
//使用计时器,定时关闭
void forceCloseWithDelay(double seconds);
void startRead(); //开始监听channel_的读事件
void stopRead(); //停止监听channel_的读事件
//获取对象内部的成员
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; } //判断是否处于断开连接的状态
bool getTcpInfo(struct tcp_info*) const;
string getTcpInfoString() const;
muduo库帮我们巧妙地实现了忽略SIGPIPE信号,即:在程序中定义了全局的IgnoreSigPipe对象(那么会在main函数之前调用构造函数,忽略SIGPIPE信号)
class IgnoreSigPipe
{
public:
IgnoreSigPipe()
{
::signal(SIGPIPE, SIG_IGN);
// LOG_TRACE << "Ignore SIGPIPE";
}
};
#pragma GCC diagnostic error "-Wold-style-cast"
IgnoreSigPipe initObj; //全局对象
} // namespace
禁用Nagle算法,避免连续发包出现延迟,这对编写低延迟网络服务很重要。
void TcpConnection::setTcpNoDelay(bool on)
{
socket_->setTcpNoDelay(on);
}
void Socket::setTcpNoDelay(bool on)
{
int optval = on ? 1 : 0;
::setsockopt(sockfd_, IPPROTO_TCP, TCP_NODELAY,
&optval, static_cast<socklen_t>(sizeof optval));
// FIXME CHECK
}
keepalive:定期查看TCP连接是否存在,一般来说,如果应用层心跳有心跳检测的话,TCP keepalive不是必须的。
实现了回射服务器EchoServer
#include
#include
#include
#include
#include
#include
#include
#include
using namespace muduo;
using namespace muduo::net;
int numThreads = 0;
class EchoServer
{
public:
EchoServer(EventLoop* loop, const InetAddress& listenAddr)
: loop_(loop),
server_(loop, listenAddr, "EchoServer")
{
server_.setConnectionCallback(
std::bind(&EchoServer::onConnection, this, _1));
server_.setMessageCallback(
std::bind(&EchoServer::onMessage, this, _1, _2, _3));
server_.setThreadNum(numThreads);
}
void start()
{
server_.start();
}
// void stop();
private:
void onConnection(const TcpConnectionPtr& conn)
{
LOG_TRACE << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
LOG_INFO << conn->getTcpInfoString();
conn->send("hello\n");
}
void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp time)
{
//接收readable中所有的数据,转换成string
string msg(buf->retrieveAllAsString());
LOG_TRACE << conn->name() << " recv " << msg.size() << " bytes at " << time.toString();
//根据msg的内容,执行相应的动作
if (msg == "exit\n")
{
conn->send("bye\n");
conn->shutdown();
}
if (msg == "quit\n")
{
loop_->quit();
}
conn->send(msg); //将msg回射回去
}
EventLoop* loop_;
TcpServer server_;
};
int main(int argc, char* argv[])
{
LOG_INFO << "pid = " << getpid() << ", tid = " << CurrentThread::tid();
LOG_INFO << "sizeof TcpConnection = " << sizeof(TcpConnection);
if (argc > 1)
{
numThreads = atoi(argv[1]);
}
bool ipv6 = argc > 2;
EventLoop loop;
InetAddress listenAddr(2000, false, ipv6);
EchoServer server(&loop, listenAddr);
server.start();
loop.loop();
}