TcpConnection

TcpConnection 的设计精髓在于 事件驱动回调机制。它将底层的socket读写、连接状态变化等事件都封装好,然后通过回调函数通知给用户,用户只需要关心如何处理这些事件(比如收到消息后该做什么业务处理),而不需要关心具体的网络I/O细节。

下面我将 TcpConnection.h 中定义的API分为几类来介绍:

1. 生命周期管理 (Lifecycle Management)

这类API负责连接的建立、状态变更和销毁。

  • 构造函数 TcpConnection(...) 和析构函数 ~TcpConnection()
    • 构造函数通常不是由用户直接调用的,而是由 TcpServer (当接受一个新连接时) 或 TcpClient (当成功连接到服务器时) 在内部创建 TcpConnection 对象。它需要一个 EventLoop 对象、连接名称、已连接的socket文件描述符 sockfd 以及本地和对端的地址。
  • connectEstablished()connectDestroyed()
    • 这两个是内部接口,TcpServer 在接受新连接并创建好 TcpConnection 后会调用 connectEstablished()。这个函数会把连接状态设为 kConnected,启用读事件,并触发用户设置的 ConnectionCallback
    • connectDestroyed() 则在连接彻底销毁前被调用,用于清理工作。
  • shutdown()
    • 优雅关闭。调用后,muduo 会确保 outputBuffer_ 中待发送的数据全部发送完毕后,再关闭写端(发送FIN包),但此时仍然可以接收数据。这是TCP的"半关闭"(half-close)功能。它是非线程安全的,需要在IO线程中调用。
  • forceClose()forceCloseWithDelay()
    • 强制关闭。立即或延迟一段时间后,直接关闭连接,outputBuffer_ 中未发送的数据可能会丢失。

2. 数据收发 (Data Transfer)

用户通过这类API来发送数据和获取接收到的数据。

  • send() 系列函数
    • 这是用户发送数据的核心API。它有多个重载版本,可以发送 const void* + lenStringPieceBuffer*
    • 关键特性:线程安全send() 可以在任何线程中调用。如果当前在 TcpConnection 所属的I/O线程,它会直接尝试发送;如果不在,它会把发送任务派发(runInLoop)到那个I/O线程去执行,从而避免了多线程并发写socket的问题。
  • inputBuffer()outputBuffer()
    • 分别返回指向内部输入/输出缓冲区的指针。用户在 MessageCallback 中通过 inputBuffer() 来读取和处理收到的数据。outputBuffer() 主要供库内部使用,用户一般不需要直接操作它。

3. 回调函数设置 (Callback Setup)

这是 TcpConnection 用法的核心,用户通过设置不同的回调函数来注入自己的业务逻辑。

  • setConnectionCallback(const ConnectionCallback& cb)
    • 设置连接建立和断开时的回调。当一个连接成功建立或断开时,这个回调函数会被调用。
  • setMessageCallback(const MessageCallback& cb)
    • 设置消息到达时的回调。当TcpConnection从socket读到数据并存入inputBuffer_后,会调用这个回调。这是处理业务逻辑最主要的地方。
  • setWriteCompleteCallback(const WriteCompleteCallback& cb)
    • 设置发送完成回调。当 outputBuffer_ 中的数据全部被发送到内核缓冲区后,这个回调会被触发。
  • setHighWaterMarkCallback(const HighWaterMarkCallback& cb, size_t highWaterMark)
    • 设置"高水位"回调。用于防止发送速度过快,导致outputBuffer_积压过多数据耗尽内存。当待发送数据量超过 highWaterMark 时,会触发此回调,用户可以在回调中暂停写入,等 WriteCompleteCallback 被触发(说明缓冲区数据量下降了)后再恢复写入,实现流量控制。

4. 状态查询与控制 (State Query & Control)

  • connected()disconnected()
    • 检查当前连接的状态。
  • getLoop(), name(), localAddress(), peerAddress()
    • 获取连接所属的EventLoop、连接名、本地地址和对端地址等信息。
  • setTcpNoDelay(bool on)
    • 开启或关闭 TCP_NODELAY 选项(Nagle算法),默认关闭。开启后可以降低小数据包的延迟。
  • startRead() / stopRead()
    • 在连接上开启或停止读数据。这也常用于实现上层应用的流量控制。

内部工作流程 (TcpConnection.cc 实现)

TcpConnection 内部通过一个 Channel 对象来关注socket上的IO事件。

  1. handleRead() : 当Channel检测到socket可读时被调用。它会从socket读取数据到inputBuffer_,然后调用用户设置的MessageCallback。如果read返回0,表示对端关闭连接,就会调用handleClose()
  2. handleWrite() : 当outputBuffer_中有数据且socket可写时被调用。它会把outputBuffer_中的数据写入socket。如果数据全部写完,它会停止关注可写事件(节省CPU),并调用WriteCompleteCallback
  3. handleClose() : 当对端关闭连接、本端主动关闭连接或发生错误时被调用。它会清理资源,禁用Channel上的所有事件,并依次调用ConnectionCallback和内部的CloseCallback(通知TcpServer将自己移除)。
  4. handleError(): 当socket发生错误时被调用,用于记录日志。

