muduo库学习之设计与实现09——完善TcpConnection

东阳的学习笔记

前面几篇所介绍的 TcpConnection 的主体功能接近完备,可以应付大部分 muduo 示例的需求了。这里再补充几个小功能.

一、SIGPIPE

SIGPIPE 的默认行为是结束进程,在命令行程序这是合理的,但是在网络编程中, 这意味着如果对方断开连接而本地继续写入的话,这会造成服务进程意外退出。

假如服务进程繁忙,没有及时处理对方断开连接的事件,就有可能出现在连接断开之后继续发送数据的情况。下面这个例子模拟了这种情况:

void onConnection(const muduo::TcpConnectionPtr& conn)
{
  if (conn->connected())
  {
    printf("onConnection(): new connection [%s] from %s\n",
           conn->name().c_str(),
           conn->peerAddress().toHostPort().c_str());
+    if (sleepSeconds > 0)
+    {
+      ::sleep(sleepSeconds);
+    }
    conn->send(message1);
    conn->send(message2);
    conn->shutdown();
  }
  else
  {
    printf("onConnection(): connection [%s] is down\n",
           conn->name().c_str());
  }
}

假设 sleepSecond 是5s,用 nc localhost 9981 创建连接之后立即CRTL-C 断开客户端,服务器进程过几秒就会退出。解决方法很简单,在程序刚开始的时候就忽略 SIGPIPE,可以用 C++ 全局对象做到这一点。

  • (禁用CTRL-C)
class IgnoreSigPipe
{
 public:
  IgnoreSigPipe()
  {
    ::signal(SIGPIPE, SIG_IGN);
  }
};

IgnoreSigPipe initObj;

二、TCP No Delay 和 TCP keepalive

TCP No Delay 和 TCP keepalive 都是常用的 TCP 选项:

  • 前者的作用是禁用 Nagle 算法,避免连续发包出现延迟,这对编写低延迟网络服务很重要
  • 后者的作用是定期探测 TCP 连接是否存在。一般来说如果有应用层心跳的话,不是必须的,但是作为一个通用的网络库应该暴露其接口。
// TcpConnection.h
void shutdown();
void setTcpNoDelay(bool on);

// TcpConnection.cc
void TcpConnection::setTcpNoDelay(bool on)
{
  socket_->setTcpNoDelay(on);
}

// Socket.cc
void Socket::setTcpNoDelay(bool on)
{
  int optval = on ? 1 : 0;
  ::setsockopt(sockfd_, IPPROTO_TCP, TCP_NODELAY,
               &optval, sizeof optval);
  // FIXME CHECK
}

TcpConnection::setKeepAlive() 的实现与之类似,此处从略,可参考 muduo 源码。

三、 WriteCompleteCallback 和 HighWaterMarkCallback

前面提到了非阻塞网络编程的发送数据比读取数据要困难的多:

  • 一方面,什么时候关注 writable 事件是一个问题,这会带来编码方面的难度;
  • 另一方面,如果发送数据的速度高与接收数据的速度,会造成数据在内存中堆集,这又带来设计及安全性方面的难度。

muduo对此的解决方法是提供两个回调,有的网络库把他们称为高水位回调低水位回调

3.1 WriteCompleteCallback(如果发送缓冲区为空,就调用它)

WriteCompleteCallback 比较容易理解,如果发送缓冲区被清空,就调用它。
TcpConnection 有两处可能触发此回调,入下:

第一处: TcpConnection::sendInLoop()

void TcpConnection::sendInLoop(const std::string& message)
{
  loop_->assertInLoopThread();
  ssize_t nwrote = 0;
  // if no thing in output queue, try writing directly
  if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0) {
    nwrote = ::write(channel_->fd(), message.data(), message.size());
    if (nwrote >= 0) {
      if (implicit_cast<size_t>(nwrote) < message.size()) {
        LOG_TRACE << "I am going to write more data";
+     } else if (writeCompleteCallback_) {
+       loop_->queueInLoop(
+           boost::bind(writeCompleteCallback_, shared_from_this()));
      }
    } else {

第二处:TcpConnection::handleWrite()

void TcpConnection::handleWrite()
{
  loop_->assertInLoopThread();
  if (channel_->isWriting()) {
    ssize_t n = ::write(channel_->fd(),
                        outputBuffer_.peek(),
                        outputBuffer_.readableBytes());
    if (n > 0) {
      outputBuffer_.retrieve(n);
      if (outputBuffer_.readableBytes() == 0) {
        channel_->disableWriting();
+       if (writeCompleteCallback_) {
+          loop_->queueInLoop(
+              boost::bind(writeCompleteCallback_, shared_from_this()));
+       }
        if (state_ == kDisconnecting) {
          shutdownInLoop();
        }
	// ...

TcpConnection 和 TcpServer 也需要相应地暴露 WriteCompleteCallback 的接口。

3.2 HighWaterMarkCallback

如果输出缓冲的长度超过用户指定的大小,就会触发回调(只在上升沿触发一次)。

如果用非阻塞的方式写一个 proxy,proxy 有 C 和 S 两个连接。只考虑 server 发给 client 的数据流(反过来也是一样),为了防止 server 发过来的数据撑爆 C 的输出缓冲区。

  • 一种方法是在 C 的 HighWaterMarkCallback 中停止读取 S 的数据,而在 C 的 WriteCompleteCallback 中恢复读取 S 的数据。这就跟用粗水管往水桶里灌水,用细水管放水一样,上下两个水龙头要轮流开合,类似 PWM。

你可能感兴趣的:(muduo网络库)