Muduo库源码剖析(5) TcpConnection
Buffer 和 TcpConnection 详解
本章我们将学习Buffer类和其上级类TcpConnection, 先了解两个类的职能 :
Buffer
和通常的缓冲区认知类似, 目的在于提升传入传出的效率, 就是在read中可以提前把数据从内核接收缓冲区中读出, 便于对端再向内核接收缓冲区输入. 在write中可以提前把要写入的数据存进缓冲区, 等待内核发送缓冲区空余.
TcpConnection
专门用来维护每条与客户端的连接的类, 也就是对Acceptor中得到的connfd进行封装, 包括其对应的Socket和Channel类. 并且连接也代表了有连接的建立与断开, 数据的传入与传出等活动, 这些活动中, 一部分需要上级设置的回调函数来处理(例如连接建立断开, 数据传入, 都需要网络库调用者设置), 一部分需要TcpConnection利用自己手头的资源自行处理(例如数据传出, 需要调用自己的send函数). 既然与数据的传入传出有关, 其内部也内置Buffer类来优化传入传出的效率.
Buffer
先看源码 :
1 |
|
1 |
|
先看成员变量 :
_buffer : 用vector存储数据, 便于扩容.
_readerIndex / _writerIndex :
这两个参数标定了可读区域和可写区域的大小. 想要理解这两个参数, 需要理解Buffer中对_buffer的划分 :
1
2
3
4|<---- prependable ---->|<---- readable ---->|<---- writable ---->|
|-----------------------|--------------------|--------------------|
^ ^ ^
begin() _readerIndex _writerIndex- prependable : 前置空余区域, 当我们发送消息时, 如果向要加一些报头之类的信息, 可以便于向其中填入, 在Muduo库中作用不大.
- readable : 已经有数据存入的部分, 如果当前要读出, 则这部分是可读部分, 如果当前要写入, 则这部分是已填充完毕的区域.
- writeable : 还没有数据的部分, 如果当前要读入, 则这部分是空余空间, 如果当前要写入, 则这部分是可以继续写入的部分.
我们可以看出Buffer既可以处理read也可以处理write, 其对应的readable和writeable区域也有不同的作用. 在TcpConnection就封装了两个Buffer, _inputBuffer和 _outputBuffer, 共同处理了缓冲任务.
再看成员函数 :
readable/writable/prependableBytes : 返回对应区域的字节数大小.
begin : 返回_buffer的首元素地址.
peek : 返回缓冲区中可读区域的起始地址.
makespace :
扩容函数, 确保_buffer有参数len的大小使write成功, 如果可写的空间不够就扩容, 够用就经过调整把前面的空余和后面的空余合并一块放在后面.
retrieve / retrieveAll :
这个函数一般会在下面的retrieveAsString中使用, 或是调用readfd/writefd后使用, 用来进行两个Index的置位.
retrieveAsString / retrieveAllAsString :
从_buffer中读出数据的函数, 这个函数会将可读区域中len长度的数据当作string读出来, 并在调用retrieve后返回, 通常被客户用来从Buffer中直接读出接收到的数据.
ensureWriteableBytes : 对makeSpace的调用.
append :
从_buffer中追加数据的函数, 向缓冲区中追加数据[data, data + len], 空间不够时会调用ensureWriteableBytes.
readFd :
核心函数, 目的是高效从一个fd上读取传来的数据到 _buffer 中, 其高效在于使用到了readv函数, 这个函数不细讲, 不知道可以去查, 简单来说就是就是其实现了在非连续的区块中依次写入同一个fd传入的信息, 函数中划分了两块区域, 一块是 _buffer的可写区域, 一块是我们在栈上开辟的64K临时空间, readv可以实现先向可写区域中写, 可写区域写满了再读入我们开辟的临时空间, 读完后加入临时空间有读入, 我们再用append加进去就行, 这样实现了灵活应对读入不同大小的情况.
writeFd :
这个就比较简单了, 因为网络输出缓冲区就一个, 就不需要考虑writev的使用, 直接调用write从Buffer中将有数据的部分发送出去即可.
TcpConnection
专门用来维护每条与客户端的连接的类, 也就是对Acceptor中得到的connfd进行封装, 包括其对应的Socket和Channel类. 并且连接也代表了有连接的建立与断开, 数据的传入与传出等活动, 这些活动中, 一部分需要上级设置的回调函数来处理(例如连接建立断开, 数据传入, 都需要网络库调用者设置), 一部分需要TcpConnection利用自己手头的资源自行处理(例如数据传出, 需要调用自己的send函数). 既然与数据的传入传出有关, 其内部也内置Buffer类来优化传入传出的效率.
通过上文我们的对TcpConnection的描述, 我们可以将其分为以下几个功能模块 :
- 构造 / 析构函数
- TcpServer对TcpConnection设置连接建立回调, 断开回调, 消息回调等, 正式开启/关闭连接.
- TcpConnection对自己负责的connfd相关联的Channel类设置读/写/关闭/错误回调.
- 用户发送信息所调用的send函数, 手动关闭连接所调用的shutdown函数.
让我们先分析头文件 :
1 |
|
这里先介绍一下公开继承enable_shared_from_this的作用 :
其可以让继承其的类的成员函数可以调用shared_from_this()函数, 作用是返回当前类对象的智能指针封装, 也就是把this指针包到智能指针中再传出来, 所以使用他的原因主要是TcpConnection对象可能随使销毁, 传出的this指针随时可能失效, 但如果我们本身传出的就是智能指针, 就可以有效防止提前销毁.
了解一下成员变量 :
- _loop : 这里保存自己所在subLoop的指针, 是为了将需要调用的函数加入到事件循环中.
- _state : 用来记录一个连接的各种状态, 状态同一时间只能有一种, 所以是atomic类型.
- _socket / _channel : 将Acceptor接收到的connfd封装为Socket和Channel.
- …Callback : 各种有关于连接的事件回调.
- _inputBuffer / _outputBuffer : 分别处理TcpConnection的消息接受和发送.
1 |
|
配合源文件来分析成员函数 :
构造函数 :
掌握一个不为空的loop指针, 利用传进的connfd构造_socket和 _channel. 随后设置 _channel的读写关闭错误回调, 就像Acceptor对 listensocketfd 设置读事件回调一样, 普通连接connfd四种事件都要关注, 所以我们四种事件都设置回调, 而回调函数分别是下面的handleRead/Write/Colse/Error.
handleRead :
当connfd的读事件触发时, 会触发该函数, 向 _inputBuffer中读入数据, 然后调用TcpServer设置的消息回调函数, 这个回调函数中一般会调用retrieveAsString从 _inputBuffer中读出数据然后进行客户希望的处理.
handleWrite :
当connfd的读事件触发时, 说明对端可写, 从_outputBuffer中读出所有或部分向对端写入, 如果全写了, 就取消对 _channel的写事件关心然后调用写完回调(如果有的话); 如果没写完就不做处理.
handleClose :
当我们关闭connfd的写端时, 就会默认触发EPOLLHUP事件, 进而调用该函数, 取消_channel对所有事件的关心, 然后调用连接建立/断开回调.
connectEstablished :
这个函数用来开启_channel读事件的关注, 也就是说在这之后就可以读connfd上传来的信息了. 至于为什么单独分出一个函数处理, 原因是在TcpConntion对象在TcpServer中创建后不能直接开启读事件, 还要进行回调函数的设置和数据处理, 在这些准备工作都做完后才可以真正开启读事件.
connectDestoryed :
这个函数用于在最后销毁连接并移除_channel, 属于销毁链路的最后一环, 由TcpServer在合适时机调用, 我们可以在后面的shutdownInLoop中有更深的认识.
send :
这个函数是提供给用户调用的, 所以调用时不一定在当前线程, 所以需要判断是否换线程.
sendInLoop :
前面handleWrite函数是在写事件触发时从_outputBuffer中读取数据, 那么这个函数就是直接发送或将要发送的数据写入 _outputBuffer.
首先如果_channel第一次开始写数据, 并且缓冲区没有待发送数据, 就直接先写一部分, 最后如果还是没写完, 说明对端接收缓冲区已经满了, 就追加到 _outputBuffer中, 再关注读事件, 那么当对端接收缓冲区可写时就会调用handleWrite从 _outputBuffer中读取数据.
shutdown :
这个函数也是提供给用户调用的, 需要判断换线程.
shutdownInLoop :
这里操作很简单, 关闭connfd写端就行, 主要是了解其中的连锁反应 :
关闭写端 -> 触发EPOLLHUP -> _channel调用handleClose -> handleClose调用TcpServer设置的回调函数 -> TcpServer设置的回调函数再调用TcpConnection中的connectDestoryed.
这样一看这个过程非常繁琐, 其主要是为了让TcpServer可以及时对关闭的连接进行反应并修改内部资源.