好的,没问题。时序图(Sequence Diagram)确实能更清晰地展示对象之间的交互和时间顺序。
User Thread IO Thread / EventLoop TcpConnection Socket / OS 1. 连接建立 new TcpConnection(loop, fd, ...) connectEstablished() setState(kConnected) enableReading() via Channel ConnectionCallback (连接成功) 2. 数据接收 数据到达, socket可读 handleRead() read(fd, inputBuffer) MessageCallback(conn, inputBuffer) 在回调中处理业务逻辑 3. 跨线程发送数据 send(data) 判断出调用者不在IO线程 runInLoop(bind(&sendInLoop, ...)) sendInLoop(data) write(fd, data) 尝试直接写入socket append data to outputBuffer alt [outputBuffer 为空] [outputBuffer 不为空] 将剩余数据存入outputBuffer enableWriting() via Channel WriteCompleteCallback alt [数据未完全写入] [数据写完且outputBuffer为空] 4. 处理待发送数据 Socket 变为可写 handleWrite() write(fd, outputBuffer.peek()) disableWriting() via Channel WriteCompleteCallback alt [outputBuffer 被清空] 5. 优雅关闭 shutdown() runInLoop(bind(&shutdownInLoop, ...)) shutdownInLoop() shutdownWrite() [发送 FIN] 将在handleWrite写完数据后, 再调用shutdownInLoop alt [outputBuffer 已清空] [outputBuffer 仍有数据] 收到对端的FIN, socket可读 handleRead() read(fd) 返回 0 handleClose() setState(kDisconnected) disableAll() via Channel ConnectionCallback (连接断开) closeCallback() (通知TcpServer移除自己) 析构TcpConnection对象 User Thread IO Thread / EventLoop TcpConnection Socket / OS

时序图解读:

  1. 连接建立 : TcpServer 在其所在的 IO Thread / EventLoop 中创建 TcpConnection 对象,并调用 connectEstablished 初始化连接,最终通过 ConnectionCallback 通知用户。
  2. 数据接收 : 物理socket收到数据后,EventLoop 监听到可读事件,并调用 TcpConnectionhandleRead 方法。handleRead 负责读取数据到缓冲区,然后通过 MessageCallback 将数据交给用户处理。
  3. 跨线程发送数据 : 当 User Thread 调用 send 时,TcpConnection 会将发送任务 sendInLoop 抛给 IO Thread 去执行,以保证线程安全。sendInLoop 会尝试直接发送,如果发送不完,就把剩余数据放入 outputBuffer_
  4. 处理待发送数据 : 当 outputBuffer_ 中有数据时,TcpConnection 会监听socket的可写事件。一旦socket可写,EventLoop 就会调用 handleWrite,将缓冲区的数据发送出去。
  5. 优雅关闭 : 用户调用 shutdown 发起半关闭。TcpConnection 会确保 outputBuffer_ 的数据发送完毕后,再向对端发送FIN包。当收到对端的FIN后(体现为read返回0),handleClose 被调用,执行最后的清理工作,并再次通过 ConnectionCallback 通知用户连接已断开。

1. 精巧的线程模型:one loop per thread + 跨线程调用

  • 核心思想 : 每个 I/O 线程(EventLoop 所在的线程)独立管理自己的一组 TcpConnection。一个 TcpConnection 的所有 I/O 操作(读、写、关闭)都必须在它所属的那个 EventLoop 线程中执行。这彻底避免了多线程访问 socket 和相关数据结构的竞态条件,因此完全不需要使用锁 来保护 TcpConnection 内部的状态(如 state_, inputBuffer_, outputBuffer_)。

  • 精巧之处 : 如何让用户在任何线程 都能安全地调用 send()shutdown() 呢?

    • TcpConnection.ccsend 方法里,你会看到这样的判断:

      cpp 复制代码
      if (loop_->isInLoopThread())
      {
        sendInLoop(message);
      }
      else
      {
        loop_->runInLoop(
            std::bind(fp,
                      this,
                      message.as_string()));
      }
    • 这就是关键:isInLoopThread() 判断当前线程是否就是管理此 TcpConnection 的 I/O 线程。

      • 如果是 ,就直接执行 sendInLoop
      • 如果不是 (即用户在自己的工作线程中调用),则通过 loop_->runInLoop()sendInLoop 这个任务打包,发送给目标 I/O 线程,让 I/O 线程在未来的某个时间点(通常是下一次事件循环)来执行它。
    • 优点 : 这种"任务派发"机制,既保证了 send 接口的线程安全性,又避免了锁带来的性能开销和死锁风险,是现代高性能网络库的典范设计。

2. 精巧的生命周期管理:shared_ptr + enable_shared_from_this + Channel::tie

