moduo网络库 Acceptor TcpConnection

引言

本篇文章将完成网络库的所有代码书写,这篇文章也是串联所有部分,特别有助于理解网络库的架构。

Acceptor

acceptor是位于baseLoop的组件,目的是接受客户的连接请求,当接受到连接之后调用新连接的回调函数,目的是把新的fd封装成channel并且分发给subLoop。所以里面会有监听的函数,设置回调的函数。

cpp 复制代码
    class Acceptor : noncopyable {
    public:
        using NewConnectionCallback = std::function<void(int sockfd, const InetAddress&)>;
        Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport);
        ~Acceptor();

        void setNewConnectionCallback(const NewConnectionCallback& cb) {
            m_newConnectionCallback = std::move(cb);
        }   

        bool listening() const {return m_listening;}
        void listen();
    private:
        void handleRead();    

        EventLoop* m_loop; // Acceptor用的就是用户定义的那个baseLoop,也称作mainLoop
        Socket m_acceptSocket;
        Channel m_acceptChannel;
        NewConnectionCallback m_newConnectionCallback;
        bool m_listening;
    };

只要是监听都是非阻塞,所以我们这里的socket也是非阻塞

在muduo库里面,几乎是所有处理事件的部分都可以划分成一个channel,因为channel的职责就是设置感兴趣的事情和执行回调函数。而acceptor也不意外,最后也是一个channel,而它的职责就是监听所有用户的连接,其所属的loop就是baseLoop,然后对其绑定了读的回调函数,这里我们记住,只要需要返回值,来处理事情的都是读的回调,这里我们绑定了一个handleRead的回调函数。

cpp 复制代码
static int createNonblocking() {
    int sockfd = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_TCP);
    if (sockfd < 0) {
        LOG_FATAL("%s:%s:%d listen socket error:%d \n", __FILE__, __FUNCTION__, __LINE__, errno);
    }
    return sockfd;
}


Litguidyo::Acceptor::Acceptor(EventLoop *loop, const InetAddress &listenAddr, bool reuseport)
            : m_loop(loop)
            , m_acceptSocket(createNonblocking())
            , m_acceptChannel(loop, m_acceptSocket.fd())
            , m_listening(false)
{   
    m_acceptSocket.setReuseAddr(true);
    m_acceptSocket.setKeepAlive(true);
    m_acceptSocket.bindAddress(listenAddr);
    // TcpServer::start() Acceptor.listen 有新用户的连接,要执行一个回调(connfd => channel => subloop)
    // baseLoop => m_acceptChannel(listened) => 
    m_acceptChannel.setReadCallback(std::bind(&Acceptor::handleRead, this));
}

Litguidyo::Acceptor::~Acceptor() {
    m_acceptChannel.disableAll();
    m_acceptChannel.remove();

}

void Litguidyo::Acceptor::listen() {
    m_listening = true;
    m_acceptSocket.listen();
    m_acceptChannel.enableReading();
}

这就是我们绑定的handleRead函数,里面先是监听得到了对方的地址,然后执行新连接的回调函数,这个函数的作用就是通过轮询找到subLoop,唤醒,分发当前新客户端的channel。这个回调函数在哪里被设置,我们接着往下面看。

cpp 复制代码
// listenfd有事件发生了,就是有新用户连接了
void Litguidyo::Acceptor::handleRead() {
    InetAddress peerAddr;
    int connfd = m_acceptSocket.accept(&peerAddr);
    if (connfd >= 0) {
        if (m_newConnectionCallback) {
            m_newConnectionCallback(connfd, peerAddr); // 轮询找到subloop,唤醒,分发当前新客户端的channel
        } else {
            ::close(connfd);
        }
    } else {
        LOG_ERROR("%s:%s:%d accept error:%d \n", __FILE__, __FUNCTION__, __LINE__, errno);
        if (errno == EMFILE) {
            LOG_ERROR("%s:%s:%d socket reached limit\n", __FILE__, __FUNCTION__, __LINE__);
        }
    }
}

callback

这个就是方便我们直接调用

cpp 复制代码
using TcpConnectionPtr = std::shared_ptr<TcpConnection>;
using ConnectionCallback = std::function<void(const TcpConnectionPtr&)>;
using CloseCallback = std::function<void(const TcpConnectionPtr&)>;
using WriteCompleteCallback = std::function<void(const TcpConnectionPtr&)>;
using MessageCallback = std::function<void(const TcpConnectionPtr&, Buffer*, Timestamp)>;
using HighWaterMarkCallback = std::function<void(const TcpConnectionPtr&, size_t)>;

