1、存在的原因
TCP是一个无边界的字节流协议,接收方必须要处理“收到的数据尚不构成一条完整的消息”和“一次收到两条消息的数据”等等情况。这些情况被称为粘包问题。考虑一下场景:发送方发送两条5k字节的数据,接收方收到的情况可能是如下情况:
- 分两次收,一次5k,第二次5k
- 分两次收,一次2k,第二次8k
- 一次收10k
- 其他的可能
因此,必须在应用层对粘包问题进行处理,需要在应用层定义一个缓冲区,用来存放传输层接收到的数。然后根据应用层协议判定是否是一个完整的包,如果不是一条完整的消息,不会取走数据,也不会进行相应的处理。如果是一条完整的消息,将取走这条消息,并进行相应的处理。如何处理就是上层应用程序的职责了。
2、缓冲区结构
两个 indices 把 vector 的内容分为三块:prependable、readable、writable,各块的大小是(公式一):
prependable = readIndex
readable = writeIndex - readIndex
writable = size() - writeIndex
在Buffer.h程序中定义了缓冲区的成员变量以及一些操作接口
class Buffer
{
public:
static const size_t kCheapPrepend = 8;//预留空间
static const size_t kInitialSize = 1024;//默认缓冲区大小
private:
std::vector<char> buffer_;//数据存放容器
size_t readerIndex_;//读位置
size_t writerIndex_;//写位置
static const char kCRLF[];//柔性数组(下面会解释)
};
柔性数组:柔性数组既数组大小待定的数组, C语言中结构体的最后一个元素可以是大小未知的数组,也就是所谓的0长度,所以我们可以用结构体来创建柔性数组。
用途:为了满足需要变长度的结构体,为了解决使用数组时内存的冗余和数组的越界问题。
用法:在一个结构体的最后 ,申明一个长度为空的数组,就可以使得这个结构体是可变长的。
3、缓冲区的操作
获取缓冲区属性:
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_;
}
retrieve和hasWritten
void retrieve(size_t len)
{
assert(len <= readableBytes());
if (len < readableBytes())
{
readerIndex_ += len;
}
else
{
retrieveAll();
}
}
void retrieveAll()
{
readerIndex_ = kCheapPrepend;
writerIndex_ = kCheapPrepend;
}
void hasWritten(size_t len)
{
assert(len <= writableBytes());
writerIndex_ += len;
}
void unwrite(size_t len)
{
assert(len <= readableBytes());
writerIndex_ -= len;
}
最重要的就是readFd()函数:
//结合栈上空间,避免内存使用过大,提高内存使用率
//如果有10K个连接,每个连接就分配64K缓冲区的话,将占用640M内存
//而大多数时候,这些缓冲区的使用率很低
ssize_t Buffer::readFd(int fd, int* savedErrno)
{
// saved an ioctl()/FIONREAD call to tell how much to read
//节省一次ioctl系统调用(获取当前有多少可读数据)
//为什么这么说?因为我们准备了足够大的extrabuf,那么我们就不需要使用ioctl取查看fd有多少可读字节数了
char extrabuf[65536];
//使用iovec分配两个连续的缓冲区
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.
const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1; //writeable一般小于65536
const ssize_t n = sockets::readv(fd, vec, iovcnt); //iovcnt=2
if (n < 0)
{
*savedErrno = errno;
}
else if (implicit_cast<size_t>(n) <= writable) //第一块缓冲区足够容纳
{
writerIndex_ += n; //直接加n
}
else //当前缓冲区,不够容纳,因而数据被接受到了第二块缓冲区extrabuf,将其append至buffer
{
writerIndex_ = buffer_.size(); //先更显当前writerIndex
append(extrabuf, n - writable); //然后追加剩余的再进入buffer当中
}
return n;
}
readv和writev
read()和write()系统调用每次在文件和进程的地址空间之间传送一块连续的数据。但是,应用有时也需要将分散在内存多处地方的数据连续写到文件中,或者反之。
readv()和writev(),它们只需一次系统调用就可以实现在文件和进程的多个缓冲区之间传送数据,免除了多次系统调用或复制数据的开销。readv()称为散布读,即将文件中若干连续的数据块读入内存分散的缓冲区中。writev()称为聚集写,即收集内存中分散的若干缓冲区中的数据写至文件的连续区域中。
#include <sys/uio.h>
ssize_t readv(int fildes, const struct iovec *iov, int iovcnt);
ssize_t writev(int fildes, const struct iovec *iov, int iovcnt);
struct iovec {
void *iov_base /* 数据区的起始地址 */
size_t iov_len /* 数据区的大小 */
}
writev()依次将iov[0]、iov[1]、...、 iov[iovcnt–1]指定的存储区中的数据写至fildes指定的文件。readv()则将fildes指定文件中的数据按iov[0]、iov[1]、...、iov[iovcnt–1]规定的顺序和长度,分散地读到它们指定的存储地址中。
扩充空间函数makeSpace():
void makeSpace(size_t len) //vector增加空间
{
if (writableBytes() + prependableBytes() < len + kCheapPrepend) //确保空间是真的不够,而不是挪动就可以腾出空间
{
// FIXME: move readable data
buffer_.resize(writerIndex_+len);
}
else
{
//内部腾挪就足够append,那么就内部腾挪一下。
// move readable data to the front, make space inside buffer
assert(kCheapPrepend < readerIndex_);
size_t readable = readableBytes();
std::copy(begin()+readerIndex_, //原来的可读部分全部copy到Prepend位置,相当于向前挪动,为writeable留出空间
begin()+writerIndex_,
begin()+kCheapPrepend);
readerIndex_ = kCheapPrepend; //更新下标
writerIndex_ = readerIndex_ + readable;
assert(readable == readableBytes());
}
}