设计缓冲
设计缓冲的目的:解决不能一次read和write全部数据、未及时将套接字接受缓冲区读出造成的反复触发读事件就绪(busy-loop)。
Linux多线程服务端编程 7.4.2 为什么non-blocking 网络编程中应用层buffer是必需的
简单想法:
-
从缓冲读出n个字节:从optr开始读n个字节,并将optr往后移n个字节
-
写入缓冲n个字节:从iptr开始写n个字节,并将iptr往后移n个字节
实现为:
- 为了使缓冲空间足够灵活,可以使用向量
struct Buffer
{
Buffer(size_t kBufferInitSize = 1024) : buffer_(kBufferInitSize), optr_(0), iptr_(0) {}
std::vector<char> buffer_;
size_t optr_;
size_t iptr_;
};
-
缓冲的要点在于: 每次可写空间放不下时,进行缓冲空间的扩充:
- 当整个(已读 + 可读 + 可写) < (可读 + 要写入的数据),将可读部分移到最前面即可
- 反之,循环将缓冲空间翻倍,直至能够放下要写的数据。同时在进行空间申请->数据复制过程中顺带将可读部分移到最前面
-
对套接字read,无法预知大小问题:在栈上准备一个额外缓冲区,使用readv分别读到我们的缓冲和额外缓冲区。这样有两种情况:
- 我们的缓冲+额外缓冲区没有被写满,将额外缓冲区写入我们的缓冲即可。
- 我们的缓冲+额外缓冲区被写满,可能套接字接受缓冲区还有数据,就得等下个循环再去读取。我们当然是希望一次就能读完,所有就应该设一个适合大小的额外缓冲区,这里我们设65536个字节。
-
例子
-
从缓冲区读出
// 读出 std::vector<char> ReadBuffer(Buffer *buf, int len = -1) { if (len == -1) // read all { len = buf->iptr_ - buf->optr_; } std::vector<char> result(buf->buffer_.begin() + buf->optr_, buf->buffer_.begin() + buf->optr_ + len); buf->optr_ = buf->iptr_; return result; }
读出较为简单,只需要拷贝数据,后移optr便可
-
写入缓冲区
若单单是写入数据也较简单,也是拷贝数据,后移iptr。
void AppendBuffer(Buffer *buf, std::vector<char>::iterator first, std::vector<char>::iterator last) { std::copy(first, last, buf->buffer_.begin() + buf->iptr_); buf->iptr_ += last - first; }
但是写入缓冲区,首先要判断缓冲区是否有足够空间可供写入。
void TryAppendBuffer(Buffer *buf, std::vector<char> &data, int len) { if (len <= 0) return; int ndestdata = buf->iptr_ - buf->optr_; // 1.若buf->buffer_.size() >= buf->iptr_ + len // 即可写部分足够写下要写入数据,不做移动和扩充 if (buf->buffer_.size() < buf->iptr_ + len) { // 2. 若new_size >= ndestdata + len // 即把可读部分移到最前便有足够空间写入要写入数据 // 只需要完成移动,new_size等于原buffer的size int new_size = buf->buffer_.size(); // 3. 存不下要写入数据,必须扩充空间 while (new_size < ndestdata + len) { new_size <<= 1; } std::vector<char> new_buf(new_size); std::copy(buf->buffer_.begin() + buf->optr_, buf->buffer_.begin() + buf->iptr_, new_buf.begin()); buf->buffer_ = new_buf; buf->iptr_ = buf->iptr_ - buf->optr_; buf->optr_ = 0; } // 写入Buffer AppendBuffer(buf, data.begin(), data.begin() + len); }
-
小总结
定义了一个Buffer结构体,内有vector类型的数据data,用以存储缓冲区数据。还有读“指针”optr和写“指针”iptr。
对Buffer操作的三个函数:
- ReadBuffer(),以string形式从缓冲区读出数据
- AppendBuffer(),写入缓冲区
- TryAppendBuffer(),在必要时扩充缓冲区,而后调用AppendBuffer()写入缓冲区
使用缓冲
在使用epoll的echo服务器上进行修改:
-
对于可读事件就绪的处理:有个明显差别在于,我们读进数据后,不再直接读出,而是将读出来的数据放入缓冲,然后开始关注套接字的可写事件,当可写事件就绪后我们再调用write去写。
// read available if (events[i].events & EPOLLIN) { // read available if (events[i].events & EPOLLIN) { // 使用介绍过的extrabuf size_t nwriteable = ibuf.buffer_.size() - ibuf.iptr_; std::vector<char> extrabuf(65536); struct iovec iov[2]; iov[0].iov_base = &*(ibuf.buffer_.begin() + ibuf.iptr_); iov[0].iov_len = nwriteable; iov[1].iov_base = &*extrabuf.begin(); iov[1].iov_len = 65536; int n = readv(events[i].data.fd, iov, 2); if (n < 0) // error { std::cout << "read error: " << errno << std::endl; } else if (n == 0) // close { close(events[i].data.fd); epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, &events[i]); } else // n > 0,有从套接字读出写入ibuf缓冲 { // 使用了extrabuf,将extrabuf写入缓冲 if (n > nwriteable) { TryAppendBuffer(&ibuf, extrabuf, n - nwriteable); } ibuf.iptr_ += n; // 从ibuf写到obuf // 是可以仅使用一个buffer,为了和以后的代码有个对应,这里不妨使用ibuf和 // obuf,并根据echo服务器的需要,手动将ibuf的可读部分搬到obuf // 模拟从ibuf中读出 std::vector<char> data = ReadBuffer(&ibuf); // 写入obuf Buffer // 调用TryAppendBuffer,当obuf可写空间不足时会进行扩充 TryAppendBuffer(&obuf, data, data.size()); // !!! // 开始关注可写事件 epoll_event ev; ev.events = events[i].events | EPOLLOUT; ev.data.fd = events[i].data.fd; epoll_ctl(epollfd, EPOLL_CTL_MOD, events[i].data.fd, &ev); } }
- 处理可写事件:当缓冲区里没有数据了,也就是写完了,我们就可以取消关注可写事件
// write available if (events[i].events & EPOLLOUT) { std::vector<char> data = ReadBuffer(&obuf); int n = write(events->data.fd, &*data.begin(), data.size()); // 从Buffer读出的并不一定能够写完 obuf.optr_ -= (data.size() - n); // 写完了,不再关注可写事件 if (obuf.optr_ == obuf.iptr_) { epoll_event ev; ev.events = events[i].events & ~EPOLLOUT; ev.data.fd = events[i].data.fd; epoll_ctl(epollfd, EPOLL_CTL_MOD, events[i].data.fd, &ev); } }
遗憾
可以看到随着缓冲的引入,代码也进一步的复杂,是时候使用OOP(object-oriented programming)了,下节介绍Reactor模式。