TcpConnection

每一个新建立的连接就是一个TcpConnection,对于一个连接,我们要有关闭函数,发送信息的函数,设置各种回调的函数,建立连接,销毁连接的函数。我们这里有一个新的概念就是水位线。因为一个连接需要接受信息,但是信息如果过多,发送的过快,会导致消息没有来得及接受因为容器无法容下而被丢掉,所以我们设置了一个水位线。

作为一个连接,不仅仅要接受信息,还要发送信息,所以有两个缓冲区。并且为了更好的操作连接,我们需要设置4个状态,也就是我们的State。

cpp 复制代码
    class TcpConnection : noncopyable, public std::enable_shared_from_this<TcpConnection>{
    public:
        TcpConnection(EventLoop* loop, const std::string& nameArg, int sockfd
                    , const InetAddress& localAddr
                    , const InetAddress& peerAddr);
        ~TcpConnection();

        EventLoop* getLoop() const {return m_loop;}
        const std::string& name() const {return m_name;}
        const InetAddress& localAddress() const {return m_localAddr;}
        const InetAddress& peerAddress() const {return m_peerAddr;}

        bool connected() const {return m_state == kConnecting;}

        // 关闭连接
        void shutdown();

        void send(std::string& buf);

        void setConnectionCallback(const ConnectionCallback& cb) {
            m_connectionCallback = cb;
        }

        void setMessageCallback(const MessageCallback& cb) {
            m_messageCallback = cb;
        }

        void setWriteCompleteCallback(const WriteCompleteCallback& cb) {
            m_writeCompleteCallback = cb;
        }

        void setHighWaterMarkCallback(const HighWaterMarkCallback& cb, size_t highWaterMark) {
            m_highWaterMarkCallback = cb;
            m_higaterMark = highWaterMark;
        }

        void setCloseCallback(const CloseCallback& cb) {
            m_closeCallback = cb;
        }

        // 连接建立
        void connectEstablished();
        // 连接销毁
        void connectDestroyed();
    private:
        enum State {
            kDisconnected,
            kConnecting,
            kConnected,
            kDisconnecting,
        };

        void setState(State state) {m_state = state;}

        void handleRead(Timestamp receiveTime);
        void handleWrite();
        void handleClose();
        void handleError();

        EventLoop* m_loop; // 这里不是baseLoop,因为TcpConnection都是在subLoop里面管理的
        const std::string m_name;
        std::atomic<int> m_state;
        bool m_reading;
        std::unique_ptr<Socket> m_socket;
        std::unique_ptr<Channel> m_channel;
        Timestamp receiveTime;

        void sendInLoop(const void* message, size_t len);
        void shutdownInLoop();

        // 这里和Acceptor类似  Acceptor =》mainLoop  TcpConnection =》subLoop
        const InetAddress m_localAddr; // 当前主机的
        const InetAddress m_peerAddr; // 对端的地址

        ConnectionCallback m_connectionCallback; // 连接状态发生改变时候的回调
        MessageCallback m_messageCallback; // 有读写消息时的回调
        WriteCompleteCallback m_writeCompleteCallback; // 消息发送   完成之后的回调
        HighWaterMarkCallback m_highWaterMarkCallback; // 控制消息发送的速率
        CloseCallback m_closeCallback;
        
        size_t m_higaterMark;  
        Buffer m_inputBuffer; // 接受数据的缓冲区
        Buffer m_outputBuffer; // 发送数据的缓冲区
    };

一个connection最后是以channel的形式传递给subLoop,在构造函数里面,我们给channel传递一些回调函数,这个回调函数我们之后马上就会实现。同时,我们设置状态是正在连接,创建了属于这个连接的channel

