muduo源码学习(五) 实现TCP网络库(下)

前 言

上一篇文章介绍了连接的创建,引出了TcpConnection类。其作用就是处理socket上的IO事件,执行各种回调。本文介绍TcpConnection对断开连接、读取数据、发送数据的处理。

断开连接

连接的关闭分为主动断开和被动断开,两者的处理方式基本一致。muduo采用的连接关闭方式:被动断开,其核心函数为TcpConnection::handleClose()。书中提到,如果需要主动断开,添加一个接口调用handleClose()即可。
对于远端连接断开的感知:在可读事件处理函数handleRead()中,当read返回值为0时,即远端断开了连接,调用TcpConnection::handleClose()。此时处理如下:
1. 取消所有关注的IO事件
2. 调用用户注册回调ConnectionCallback
3. 调用closeCallback_(),此回调绑定到TcpServer::removeConnection()
removeConnection()中处理如下:
4. 将对应的TcpConnection对象从TcpServer中移除
5. 调用TcpConnection::connectDestroyed(),并通过std::bind()TcpConnection对象的生命周期延长到执行完成connectDestroyed()
6. 将连接对应的ChannelEventLoop中移除
7. TcpConnection析构,成员socket_引用计数为0,其析构时会调用close(),关闭连接的fd

读取数据

新连接建立时,通过TcpConnection::connectEstablished()注册可读事件,当触发可读事件时调用回调,即TcpConnection::handleRead(),其主要内容如下:

void TcpConnection::handleRead(Timestamp receiveTime) {
    ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);
    if (n > 0) {
        messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
    } else if (n == 0) {
        handleClose();
    } else {
        handleError();
    }
}

这里主要进行了两个处理:
1. 读取数据到inputBuffer_中,其使用Buffer::readFd()来实现,具体如下。
2. 调用用户回调messageCallback_,此函数是在建立新连接时,将TcpServer的成员函数MessageCallback设置为回调,其由用户提供。

Buffer::readFd()实现(非源码)如下:

ssize_t Buffer::readFd(int fd, int & savedErrno) {
    // 申请栈上空间
    char extrabuf[65536];
    struct iovec vec[2];
    const size_t writable = writableBytes();

    // 两块iovec分别指向内部buffer的可写空间和栈上空间
    vec[0].iov_base = begin() + m_writerIndex;
    vec[0].iov_len = writable;
    vec[1].iov_base = extrabuf;
    vec[1].iov_len = sizeof extrabuf;

    // 判断内部buffer的可写空间是否足够
    const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1;
    const ssize_t n = sockets::readv(fd, vec, iovcnt);
    if (n < 0) {
        savedErrno = errno;
    } else if (static_cast(n) <= writable) {
        // 内部空间足够,直接写入,移动可写索引
        m_writerIndex += n;
    } else {
        // 内部空间不足,先写入栈上空间,再将栈上数据append到内部空间
        m_writerIndex = m_buffer.size();
        append(extrabuf, n - writable);
    }
    return n;
}

书中提到,此实现一是使用了scatter/gather IO(分离/聚散IO),配合内部栈空间使用;二是muduo采用level trigger(LT)模式,只需要调用一次read(2)且不会丢失数据。从而兼顾了内存使用量和效率。

发送数据

数据的发送通过TcpConnection::send()实现,代码如下:

void TcpConnection::send(const StringPiece& message)
{
    if (state_ == kConnected) {
        if (loop_->isInLoopThread()) {
            sendInLoop(message);
        } else {
            loop_->runInLoop(std::bind(&TcpConnection::sendInLoop, this, message.as_string()));
        }
    }
}

在确保是连接状态的情况下,如果在当前IO线程触发就调用TcpConnection::sendInLoop(),反之则使用runInLoop将该任务抛给IO线程执行。有关runInLoop的内容在上一篇已经介绍过,这里不再赘述。
TcpConnection::sendInLoop()中,处理如下:
1.outputBuffer_为空,直接发送数据
2. 若发送数据没有写完,统计剩余的字节数,将剩余数据写入outputBuffer_
3. 注册可写事件
当socket可写时,调用TcpConnection::handleWrite(),继续发送outputBuffer_中的数据,一旦发送完成,立刻将可写事件移除。
此流程需要注意的是可写事件观察的范围,可以看出只有在outputBuffer_中有数据时,才会注册观察可写事件,因为当outputBuffer_中没数据时,此时socket一直是处于可写状态的, 这将会导致一直触发TcpConnection::handleWrite(),而我们并没有数据需要发送。所以此触发没有意义,不需要去关注。

此外,数据发送的流程中:
outputBuffer_中的旧数据字节和剩余数据字节之和大于highWaterMark_时,会将highWaterMarkCallback_放入待执行队列中
当数据发送完毕时,会调用writeCompleteCallback_
两者配合使用,可以起到限流的作用。

更多内容,详见github NetLib
参考:
《Linux多线程服务端编程》陈硕 著
muduo源码

你可能感兴趣的:(muduo源码学习(五) 实现TCP网络库(下))