muduo笔记 网络库(七)TCP连接TcpConnection、Acceptor、TcpServer

先回顾下Tcp连接建立、数据通信、连接断开的过程。一个典型的TCP连接、通信过程:

muduo笔记 网络库(七)TCP连接TcpConnection、Acceptor、TcpServer

from UNP第3版 P36

TCP连接中,两端是对等的,TCP协议也没有区分客户端(client)与服务器端(server),但互联网中通信中,往往有一端提供资源给另一端访问,我们把拥有资源的一端称为服务器端,请求资源的一端称为客户端。

对于server,会启用一个监听循环,不断接受client连接请求(三次握手),进行数据通信,通信完成以后断开连接(四次挥手)。

对于client,在server启用监听循环时,向server发出连接请求,接收server数据,如有必要向server发送数据,通信完成后,断开连接。

连接是指物理上一端(client)到另一端(server)的通信链路,通过server端<ip, port>与客户端<ip, port>,来唯一标识一个TCP连接。

TCP连接有长连接、短连接之分:
短连接:client和server建立连接后,一般只传递一次读写操作,然后由client发起close,发送FIN分节,关闭连接。短连接只完成一次read/write操作,就会自动关闭。
长连接:client和server建立连接后,并不会自动关闭,后续的read/write操作会继续用这个连接。长连接没有确切时间限制,可能会长时间存在。

本文主要从Tcp连接的建立与关闭,来分析源码。


TcpServer接受新连接

muduo使用TcpConnection类来管理TCP连接,使用接受器Acceptor来接受连接,连接器Connector发起连接。TcpServer管理accept获得TcpConnection,生命周期由用户控制。

下图是TcpServer新建连接的相关函数调用顺序。当Channel::handleEvent()的触发条件是listening socket可读时,表明有新连接请求达到。TcpServer为新连接创建对应的TcpConnection对象。

muduo笔记 网络库(七)TCP连接TcpConnection、Acceptor、TcpServer

Acceptor类

Acceptor是TcpServer的一个内部类,主要职责是用来获得新连接的fd。保存用户提供的Connection-Callback和MessageCallback,新建TcpConnection对象(newConn())的时候直接传递给TcpConnection的构造函数。

/**
* TCP连接接受器
* 基础调用为accept(2)/accept4(2)
*/
class Acceptor : private noncopyable
{
public:
    typedef std::function<void(int sockfd, const InetAddress &)> NewConnectionCallback;

    Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport);
    ~Acceptor();

    /* 设置新连接回调 */
    void setNewConnectionCallback(const NewConnectionCallback& cb)
    { newConnectionCallback_ = cb; }

    /* 监听本地端口 */
    void listen();

    /* 判断当前是否正在监听端口 */
    bool listening() const { return listening_; }

private:
    void handleRead();      // 处理读事件

    EventLoop *loop_;        // 所属EventLoop
    Socket acceptSocket_;    // 专门用于接受连接的套接字(sock fd)
    Channel acceptChannel_;  // 专门接受连接通道, 监听conn fd
    NewConnectionCallback newConnectionCallback_; // 新建连接回调
    bool listening_;         // 监听状态
    int idleFd_;             // 空闲fd, 用于fd资源不够用时, 可以空一个出来作为新建连接conn fd
};

如果fd资源不够用了,导致accept(2)/accept4(2)创建连接失败,比如达到系统上限,怎么办?
Accetor用了这样一种技术:先申请一个空闲的fd(idleFd_),等到发生由于fd资源不够用时,就把这个备用fd暂时用于accept接收连接,然后再马上关闭,以防止不断产生可读事件(连接请求),从而回调相同的失败代码。及早建立连接后并关闭连接,让程序不会频繁响应同一个连接请求。

Acceptor构造与析构

Acceptor构造时,创建sockfd(套接字),待后续交给TcpServer来start监听套接字。
空闲fd指向文件"/dev/null",用来解决服务器fd资源耗尽问题。

// Acceptor.cc
Acceptor::Acceptor(EventLoop *loop, const InetAddress &listenAddr, bool reuseport)
: loop_(loop),
  acceptSocket_(sockets::createNonblockingOrDie(listenAddr.family())),
  acceptChannel_(loop, acceptSocket_.fd()),
  listening_(false),
  idleFd_(::open("/dev/null", O_RDONLY | O_CLOEXEC)) // 申请空闲fd
{
    assert(idleFd_ >= 0);
    acceptSocket_.setReuseAddr(true);
    acceptSocket_.setReusePort(reuseport);
    acceptSocket_.bindAddress(listenAddr);
    acceptChannel_.setReadCallback(
            std::bind(&Acceptor::handleRead, this));
}

Acceptor::~Acceptor()
{
    acceptChannel_.disableAll(); // disable all event of the channel
    acceptChannel_.remove(); // remove the channel from poller
    ::close(idleFd_);
}

Acceptor监听