cpp 复制代码
Litguidyo::TcpConnection::TcpConnection(EventLoop *loop, const std::string &nameArg, int sockfd, const InetAddress &localAddr, const InetAddress &peerAddr)
        : m_loop(CheckLoopNotNull(loop))
        , m_name(nameArg)
        , m_localAddr(localAddr)
        , m_peerAddr(peerAddr)
        , m_state(kConnecting)
        , m_reading(true)
        , m_socket(new Socket(sockfd))
        , m_channel(new Channel(loop, sockfd))
        , m_higaterMark(64 * 1024 * 1024) // 64M
{
    // 下面给channel设置了相应的回调函数,poller给channel通知感兴趣的事情发生了,channel会回调相应的操作函数
    m_channel->setReadCallback(std::bind(&TcpConnection::handleRead, this, std::placeholders::_1));
    m_channel->setWriteCallback(std::bind(&TcpConnection::handleWrite, this));
    m_channel->setCloseCallback(std::bind(&TcpConnection::handleClose, this));
    m_channel->setErrorCallback(std::bind(&TcpConnection::handleError, this));

    LOG_INFO("TcpConnection::ctor[%s] at fd = %d\n", m_name.c_str(), sockfd);
    m_socket->setKeepAlive(true); // 启动了广播机制
}

这个函数是用户来调用,而用户是不知道这个操作到底安全与否,所以在调用之前我们检查一下子状态是不是被连接的,如果被连接,就设置为正在连接,然后让对应的loop执行删除连接的函数。当这个函数执行完了之后才会设置为被删除。删除的操作其实也就是关闭socket

cpp 复制代码
// 这个是用户调用的,所以我们内部要检查数据是否完全发送
void Litguidyo::TcpConnection::shutdown() {
    if (m_state == kConnected) {
        setState(kDisconnecting);
        m_loop->runInLoop(std::bind(&TcpConnection::shutdownInLoop, this));
    }
}

void Litguidyo::TcpConnection::shutdownInLoop() {
    if (!m_channel->isWriting()) { // 当前outputBuffer中的数据已经全部发送完成了
        m_socket->shutdownWrite();
    }
}

建立连接第一件事情就设置状态,然后channel要绑定对应的connection,同时设置可读,因为一般来说一个channel都会有可读的事件。最后因为连接的状态发生了变化,所以需要执行对应的回调函数。

销毁连接也是一样,先把channel的感兴趣事件去掉,然后把channel移除(这里其实最后调用的是poller里面的remove)。

cpp 复制代码
void Litguidyo::TcpConnection::connectEstablished() {
    setState(kConnected);
    m_channel->tie(shared_from_this());
    m_channel->enableReading();  // 向poller注册channel的epollin事件

    // 新连接建立了,执行回调
    m_connectionCallback(shared_from_this());
}

void Litguidyo::TcpConnection::connectDestroyed() {
    if (m_state == kConnected) {
        setState(kDisconnected);
        m_channel->disableAll();
        m_connectionCallback(shared_from_this());
    }
    m_channel->remove();
}

还记得我们在构造函数里面给channel设置了所有的回调函数嘛~~~我们接下来就开始实现这些回调函数。读的回调函数就是从接受缓冲区里面读取数据然后调用消息回调函数。这个消息回调函数是用户设置的,不过传递的过程是 用户 =》 TcpServer =》 TcpConnection

cpp 复制代码
void Litguidyo::TcpConnection::handleRead(Timestamp receiveTime) {
    int saveErrno = 0;
    ssize_t n = m_inputBuffer.readFd(m_channel->getFd(), &saveErrno);
    if (n > 0) {
        // 已建立连接的用户,又可读的事件发生了,调用用户传入的回调操作onMessage
        m_messageCallback(shared_from_this(), &m_inputBuffer, receiveTime);
    } else if (n == 0) {
        handleClose();
    } else {
        errno = saveErrno;
        LOG_ERROR("TcpConnection::handleRead");
        handleError();
    }
}

这个是写函数的回调,我们操作的就是发送缓冲区。可能大家会有疑问,为什么接受缓冲区没有这么多的操作,而发送缓冲区却需要判断这么多,因为接受缓冲区由 TCP 流量控制解决;而发送缓冲区满时,应用需要自己保存还没发送的数据,所以网络库重点维护 outputBuffer。其实主要就是一个是主动发送,一个是被动接受。

这个过程是:

我们先写数据进入Linux的内核tcp缓冲里面,当缓冲容不下来的时候或者写完的时候,就会停止write,并且发送出去,而剩下的部分就会被保存到我们的writeBuffer里面。所以我们写完一次的时候就会重置一下子readerIndex的位置,如果已经没有数据了,那么关闭对读事件感兴趣,然后调用其回调函数(这个函数也是由用户设置的)。如果在发送的过程中,有一个地方不小心调用了shutdown,但是数据其实没有真正发送完(毕竟这个不是while循环),所以在shutdownInLoop()里面还需要额外的判断。

