streambuf を拡張し, ソケット対応 [C++]

[[2018.6]]

C++ の IOストリームで, 新しい対象に対して入出力できるようにしたい場合, istream, ostream, iostream の各クラスではなく, basic_streambuf クラスの方を拡張します。

iostream クラスは書式付き入出力を担当し, 媒体への書き込み・読み込みは streambuf クラスで行なうようになっています。

とはいうものの実際には, 例えばファイルに対しては fstream クラスと filebuf のように, ペアで両方とも必要になってしまっていますが。

ソースコードはこちら; network · master · netsphere / my-cpp-lib · GitLab

仕様

全体的に, なんでこんな仕様になってるの? というところが多い。

書式化は iostream のほうでするのに, ロケールを streambuf のほうに持たせるのは何でだ? streambuf がえらく複雑になっている。

バッファリング

読み込みバッファと書き込みバッファを別に設定できる。読み込みバッファは protected setg() で, 書き込みバッファは protected setp() で設定する。これらを呼び出さなければ, バッファリングしない.

バッファリングしない場合, public sungetc(), public sputbackc() が失敗する.

sgetn() の挙動から, 読み込みバッファは必須.

読み込み

public sgetn() が固定長の読み込み. protected xsgetn() に投げてる.

完全に埋めるまで, ブロックする. ::recv() と挙動が違う。recv() のつもりで呼び出すと, スタックしてしまう。

行末まで読みたい、など、どれだけ読めばいいか事前に分からないときは, sgetn() は使えない. そうすると, 1文字ずつ読むしかなく、バッファリングが必須.

実際の読み込みは, uflow() で行っている。uflow() は単に underflow() に投げてる.

basic_streambuf<>::underflow()
{ return traits_type::eof(); }

このメソッドだけを override すれば, とりあえず動くようになる。

エラーハンドリング

underflow() は, 高々1バイトを読み込む。エラーが起こったときの通知方法がない。握りつぶすよりかは, とりあえず例外でも投げるか。

書き込み側, xsputn() も, エラーが起こったときの通知ができない。こちらも, とりあえず例外にするか。

ヘッダファイル (実装)

socket_streambuf.h:

C
[POPUP]
  1. // TCP socket
  2. class SocketStreamBuf: public std::streambuf
  3. {
  4. // sgetn() は読めた分だけ返すのではなく, 指定のバイト数に達するまで,
  5. // ブロックする。
  6. // そのため, どれだけ読むべきか事前に不明な場合は sgetn() は使えず,
  7. // 1バイトずつ読まなければならない。
  8. // 実装としては, バッファが (ほぼ) 必須.
  9. char* m_buf;
  10. #define max_headroom ((ssize_t) 16)
  11. static constexpr long bufsiz = 4096 + max_headroom;
  12. protected:
  13. SOCKET m_sock;
  14. public:
  15. SocketStreamBuf(): m_buf(nullptr), m_sock(INVALID_SOCKET) { }
  16. virtual ~SocketStreamBuf() {
  17. close();
  18. if (m_buf) {
  19. free(m_buf);
  20. m_buf = nullptr;
  21. }
  22. }
  23. void attach( SOCKET sock ) {
  24. assert(sock != INVALID_SOCKET );
  25. if (m_sock != INVALID_SOCKET )
  26. close();
  27. m_sock = sock;
  28. if (!m_buf)
  29. m_buf = (char*) malloc(bufsiz);
  30. setg(m_buf, m_buf, m_buf);
  31. }
  32. // std::streambuf<> には close() がない. => オブジェクトを破壊する必要.
  33. virtual int close() {
  34. if (m_sock == INVALID_SOCKET )
  35. return 0;
  36. // なくてもいいはずだが、OSなどの不具合などによりデータ消失があるため (?),
  37. // 入れた方がいいという意見もある.
  38. // See TCPのはなし
  39. // http://www.silex.jp/blog/wireless/2015/11/tcp.html
  40. // See close vs shutdown socket?
  41. // https://code.i-harness.com/en/q/3f7b5b
  42. ::shutdown(m_sock, SHUT_RDWR);
  43. int r;
  44. #ifdef _WIN32
  45. r = ::closesocket(m_sock);
  46. #else
  47. r = ::close(m_sock);
  48. #endif
  49. m_sock = INVALID_SOCKET;
  50. return r;
  51. }
  52. bool is_open() const throw() {
  53. return m_sock != INVALID_SOCKET;
  54. }
  55. protected:
  56. // バッファを無視して, 読み込む.
  57. // length に満たない場合がある.
  58. // @return エラーの場合, -1
  59. virtual ssize_t sysread(void* buffer, size_t length) {
  60. ssize_t ret;
  61. do {
  62. ret = ::recv(m_sock, (char*) buffer, length, 0);
  63. } while (ret == -1 && errno == EINTR);
  64. return ret;
  65. }
  66. // 読み込み用で最低限 override しなければならないのは, underflow() だけ.
  67. // @return 読み込んだ 1文字. EOF だった場合, traits_type::eof().
  68. virtual int_type underflow() {
  69. if ( !is_open() )
  70. return traits_type::eof();
  71. // protected eback() 先頭, gptr() 現在の位置, egptr() 終端. 名前が??
  72. if (!gptr() || gptr() >= egptr()) {
  73. // バッファを読み切った.
  74. // sungetc() できるようにする.
  75. long rest = std::min(egptr() - eback(), max_headroom);
  76. if (rest > 0)
  77. memmove(m_buf, gptr() - rest, rest);
  78. ssize_t r = this->sysread(m_buf + rest, bufsiz - rest);
  79. if (r == 0) // EOF
  80. return traits_type::eof();
  81. else if (r < 0)
  82. throw errno;
  83. setg(m_buf, m_buf + rest, m_buf + rest + r);
  84. }
  85. return *gptr(); // ポインタ進めない.
  86. }
  87. // 同様に, 書き込み側については, 最低限 overflow() を override すればよい.
  88. // 1バイトを書き込む.
  89. // @param __c 書き込む値.
  90. // @return 成功したら EOF 以外の何か. 失敗したら EOF.
  91. // ここの解説では, __c が EOF の場合, 失敗 (EOF) を返す.
  92. // http://www.cplusplus.com/reference/streambuf/basic_streambuf/overflow/
  93. // basic_filebuf<>::overflow() の実装では, __c が EOF の場合も
  94. // 「成功」を返している.
  95. virtual int_type overflow(int_type __c = traits_type::eof() )
  96. {
  97. if ( traits_type::eq_int_type(__c, traits_type::eof()) )
  98. return __c; // error とする.
  99. char cc = __c;
  100. if ( xsputn(&cc, 1) <= 0 )
  101. return traits_type::eof();
  102. return 1; // 成功
  103. }
  104. // 効率悪いので, xsputn() も override.
  105. // @return 書き込めたバイト数.
  106. // basic_streambuf<>::xsputn() の実装を見ると, overflow() が EOF
  107. // を返すと, そこまでのバイト数を返す.
  108. // エラーで1バイトも書き込めなかったときも, 0 を返す. マジか!!
  109. virtual std::streamsize xsputn( const char_type* s,
  110. std::streamsize count )
  111. {
  112. if ( !is_open() )
  113. return 0;
  114. ssize_t r = ::send(m_sock, s, count, 0 );
  115. if (r >= 0)
  116. return r;
  117. else
  118. throw errno;
  119. }
  120. };