Acceptor包含2类监听:1)监听套接字,即本地ip地址&端口。2)监听通道事件,读事件。

  • 为什么不在构造时,就调用listen监听sockfd呢?
    将非必要资源的初始化,延迟到需要时,用户可以通过调用TcpSever::start()来启动。这样,用户可以更灵活控制资源的申请和释放。
// Acceptor.cc
/**
* 监听本地sock fd, 使能监听Channel读事件
*/
void Acceptor::listen()
{
    loop_->assertInLoopThread();
    listening_ = true;
    acceptSocket_.listen();         // 使能监听本地sock fd(ip, port)
    acceptChannel_.enableReading(); // 使能监听通道读事件
}

Acceptor接受连接

Acceptor内部有一个Channel成员,当Poller监听到有Tcp连接请求时,就通过Channel的可读事件,在loop线程,来回调Acceptor::handleRead()。从而将conn fd和IP地址传递给上一层TcpServer,用于创建TcpConnection对象管理Tcp连接。

// Acceptor.cc
/**
* 处理读Channel事件, accept连接
* @note 先accept, 然后将相关资源通过回调交由上一层的TcpServer进行处理(管理)
*/
void Acceptor::handleRead()
{
    loop_->assertInLoopThread();
    InetAddress peerAddr;
    // FIXME loop until no more
    int connfd = acceptSocket_.accept(&peerAddr); // 获取连接fd及对端ip地址
    if (connfd >= 0)
    {
        if (newConnectionCallback_)
        { // 创建新连接回调
            newConnectionCallback_(connfd, peerAddr);
        }
        else
        {
            sockets::close(connfd);
        }
    }
    else
    { // 错误
        LOG_SYSERR << "in Acceptor::handleRead";
        /*
         * Read the section named "The special problem of
         * accept()ing when you can't" in libev's doc.
         * By Marc Lehmann, author of libev.
         *
         * The per-process limit of open file descriptors has been reached.
         */
        if (errno == EMFILE)
        { // 文件描述符资源耗尽错误
            ::close(idleFd_);
            idleFd_ = ::accept(acceptSocket_.fd(), NULL, NULL);
            ::close(idleFd_);
            // reopen /dev/null, it dose not matter whether it succeeds or fails.
            idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
        }
    }
}

TcpServer类

TcpServer类管理TcpConnection,供用户直接使用,生命周期由用户控制。接口如下,用户只需要设置好callback,然后调用start()即可。

// TcpServer.h
/**
* Tcp Server, 支持单线程和thread-poll模型.
* 接口类, 因此不要暴露太多细节.
*/
class TcpServer : private noncopyable
{
public:
    typedef std::function<void (EventLoop*)> ThreadInitCallback;
    enum Option
    enum Option
    {
        kNoReusePort, // 不允许重用本地端口
        kReusePort,   // 允许重用本地端口
    };

//    TcpServer(EventLoop* loop, const InetAddress& listenAddr);
    TcpServer(EventLoop* loop,
              const InetAddress& listenAddr,
              const std::string& nameArg,
              Option option = kNoReusePort);
    ~TcpServer(); // force out-line dtor, for std::unique_ptr members.

    /**
     * 如果没有监听, 就启动服务器(监听).
     * 多次调用没有副作用.
     * 线程安全.
     */
    void start();

    /**
     * 设置连接回调.
     * 非线程安全.
     */
    void setConnectionCallback(const ConnectionCallback& cb)
    { connectionCallback_ = cb; }

    /**
     * 设置消息回调.
     * 非线程安全.
     */
    void setMessageCallback(const MessageCallback & cb)
    { messageCallback_ = cb; }

    /**
     * 设置写完成回调.
     * 非线程安全.
     */
    void setWriteCompleteCallback(const WriteCompleteCallback& cb)
    { writeCompleteCallback_ = cb; }

注意到并没有连接关闭回调,这是由TcpServer::removeConnection()负责的,进而把工作转交给TcpConnection::connectDestroyed(),用户不可更改设置。

TcpServer的构造与析构

TcpServer构造函数主要工作是为成员申请资源,为各回调设置回调函数

TcpServer::TcpServer(EventLoop* loop,
                     const InetAddress& listenAddr,
                     const std::string& nameArg,
                     Option option)
: loop_(CHECK_NOTNULL(loop)), // 确保loop非空
  ipPort_(listenAddr.toIpPort()),  // 将Ip, port转换为字符串
  name_(nameArg), // 名称
  acceptor_(new Acceptor(loop, listenAddr, option == kReusePort)),
  threadPool_(new EventLoopThreadPool(loop, name_)), // 初始化事件循环线程池
  connectionCallback_(defaultConnectionCallback),    // 连接回调为默认连接回调
  messageCallback_(defaultMessageCallback),          // 消息回调为默认消息回调
  nextConnId_(1)  // 连接id
{
    acceptor_->setNewConnectionCallback(
            std::bind(&TcpServer::newConnection, this, _1, _2)); // 设置新建连接时的回调
}

同样是连接回调,TcpServer::newConnection()和connectionCallback_有何区别?
前者是Acceptor发生连接请求事件时,回调,用来新建一个Tcp连接;后者是在TcpServer内部新建连接即调用TcpServer::newConnection()时,回调connectionCallback_。

TcpServer析构工作内容很简单,主要销毁ConnectionMap中所有Tcp连接,而每个Tcp连接用的是一个TcpConnection对象来管理的。

/**
* 析构TcpServer对象, 销毁ConnectionMap中所有连接
*/
TcpServer::~TcpServer()
{
    loop_->assertInLoopThread();
    LOG_TRACE << "TcpServer::~TcpServer [" << name_ << "] destructing";


    // reset all connection of @c connections_
    for (auto& item : connections_)
    {
        TcpConnectionPtr conn(item.second); // shared_ptr manage TcpConnection
        item.second.reset();
        conn->getLoop()->runInLoop(
                std::bind(&TcpConnection::connectDestroyed, conn));
    }
}

TcpServer启动Tcp服务器,主要完成1)线程池的启动;2)Acceptor监听Tcp连接请求。