我们这里就要串联一下子整个的过程了,当有数据没有写完的时候,也就是outputBufferr里面还有数据而tcp的缓存里面没有空间写入了的时候,我们会等待客户端读取完数据,直到tcp的发送缓冲区里有空间了,那么内核直到tcp可以写了,就会返回EPOLLOUT,在EventLoop里面控制的poller就会停止阻塞,开始执行对应的channel的回调,也就是writehandle(),而写的回调正是这个函数提供的,于是又会重复执行这个过程,直到有一次缓冲区再也没有了数据,那么这个时候当返回EPOLLOUT的时候,再一次执行这个函数,发现缓冲区已经没有了数据,就会关闭写事件,并且执行相应的回调(这个是用户传递的)。

cpp 复制代码
void Litguidyo::TcpConnection::handleWrite() {
    if (m_channel->isWriting()) {
        int saveErrno = 0;
        ssize_t n = m_outputBuffer.writeFd(m_channel->getFd(), &saveErrno);
        if (n > 0) {
            m_outputBuffer.retrieve(n);
            if (m_outputBuffer.readableBytes() == 0) {
                m_channel->disableWriting();
                if (m_writeCompleteCallback) {
                    // 唤醒m_loop对应的thread线程,执行回调
                    m_loop->queueInLoop(std::bind(m_writeCompleteCallback, shared_from_this()));
                }
            }
            if (m_state == kDisconnecting) { // 在发送的过程中有个地方调用了shutdown,但是其实没有真正发送完,所以这个 shutdownInLoop()里面要判断
                shutdownInLoop();
            }
        } else {
            LOG_ERROR("TcpConnection::handleWrite close");
        }
    } else {
        LOG_ERROR("Connection fd=%d is down, no more writing", m_channel->getFd());
    }
}

下面可能涉及到了一些回调函数,我们这一个类并没有涉及到,在下一篇文章里面我们会提到。

cpp 复制代码
// poller => channel::closeCallback => TcpConnection::handleClose
void Litguidyo::TcpConnection::handleClose() {
    LOG_INFO("fd=%d state=%d \n", m_channel->getFd(), static_cast<int>(m_state));
    setState(kConnected);
    m_channel->disableAll();

    TcpConnectionPtr connPtr(shared_from_this());
    m_connectionCallback(connPtr); // 执行连接关闭的回调
    m_closeCallback(connPtr); // 执行关闭的回调   执行的是TcpServer::removeConnection
}

void Litguidyo::TcpConnection::handleError() {
    int optval;
    int err = 0;
    socklen_t optlen = sizeof(optlen);
    if (::getsockopt(m_channel->getFd(), SOL_SOCKET, SO_ERROR, &optval, &optlen) < 0) {
        err = errno;
    } else {
        err = optval;
    }
    LOG_ERROR("TcpConnection::handleError name:%s", m_name.c_str());
}

接下来就是发送信息的逻辑,如果是在当前的线程那么就调用sendInLoop,如果不是就把这个函数绑定在runInLoop里面,等待去唤醒这个线程。

这里还原一下唤醒的逻辑,我们的连接是一定会对应一个loop的,所以我们直接对m_loop进行操作,那么就直接切换到了EventLoop里面,然后给Poller里面发消息。

cpp 复制代码
void Litguidyo::TcpConnection::send(std::string& buf) {
    if (m_state == kConnected) {
        if (m_loop->isInLoopThread()) {
            sendInLoop(buf.c_str(), sizeof(buf));
        } else {
            m_loop->runInLoop(std::bind(&TcpConnection::sendInLoop, this, buf.c_str(), sizeof(buf)));
        }
    }
}

这个就是发送的细节,如果状态是正在断开,那么就不可以发送连接了,如果是第一次写的数据,而且缓冲区里面没有数据,那么就往对应的fd开始发送数据,如果全部发送完成,那么就直接调用回调函数,如果没有全部发送完成,剩余的数据就要全部保存到缓冲区里面。

保存到缓冲区的操作:

先得到缓冲区里面原本还没有发送的数据的长度,也就是readableTypes(),然后当加上这些数据之后如果超过了水位线,那么就要执行其超过水位线的回调函数。

然后我们把数据通过append()添加到缓冲区的最后面,原理也就是移动writeIndex

