muduo 라이브러리 학습 08-Buffer 데이터 읽기 및 쓰기 설계 및 구현

동양의 연구 노트

Buffer는 non-blocking TCP 네트워크 프로그래밍에 없어서는 안될 것입니다.이 섹션에서는 Buffer를 사용하여 데이터 입출력을 처리하는 방법을 소개합니다.

1. 버퍼는 데이터를 읽습니다.

먼저 s07 / Callbacks.h에서 MessageCallback의 정의를 수정합니다. 현재 매개 변수는 원본 (const char * buf, int len) 대신 muduo, Buffer * 및 Timestamp와 동일합니다.

typedef boost::function<void (const TcpConnectionPtr&,
                              Buffer* buf,
                              Timestamp)> MessageCallback;

1.1 매개 변수 목록의 타임 스탬프

타임 스탬프 poll(2)는 반환 시간, 즉 메시지가 도착한 시간입니다.

  • 이 시간은 데이터를 읽은 시간 (read (2) 호출 또는 반환)보다 빠릅니다.
  • 따라서 메시지를 처리하는 프로그램의 내부 지연을보다 정확하게 측정하려면이 시간을 시작점으로 사용해야합니다. 그렇지 않으면 측정 결과가 너무 작습니다 ( 特别是处理并发连接时效果更明显).
  • 이를 위해 다음과 같이 Channel :: ReadEventCallback ()의 ​​프로토 타입을 수정해야합니다.
class Channel : boost::noncopyable
{
    
    
 public:
  typedef boost::function<void()> EventCallback;
+ typedef boost::function<void(Timestamp)> ReadEventCallback;

  Channel(EventLoop* loop, int fd);
  ~Channel();

! void handleEvent(Timestamp receiveTime);
! void setReadCallback(const ReadEventCallback& cb)
  {
    
     readCallback_ = cb; }

1.2 TcpConnection은 Buffer를 입력 버퍼로 사용합니다.

먼저 inputBuffer_ 멤버 변수를 TcpConnection에 추가합니다 .

   ConnectionCallback connectionCallback_;
 MessageCallback messageCallback_;
 CloseCallback closeCallback_;
 Buffer inputBuffer_;
};

그런 다음 TcpConnection :: handleRead () 멤버 함수를 수정하여 Buffer를 사용하여 데이터를 읽습니다.

void TcpConnection::handleRead(Timestamp receiveTime)
{
     
     
! 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();
 }
}

1.3 버퍼 :: readFd ()

이 구현의 몇 가지 사항을 언급 할 가치가 있습니다.

  • 하나는 분산 / 수집 IO를 사용하고 버퍼의 일부를 스택에서 가져와 입력 버퍼가 충분히 커지도록하는 것 通常一次 readv(2) 调用就能取完全部数据입니다.
    • 입력 버퍼가 ioct1(socketFd, FIONREAD, &length)시스템 호출을 저장할만큼 충분히 크기 때문에
    • 미리 읽을 수있는 데이터의 양을 알지 못한 채 Buffer의 capacity ()를 미리 예약하면 한 번 읽은 후 extrabuf의 데이터를 Buffer에 추가 () 할 수 있습니다.
  • 두 번째는 Buffer :: readFd ()가 read (2)를 한 번만 호출하고 EAGAIN을 반환 할 때까지 반복적으로 호출하지 않는다는 것입니다.
    • 이 작업을 먼저하는 것이 맞습니다.
    • muduo는 레벨 트리거를 사용하기 때문에 메시지 나 데이터가 손실되지 않습니다.
    • 둘째, 对于追求低延迟的程序来说데이터를 읽을 때마다 하나의 시스템 호출 만 필요하기 때문에 이렇게하는 것이 효율적입니다.
    • 다시 말하지만 이것은 여러 연결의 공정성을 관리하며 한 연결의 과도한 양의 데이터로 인해 다른 연결의 메시지 처리에 영향을 미치지 않습니다.
ssize_t Buffer::readFd(int fd, int* savedErrno)
{
     
     
 char extrabuf[65536];
 struct iovec vec[2];
 const size_t writable = writableBytes();
 vec[0].iov_base = begin()+writerIndex_;
 vec[0].iov_len = writable;
 vec[1].iov_base = extrabuf;
 vec[1].iov_len = sizeof extrabuf;
 const ssize_t n = readv(fd, vec, 2);
 if (n < 0) {
     
     
   *savedErrno = errno;
 } else if (implicit_cast<size_t>(n) <= writable) {
     
     
   writerIndex_ += n;
 } else {
     
     
   writerIndex_ = buffer_.size();
   append(extrabuf, n - writable);
 }
 return n;
}