线程池需要指定其初始数量,当然,这需要在start()之前调用TcpServer::setThreadNum()设置。

/**
* 启动TcpServer, 初始化线程池, 连接接受器Accept开始监听(Tcp连接请求)
*/
void TcpServer::start()
{
    if (started_.getAndSet(1) == 0)
    {
        threadPool_->start(threadInitCallback_);


        assert(!acceptor_->listening());
        loop_->runInLoop(
                std::bind(&Acceptor::listen, get_pointer(acceptor_)));
    }
}
  • TcpServer如何获得新连接的conn fd(accept返回值)?
    TcpServer内部用Acceptor,保存用户提供的Connection-Callback和MessageCallback,新建TcpConnection对象(newConn())的时候直接传递给TcpConnection的构造函数。

  • 如何创建TcpConnection对象?
    新连接请求到达时,Acceptor回调newConnection(),通过TcpServer::newConnection创建一个新TcpConnection对象,用于管理一个Tcp连接。
    即,TcpServer::newConnection回调顺序 EventLoop => Channel => Acceptor => TcpServer

下面是用于创建TcpConnection对象的函数TcpServer::newConnectio()

// TcpServer.cc
/**
* 新建一个TcpConnection对象, 用于连接管理.
* @details 新建的TcpConnection对象会加入内部ConnectionMap.
* @param sockfd accept返回的连接fd (accepted socket fd)
* @param peerAddr 对端ip地址信息
* @note 必须在所属loop线程运行
*/
void TcpServer::newConnection(int sockfd, const InetAddress &peerAddr)
{
    loop_->assertInLoopThread();
    /* 从EventLoop线程池中,取出一个EventLoop对象构造TcpConnection对象,便于均衡各EventLoop负责的连接数 */
    EventLoop* ioLoop = threadPool_->getNextLoop(); // next event loop from the event loop thread pool

    /* 设置连接对象名称, 包含基础名称+ip地址+端口号+连接Id
     * 因为要作为ConnectionMap的key, 要确保运行时唯一性 */
    char buf[32];
    snprintf(buf, sizeof(buf), "-%s#%d", ipPort_.c_str(), nextConnId_);
    ++nextConnId_;
    std::string connName = name_ + buf;

    LOG_INFO << "TcpServer::newConnection [" << name_
    << "] - new connection [" << connName
    << "] from " << peerAddr.toIpPort();

    InetAddress localAddr(sockets::getLocalAddr(sockfd)); // 本地ip地址信息
    // FIXME poll with zero timeout to double confirm the new connection
    // FIXME use make_shared if necessary
    /* 新建TcpConnection对象, 并加入ConnectionMap */
    TcpConnectionPtr conn(new TcpConnection(ioLoop, connName, sockfd, localAddr, peerAddr));
    connections_[connName] = conn;
    /* 为新建TcpConnection对象设置各种回调 */
    conn->setConnectionCallback(connectionCallback_); // 连接回调
    conn->setMessageCallback(messageCallback_);       // 消息回调
    conn->setWriteCompleteCallback(writeCompleteCallback_); // 写完成回调
    conn->setCloseCallback( // 关闭连接回调
            std::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe
    /* 确认连接是否已建立, 并初始化连接建立后的状态 */
    ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
}

TcpConnection类

TcpConnection类是muduo最核心的类,唯一默认用shared_ptr来管理的类,唯一继承自enable_shared_from_this的类。这是因为其生命周期模糊:可能在连接断开时,还有其他地方持有它的引用,贸然delete会造成空悬指针。只有确保其他地方没有持有该对象的引用的时候,才能安全地销毁对象。

// TcpConnection.h
/**
* Tcp连接, 为服务器和客户端使用.
* 接口类, 因此不要暴露太多细节.
* 
* @note 继承自std::enable_shared_from_this的类, 可以用getSelf返回用std::shared_ptr管理的this指针
*/
class TcpConnection : noncopyable,
        public std::enable_shared_from_this<TcpConnection> // std::shared_ptr<TcpConnection> getSelf()
{
public:

TcpConnection不提供用户使用的函数,所提供的接口主要给TcpServer/TcpClient使用。TcpConnection的状态有4个:kDisconnected, kConnecting, kConnected, kDisconnecting。TcpConnection使用Channel获得socket上IO事件,它会自己处理writable事件,把readable事件通过MessageCallback传递给客户。TcpConnection拥有TCP socket(Socket类),后者析构函数会close(fd)。

// TcpConnection.h
private:
    enum StateE { kDisconnected, kConnecting, kConnected, kDisconnecting }; // TCP连接状态定义
    void handleRead(Timestamp receiveTime); // 处理读事件
    void handleWrite();                     // 处理写事件
    void handleClose();                     // 处理关闭连接事件
    void handleError();                     // 处理错误事件

    /* loop线程中排队发送消息 */
//    void sendInLoop(std::string&& message); // C++11 // add by martin
    void sendInLoop(const StringPiece& message);
    void sendInLoop(const char* message, size_t len);

    /* loop线程中排队关闭写连接 */
    void shutDownInLoop();
//    void shutDownAndForceCloseInLoop(double seconds); // add by martin
    /* loop线程中排队关闭连接 */
    void forceCloseInLoop();

    /* 设置TcpConnection状态 */
    void setState(StateE s) { state_ = s; }

    /* 将状态转换为字符串 */
    const char* stateToString() const;

    /* loop线程中排队开始监听读事件 */
    void startReadInLoop();
    /* loop线程中排队关闭监听读事件 */
    void stopReadInLoop();

    EventLoop* loop_;     // 所属 EventLoop
    std::string name_;    // 名称
    StateE state_; // FIXME: use atomic varaible // 状态
    bool reading_; // whether the connection is reading // 连接是否正在监听读事件
    // we don't expose those classes to client.
    std::unique_ptr<Socket> socket_;              // 连接套接字, 用于对连接进行底层操作
    std::unique_ptr<Channel> channel_;            // 通道, 用于绑定要监听的事件
    InetAddress localAddr_;                       // 本地IP地址
    InetAddress peerAddr_;                        // 对端IP地址
    ConnectionCallback connectionCallback_;       // 连接回调
    MessageCallback messageCallback_;             // 收到消息回调
    WriteCompleteCallback writeCompleteCallback_; // 写完成回调
    HighWaterMarkCallback highWaterMarkCallback_; // 高水位回调
    CloseCallback closeCallback_;                 // 关闭连接回调
    size_t highWaterMark_;                        // 高水位阈值
    Buffer inputBuffer_;                          // 输入缓冲区
    Buffer outputBuffer_; // FIXME: use list<Buffer> as output buffer. // 输出缓冲区
    boost::any contex_;                           // 用户自定义参数
    // FIXME: createTime_, lastReceiveTime_
    //        bytesReceived_, bytesSent_
};

TcpConnection表示的是“一次Tcp连接”,不可再生,一旦连接断开,该TcpConnection对象就没用了。TcpConnection没用发起连接的功能,构造函数参数是已经建立好连接的socket fd,初始状态是kConnecting。连接可以是TcpServer或TcpClient发起。

接收到消息时,通过Channel::handleEvent会将可读事件转交给TcpConnection::handleRead处理,而TcpConnection::handleRead又会通过messageCallback_将可读事件转交给TcpServer::messageCallback_,进而传递给用户。

// TcpConnection.cc
/**
* 从输入缓存inputBuffer_读取数据, 交给回调messageCallback_处理
* @param receiveTime 接收到读事件的时间点
* @details 通常是TcpServer/TcpClient运行回调messageCallback_, 将处理机会传递给用户
*/
void TcpConnection::handleRead(Timestamp receiveTime)
{
    loop_->assertInLoopThread();
    int savedErrno = 0;
    ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno); // 从指定fd读取数据到内部缓冲
    if (n > 0)
    {
        messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
    }
    else if (n == 0)
    {
        handleClose();
    }
    else
    {
        errno = savedErrno;
        LOG_SYSERR << "TcpConnection::handleRead";
        handleError();
    }
}