在最后一定要确保注册了m_channel->enableWriting(),因为如果没有注册,poller就不会通知EPOLLOUT。这个过程我们可以再一次回顾一下,当注册成功,就可以执行write的回调函数了,那么回调函数里面执行的就是writefd,就给对应的fd发送了数据,当数据发送完成之后,缓冲区又可以再一次写入数据的时候,poller就会接收到其fd的EPOLLOUT,然后在eventLoop里面就执行回调函数,也就是再一次执行TcpConnection::handleWrite(),然后就开始执行我们之前说过的那个不断的循环,直到数据全部发送完毕。

所以说我们调用send函数其实就是先发送一遍,如果没有全部发送完成,那么就不断的调用channel里面write的回调函数

cpp 复制代码
// 发送数据,应用写的快,而内核数据发送的慢,需要把待发送的数据写入缓冲区,而且设置了水位回调
void Litguidyo::TcpConnection::sendInLoop(const void *message, size_t len)
{
    ssize_t nwrite = 0;
    size_t remaining = len;
    bool faultError = false;

    // 之前调用过该connection的shutdown,不能再进行发送了
    if (m_state == kDisconnecting) {
        LOG_ERROR("disconnected, give up writing!");
        return;
    }

    // 表示channel第一次开始写数据,而且缓冲区没有待发送的数据
    if (!m_channel->isWriting() && m_outputBuffer.readableBytes() == 0) {
        nwrite = ::write(m_channel->getFd(), message, len);
        if (nwrite > 0) {
            remaining = len - nwrite;
            if (remaining == 0 && m_writeCompleteCallback) {
                // 既然里面的数据已经全部发送完毕,就不用再给channel设置epollout事件了
                m_loop->queueInLoop(std::bind(m_writeCompleteCallback, shared_from_this()));
            }
        } else { // nwrite < 0
            nwrite = 0;
            if (errno != EWOULDBLOCK) { // 由于非阻塞没有数据,正常返回
                LOG_ERROR("TcpConnection::sendInLoop");
                if (errno == EPIPE || errno == ECONNRESET) {
                    faultError = true;
                }
            }
        }
    }

    // 说明当前这一次write,并没有把数据全部发送出去,剩余的数据需要保存到缓冲区当中,然后给channel
    // 注册epollout事件的时候,poller发现tcp的发送缓冲区有空间,会通知相应的sock-channel,调用handleWrite回调方法
    // 也就是调用TcpConnection::handleWrite方法,把发送缓冲区中的数据全部发送完成
    if (!faultError && remaining > 0) {
        // 目前发送缓冲区剩余的待发送的数据长度
        size_t oldlen = m_outputBuffer.readableBytes();
        if (oldlen + remaining >= m_higaterMark && oldlen < m_higaterMark
            && m_highWaterMarkCallback) {
                m_loop->queueInLoop(std::bind(m_highWaterMarkCallback, shared_from_this(), remaining + oldlen));
        }
        m_outputBuffer.append((char*)message + nwrite, remaining);
        if (!m_channel->isWriting()) {
            m_channel->enableWriting(); // 一定要注册channel的写事件,否则poller不会给channel通知epollout
        }
    }
}

总结

本篇文章到这里就结束了!!!!希望可以帮助大家理解~~~~

相关推荐
键盘会跳舞1 小时前
C++:右值引用与移动语义源码级深度拆解——现代C++性能优化的语言基石
c++·右值引用·移动语义
星卯教育tony1 小时前
NOI Linux 2.0 服务器多用户网页桌面部署方案(腾讯云轻量Ubuntu20.04专属版 CSP复赛比赛环境搭建免安装虚拟机 )
linux·服务器·腾讯云
码匠许师傅1 小时前
【C++ 面试真题】聊聊 C++ 的虚函数和多态
java·c++·面试
Forever Nore2 小时前
LeetCode 14 最长公共前缀 - 纵向扫描
linux·服务器·leetcode
djjjx.2 小时前
【 C++ 】多态
开发语言·c++·多态
Shadow(⊙o⊙)2 小时前
Linux网络部分——TCP协议服务端客户端交互接口,入门级硬核解析1.0
linux·网络·tcp/ip
小新讲网安2 小时前
HTTP请求走私攻击实战:CL.TE与TE.CL绕过前端服务器全解析
服务器·前端·网络·web安全·http·架构·漏洞
鸿芯微控科技3 小时前
MFC关断后还有流量怎么办?零流量、阀门泄漏、压差与Python分析
c++·python·mfc·质量流量控制器·关断泄漏·零流量测试
我是谁??13 小时前
Ubuntu22.04更换清华源
linux·运维·服务器