muduo网络库学习(六)缓冲区Buffer及TcpConnection的读写操作
2017-10-26 19:15
453 查看
在
当调用
当有数据到达内核的
但是,内核维护的
如果调用
如果调用
这就导致要不阻塞当前线程,要不无法正常写入数据,而如果采用判断返回值是否出错的方法,仍然是一直忙循环检测
而且,
如果是水平触发,那么套接字会一直处于可读状态,
如果是边缘触发,那么就只会触发一次,即使第一次触发没有将所有数据都读走,下次进行到
所以,设计应用层自己的缓冲区是很有必要的,也就是由应用程序来管理缓冲区问题
应用层缓冲区通常很大,也可以初始很小,但可以通过动态调整改变大小(
应用层缓冲区需要有读/写两个(缓冲区类只有一个,既可被用作读缓冲区,也可被用作写缓冲区)
当用户想要调用
当有数据到达内核缓冲区,应用层的读缓冲区会自动将这些数据读到自己那里,当用户调用
应用层缓冲区对用户而言是隐藏的,用户可能根本不知道有应用层缓冲区的存在,只需读/取数据,而且也不会阻塞当前线程
缓冲区
注释中写明了缓冲区的设计方法,主要就是利用两个指针
缓冲区默认大小为
注意这个是
在
如果读缓冲区大小不够,其他数据就会写入到栈空间,接下来需要将栈空间的数据追加到缓冲区的末尾,使用
函数首先调用
如果空间不够,就需要调整空间大小
此时应用层读缓冲区从内核中读取数据完成,在用户可读的回调函数中(在
这是用户提供给
可以看到
这两个函数从读缓冲区中读取数据,一个是全读,一个是读取指定字节个数的数据,读完之后,缓冲区需要调整
如果数据全部被用户读出,就重新调整
但是怎样才能直到内核
但是如果内核
不同于读取数据的是,发送数据使用的是
发送时会先判断写缓冲区是否已经有数据存在,如果有,就不能直接向tcp缓冲区写了,因为数据要有顺序的发送,所以需要追加到写缓冲区中
如果写缓冲区中没有数据,就可以尝试向
如果
这里的细节问题就是如果想要关闭连接,那么通常是先关闭读端,等到将写缓冲区所有数据都写到
muduo没有提供
至此发送数据的操作完成,所以数据都在tcp缓冲区中等待着或正在运往对端(客户端)
tcp的通信过程中,内核其实为
tcp维护着一个缓冲区
当调用
write/send时,会向内核缓冲区中写入数据,内核和
tcp协议栈负责将缓冲区中的数据发送到指定
<ip,port>的目标位置。
当有数据到达内核的
tcp缓冲区中,如果开启了对套接字可读事件的监听,那么内核会让套接字变为可读状态,从而从
poll函数中返回,调用
read/recv进行读操作。
但是,内核维护的
tcp缓冲区通常都比较小
如果调用
write/send时,内核缓冲区已满,那么阻塞
io将会阻塞在
io函数上直到内核缓冲区有足够的空间容纳要写入的数据,非阻塞io将会返回错误,通常是
EAGAIN/EWOULDBLOCK。
如果调用
write/send时,内核缓冲区未满,但是不能容纳要写入的字节数,可用空间不足,那么只会写入能写入的那么多字节数,此时,仍然有一些数据没有发送,可是这些数据还非发送不可,就出现缓冲区已满的情况
这就导致要不阻塞当前线程,要不无法正常写入数据,而如果采用判断返回值是否出错的方法,仍然是一直忙循环检测
io写入状态,仍然是
busy loop,仍然会阻塞当前线程
而且,
io多路复用分水平触发和边缘触发两种,当内核
tcp缓冲区中一直有数据时
如果是水平触发,那么套接字会一直处于可读状态,
io多路复用函数会一直认为这个套接字被激活,也就是说如果第一次触发后没有将
tcp缓冲区中的数据全部读出,那么下次进行到
poll函数时会立即返回,因为套接字一直是可读的。这会导致了
busy loop问题
如果是边缘触发,那么就只会触发一次,即使第一次触发没有将所有数据都读走,下次进行到
poll也不会再触发套接字的可读状态,直到下次又有一批数据送至
tcp缓冲区中,才会再次触发可读。所以有可能存在漏读数据的问题,万一不会再有数据到来呢,此时
tcp缓冲区中仍然有数据,而应用程序却不知道
所以,设计应用层自己的缓冲区是很有必要的,也就是由应用程序来管理缓冲区问题
应用层缓冲区通常很大,也可以初始很小,但可以通过动态调整改变大小(
vector)
应用层缓冲区需要有读/写两个(缓冲区类只有一个,既可被用作读缓冲区,也可被用作写缓冲区)
当用户想要调用
write/send写入数据给对端,如果数据可以全部写入,那么写入就好了。如果写入了部分数据或者根本一点数据都写不进去,此时表明内核缓冲区已满,为了不阻塞当前线程,应用层写缓冲区会接管这些数据,等到内核缓冲区可以写入的时候自动帮用户写入。
当有数据到达内核缓冲区,应用层的读缓冲区会自动将这些数据读到自己那里,当用户调用
read/recv想要读取数据时,应用层读缓冲区将已经从内核缓冲区取出的数据返回给用户,实际上就是用户从应用层读缓冲区读取数据
应用层缓冲区对用户而言是隐藏的,用户可能根本不知道有应用层缓冲区的存在,只需读/取数据,而且也不会阻塞当前线程
缓冲区Buffer的设计
muduo应用层缓冲区的设计采用
std::vector数据结构,一方面内存是连续的方便管理,另一方面,
vector自带的增长模式足以应对动态调整大小的任务
缓冲区
Buffer的定义如下,只列出了一些重要部分
注释中写明了缓冲区的设计方法,主要就是利用两个指针
readerIndex,
writerIndex分别记录着缓冲区中数据的起点和终点,写入数据的时候追加到
writeIndex后面,读出数据时从
readerIndex开始读。在
readerIndex前面预留了几个字节大小的空间,方便日后为数据追加头部信息。缓冲区在使用的过程中会动态调整
readerIndex和
writerIndex的位置,初始缓冲区为空,
readerIndex == writerIndex
缓冲区默认大小为
1KB,头部预留空间为
8 bytes,如果使用过程中发现缓冲区大小不够,会增加缓冲区大小,方法见
readFd函数
/// A buffer class modeled after org.jboss.netty.buffer.ChannelBuffer /// /// @code /// +-------------------+------------------+------------------+ /// | prependable bytes | readable bytes | writable bytes | /// | | (CONTENT) | | /// +-------------------+------------------+------------------+ /// | | | | /// 0 <= readerIndex <= writerIndex <= size /// @endcode /* * * 缓冲区的设计方法,muduo采用vector连续内存作为缓冲区,libevent则是分块内存 * 1.相比之下,采用vector连续内存更容易管理,同时利用std::vector自带的内存 * 增长方式,可以减少扩充的次数(capacity和size一般不同) * 2.记录缓冲区数据起始位置和结束位置,写入时写到已有数据的后面,读出时从 * 数据起始位置读出 * 3.起始/结束位置如上图的readerIndex/writeIndex,其中readerIndex为缓冲区 * 数据的起始索引下标,writeIndex为结束位置下标。采用下标而不是迭代器的 * 原因是删除(erase)数据时迭代器可能失效 * 4.开头部分(readerIndex以前)是预留空间,通常只有几个字节的大小,可以用来 * 写入数据的长度,解决粘包问题 * 5.读出和写入数据时会动态调整readerIndex/writeIndex,如果没有数据,二者 * 相等 */ class Buffer : public muduo::copyable { public: static const size_t kCheapPrepend = 8; static const size_t kInitialSize = 1024; explicit Buffer(size_t initialSize = kInitialSize) : buffer_(kCheapPrepend + initialSize), readerIndex_(kCheapPrepend), writerIndex_(kCheapPrepend) { assert(readableBytes() == 0); assert(writableBytes() == initialSize); assert(prependableBytes() == kCheapPrepend); } /* 可读的数据就是起始位置和结束位置中间的部分 */ size_t readableBytes() const { return writerIndex_ - readerIndex_; } size_t writableBytes() const { return buffer_.size() - writerIndex_; } size_t prependableBytes() const { return readerIndex_; } /* 返回数据起始位置 */ const char* peek() const { return begin() + readerIndex_; } /// Read data directly into buffer. /// /// It may implement with readv(2) /// @return result of read(2), @c errno is saved /* 从套接字(内核tcp缓冲区)中读取数据放到读缓冲区中 */ ssize_t readFd(int fd, int* savedErrno); private: char* begin() { return &*buffer_.begin(); } const char* begin() const { return &*buffer_.begin(); } private: /* 缓冲区 */ std::vector<char> buffer_; /* 数据起始点 */ size_t readerIndex_; /* 数据结束点 */ size_t writerIndex_; /* \r\n */ static const char kCRLF[]; };
TcpConnection的读操作
当Poller检测到套接字的
Channel处于可读状态时,会调用
Channel的回调函数,回调函数中根据不同激活原因调用不同的函数,这些函数都由
TcpConnection在创建
Channel之初提供,当可读时,调用
TcpConnection的可读函数
handleRead,而在这个函数中,读缓冲区就会从内核的
tcp缓冲区读取数据
注意这个是
TcpConnection的函数
/* * 1.TcpConnection构造时,创建一个监听服务器/客户端连接的fd的Channel,设置各种回调函数 * 2.TcpServer设置各种回调函数(可读等),然后调用connectEstablished,将Channel添加到Poller中 * 3.EventLoop继续监听事件,调用Poller * 4.poll返回,处理激活的Channel,调用Channel的handleEvent * 5.hanleEvent根据激活事件的类型(可读/可写/挂起/错误)调用不同的处理函数 * 6.若可读,调用hanleRead,TcpConnection中的读缓冲区将内核tcp缓冲区的数据全部读出 * 7.调用用户提供的当可读时执行的回调函数,用户可直接从应用层缓冲区读数据 */ void TcpConnection::handleRead(Timestamp receiveTime) { loop_->assertInLoopThread(); int savedErrno = 0; /* 读缓冲区从内核tcp中读取数据 */ ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno); if (n > 0) { /* 如果成功读取数据,调用用户提供的可读时回调函数 */ messageCallback_(shared_from_this(), &inputBuffer_, receiveTime); } else if (n == 0) { /* 如果返回0,说明对端已经close连接,处理close事件,关闭tcp连接 */ handleClose(); } else { /* 出错 */ errno = savedErrno; LOG_SYSERR << "TcpConnection::handleRead"; handleError(); } }
在
TcpConnection的
handleRead函数中,读缓冲区读取数据,调用readFd函数,
readFd函数是将数据从内核tcp缓冲区中读出,存放到自己的读缓冲区中,也是缓冲区最重要的函数,其中用到了
readv(分散读)/writev(集中写)系统调用解决缓冲区大小不足的问题
/* * 从tcp缓冲区(sockfd)中读取数据,存放到应用层缓冲区中 * 两种情况 * 1.应用层缓冲区足以容纳所有数据 * 直接读取到buffer_中 * 2.应用层缓冲区不够 * 开辟一段栈空间(128k)大小,使用分散读(readv)系统调用读取数据 * 然后为buffer_开辟更大的空间,存放读到栈区的那部分数据 * * 为什么不在Buffer构造时就开辟足够大的缓冲区 * 1.每个tcp连接都有输入/输出缓冲区,如果连接过多则内存消耗会很大 * 2.防止客户端与服务器端数据交互比较少,造成缓冲区的浪费 * 3.当缓冲区大小不足时,利用vector内存增长的优势,扩充缓冲区 * * 为什么不在读数据之前判断一下应用层缓冲区是否可以容纳内核缓冲区的全部数据 * 1.采用这种方式就会调用一次recv,传入MSG_PEEK,即recv(sockfd,, extrabuf, sizeof(extrabuf), MSG_PEEK) * 可根据返回值判断缓冲区还有多少数据没有接收,然后再调用一次recv从内核冲读取数据 * 2.但是这样会执行两次系统调用,得不偿失,尽量使用一次系统调用就将所有数据读出,这就需要一个很大的空间 * * struct iovec * 1.iov_base,存放数据的缓冲区起始位置,写时往这个位置写入iov_len个字节,读时从这个位置读出iov_len个字节 * 2.iov_len,要读入多少数据从内核缓冲区/要写入多少数据到内核缓冲区 * * readv(int fd, const struct iovec *iov, int iovcnt);分散读 * writev(int fd, const struct iovec *iov, int iovcnt);集中写 */ ssize_t Buffer::readFd(int fd, int* savedErrno) { // saved an ioctl()/FIONREAD call to tell how much to read /* 开辟的栈空间,128k */ char extrabuf[65536]; /* readv用到的数据结构,定义如上 */ struct iovec vec[2]; /* 缓冲区接口,返回缓冲区还可以写入多少字节 */ const size_t writable = writableBytes(); /* 定义两块内存,一块是读缓冲区,一块是栈空间 */ vec[0].iov_base = begin()+writerIndex_; vec[0].iov_len = writable; vec[1].iov_base = extrabuf; vec[1].iov_len = sizeof extrabuf; // when there is enough space in this buffer, don't read into extrabuf. // when extrabuf is used, we read 128k-1 bytes at most. /* 如果应用层读缓冲区足够大(大于128k,初始时才1k -.-),就不需要往栈区写数据了 */ const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1; /* 分散读,返回读取的字节数 */ const ssize_t n = sockets::readv(fd, vec, iovcnt); if (n < 0) { *savedErrno = errno; } /* * 读取的字节数比较少,读缓冲区足以容纳 * 因为读缓冲区是readv的第一块内存,所以率先向这块内存写数据 */ else if (implicit_cast<size_t>(n) <= writable) { writerIndex_ += n; } else { /* * 将栈空间的数据追加到缓冲区末尾 * 因为读缓冲区已经写满了,所以writerIndex指针就指向缓冲区的末尾 */ writerIndex_ = buffer_.size(); append(extrabuf, n - writable); } // if (n == writable + sizeof extrabuf) // { // goto line_30; // } return n; }
如果读缓冲区大小不够,其他数据就会写入到栈空间,接下来需要将栈空间的数据追加到缓冲区的末尾,使用
append函数
void append(const char* /*restrict*/ data, size_t len) { /* 确保有足够的空间容纳len大小的数据 */ ensureWritableBytes(len); /* 将数据copy到writerIndex后面,beginWrite返回的就是writerIndex位置的地址(writerIndex是下标) */ std::copy(data, data+len, beginWrite()); /* 写完数据,更新writerIndex */ hasWritten(len); }
函数首先调用
ensureWritableBytes函数确保读缓冲区有足够的空间,如果没有,就需要调用
resize函数重新设置空间大小(
std::vector的内存增长就体现在这里,因为
capacity和
size通常不同,所以如果
resize设置的大小没有超过
capacity,那么空间仍然足够,不会重新开辟内存,将数据拷贝到新内存上)
void ensureWritableBytes(size_t len) { /* 返回剩余可用空间大小,如果不足len,开辟新空间(调用resize) */ if (writableBytes() < len) { makeSpace(len); } assert(writableBytes() >= len); }
如果空间不够,就需要调整空间大小
void makeSpace(size_t len) { /* * 在多次从缓冲区读数据后,readerIndex会后移很多,导致预留空间变大 * 在增大空间之前,先判断调整预留空间的大小后能否容纳要求的数据 * 如果可以,则将预留空间缩小为8字节(默认的预留空间大小) * 如果不可以,那么就只能增加空间 */ if (writableBytes() + prependableBytes() < len + kCheapPrepend) { // FIXME: move readable data /* writerIndex代表当前缓冲区已使用的大小,调整只需调整到恰好满足len大小即可 */ buffer_.resize(writerIndex_+len); } else { /* 通过缩小预留空间大小可以容纳len个数据,就缩小预留空间 */ // move readable data to the front, make space inside buffer assert(kCheapPrepend < readerIndex_); /* 返回缓冲区数据个数,writerIndex - readerIndex */ size_t readable = readableBytes(); /* 将所有数据前移 */ std::copy(begin()+readerIndex_, begin()+writerIndex_, begin()+kCheapPrepend); /* 更新两个指针(下标) */ readerIndex_ = kCheapPrepend; writerIndex_ = readerIndex_ + readable; assert(readable == readableBytes()); } }
此时应用层读缓冲区从内核中读取数据完成,在用户可读的回调函数中(在
readFd函数执行完调用),用户可以调用
Buffer的接口从缓冲区中读取数据,程序示例如下
这是用户提供给
TcpServer的可读时的回调函数,又由
TcpServer提供给
TcpConnection,当
TcpConnection的读缓冲区执行完
readFd返回后,会执行用户的回调函数,图片程序来自
muduo的测试用例。
可以看到
buf->readableBytes()返回缓冲区中可读字节数
conn->name()返回
TcpConnection的名字(由
TcpServer设置)
receiveTime是
poll函数返回的时间,一直作为参数传到
Channel,
TcpConnection,
onMessage
buf->retrieveAsString()读取缓冲区所有数据
/* 从缓冲区中读取所有数据 */ string retrieveAllAsString() { return retrieveAsString(readableBytes()); } /* 从缓冲区中读取len个字节的数据 */ string retrieveAsString(size_t len) { assert(len <= readableBytes()); /* peek返回数据的起点 */ /* 调用string(const char* s, size_type n);构造函数,初始化为从地址s开始的n个字节 */ string result(peek(), len); /* 调整缓冲区,即改变readerIndex的位置,后移len */ retrieve(len); return result; }
这两个函数从读缓冲区中读取数据,一个是全读,一个是读取指定字节个数的数据,读完之后,缓冲区需要调整
readerIndex位置以指向新的数据起点
/* 调整readerIndex,后移len */ void retrieve(size_t len) { assert(len <= readableBytes()); /* * 如果调整后仍然有数据,就将readerIndex增加len * 如果已经将数据全部读完(len >= readableBytes),那么就初始化readerIndex/writerIndex位置 */ if (len < readableBytes()) { readerIndex_ += len; } else { retrieveAll(); } }
如果数据全部被用户读出,就重新调整
readerIndex/writerIndex位置
/* 初始化readerIndex/writerIndex位置,通常在用户将数据全部读出之后执行 */ void retrieveAll() { readerIndex_ = kCheapPrepend; writerIndex_ = kCheapPrepend; }
TcpConnection的写操作
发送数据使用的是写缓冲区,当内核tcp缓冲区空间不足时,会把数据写到写缓冲区,由写缓冲区在合适的时机写入内核tcp缓冲区,合适的时机指内核
tcp缓冲区有多余空间时。
但是怎样才能直到内核
tcp缓冲区有多余的空间呢,通过监听可写事件即可。
但是如果内核
tcp缓冲区一直不满,那么就一直可写,就会一直触发
poll,导致
busy loop,所以
muduo只有在需要的时候才会检测内核
tcp缓冲区的可写事件,即只有当
tcp缓冲区已满,但是写缓冲区中有数据等待写入
tcp缓冲区时才会监听。
不同于读取数据的是,发送数据使用的是
TcpConnection提供的接口,而不是直接向
Buffer中写。
/* 几个重载的send函数,用于用户想要发送数据到对端 */ void TcpConnection::send(const void* data, int len) { send(StringPiece(static_cast<const char*>(data), len)); } void TcpConnection::send(const StringPiece& message) { if (state_ == kConnected) { /* * 如果当前线程和TcpConnection所属线程相同,直接在当前线程发送 * 否则,需要使用std::bind绑定函数和对象,并添加到自己所在线程的事件循环中 */ if (loop_->isInLoopThread()) { sendInLoop(message); } else { /* 可以直接在bind中绑定函数 ? */ void (TcpConnection::*fp)(const StringPiece& message) = &TcpConnection::sendInLoop; loop_->runInLoop( std::bind(fp, this, // FIXME message.as_string())); //std::forward<string>(message))); } } }
send函数调用
sendInLoop函数,保证在
TcpConnection所属线程发送数据
发送时会先判断写缓冲区是否已经有数据存在,如果有,就不能直接向tcp缓冲区写了,因为数据要有顺序的发送,所以需要追加到写缓冲区中
如果写缓冲区中没有数据,就可以尝试向
tcp缓冲区写数据,如果全部写入,当然很happy,但是如果只写入一部分或者一点也没写进去(
tcp缓冲区已满),就需要添加到写缓冲区中,同时开启对
tcp缓冲区(其实就是用于通信的套接字)的可写事件的监听,等待
tcp缓冲区可写
void TcpConnection::sendInLoop(const StringPiece& message) { sendInLoop(message.data(), message.size()); } /* * 写入数据 * 1.如果Channel没有监听可写事件且输出缓冲区为空,说明之前没有出现内核缓冲区满的情况,直接写进内核 * 2.如果写入内核出错,且出错信息(errno)是EWOULDBLOCK,说明内核缓冲区满,将剩余部分添加到应用层输出缓冲区 * 3.如果之前输出缓冲区为空,那么就没有监听内核缓冲区(fd)可写事件,开始监听 */ void TcpConnection::sendInLoop(const void* data, size_t len) { loop_->assertInLoopThread(); /* 写入tcp缓冲区的字节数 */ ssize_t nwrote = 0; /* 没有写入tcp缓冲区的字节数 */ size_t remaining = len; /* 调用write时是否出错 */ bool faultError = false; /* 当前TcpConnection状态,TcpConnection有四种状态,kDisconnected表示已经断开连接,不能再写了,直接返回 */ if (state_ == kDisconnected) { LOG_WARN << "disconnected, give up writing"; return; } // if no thing in output queue, try writing directly /* 如果输出缓冲区有数据,就不能尝试发送数据了,否则数据会乱,应该直接写到缓冲区中 */ if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0) { /* 读取函数 */ nwrote = sockets::write(channel_->fd(), data, len); if (nwrote >= 0) { /* 写入了一些数据 */ remaining = len - nwrote; /* * 完全写入tcp缓冲区,且用户有提供写数据的回调函数,等待执行完后调用 * 因为当前TcpConnection和EventLoop所在同一个线程, * 而且此时EventLoop通常处在正在处理激活Channel的过程中(当前函数有可能也是在这个过程) * 所以等待这个函数执行完再调用回调函数 */ if (remaining == 0 && writeCompleteCallback_) { loop_->queueInLoop(std::bind(writeCompleteCallback_, shared_from_this())); } } else // nwrote < 0 { /* 一点也没写进去 * 如果错误为EWOULDBLOCK,表明tcp缓冲区已满 */ nwrote = 0; if (errno != EWOULDBLOCK) { /* EPIPE表示客户端已经关闭了连接,服务器仍然尝试写入,就会出现EPIPE */ LOG_SYSERR << "TcpConnection::sendInLoop"; if (errno == EPIPE || errno == ECONNRESET) // FIXME: any others? { faultError = true; } } } } assert(remaining <= len); /* 没出错,且仍有一些数据没有写到tcp缓冲区中,那么就添加到写缓冲区中 */ if (!faultError && remaining > 0) { /* 获取写缓冲区数据总量 */ size_t oldLen = outputBuffer_.readableBytes(); /* 到达高水位,调用回调函数,这个函数没有设置? */ if (oldLen + remaining >= highWaterMark_ && oldLen < highWaterMark_ && highWaterMarkCallback_) { loop_->queueInLoop(std::bind(highWaterMarkCallback_, shared_from_this(), oldLen + remaining)); } /* 把没有写完的数据追加到输出缓冲区中,然后开启对可写事件的监听(如果之前没开的话) */ outputBuffer_.append(static_cast<const char*>(data)+nwrote, remaining); if (!channel_->isWriting()) { channel_->enableWriting(); } } }
如果
tcp缓冲区不足以全部容纳数据,就会开启对可写事件的监听,当
tcp缓冲区可写,就调用
Channel的回调函数,这个回调函数也是在
TcpConnection构造函数中传给
Channel的
channel_->setWriteCallback( std::bind(&TcpConnection::handleWrite, this));
/* 当tcp缓冲区可写时调用 */ void TcpConnection::handleWrite() { loop_->assertInLoopThread(); if (channel_->isWriting()) { /* 尝试写入写缓冲区的所有数据,返回实际写入的字节数(tcp缓冲区很有可能仍然不能容纳所有数据) */ ssize_t n = sockets::write(channel_->fd(), outputBuffer_.peek(), outputBuffer_.readableBytes()); if (n > 0) { /* 调整写缓冲区的readerIndex */ outputBuffer_.retrieve(n); if (outputBuffer_.readableBytes() == 0) { /* 全部写到tcp缓冲区中,关闭对可写事件的监听 */ channel_->disableWriting(); /* 如果有写入完成时的回调函数(用户提供,则等待函数结束后调用 */ if (writeCompleteCallback_) { loop_->queueInLoop(std::bind(writeCompleteCallback_, shared_from_this())); } /* * 如果连接正在关闭(通常关闭读端),那么关闭写端,但是是在已经写完的前提下 * 如果还有数据没有写完,不能关闭,要在写完再关 */ if (state_ == kDisconnecting) { shutdownInLoop(); } } } else { LOG_SYSERR << "TcpConnection::handleWrite"; // if (state_ == kDisconnecting) // { // shutdownInLoop(); // } } } else { LOG_TRACE << "Connection fd = " << channel_->fd() << " is down, no more writing"; } }
这里的细节问题就是如果想要关闭连接,那么通常是先关闭读端,等到将写缓冲区所有数据都写到
tcp缓冲区后,再关闭写端,否则这些数据就不能发送给对端了
muduo没有提供
close函数,关闭是分两步进行的(使用
shutdown而不适用
close),这样更容易控制
handleWrite函数中调用的
shutdownInLoop函数如下,用于关闭写端
void TcpConnection::shutdownInLoop() { loop_->assertInLoopThread(); if (!channel_->isWriting()) { // we are not writing socket_->shutdownWrite(); } }
至此发送数据的操作完成,所以数据都在tcp缓冲区中等待着或正在运往对端(客户端)
相关文章推荐
- JAVA基础学习之流的简述及演示案例、用缓冲区方法buffer读写文件、File类对象的使用、Serializable标记接口(6)
- ArcGIS API for JavaScript 4.2学习笔记[20] 使用缓冲区结合Query对象进行地震点查询【重温异步操作思想】
- php学习基础-文件系统(二) 文件读写操作、文件资源处理
- 缓冲区方式读写操作
- SE高阶(2):NIO流—理解Buffer、Channel概念和NIO的读写操作方式
- opencv 学习笔记2—XML读写操作
- 【muduo网络库学习】之基本的TCP Server工作机制
- 手机内存、sdcard读写操作学习笔记
- 阻塞/非阻塞读写总结、tcp网络编程的本质、muduo::Buffer设计简介
- TCP/IP学习之 TCP缓冲区大小及限制
- 由于系统缓冲区空间不足或队列已满,不能执行套接字上的操作(MaxUserPort,TcpTimedWaitDelay)
- 一个非常非常非常基础的程序,写的不好,但是一般的文件读写操作及字符处理函数都涉及到了..新手学习用的
- linux直接写framebuffer linux 直接 对 Frame Buffer 操作,写画面缓存例子,c语言读写framebuffer
- Ceph学习——客户端读写操作分析
- STM8S学习05——EEPROM读写操作C语言程序
- python学习文件简单读写操作
- muduo网络库学习(五)服务器监听类Acceptor及Tcp连接TcpConnection的建立与关闭
- Qt学习之XML读写操作小结
- 一步一步跟我学习hadoop(7)----hadoop连接mysql数据库执行数据读写数据库操作
- 【19】IO流2_读写缓冲区,装饰设计模式,图片拷贝,IO流操作规律及其他