断开Tcp连接

断开连接方式

muduo中有2种关闭连接的方式:
1)被动关闭:即对端先关闭连接,本地read(2)返回0,触发关闭逻辑,调用handleClose。
2)主动关闭:利用forceClose()或forceCloseWithDelay()成员函数调用handleClose,强制关闭或强制延时关闭连接。

被动关闭流程见下图,图中“X”表示TcpConnection对象通常在此析构。

muduo笔记 网络库(七)TCP连接TcpConnection、Acceptor、TcpServer

Channel与断开连接

Channel中有关关闭连接的事件回调CloseCallback,由Channel::handleEvent()调用,从而触发TcpConnection::handleClose():

调用链路:
Poller::poll()检测到Channel事件就绪 => EventLoop::loop() =>Channel::handleEvent() => Channel::closeCallback_ => TcpConnection::handleClose()

// Channel.cc
void Channel::handleEvent(Timestamp recevieTime)
{
    std::shared_ptr<void> guard;
    if (tied_)
    {
        guard = tie_.lock();
        if (guard)
        {
            handleEventWithGuard(recevieTime);
        }
    }
    else
    {
        handleEventWithGuard(recevieTime);
    }
}

/**
* 根据不同的激活原因, 调用不同的回调函数
*/
void Channel::handleEventWithGuard(Timestamp receiveTime)
{
    eventHandling_ = true; // 正在处理事件
    LOG_TRACE << reventsToString(); // 打印fd及就绪事件
    if ((revents_ & POLLHUP) && !(revents_ & POLLIN))
    { // fd挂起(套接字已不在连接中), 并且没有数据可读
        if (logHup_)
        { // 打印挂起log
            LOG_WARN << "fd = " << fd_ << " Channel::handle_event() POLLHUP";
        }
        // 调用关闭回调
        if (closeCallback_) closeCallback_();
    }
    ...
}