在异步回调的环境中,对象的生命周期管理是个大难题。比如,一个 TcpConnection 可能在它的一个回调函数还没来得及执行时就被销毁了,导致野指针访问。muduo 用一套组合拳完美解决了这个问题。

  • shared_ptr<TcpConnection> : TcpServer 中用一个 map 持有所有 TcpConnectionshared_ptr,确保了只要连接存在,对象就不会被销毁。
  • enable_shared_from_this : 这让 TcpConnection 对象内部能安全地获取到自身的 shared_ptr(通过 shared_from_this())。这在注册回调函数时至关重要,因为回调函数需要持有 TcpConnectionshared_ptr,以延长其生命周期,确保回调执行时对象依然有效。例如 handleClose 中的 TcpConnectionPtr guardThis(shared_from_this()); 就是一个经典的用法。
  • 最精巧的一点:Channel::tie
    • Channel 对象是 TcpConnectionEventLoop 之间的桥梁,它直接和 Poller 交互。Channel 的生命周期和 TcpConnection 绑定,但 Channel 的回调函数是在 EventLoop 中被调用的。
    • 考虑一个场景:TcpConnection 析构了,但 Poller 中可能还有该连接的残留事件,EventLoop 稍后处理这个事件时,会通过 Channel 调用 handleRead 等成员函数,此时 TcpConnection 对象已经销毁,程序崩溃。
    • muduo 的解决方案是:在 connectEstablished 中调用 channel_->tie(shared_from_this());tie 内部存储了一个 weak_ptr<void> 指向 TcpConnection
    • Channel 准备执行回调(如 handleEvent)时,它会先尝试将这个 weak_ptr提升(lock())为 shared_ptr
      • 如果提升成功 ,说明 TcpConnection 对象还活着,就安全地执行回调。
      • 如果提升失败 ,说明 TcpConnection 对象已经被销毁了,那就放弃执行回调。
    • 这个 tie 机制像一根安全绳,优雅地解决了跨对象的生命周期依赖问题,防止了 use-after-free。

3. 精巧的缓冲区设计:Buffer

  • 解决 TCP 粘包/半包问题 : Buffer 是处理 TCP 字节流的基础。handleRead 一次性将 socket 中所有可读数据读入 Buffer,然后 MessageCallbackBuffer 中按照应用层协议解析出完整的消息包,解决了 TCP 本身不提供消息边界的问题。
  • 内存使用效率 : Buffer 内部维护了 prependable bytesreadable byteswritable bytes 三个区域。当已读数据(readable bytes)被消耗后,这部分空间并不会立即释放,而是变成了 prependable bytes。这样可以避免频繁的内存移动。当 writable bytes 不够用时,才会考虑移动数据或重新分配内存。
  • 零拷贝操作 : Buffer::readFd 内部使用了 readv 这个 scatter/gather I/O 操作。它可以直接将数据从内核缓冲区读到 Bufferwritable 空间,同时还可以利用一个栈上的临时缓冲区,避免了"内核 -> 用户临时buffer -> Buffer"的两次拷贝,提升了读取效率。

4. 精巧的流量控制与关闭机制

  • 高水位回调 (HighWaterMarkCallback) : 这是防止发送速度远大于接收速度导致内存耗尽的关键机制。当 outputBuffer_ 中积压的数据超过一个阈值(highWaterMark_)时,会触发回调。用户可以在这个回调中暂停产生数据(比如停止读取上游数据源)。当 outputBuffer_ 的数据量降下来后,WriteCompleteCallback 会被触发,用户此时可以恢复产生数据。这是一个简单的应用层反压/流控机制。
  • 优雅关闭 (shutdown) : shutdown 并非直接 close 套接字,而是调用 socket->shutdownWrite(),这对应于 TCP 的半关闭(half-close)。它会向对端发送 FIN 包,表示"我这边的数据已经发完了",但仍然可以接收来自对端的数据。这对于需要确保所有待发数据都成功送达的协议至关重要。
相关推荐
linmengmeng_13148 小时前
【Centos】服务器硬盘扩容之新加硬盘扩容到现有路径下
linux·服务器·centos
边疆.8 小时前
【Linux】版本控制器Git和调试器—gdb/cgdb的使用
linux·服务器·git·gdb调试·cgdb
华如锦8 小时前
使用SSE进行实时消息推送!替换WebSocket,轻量好用~
java·开发语言·网络·spring boot·后端·websocket·网络协议
明天…ling8 小时前
Linux+Apache+MySQL+PHP 架构下搭建 Discuz 社区论坛
linux·服务器·apache
Ghost Face...9 小时前
GRUB配置文件解析与启动流程详解
linux·运维·服务器
kyle~9 小时前
计算机网络---ICMP协议(Internet Control Message Protocol,互联网控制消息协议)
网络·计算机网络·智能路由器
掘根10 小时前
【Docker】容器操作和实战
运维·docker·容器
肩上风骋10 小时前
ubuntu系统使用ifconfig查询网络IP的时候,只能查到本地回环地址,无其他网络接口IP
网络·ubuntu·只能查到本地回环地址
yics.14 小时前
网络原理——初识网络
网络·计算机网络·通信基础