muduo가 에지 트리거를 사용하는 경우 각 handleRead ()는 read (2)를 적어도 두 번 호출하는데, 이는 평균 레벨 트리거보다 하나 더 많은 시스템 호출입니다. 엣지 트리거가 반드시 효율적인 것은 아닙니다.

향후 개선 조치는 다음과 같습니다. n == 쓰기 가능 + extrabuf 크기 인 경우 다시 읽으십시오.

2. 데이터 보내기 (출력 / 쓰기)

데이터 전송은 활성 상태이고 읽기 데이터 수신은 수동적이므로 데이터 전송은 데이터 수신보다 어렵습니다. 지금까지 채널의 ReadCallback 만 사용했습니다.

  • TimerQueue는이를 사용하여 timerfd (2)를 읽습니다.
  • EventLoop은이를 사용하여 eventfd (2)를 읽습니다.
  • TcpSever / Acceptor는이를 사용하여 청취 소켓을 읽습니다.
  • TcpConnection은이를 사용하여 일반 Tcp 소켓을 읽습니다.

2.1 채널 변경

이 섹션에서는 채널의 WriteCallback을 사용합니다. muduo는이를 사용하기 때문에 필요할 level trigger때만 쓰기 가능한 이벤트 에만 주의를 기울입니다 ( Poller가 관리하는 채널 목록으로 업데이트 업데이트 ). 그렇지 않으면 바쁜 루프가 발생합니다.

  void enableReading() {
     
      events_ |= kReadEvent; update(); }
! void enableWriting() {
     
      events_ |= kWriteEvent; update(); }
! void disableWriting() {
     
      events_ &= ~kWriteEvent; update(); }
  void disableAll() {
     
      events_ = kNoneEvent; update(); }
+ bool isWriting() const {
     
      return events_ & kWriteEvent; }

2.2 TcpConnection :: send ()와 shutdown ()

send () 및 shutdown () 두 가지 함수가 TcpConnection 인터페이스에 추가되었습니다 这两个函数都可以跨线程调用. 간단하게하기 위해이 장에서는 send () 오버로드를 하나만 제공합니다.

  • TcpConnection의 상태는 현재 muduo 구현과 일치하는 4로 증가되었습니다. (새로운 kDisconnecting 상태로 인해 TcpConnection :: connectDestroyed () 및 TcpConnection :: handleClose ()의 assert도 변경해야합니다)
  • 두 개의 * InLoop 멤버 함수가 내부에 구현되어 이전 두 개의 새 인터페이스 함수에 해당 Buffer하며 출력 버퍼로 사용 됩니다.
  // void send(const void* message, size_t len);
  // Thread safe.
  void send(const std::string& message);
  // Thread safe.
  void shutdown();

  enum StateE {
    
     kConnecting, kConnected, kDisconnecting, kDisconnected, };

  void handleClose();
  void handleError();
+ void sendInLoop(const std::string& message);
+ void shutdownInLoop();
  
  Buffer inputBuffer_;
+ Buffer outputBuffer_;

TcpConnection에는 매우 간단한 상태 다이어그램이 있습니다.

  • 연결을 닫는 과정에서 TcpConnection과 다른 작업 (이벤트 읽기 및 쓰기)의 상호 작용은 더 복잡하며 다양한 타이밍에서 정확성을 확인하기 위해 완전한 단위 테스트가 필요합니다.必要时可以新增状态
    여기에 사진 설명 삽입

2.2.1 shutdown ()

종료 ()가 실제로 넣어 작동, 스레드 안전 shutdownInLoop(), 수행하는에 后者保证在IO线程调用. IO 스레드에서 호출되는 것이 보장되는 경우. 현재 쓰기가 없으면 쓰기 터미널을 닫습니다 (p. 191). 代码注释给出了两个值得改进的地方.

void TcpConnection::shutdown()
{
     
     
 // FIXME: use compare and swap
 if (state_ == kConnected)
 {
     
     
   setState(kDisconnecting);
   // FIXME: shared_from_this()?
   loop_->runInLoop(boost::bind(&TcpConnection::shutdownInLoop, this));
 }
}

void TcpConnection::shutdownInLoop()
{
     
     
 loop_->assertInLoopThread();
 if (!channel_->isWriting())
 {
     
     
   // we are not writing
   socket_->shutdownWrite();
 }
}

2.2.2 send ()

