1. 程式人生 > >C++ I/O 重定向方法(定向到串列埠或Socket)

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

C++ 標準輸入輸出模組,為字元流操作提供了便捷的途徑,軟體開發當中,尤其是嵌入式系統開發當中,有時候需要把流資訊重新定向到特定的埠,如串列埠,乙太網,USB等。如標準輸入輸出cout cin預設將字元流定向到螢幕或控制檯。本文介紹瞭如何過載streambuf使得使用ostream istream 將字元流定向到特定的外設。

首先來看一下標準庫中有關IO的類體系結構:

除了ios_base之外,其它類都定義為模板,這是因為C++中有兩種字元型別:charwchar_tios_base定義了同字元型別無關的屬性和操作,basic_ios則定義了同字元型別相關的屬性和操作,basic_istream

basic_ostream分別定義了同輸入和輸出相關的操作,basic_iostream同時支援輸入和輸出。

在整個類體系結構中,最重要的的是basic_streambuf,它提供了緩衝功能以及真正地操作外部裝置,其它類則只負責字串的格式化操作。這體現了職責分離的設計原則,basic_streambuf和其它類之間是鬆耦合關係,對其中一方進行修改不會影響到另一方,因此,我們只需要繼承basic_streambuf,定義出一個使用套接字進行IO操作的類即可。

basic_streambuf是一個模板,IO庫根據它分別定義了兩個類(真正的定義語句並不是這樣的,模板引數不僅僅是一個,這裡只是為了方便說明):

?

1

2

typedefbasic_streambuf<char> streambuf;

typedefbasic_streambuf<wchar_t> wstreambuf;

我們可以根據字元的實際型別選擇繼承streambufwstreambuf。當然,也可以將自己的類定義為模板,繼承basic_streambuf,不過這樣的話需要多寫一些程式碼,具體操作可以參考《C++標準程式庫》,本文的例子直接繼承streambuf

basic_streambuf既定義了輸出相關操作,也定義了輸入相關操作,這意味它同時支援輸入和輸出。我們也可以只實現輸出或者輸入,讓它只支援某種操作。首先來看下如何實現輸出。

用於輸出的streambuf

basic_streambuf中輸出相關的操作主要有sputcsputn,前者輸出一個字元,後者輸出多個字元。如果提供了緩衝區,那麼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>

classSocketOutStreamBuf : publicstd::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) {

                returnEOF;

            }

        }

        returnc;

    }

private:

    SOCKET m_socket;

};

可以看到,無緩衝方式的實現非常簡單,只要將引數直接寫入到套接字中就可以了,如果寫入成功,返回剛寫入的那個字元;如果失敗,返回EOF,也可以丟擲異常——這個由你決定。int_type是在字元特性類(traits)中定義的型別,表示能容納所有字元的型別,這個型別肯定不是charwchar_t,因為EOFWEOF超出了這些型別的範圍。

有緩衝方式

把字元一個一個地寫入套接字是非常低效的,因此我們希望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>

classSocketOutStreamBuf : publicstd::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) {

            returnEOF;

        }

        returnc;

    }

    intsync() {

        if(FlushBuffer() == EOF) {

            return-1;

        }

        return0;

    }

private:

    intFlushBuffer() {

        intlen = pptr() - pbase();

        if(send(m_socket, m_buffer, len, 0) <= 0) {

            returnEOF;

        }

        pbump(-len);

        returnlen;

    }

    SOCKET m_socket;

    staticconstintBufferSize = 512;

    charm_buffer[BufferSize];

};

首先我們需要自己定義一個緩衝區,然後在構造方法中通過setp方法把緩衝區的頭尾指標告訴basic_streambuf,這樣一來就具有了緩衝功能。有三個方法可以獲取與緩衝區相關的指標:pbasepptrepptr,它們分別獲取的是緩衝區的頭指標,當前寫入位置的指標以及緩衝區尾部下一個位置的指標,如下圖所示:

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中輸入相關的操作有sgetcsbumpcsgetnsungetcsputbackc。其中sungetcsputbackc用於回退字元,這個功能不常用到,而且也不太可能在套接字上回退字元,因此這裡省略對回退字元的介紹,關於這方面的內容可以參考《C++標準程式庫》。

sgetcsbumpc都用於讀取一個字元,區別是後者會將讀取位置向後移動一個位置,而前者不會改變讀取位置。如果沒有提供緩衝區,或者緩衝區的內容已經讀完,那麼sgetc會呼叫underflow方法,而sbumpc會呼叫uflow方法,從外部裝置讀取更多資料。uflow的預設行為是呼叫underflow,然後移動緩衝區的讀取指標,如果沒有提供緩衝區,則必須同時重寫underflowuflowsgetn用於讀取多個字元,它會呼叫xsgetn,而xsgetn的預設行為是依次呼叫sbumpc,如果為了改善讀取多個字元的效能,可以重寫xsgetn方法。

無緩衝方式

首先來看下無緩衝方式的輸入實現,如下所示:

?

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>

classSocketInStreamBuf : publicstd::streambuf {

public:

    SocketInStreamBuf(SOCKET socket) : m_socket(socket) {

    }

    int_type underflow() {

        charc;

        if(recv(m_socket, &c, 1, MSG_PEEK) <= 0) {

            returnEOF;

        }

        returnc;

    }

    int_type uflow() {

        charc;

        if(recv(m_socket, &c, 1, 0) <= 0) {

            returnEOF;

        }

        returnc;

    }

private:

    SOCKET m_socket;

};

無緩衝的實現需要同時重寫underflowuflow,根據這兩個方法的定義,前者不移動讀取位置,後者反之,而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>

classSocketInStreamBuf : publicstd::streambuf {

public:

    SocketInStreamBuf(SOCKET socket) : m_socket(socket) {

        setg(m_buffer, m_buffer, m_buffer);

    }

    int_type underflow() {

        intrecvLen = recv(m_socket, m_buffer, BufferSize, 0);

        if(recvLen <= 0) {

            returnEOF;

        }

        setg(m_buffer, m_buffer, m_buffer + recvLen);

        return*gptr();

    }

private:

    SOCKET m_socket;

    staticconstintBufferSize = 512;

    charm_buffer[BufferSize];

};

跟輸出的實現一樣,我們也需要自己定義一個緩衝區,然後用setg方法設定緩衝區的指標。與setp不同,setg方法需要設定三個指標,分別是緩衝區頭指標,當前讀取位置指標以及緩衝區尾部下一個位置指標,這些指標可通過eback()gptr()egptr()方法獲取。這比輸出緩衝區複雜,因為輸入緩衝區需要支援回退功能。輸入緩衝區圖示如下:

當讀取字元時,gptr向右移動,直到gptr() == egptr()時,呼叫underflow從外部裝置補充資料。當回退字元時,gptr向左移動,直到gptr() == gback()時,就不能再回退字元了。

在上面程式碼的構造方法中,用setg把三個指標都設定到緩衝區頭部,這樣一來,就不支援回退了,而且第一次讀取會導致underflow被呼叫。在underflow中,將資料讀取到緩衝區之後還要呼叫setg重新設定一下緩衝區指標,由於是gptr() == eback(),所以仍然不支援回退。

上文說過,如果提供了緩衝區,那麼就不需要重寫uflow了,所以提供了緩衝功能的SocketInStreamBuf看上去比無緩衝功能的還要簡單。

使用自定義的輸入streambuf

跟輸出的一樣,只要將SocketInStreamBufistream組合在一起,就可以利用強大的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;

}

上面的程式碼從套接字讀取資料,然後輸出到控制檯上。