TcpConnection与断开连接

包含CloseCallback事件回调,不过是给TcpServer和TcpClient用的,用于通知它们移除所持有的TcpConnectionPtr,而非给普通用户直接用的,普通用户应使用ConnectionCallback。

与断开连接有关的部分:

// TcpConnection.h
public:
    /* 关闭写半连接 */
    void shutdown(); // NOT thread safe, no simultaneous calling
//    void shutdownAndForceCloseAfter(double seconds); // NOT thread safe, no simultaneous calling

    /* 强制关闭连接 */
    void forceClose();
    /* 强制延时关闭连接 */
    void forceCloseWithDelay(double seconds);

    ...

    /* Internal use only */
    void setCloseCallback(const CloseCallback& cb)
    { closeCallback_ = cb; }

    // called when TcpServer accepts a new connection
    void connectEstablished(); // should be called only once per connection
    // called when TcpServer has removed me from its map
    void connectDestroyed(); // should be called only once per connection

private:
    void handleClose();                     // 处理关闭连接事件
  • 被动关闭连接
    当收到对端FIN分节时,本地read返回0,,Tcp连接被动关闭,会触发调用本地TcpConnection::handleClose(),其定义如下:
/**
* 处理Tcp连接关闭
* @details 更新状态为kDisconnected, 清除所有事件通道监听
* @note 必须在所属loop线程中运行.
*/
void TcpConnection::handleClose()
{
    loop_->assertInLoopThread(); // 确保在所属loop线程中运行
    LOG_TRACE << "fd = " << channel_->fd() << " state = " << stateToString();
    assert(state_ == kConnected || state_ == kDisconnecting);
    // we don't close fd, leave it to dtor, so we can find leaks easily.
    setState(kDisconnected); // 更新Tcp连接状态
    channel_->disableAll();     // 停止监听所有通道事件(读写事件)

    TcpConnectionPtr guardThis(shared_from_this());
    connectionCallback_(guardThis); // 连接回调
    // must be the last line
    closeCallback_(guardThis);      // 关闭连接回调
}

closeCallback_在TcpServer::newConnection()为新连接新建TcpConnection时,已设为TcpServer::removeConnection(),而removeConnection()最终会调用TcpConnection::connectDestroyed()来销毁连接资源。

/**
* 主动销毁当前tcp连接, 移除通道事件
* @note 只有处于已连接状态(kConnected)的tcp连接, 才需要先更新状态, 关闭通道事件监听
*/
void TcpConnection::connectDestroyed()
{
    loop_->assertInLoopThread();
    if (state_ == kConnected) // 只有kConnected的连接, 才有必要采取断开连接动作
    {
        setState(kDisconnected);
        channel_->disableAll(); // 关闭通道事件监听

        connectionCallback_(shared_from_this()); // 调用连接回调
    }
    channel_->remove(); // 从EventLoop和Poller中移除监听通道事件
}
  • 主动关闭连接
    按连接方向,有2类关闭方式:1)强制close连接;2)关闭一个方向的连接(读或写方向)。
/**
* 强制关闭连接, 只对连接为kConnected或kDisconnecting状态才有效
* @details 为防止意外, 动作应该放到loop末尾去做
*/
void TcpConnection::forceClose()
{
    // FIXME: use compare and swap
    if (state_ == kConnected || state_ == kDisconnecting)
    {
        setState(kDisconnecting);
        loop_->queueInLoop(std::bind(&TcpConnection::forceCloseInLoop, shared_from_this()));
    }
}

/**
* 在所属loop循环中强制关闭连接, 只对连接为kConnected或kDisconnecting状态才有效
*/
void TcpConnection::forceCloseInLoop()
{
    loop_->assertInLoopThread();
    if (state_ == kConnected || state_ == kDisconnecting)
    {
        // as if we received 0 byte in handleRead()
        handleClose();
    }
}

可以看到,除了状态更新,对于关闭连接的真正操作,被动、主动关闭连接都是由handleClose来完成的。

假设想延迟一段时间再关闭连接,可以调用forceCloseWithDelay(),区别在于交给EventLoop:runAfter()延时运行,而不是交给EventLoop::queueInLoop()。