send ()도 마찬가지입니다. IO가 아닌 스레드에서 호출되면 메시지의 복사본을 만들어 전송을 위해 IO 스레드의 sendInLoop ()에 전달합니다. 이렇게하면 약간의 효율성이 떨어질 수 있지만 스레드 안전성 성능은 쉽게 확인할 수 있으며 저자는 利大于弊. 이 성능에 정말로 관심이 있다면 프로그램이 IO 스레드에서만 send ()를 호출하도록하는 것이 좋습니다. C ++ 11은 메모리 복사의 오버 헤드를 피하기 위해 이동 의미 체계를 사용할 수 있습니다.

void TcpConnection::send(const std::string& message)
{
     
     
 if (state_ == kConnected) {
     
     
   if (loop_->isInLoopThread()) {
     
     
     sendInLoop(message);
   } else {
     
     
     loop_->runInLoop(
         boost::bind(&TcpConnection::sendInLoop, this, message));
   }
 }
}
  • sendInLoop ()은 먼저 데이터를 직접 전송하려고 시도하며 전송이 한 번 완료되면 WriteCallback 이벤트가 활성화되지 않습니다.
  • 데이터의 일부만 전송되면 남은 데이터를 outputBuffer_에 넣고 쓰기 가능한 이벤트에주의를 기울인 다음 나머지 데이터를 handleWrite로 보냅니다. 현재 outputBuffer_에 이미 전송할 데이터가있는 경우 먼저 전송을 시도 할 수 없습니다 因为这会造成数据乱序.
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 {
     
     
     nwrote = 0;
     if (errno != EWOULDBLOCK) {
     
     
       LOG_SYSERR << "TcpConnection::sendInLoop";
     }
   }
 }

 assert(nwrote >= 0);
 if (implicit_cast<size_t>(nwrote) < message.size()) {
     
     
   outputBuffer_.append(message.data()+nwrote, message.size()-nwrote);
   if (!channel_->isWriting()) {
     
     
     channel_->enableWriting();
   }
 }
}

2.3 TcpConnection :: handleWrite ()

소켓이 쓰기 가능 해지면 Channel은 TcpConnection :: handleWrite ()를 호출하고 handleWrite에서 outputBuffer_데이터를 계속 전송 ()합니다 .

  • 전송이 완료되면 즉시 쓰기 가능한 이벤트 (& 11) 관찰을 중지하여 바쁜 루프를 방지하십시오.
  • 이 때 연결이 닫히면 shutdownInLoop ()을 호출하여 종료 프로세스를 계속합니다.
  • 오류가 발생하면 handleRead ()가 0 바이트를 읽은 다음 연결을 종료하므로 여기서 오류를 처리 할 필요가 없습니다.
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 (state_ == kDisconnecting) {
    
    
          shutdownInLoop();
        }
      } else {
    
    
        LOG_TRACE << "I am going to write more data";
      }
    } else {
    
    
      LOG_SYSERR << "TcpConnection::handleWrite";
    }
  } else {
    
    
    LOG_TRACE << "Connection is down, no more writing";
  }
}

참고 : SendInLoop () 및 handleWrite ()는 모두 한 번만 호출되며 write (2)는 EAGAIN을 반환 할 때까지 반복적으로 호출되지 않습니다. 그 이유는 첫 번째 write (2)가 데이터를 완전히 보내지 못하면 두 번째 호출이 거의 확실하게 EAGAIN을 반환하기 때문입니다.

셋, 약간의 개선 (아마도)

개선 조치 : TcpConnection의 출력 버퍼는 연속적 일 필요가 없습니다 (outputBuffer_가 ptr_vector로 변경됨), handleWrite ()는 writev (2)를 사용하여 여러 데이터 블록을 전송할 수 있으므로 메모리 사본 수를 줄이고 약간 개선 할 수 있습니다. 성능 (외부 세계에서 반드시인지 할 수있는 것은 아닙니다).

레벨 트리거 모드에서는 쓰기 가능한 이벤트에 항상주의를 기울일 수 없기 때문에 데이터 전송이 더 번거롭지 만 데이터 읽기는 매우 간단합니다. 저자는 다음과 같이 믿습니다. 이상적인 방법은 다음과 같습니다.

  • 읽을 수있는 이벤트에는 레벨 트리거를 사용하십시오.
  • 쓰기 가능한 이벤트에 에지 트리거를 사용합니다.
    그러나 현재 Linux는이 설정을 지원하지 않습니다.

왜 계속 주목할 수 写事件없나요 ???

추천

출처blog.csdn.net/qq_22473333/article/details/113742500