muduo_net代码剖析之TcpConnection

版权声明:guojawee https://blog.csdn.net/weixin_36750623/article/details/84636886

TcpConnection类可谓是muduo最核心也是最复杂的类,它的头文件和源文件一共有450多行,是muduo最大的类。
TcpConnection是muduo里唯一默认使用shared_ptr来管理的class,也是唯一继承enable_shared_from_this的类,这源于其模糊的生命期。

1、TcpServer在接收并建立连接的过程中,TcpConnection做了什么工作?

在这里插入图片描述

经过前文对TcpServer类的讲解以及上图的展示,我们知道:

  1. TcpConnection是TcpServer的一个成员变量
  2. 当TcpServer接收到client的连接请求后,将会回调newConnection()函数:先创建TcpConnection对象用来与client连接,再执行connectEstablished()函数
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);
}
  1. 在connectEstablished()函数中,实现了:关注sockfd/channel_的读事件;回调connectionCallback_(即defaultConnectionCallback)
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进行初始化。

2、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. 有应用层的发送和接收缓冲区:inputBuffer_、outputBuffer_
  2. highWaterMark_与highWaterMarkCallback_:分别表示高水位线和高水位线回调函数
    Q:什么情况下会调用highWaterMarkCallback_?
    A:当Buffer中的readIndex_到达highWaterMark_时,将会触发highWaterMarkCallback_
  3. 非常重要的参数:writeCompleteCallback_
    Q1:什么时候会触发writeCompleteCallback_回调函数?
    A1:两种情况,分别是 ==> ①当用户直接调用write(channel_->fd(), data, len),并且将所有的数据全部都发送给了内核缓冲区 ②POLLOUT事件触发(回调了TcpConnection::handleWrite),导致outputBuffer_中的数据全部转移到内核缓冲区
    Q2:什么情况下需要关注writeCompleteCallback_回调函数?
    A2:编写大流量的应用程序,才需要关心writeCompleteCallback_;而编写低流量的应用程序,不需要关心writeCompleteCallback_
    Q3:为什么大流量的应用程序,才需要关心writeCompleteCallback_?
    A3:
    大流量的应用程序将会导致内存被撑爆,具体分析 ==> 假设应用程序不断的调用TcpConnection::send()函数想发送数据给内核中的缓冲区,但是由于发送的数据量非常大,将会导致内核缓冲区被填满; 当内核缓冲区被填满后,将会把数据暂时发送到outputBuffer_中,但是随着outputBuffer_不断自增,将会撑爆内存。
    解决方案:使用writeCompleteCallback_控制TcpConnection::send()的发送频率,具体步骤 ==> ①关注writeCompleteCallback_ ②当用户调用TcpConnection::send()向内核缓冲区发送数据时,通过关注writeCompleteCallback_通知用户,用户才能发送下一条数据给内核缓冲区(显然,这样就避免了数据不断的堆积到应用层的outputBuffer_中,不会造成outputBuffer_被撑爆。)

3、使用TcpConnection::send发送消息

实际上,即便用户将数据发送给内核中的接收缓冲区过程十分复杂,但是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";
  }
}

4、TcpConnection自动接收消息

一旦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回射回去
}

5、关闭连接

//半关闭,关闭本端的写
void shutdown(); // NOT thread safe, no simultaneous calling
//强制关闭链接,其实就是调用close而已。就是个close
void forceClose();
//使用计时器,定时关闭
void forceCloseWithDelay(double seconds);

6、不是很重要的函数

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;

7、完善TcpConnection

7.1 SIGPIPE

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

7.2 TCP No Delay 和 TCP keepalive

禁用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不是必须的。

8、示例代码

实现了回射服务器EchoServer

#include <muduo/net/TcpServer.h>

#include <muduo/base/Logging.h>
#include <muduo/base/Thread.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/InetAddress.h>

#include <utility>

#include <stdio.h>
#include <unistd.h>

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();
}

猜你喜欢

转载自blog.csdn.net/weixin_36750623/article/details/84636886
今日推荐