muduo库TcpConnection对send、shutdown、SIGPIPE的处理


muduo库对send的处理:

muduo库的send()函数重载了三个:

 void send(const void* message, int len);
 void send(const StringPiece& message);   //短字符优化子string类
 void send(Buffer* message);  // this one will swap data

分别发送不同的类型,实际上它们有调用关系的,我们挑一种分析即可,发送字符数组:

//调用发送字符串的send,发送字符缓冲区
void TcpConnection::send(const void* data, int len)
{
  send(StringPiece(static_cast<const char*>(data), len));
}
转化为发送StringPiece类型:

void TcpConnection::send(const StringPiece& message)
{
  if (state_ == kConnected)
  {
    //如果在I/O线程,直接调用
    if (loop_->isInLoopThread())
    {
      sendInLoop(message);
    }
    else
    {
      //如果不在,跨线程调用,需要传递缓冲区message过去,有一定的开销
      loop_->runInLoop(
          boost::bind(&TcpConnection::sendInLoop,
                      this,     // FIXME
                      message.as_string()));
                    //std::forward<string>(message)));
    }
  }
}

可以看出如果处于已连接状态,且当前线程是I/O线程,有EventLoop,那么直接调用sendInLoop()发送,如果不是I/O线程,需要跨线程传入I/O线程,不过还是执行sendInLoop()函数,有一定的开销。

这个函数是这样实现的:

void TcpConnection::sendInLoop(const StringPiece& message)
{
  sendInLoop(message.data(), message.size());
}

又调用下一层:

void TcpConnection::sendInLoop(const StringPiece& message)
{
  sendInLoop(message.data(), message.size());
}


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;
  }
  //没有关注可写事件,并且缓冲区数目为0,直接发送
  // if no thing in output queue, try writing directly
  if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)
  {
    nwrote = sockets::write(channel_->fd(), data, len);
    if (nwrote >= 0)
    {
      remaining = len - nwrote;
      if (remaining == 0 && writeCompleteCallback_)
      {
        loop_->queueInLoop(boost::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;
        }
      }
    }
  }


  //没有错误,并且还有未写完的数据(说明内核缓冲区满,要将未写完的数据添加到output buffer中
  assert(remaining <= len);
  if (!faultError && remaining > 0)
  {
    //如果超过高水位标志,逻辑可能有问题,回调highWaterMarkCallback,不过还是要写
    size_t oldLen = outputBuffer_.readableBytes();
    if (oldLen + remaining >= highWaterMark_
        && oldLen < highWaterMark_
        && highWaterMarkCallback_)
    {
      loop_->queueInLoop(boost::bind(highWaterMarkCallback_, shared_from_this(), oldLen + remaining));
    }


    //把剩余数据追加到outputbuffer,并注册POLLOUT事件
    outputBuffer_.append(static_cast<const char*>(data)+nwrote, remaining);
    if (!channel_->isWriting())
    {
      channel_->enableWriting();
    }
  }
}

函数有两个大情况:

    情况1:当我们调用send()时,如果没有关注可写事件并且缓冲区无缓存,那我们直接立即写入。这是为什么呢?因为muduo这就是一般异步网络库的通用方法,缓存应用层传过来的数据,在I/O设备可写的情况下尽量写入数据。如果关注了POLLOUT事件,意味着我们要等待POLLOUT事件发生才能写入。如果缓冲区有数据,意味者我们必须先写完缓冲区数据,所以都不能立即写。写入之后,可能没有写完需要注册writeCompleteCallback事件(这个函数是用户注册的写完数据的回调函数)。如果写出错,且errno!=EWOULDBLOCK,也就是EAGAIN,那就表示出错了。

    writeComplateCallback()函数表示数据发送完毕回调函数,即所有的用户数据都已拷贝到内核缓冲区时调用该回调函数,Buffer被清空也要调用该回调函数,可以理解为lowWaterMarkCallback()函数。上层用户只管调用conn->send()函数发送数据,网络库负责将用户数据拷贝到内核缓冲区。通常情况下编写大流量的应用程序,才需要关注writeCompleteCallback()。大流量的应用程序不断生成数据,然后send(),如果对等方接收不及时,收到通告窗口的影响,发送缓冲区会不足这个时候,就会将用户数据添加到应用层的发送缓冲区,可能会撑爆outputBuffer。解决方法就是调整发送频率,只需要关注writeCompleteCallback,当所有用户数据都拷贝到内核缓冲区后,上层用户会得到通知,就可以继续发送数据。这需要上层用户的配合。对于低流量的应用程序,通常不需要关注writeCompleteCallback()。


    情况2:情况2与情况1并不能完全分割,如果我们没能够立即写入,即没有执行情况1的if语句,或者执行了但是没有写完,我们需要把剩余数据追加到outputBuffer中。如果我们追加数据,但是readable长度加上我们要追加的超度超过了高水位线,意味着我们逻辑可能有问题。不能总是一加数据就超标,高水位线或许不够高。所以需要让I/O线程执行highWaterMarkCallback()函数,做相应处理(这个函数是用户定义,由用户决定超高水位标是否进行修改)。然后把数据追加到outputBuffer,或许空间不够,但append()函数内部可以处理这个问题,不论是内部调整,还是再申请空间都可以。最后,如果没有注册POLLOUT事件,我们需要注册POLLOUT事件,但它发生时,我们的缓冲区数据就可以安心的发送了。

    highWaterMarkCallback()是高水位标回调函数,outputBuffer撑到一定程度时最好回调该函数。这意味着对等方接收不及时,导致outputBuffer不断增大,很可能因为我们没有关注completeWaterMarkCallback()。在这个回调函数中用户就可以断开这个连接,避免内存不足。


    讲到这里,不得不再提一下muduo库所谓的“三个半事件”:

1.连接建立:服务器accept(被动)接收连接,客户端connect(主动)发起连接。
2.连接断开:主动断开(close、shutdown),被动断开(read返回0)
3.消息到达:文件描述符可读
4.消息发送完毕:这算半个事件。对于低流量的服务,可以不关心这个事件。这里的发送完毕是数据写入操作系统缓冲区,将由TCP协议栈法则数据的发送与重传,不代表对方已经接数据。高流量服务可能导致内核缓冲区满了,数据还会追加到应用层缓冲区。

    上面我们进行实际上就是最后的这“消息发送完毕”的半个事件直接写入套接字实际上就是直接写入操作系统缓冲区,然后写入数据有剩余的情况下,我们把数据追加到Buffer中。至于数据到底有没有发送到对端,这让TCP去负责,所以就是半个事件。


此外,还有一个函数,我们上面那个函数末尾进行了enableWriting,注册了关注POLLOUT事件,事件发生后会调用TcpConnection的handleWrite()函数:

//内核缓冲区有空间了,回调该函数
void TcpConnection::handleWrite()
{
  loop_->assertInLoopThread();
  if (channel_->isWriting())  //关注了可写事件
  {
    //把outputBuffer缓冲区内容写入fd
    ssize_t n = sockets::write(channel_->fd(),
                               outputBuffer_.peek(),  //返回可读下标,
                               outputBuffer_.readableBytes());
    if (n > 0)
    {
      outputBuffer_.retrieve(n);  //写了n个字节,移动下标n个
      if (outputBuffer_.readableBytes() == 0)  //如果全部写完
      {
        channel_->disableWriting();   //数据发送完毕,停止关闭POLLOUT事件,以免出现busy loop
        if (writeCompleteCallback_) //回调writeCompleteCallback
        {
          //彻底写完调用writeCompleteCallback
          loop_->queueInLoop(boost::bind(writeCompleteCallback_, shared_from_this()));
        }
        if (state_ == kDisconnecting)  
        {
          shutdownInLoop();   //发送完毕关闭连接
        }
      }
    }
    else
    {
      LOG_SYSERR << "TcpConnection::handleWrite";
      // if (state_ == kDisconnecting)
      // {
      //   shutdownInLoop();
      // }
    }
  }
  else
  {
    LOG_TRACE << "Connection fd = " << channel_->fd()
              << " is down, no more writing";
  }
}

缓冲区有空间回调handleWrite()函数,发送数据完了会关闭连接,在这里由于数据发送完毕,调用了channel_->disableWriting()函数通知管制POLLOUT,会关闭连接。


muduo库对shutdown的处理:

一般情况下shutdown()函数也不是直接关闭的:

void TcpConnection::shutdownInLoop()
{
  loop_->assertInLoopThread();
  if (!channel_->isWriting())  //如果不再关注POLLOUT事件,否则不能关闭,那就仅仅是将状态更改为kDisconnecting,不能关闭连接
  {
    // we are not writing
    socket_->shutdownWrite();
  }
}
如果不再关注POLLOUT事件,才立即关闭连接,调用socket的shutdown()函数。否则不会关闭。