void TcpConnection::forceCloseWithDelay(double seconds)
{
    if (state_ == kConnected || state_ == kDisconnecting)
    {
        setState(kDisconnecting);
        loop_->runAfter(seconds,
                        makeWeakCallback(shared_from_this(),
                                          &TcpConnection::forceClose)); // not forceCloseInLoop to avoid race condition
    }
}

关闭连接写方向,相当于库函数shutdown(2)

/**
* 关闭连接写方向, 只有已连接状态才有效
*/
void TcpConnection::shutdown()
{
    // FIXME: use compare and swap
    if (state_ == kConnected)
    {
        setState(kDisconnecting);
        // FIXME: shared_from_this()?
        loop_->runInLoop(std::bind(&TcpConnection::shutDownInLoop, shared_from_this()));
    }
}

关于close和shutdown区别,参见:https://www.cnblogs.com/fortunely/p/14854299.html

TcpServer与断开连接

当新建一个tcp连接时,TcpServer会调用newConnection创建一个新TcpConnection对象管理Tcp连接,并将对象加入自己的ConnectionMap进行管理。

而当tcp连接断开时,需要调用removeConnection进行移除工作,而removeConnection会将工作转交给removeConnectionInLoop, 确保在所属loop线程中执行。

/**
* 转交给removeConnectionInLoop, 在所属loop线程中执行
*/
void TcpServer::removeConnection(const TcpConnectionPtr &conn)
{
    // FIXME: unsafe
    loop_->runInLoop(std::bind(&TcpServer::removeConnectionInLoop, this, conn));
}

removeConnectionInLoop要做的工作是将要移除的tcp连接对应TcpConnection对象,从ConnectionMap移除,然后销毁该对象。

/**
* 在所属loop线程循环中, 排队移除指定tcp连接 conn
* @param conn 指向待移除tcp连接对应TcpConnection对象
*/
void TcpServer::removeConnectionInLoop(const TcpConnectionPtr &conn)
{
    loop_->assertInLoopThread();
    LOG_INFO << "TcpServer::removeConnectionInLoop [" << name_
    << "] - connection " << conn->name();
    size_t n = connections_.erase(conn->name()); // 从ConnectionMap中擦除待移除TcpConnection对象
    (void)n;
    assert(n == 1);
    EventLoop* ioLoop = conn->getLoop();
    ioLoop->queueInLoop(
            std::bind(&TcpConnection::connectDestroyed, conn)); // 在所属loop线程中排队销毁TcpConnection对象
}

错误处理

Channel在处理通道事件handleEvent时,如果发生错误(既不是读取数据,也不是写数据完成),

调用链路:
Channel::handleEvent() => 检测到错误,调用Channel::errorCallback_ => TcpConnection::handleError()

/**
* 处理tcp连接错误, 打印错误log
* @details 从tcp协议栈获取错误信息
*/
void TcpConnection::handleError()
{
    int err = sockets::getSocketError(channel_->fd());
    LOG_ERROR << "TcpConnection::handleError [" << name_
    << "] - SO_ERROR = " << err << " " << strerror_tl(err);
}

发送数据

用于发送数据的TcpConnection::send()重载了下面几个版本

public:
    ...
    /* 发送消息给连接对端 */
//    void send(std::string&& mesage); // C++11
    void send(const void* message, int len);
    void send(const StringPiece& message);
//    void send(Buffer&& message);
    void send(Buffer* message); // this one will swap data

3个send() 重载版本,最终都会转交给sendInLoop(const char*, int),在所属loop线程中执行发送工作。


// 转发给send(const StringPiece&), 最终转交给sendInLoop(const char*, int)
void TcpConnection::send(const void *message, int len)
{
    send(StringPiece(static_cast<const char*>(message), len));
}

/**
* 转交给 sendInLoop(const char*, int)
* 发送消息给对端, 允许在其他线程调用
* @param message 要发送的消息. StringPiece兼容C/C++风格字符串, 二进制缓存, 提供统一字符串接口
*/
void TcpConnection::send(const StringPiece& message)
{
    if (state_ == kConnected)
    {
        if (loop_->isInLoopThread())
        { // 当前线程是所属loop线程
            sendInLoop(message);
        }
        else
        { // 当前线程并非所属loop线程
            void (TcpConnection::*fp)(const StringPiece& message);
            fp = &TcpConnection::sendInLoop;
            loop_->runInLoop(
                    std::bind(fp,
                              this, // FIXME
                              message.as_string()));
        }
    }
}

// 转交给sendInLoop(const char*, int)
void TcpConnection::sendInLoop(const StringPiece &message)
{
    sendInLoop(message.data(), message.size());
}

// 转交给sendInLoop(const char*, int)
// FIXME efficiency!!!
void TcpConnection::send(Buffer *buf)
{
    if (state_ == kConnected)
    {
        if (loop_->isInLoopThread())
        {
            // send all readable bytes
            sendInLoop(buf->peek(), buf->readableBytes());
            buf->retrieveAll();
        }
    }
}

sendInLoop定义如下,主要是向对端发送一次数据,如果发送完一次,就进行一次回调;如果待发送数据超高水位,就进行高水位回调;如果发生错误,就进行错误回调。

