C++ I/O 重定向方法(定向到串口或Socket)

本文参考:http://blog.csdn.net/turkeyzhou/article/details/8983379

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

C++ I/O 重定向方法(定向到串口或Socket)

 

除了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 <streambuf>
#include <WinSock2.h>
 
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 <streambuf>
#include <WinSock2.h>
 
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,它们分别获取的是缓冲区的头指针,当前写入位置的指针以及缓冲区尾部下一个位置的指针,如下图所示:

C++ I/O 重定向方法(定向到串口或Socket)

 

当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 outBuf(socket);
std::ostream outStream(&outBuf);
 
std::string line;
while (std::getline(std::cin, line)) {
    outStream << line << std::endl;
}

上面的代码用于将控制台上的输入写入到套接字中。

 

用于输入的streambuf

basic_streambuf中输入相关的操作有sgetc,sbumpc,sgetn,sungetc和sputbackc。其中sungetc和sputbackc用于回退字符,这个功能不常用到,而且也不太可能在套接字上回退字符,因此这里省略对回退字符的介绍,关于这方面的内容可以参考《C++标准程序库》。

 

sgetc和sbumpc都用于读取一个字符,区别是后者会将读取位置向后移动一个位置,而前者不会改变读取位置。如果没有提供缓冲区,或者缓冲区的内容已经读完,那么sgetc会调用underflow方法,而sbumpc会调用uflow方法,从外部设备读取更多数据。uflow的默认行为是调用underflow,然后移动缓冲区的读取指针,如果没有提供缓冲区,则必须同时重写underflow和uflow。sgetn用于读取多个字符,它会调用xsgetn,而xsgetn的默认行为是依次调用sbumpc,如果为了改善读取多个字符的性能,可以重写xsgetn方法。

 

basic_streambuf的源码中:

  1.      int_type   
  2.      sputc(char_type __c)  
  3.      {  
  4. int_type __ret;  
  5. if (__builtin_expect(this->pptr() < this->epptr(), true))  
  6.   {  
  7.     *this->pptr() = __c;  
  8.     this->pbump(1);  
  9.     __ret = traits_type::to_int_type(__c);  
  10.   }  
  11. else  
  12.   __ret = this->overflow(traits_type::to_int_type(__c));  
  13. return __ret;  
  14.      }  
  15.   
  16.      /** 
  17.       *  @brief  Multiple character insertion. 
  18.       *  @param  s  A buffer area. 
  19.       *  @param  n  Maximum number of characters to write. 
  20.       *  @return  The number of characters written. 
  21.       * 
  22.       *  Writes @a s[0] through @a s[n-1] to the output sequence, as if 
  23.       *  by @c sputc().  Stops when either @a n characters have been 
  24.       *  copied, or when @c sputc() would return @c traits::eof(). 
  25.       * 
  26.       *  It is expected that derived classes provide a more efficient 
  27.       *  implementation by overriding this definition. 
  28.      */  
  29.      virtual streamsize   
  30.      xsputn(const char_type* __s, streamsize __n);  
  31.   
  32.   
  33.      /** 
  34.       *  @brief  Consumes data from the buffer; writes to the 
  35.       *          controlled sequence. 
  36.       *  @param  c  An additional character to consume. 
  37.       *  @return  eof() to indicate failure, something else (usually 
  38.       *           @a c, or not_eof()) 
  39.       * 
  40.       *  Informally, this function is called when the output buffer 
  41.       *  is full (or does not exist, as buffering need not actually 
  42.       *  be done).  If a buffer exists, it is @a consumed, with 
  43.       *  <em>some effect</em> on the controlled sequence. 
  44.       *  (Typically, the buffer is written out to the sequence 
  45.       *  verbatim.)  In either case, the character @a c is also 
  46.       *  written out, if @a c is not @c eof(). 
  47.       * 
  48.       *  For a formal definition of this function, see a good text 
  49.       *  such as Langer & Kreft, or [27.5.2.4.5]/3-7. 
  50.       * 
  51.       *  A functioning output streambuf can be created by overriding only 
  52.       *  this function (no buffer area will be used). 
  53.       * 
  54.       *  @note  Base class version does nothing, returns eof(). 
  55.      */  
  56.      virtual int_type   
  57.      overflow(int_type /* __c */ = traits_type::eof())  
  58.      { return traits_type::eof(); }  
  59.   
  60.   
  61.   
  62.   
  63.      /** 
  64.       *  @brief  Synchronizes the buffer arrays with the controlled sequences. 
  65.       *  @return  -1 on failure. 
  66.       * 
  67.       *  Each derived class provides its own appropriate behavior, 
  68.       *  including the definition of @a failure. 
  69.       *  @note  Base class version does nothing, returns zero. 
  70.      */  
  71.      virtual int   
  72.      sync() { return 0; }  
  73.   
  74.   
  75.      /** 
  76.       *  @brief  Entry point for all single-character output functions. 
  77.       *  @param  s  A buffer read area. 
  78.       *  @param  n  A count. 
  79.       * 
  80.       *  One of two public output functions. 
  81.       * 
  82.       * 
  83.       *  Returns xsputn(s,n).  The effect is to write @a s[0] through 
  84.       *  @a s[n-1] to the output sequence, if possible. 
  85.      */  
  86.      streamsize   
  87.      sputn(const char_type* __s, streamsize __n)  
  88.      { return this->xsputn(__s, __n); }  
  89.   
  90.   
  91.   
  92.      /** 
  93.       *  @brief  Synchronizes the buffer arrays with the controlled sequences. 
  94.       *  @return  -1 on failure. 
  95.       * 
  96.       *  Each derived class provides its own appropriate behavior, 
  97.       *  including the definition of @a failure. 
  98.       *  @note  Base class version does nothing, returns zero. 
  99.      */  
  100.      virtual int   
  101.      sync() { return 0; }  


无缓冲方式

首先来看下无缓冲方式的输入实现,如下所示:

?
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 <streambuf>
#include <WinSock2.h>
 
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 <streambuf>
#include <WinSock2.h>
 
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()方法获取。这比输出缓冲区复杂,因为输入缓冲区需要支持回退功能。输入缓冲区图示如下:

C++ I/O 重定向方法(定向到串口或Socket)

 

当读取字符时,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;
}

上面的代码从套接字读取数据,然后输出到控制台上。

C++ I/O 重定向方法(定向到串口或Socket)

上一篇:微软、百度、联想等名企面试笔试题60题(C++)


下一篇:C++ 语言的 15 个晦涩特性