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包,表示"我这边的数据已经发完了",但仍然可以接收来自对端的数据。这对于需要确保所有待发数据都成功送达的协议至关重要。