/**
* 在所属loop线程中, 发送data[len]
* @param data 要发送的缓冲区首地址
* @param len 要发送的缓冲区大小(bytes)
* @details 发生write错误, 如果发送缓冲区未满, 对端已发FIN/RST分节 表明tcp连接发生致命错误(faultError为true)
*/
void TcpConnection::sendInLoop(const char *data, size_t len)
{
    loop_->assertInLoopThread();
    ssize_t nwrote = 0;
    size_t remaining = len;
    bool faultError = false;
    if (state_ == kDisconnected) // 如果已经断开连接(kDisconnected), 就无需发送, 打印log(LOG_WARN)
    {
        LOG_WARN << "disconnected, give up writing";
        return;
    }

    // write一次, 往对端发送数据, 后面再看是否发生错误, 是否需要高水位回调
    // if no thing output queue, try writing directly
    if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)
    { // 如果通道没有使能监听写事件, 并且outputBuffer 没有待发送数据, 就直接通过socket写
        nwrote = sockets::write(channel_->fd(), data, len);
        if (nwrote >= 0)
        {
            remaining = len - nwrote;
            if (remaining == 0 && writeCompleteCallback_)
            {
                loop_->queueInLoop(std::bind(writeCompleteCallback_, shared_from_this()));
            }
        }
        else // nwrote < 0, error
        {
            nwrote = 0;
            if (errno != EWOULDBLOCK) // EWOULDBLOCK: 发送缓冲区已满, 且fd已设为nonblocking
            { // O_NONBLOCK fd, write block but return EWOULDBLOCK error
                LOG_SYSERR << "TcpConnection::sendInLoop";
                if (errno == EPIPE || errno == ECONNRESET) // FIXME: any others?
                { // EPIPE: reading end is closed; ECONNRESET: connection reset by peer
                    faultError = true;
                }
            }
        }
    }

    // 处理剩余待发送数据
    assert(remaining <= len);
    if (!faultError && remaining > 0) // 没有故障, 并且还有待发送数据, 可能是发送太快, 对方来不及接收
    { // no error and data remaining to be written
        size_t oldLen = outputBuffer_.readableBytes(); // Buffer中待发送数据量

        if (oldLen + remaining >= highWaterMark_ // Buffer及当前要发送的数据量之和 超 高水位(highWaterMark)
        && oldLen < highWaterMark_ // 单独的Buffer中待发送数据量 未超 高水位
        && highWaterMarkCallback_)
        {
            loop_->queueInLoop(std::bind(highWaterMarkCallback_, shared_from_this(), oldLen + remaining));
        }
        // append data to be written to the output buffer
        outputBuffer_.append(static_cast<const char*>(data) + nwrote, remaining);
        // enable write event for channel_
        if (!channel_->isWriting())
        { // 如果没有在监听通道写事件, 就使能通道写事件
            channel_->enableWriting();
        }
    }
}

用户使用TcpServer与客户端进行通信

假设客户端请求与服务器建立连接,用户(库的使用者)如何在建立连接、发送数据时,执行自定义任务?
可以在TcpServer启动前,利用TcpServer::setConnectionCallback()、TcpServer::setMessageCallback()等设置回调接口注册任务,等Tcp连接达到指定状态后,会回调用户任务。

示例:使用TcpServer接口类创建Tcp连接
例如,用户自定义服务EchoServer回传收到的客户端消息,EchoServer使用TcpServer注册连接回调(setConnectionCallback)和消息回调(setMessageCallback)。
如果需要分阶段向客户端发送数据,还可以注册写完成回调(setWriteCompleteCallback)。

// examples/simple/echo
EchoServer::EchoServer(muduo::net::EventLoop *loop, const muduo::net::InetAddress &listenAddr)
: server_(loop, listenAddr, "EchoServer")
{
    server_.setConnectionCallback(
            std::bind(&EchoServer::onConnection, this, _1));
    server_.setMessageCallback(
            std::bind(&EchoServer::onMessage, this, _1, _2, _3));

    LOG_INFO << server_.name() << " listen on " << listenAddr.toIpPort();
}

void EchoServer::start()
{
    server_.start();
}

void EchoServer::onConnection(const muduo::net::TcpConnectionPtr &conn)
{
    LOG_INFO << "EchoServer - " << conn->peerAddress().toIpPort() << " -> "
    << conn->localAddress().toIpPort() << " is "
    << (conn->connected() ? "UP" : "DOWN");
}

void EchoServer::onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp time)
{
    muduo::string msg(buf->retrieveAllAsString());
    LOG_INFO << conn->name() << " echo " << msg.size() << " bytes, "
    << "data received at " << time.toString();
    conn->send(msg);
}

int main()
{
    LOG_INFO << "pid = " << getpid();
    muduo::net::EventLoop loop; // 创建EventLoop对象
    muduo::net::InetAddress listenAddr(2007); // 创建包含ip地址、端口号的对象
    EchoServer server(&loop, listenAddr); // 创建用户自定义EchoServer对象
    server.start();  // 启动EchoServer服务器
    loop.loop();     // 启动loop循环
    return 0;
}

