동양의 연구 노트
기사 디렉토리
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는이 설정을 지원하지 않습니다.
왜 계속 주목할 수
写事件
없나요 ???