muduo库对SIGPIPE的处理:

    对一个对端已经关闭的socket调用两次write, 第二次将会生成SIGPIPE信号, 该信号默认结束进程.  
    具体的分析可以结合TCP的”四次握手”关闭. TCP是全双工的信道, 可以看作两条单工信道, TCP连接两端的两个端点各负责一条. 当对端调用close时, 虽然本意是关闭整个两条信道, 但本端只是收到FIN包. 按照TCP协议的语义, 表示对端只是关闭了其所负责的那一条单工信道, 仍然可以继续接收数据. 也就是说, 因为TCP协议的限制, 一个端点无法获知对端已经完全关闭.
    对一个已经收到FIN包的socket调用read方法, 如果接收缓冲已空, 则返回0, 这就是常说的表示连接关闭. 但第一次对其调用write方法时, 如果发送缓冲没问题, 会返回正确写入(发送). 但发送的报文会导致对端发送RST报文, 因为对端的socket已经调用了close, 完全关闭, 既不发送, 也不接收数据. 所以, 第二次调用write方法(假设在收到RST之后), 会生成SIGPIPE信号, 导致进程退出.

    服务器可是不能退出的,所以要忽略该信号。那么muduo库是怎么处理的呢?

    为了避免客户端每次都需要写一个signal()函数去捕获SIGPIPE信号,muduo库利用了RAII机制,在EventLoop.cc中有这样的定义:

class IgnoreSigPipe
{
 public:
  IgnoreSigPipe()
  {
    ::signal(SIGPIPE, SIG_IGN);
    // LOG_TRACE << "Ignore SIGPIPE";
  }
};

IgnoreSigPipe initObj;

可见,每当客户使用EventLoop时,由于该全局对象的初始化,SIGPIPE信号被自动设置为忽略了,好省事。


### muduo 中的 TCP 服务器实现与用法 muduo 是一个高性能的 C++ 网络,专为 Linux 平台设计,支持多线程事件驱动模型。它封装了许多复杂的底层操作,使得开发者能够专注于业务逻辑而不是网络编程细节。以下是关于 muduo 中 TCP 服务器实现和使用的详细介绍。 #### 泛型 TCP 服务器的设计 muduo 提供了一个通用的 `TcpServer` 类,用于构建基于 TCP 协议的服务端应用程序。该类的核心设计理念是通过事件循环机制处理连接请求、数据读写以及错误管理等功能[^1]。具体来说: - **EventLoop**: 这是一个核心组件,负责监听并分发各种 I/O 和定时器事件。 - **Channel**: 表示文件描述符上的事件注册状态及其回调函数绑定关系。 - **Acceptor**: 负责接受新的客户端连接,并将其交给主线程或其他工作线程进行进一步处理。 #### 创建一个简单的 TCP 服务器实例 下面展示如何利用 muduo 构建一个基本的回显服务器(Echo Server),即接收到的数据会被原样返回给发送方。 ```cpp #include "muduo/net/EventLoop.h" #include "muduo/net/TcpServer.h" using namespace muduo; using namespace muduo::net; void onConnection(const TcpConnectionPtr& conn) { LOG_INFO << (conn->connected() ? "UP" : "DOWN"); } void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp time) { std::string msg(buf->retrieveAllAsString()); conn->send(msg); } int main() { EventLoop loop; InetAddress listenAddr(2023); TcpServer server(&loop, listenAddr, "EchoServer"); server.setConnectionCallback(onConnection); server.setMessageCallback(onMessage); server.start(); loop.loop(); return 0; } ``` 上述代码片段展示了几个重要部分: - 设置地址 (`InetAddress`) 及初始化 `TcpServer`. - 定义连接建立或断开时触发的动作 (`onConnection`)。 - 处理消息接收后的响应行为 (`onMessage`)【^2】。 #### 关键概念解析 为了更好地理解以上代码的功能,这里解释一些重要的术语和技术点: - **Buffer**: 数据缓冲区对象,在异步通信场景下尤为重要;它可以有效减少频繁内存分配带来的性能损耗。 - **Timestamp**: 时间戳结构体,用来记录特定时刻以便后续统计或者超时判断之用。 - **TcpConnectionPtr**: 对应于每条已建立成功的TCP链路的一个智能指针形式表示。 此外值得注意的是,muduo内部大量采用了RAII原则来自动释放资源从而降低潜在泄漏风险;同时也提供了丰富的日志工具帮助调试复杂问题. #### 性能优化建议 虽然上面的例子已经足够简单易懂,但在实际生产环境中还需要考虑更多因素才能达到最佳效果。例如可以通过调整线程池大小匹配硬件能力、启用零拷贝技术提高吞吐量等方式来进行针对性改进[^4]. ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值