重要知识点

宏函数CHECK_NOTNULL

这是一个模板函数重定义以后的宏函数,可以有效检查指针是否为NULL。如果是就直接终止程序(LOG_FATAL),并记录log,避免后续意外情况发生。

// Logging.h

//    Taken from glog/logging.h
//
//    Check that the input is non NULL. This very useful in constructor
//    initializer lists

#define CHECK_NOTNULL(val) 
  ::muduo::CheckNotNull(__FILE__, __LINE__, "'" #val "' Must be non NULL", (val))

// A small helper for CHECK_NOTNULL()
template <typename T>
T* CheckNotNull(Logger::SourceFile file, int line, const char* names, T* ptr)
{
    if (ptr == NULL)
    {
        Logger(file, line, Logger::FATAL).stream() << names;
    }
    return ptr;
}

函数模板get_pointer

用shared_ptr, unique_ptr自带的get()方法,从中取得raw pointer(原生指针)。当然,可以直接调用shared_ptr, unique_ptr,用函数模板重载可以使其含义明确,而且同一的接口方便参数类型可以在允许的类型中切换。

/* get raw pointer from std::shared_ptr */
template<typename T>
inline T* get_pointer(const std::shared_ptr<T>& ptr)
{
    return ptr.get();
}

/* get raw pointer from std::unique_ptr */
template<typename T>
inline T* get_pointer(const std::unique_ptr<T>& ptr)
{
    return ptr.get();
}

客户端:
可以像下面这样,安全的异步调用acceptor_->listen(),注意acceptor_是std::unique_ptr,不能直接拷贝,传递给runInLoop的实参是raw pointer

loop_->runInLoop(std::bind(&Acceptor::listen, get_pointer(acceptor_)));

类模板WeakCallback<>

当我们有一个shared_ptr指针object指向一个对象,那么如何安全地调用某个成员函数func呢?
比如,这个shared_ptr object来自shared_from_this(),如果用object->func(),只能同步调用,没办法异步调用(回调),因为等到异步调用的时候,原来的object对象可能已经释放。此时,可以考虑将object转化为weak_ptr,保存weak_ptr,在需要先提升为shared_ptr,再调用之。

函数子类模板WeakCallback就是这样一种思想。用std::weak_ptr保存一个对象object,std::function保存一个不定参数的函数对象function,并重载函数调用运算符operator(),用于安全地将object作为function的参数来调用。

/**
* 以一种安全方式调用 形如 function(object, ...), 如果object指向一个class, 那么相当于即object->function()
*/
template<typename CLASS, typename... ARGS> // ARGS是可变参数模板参数包
class WeakCallback
{
public:
    WeakCallback(const std::weak_ptr<CLASS>& object, // weak_ptr安全地使用传入实參
                 const std::function<void (CLASS*, ARGS...)>& function) // function参数个数不确定
                 : object_(object), function_(function)
    {
    }

    // Default dtor, copy ctor and assignment are okay

    void operator()(ARGS&&... args) const // args参数包
    {
        // 安全地使用weak_ptr所指对象
        std::shared_ptr<CLASS> ptr(object_.lock()); // 先lock, 再提升为shared_ptr, 只在local作用域影响引用计数
        if (ptr)
        {
            function_(ptr.get(), std::forward<ARGS>(args)...);
        }
//        else
//        {
//            LOG_TRACE << "expired";
//        }
    }

private:
    std::weak_ptr<CLASS> object_;
    std::function<void (CLASS*, ARGS...)> function_;
};

为了简化使用,可以通过重载函数makeWeakCallback的2个版本,制造一个临时WeakCallback对象,用来保存要调用的函数及参数。

/* 制造一个临时WeakCallback对象, 便于调用object->function(...) */
template<typename CLASS, typename... ARGS>
WeakCallback<CLASS, ARGS...> makeWeakCallback(const std::shared_ptr<CLASS>& object,
                                              void (CLASS::*function)(ARGS...)) // 非const 成员函数
{
    return WeakCallback<CLASS, ARGS...>(object, function);
}

/* 制造一个临时WeakCallback对象, 便于调用object->function(...) const */
template<typename CLASS, typename ... ARGS>
WeakCallback<CLASS, ARGS...> makeWeakCallback(const std::shared_ptr<CLASS>& object,
                                              void (CLASS::*function)(ARGS...) const) // const 成员函数
{
    return WeakCallback<CLASS, ARGS...>(object, function);
}

客户端:可以像下面那样,安全地异步调用shared_from_this()->forceCloseInLoop()。

loop_->queueInLoop(std::bind(&TcpConnection::forceCloseInLoop, shared_from_this()));

原文链接: https://www.cnblogs.com/fortunely/p/16048017.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    muduo笔记 网络库(七)TCP连接TcpConnection、Acceptor、TcpServer

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/402696

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月21日 上午11:10
下一篇 2023年4月21日 上午11:10

相关推荐