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*+len、StringPiece或Buffer*。 - 关键特性:线程安全 。
send()可以在任何线程中调用。如果当前在TcpConnection所属的I/O线程,它会直接尝试发送;如果不在,它会把发送任务派发(runInLoop)到那个I/O线程去执行,从而避免了多线程并发写socket的问题。
- 这是用户发送数据的核心API。它有多个重载版本,可以发送
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事件。
handleRead(): 当Channel检测到socket可读时被调用。它会从socket读取数据到inputBuffer_,然后调用用户设置的MessageCallback。如果read返回0,表示对端关闭连接,就会调用handleClose()。handleWrite(): 当outputBuffer_中有数据且socket可写时被调用。它会把outputBuffer_中的数据写入socket。如果数据全部写完,它会停止关注可写事件(节省CPU),并调用WriteCompleteCallback。handleClose(): 当对端关闭连接、本端主动关闭连接或发生错误时被调用。它会清理资源,禁用Channel上的所有事件,并依次调用ConnectionCallback和内部的CloseCallback(通知TcpServer将自己移除)。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
时序图解读:
- 连接建立 :
TcpServer在其所在的IO Thread / EventLoop中创建TcpConnection对象,并调用connectEstablished初始化连接,最终通过ConnectionCallback通知用户。 - 数据接收 : 物理socket收到数据后,
EventLoop监听到可读事件,并调用TcpConnection的handleRead方法。handleRead负责读取数据到缓冲区,然后通过MessageCallback将数据交给用户处理。 - 跨线程发送数据 : 当
User Thread调用send时,TcpConnection会将发送任务sendInLoop抛给IO Thread去执行,以保证线程安全。sendInLoop会尝试直接发送,如果发送不完,就把剩余数据放入outputBuffer_。 - 处理待发送数据 : 当
outputBuffer_中有数据时,TcpConnection会监听socket的可写事件。一旦socket可写,EventLoop就会调用handleWrite,将缓冲区的数据发送出去。 - 优雅关闭 : 用户调用
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.cc的send方法里,你会看到这样的判断:cppif (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持有所有TcpConnection的shared_ptr,确保了只要连接存在,对象就不会被销毁。enable_shared_from_this: 这让TcpConnection对象内部能安全地获取到自身的shared_ptr(通过shared_from_this())。这在注册回调函数时至关重要,因为回调函数需要持有TcpConnection的shared_ptr,以延长其生命周期,确保回调执行时对象依然有效。例如handleClose中的TcpConnectionPtr guardThis(shared_from_this());就是一个经典的用法。- 最精巧的一点:
Channel::tieChannel对象是TcpConnection和EventLoop之间的桥梁,它直接和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,然后MessageCallback从Buffer中按照应用层协议解析出完整的消息包,解决了 TCP 本身不提供消息边界的问题。 - 内存使用效率 :
Buffer内部维护了prependable bytes、readable bytes和writable bytes三个区域。当已读数据(readable bytes)被消耗后,这部分空间并不会立即释放,而是变成了prependable bytes。这样可以避免频繁的内存移动。当writable bytes不够用时,才会考虑移动数据或重新分配内存。 - 零拷贝操作 :
Buffer::readFd内部使用了readv这个 scatter/gather I/O 操作。它可以直接将数据从内核缓冲区读到Buffer的writable空间,同时还可以利用一个栈上的临时缓冲区,避免了"内核 -> 用户临时buffer -> Buffer"的两次拷贝,提升了读取效率。
4. 精巧的流量控制与关闭机制
- 高水位回调 (
HighWaterMarkCallback) : 这是防止发送速度远大于接收速度导致内存耗尽的关键机制。当outputBuffer_中积压的数据超过一个阈值(highWaterMark_)时,会触发回调。用户可以在这个回调中暂停产生数据(比如停止读取上游数据源)。当outputBuffer_的数据量降下来后,WriteCompleteCallback会被触发,用户此时可以恢复产生数据。这是一个简单的应用层反压/流控机制。 - 优雅关闭 (
shutdown) :shutdown并非直接close套接字,而是调用socket->shutdownWrite(),这对应于 TCP 的半关闭(half-close)。它会向对端发送FIN包,表示"我这边的数据已经发完了",但仍然可以接收来自对端的数据。这对于需要确保所有待发数据都成功送达的协议至关重要。