首先来看一下标准库中有关IO的类体系结构:

除了ios_base之外,其它类都定义为模板,这是因为C++中有两种字符类型:char和wchar_t。ios_base定义了同字符类型无关的属性和操作,basic_ios则定义了同字符类型相关的属性和操作,basic_istream和basic_ostream分别定义了同输入和输出相关的操作,basic_iostream同时支持输入和输出。
在整个类体系结构中,最重要的的是basic_streambuf,它提供了缓冲功能以及真正地操作外部设备,其它类则只负责字符串的格式化操作。这体现了“职责分离”的设计原则,basic_streambuf和其它类之间是松耦合关系,对其中一方进行修改不会影响到另一方,因此,我们只需要继承basic_streambuf,定义出一个使用套接字进行IO操作的类即可。
basic_streambuf是一个模板,IO库根据它分别定义了两个类(真正的定义语句并不是这样的,模板参数不仅仅是一个,这里只是为了方便说明):
| 1 2 | typedef basic_streambuf<char> streambuf; typedef basic_streambuf<wchar_t> wstreambuf; |
我们可以根据字符的实际类型选择继承streambuf或wstreambuf。当然,也可以将自己的类定义为模板,继承basic_streambuf,不过这样的话需要多写一些代码,具体操作可以参考《C++标准程序库》,本文的例子直接继承streambuf。
basic_streambuf既定义了输出相关操作,也定义了输入相关操作,这意味它同时支持输入和输出。我们也可以只实现输出或者输入,让它只支持某种操作。首先来看下如何实现输出。
用于输出的streambuf
basic_streambuf中输出相关的操作主要有sputc和sputn,前者输出一个字符,后者输出多个字符。如果提供了缓冲区,那么sputc将字符复制到缓冲区内,如果缓冲区已经满了或者没有提供缓冲区,sputc会调用overflow,将数据写入外部设备并清空缓冲区。sputn会调用xsputn,而xsputn的默认操作是对每个字符调用sputc。由此可见,实现输出要做的事情很简单,只要重写overflow方法即可。另外也可以重写xsputn方法,以优化多个字符的输出。
无缓冲方式
下面是不使用缓冲区的实现方式:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #include
#include
class SocketOutStreamBuf : public std::streambuf { public: SocketOutStreamBuf(SOCKET socket) : m_socket(socket) { } protected: int_type overflow(int_type c) { if (c != EOF) { if (send(m_socket, (char*)&c, 1, 0) <= 0) { return EOF; } } return c; } private: SOCKET m_socket; }; |
可以看到,无缓冲方式的实现非常简单,只要将参数直接写入到套接字中就可以了,如果写入成功,返回刚写入的那个字符;如果失败,返回EOF,也可以抛出异常——这个由你决定。int_type是在字符特性类(traits)中定义的类型,表示能容纳所有字符的类型,这个类型肯定不是char或wchar_t,因为EOF和WEOF超出了这些类型的范围。
有缓冲方式
把字符一个一个地写入套接字是非常低效的,因此我们希望SocketOutStreamBuf能提供缓冲功能,有缓冲方式的实现如下所示:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | #include
#include
class SocketOutStreamBuf : public std::streambuf { public: SocketOutStreamBuf(SOCKET socket) : m_socket(socket) { setp(m_buffer, m_buffer + BufferSize - 1); } ~SocketOutStreamBuf() { sync(); } protected: int_type overflow(int_type c) { if (c != EOF) { *pptr() = c; pbump(1); } if (FlushBuffer() == EOF) { return EOF; } return c; } int sync() { if (FlushBuffer() == EOF) { return -1; } return 0; } private: int FlushBuffer() { int len = pptr() - pbase(); if (send(m_socket, m_buffer, len, 0) <= 0) { return EOF; } pbump(-len); return len; } SOCKET m_socket; static const int BufferSize = 512; char m_buffer[BufferSize]; }; |
首先我们需要自己定义一个缓冲区,然后在构造方法中通过setp方法把缓冲区的头尾指针告诉basic_streambuf,这样一来就具有了缓冲功能。有三个方法可以获取与缓冲区相关的指针:pbase,pptr和epptr,它们分别获取的是缓冲区的头指针,当前写入位置的指针以及缓冲区尾部下一个位置的指针,如下图所示:

当pptr() != epptr()时,缓冲区是未满的,此时sputc只是把字符复制到pptr所在位置,然后把pptr移动到下一个位置,不会调用overflow;当pptr() == epptr()时,缓冲区是满的,此时sputc会调用overflow,并把放不进缓冲区内的字符作为overflow的参数。在上面代码的构造方法中,之所以把缓冲区的最后一个位置作为尾指针(用m_buffer + BufferSize - 1作为第二个参数,而不是m_buffer + BufferSize),是因为这样可以在overflow中手动将参数放到最后一个位置,然后将整个缓冲区的数据一起发送出去。pbump方法用来移动当前写入位置的指针,参数的值是相对位置,在发送完数据之后需要用pbump将指针移回到缓冲区头部。
另外,提供了缓冲功能的话还需要重写sync方法,该方法用于同步缓冲区同外部设备的数据,意思就是将缓冲区的数据写入到外部设备中,不管它有没有满。如果该方法成功的话, 返回0,否则返回-1。在析构方法中也要调用sync,确保数据被写入到外部设备中。
使用自定义的输出streambuf
定义好了我们自己的SocketOutStreamBuf之后,只要将它与ostream组合在一起就能在套接字上使用IO库的强大功能,如下所示:
| 1 2 3 4 5 6 7 8 9 | SOCKET socket; … SocketOutStreamBuf ou
|