| 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 | #include
#include
class SocketInStreamBuf : public std::streambuf { public: SocketInStreamBuf(SOCKET socket) : m_socket(socket) { } int_type underflow() { char c; if (recv(m_socket, &c, 1, MSG_PEEK) <= 0) { return EOF; } return c; } int_type uflow() { char c; if (recv(m_socket, &c, 1, 0) <= 0) { return EOF; } return c; } private: SOCKET m_socket; }; |
无缓冲的实现需要同时重写underflow和uflow,根据这两个方法的定义,前者不移动读取位置,后者反之,而recv函数的MSG_PEEK选项刚好可以对应这两种行为。
有缓冲方式
从套接字逐个读取字符也是非常低效的过程,添加缓冲功能是再自然不过的事情,如下所示:
| 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 | #include
#include
class SocketInStreamBuf : public std::streambuf { public: SocketInStreamBuf(SOCKET socket) : m_socket(socket) { setg(m_buffer, m_buffer, m_buffer); } int_type underflow() { int recvLen = recv(m_socket, m_buffer, BufferSize, 0); if (recvLen <= 0) { return EOF; } setg(m_buffer, m_buffer, m_buffer + recvLen); return *gptr(); } private: SOCKET m_socket; static const int BufferSize = 512; char m_buffer[BufferSize]; }; |
跟输出的实现一样,我们也需要自己定义一个缓冲区,然后用setg方法设置缓冲区的指针。与setp不同,setg方法需要设置三个指针,分别是缓冲区头指针,当前读取位置指针以及缓冲区尾部下一个位置指针,这些指针可通过eback(),gptr(),egptr()方法获取。这比输出缓冲区复杂,因为输入缓冲区需要支持回退功能。输入缓冲区图示如下:

当读取字符时,gptr向右移动,直到gptr() == egptr()时,调用underflow从外部设备补充数据。当回退字符时,gptr向左移动,直到gptr() == gback()时,就不能再回退字符了。
在上面代码的构造方法中,用setg把三个指针都设置到缓冲区头部,这样一来,就不支持回退了,而且第一次读取会导致underflow被调用。在underflow中,将数据读取到缓冲区之后还要调用setg重新设置一下缓冲区指针,由于是gptr() == eback(),所以仍然不支持回退。
上文说过,如果提供了缓冲区,那么就不需要重写uflow了,所以提供了缓冲功能的SocketInStreamBuf看上去比无缓冲功能的还要简单。
使用自定义的输入streambuf
跟输出的一样,只要将SocketInStreamBuf与istream组合在一起,就可以利用强大的IO功能了:
| 1 2 3 4 5 6 7 8 9 | SOCKET socket; … SocketInStreamBuf inBuf(socket); std::istream socketStream(&inBuf); std::string line; while (std::getline(socketStream, line)) { std::cout << line << std::endl; } |
上面的代码从套接字读取数据,然后输出到控制台上。