[[C++のiostream]] * C++ iostream 実装例 その 3 [#z4058b31] 前節までは、効率を無視して、1 octet 入出力だけで TCP/IP の streambuf化を 行ってみた。 さすがに 1 octet 入出力のたびに send()/recv() call を 実行するのは、あまりにも非効率だろう。 そこで、ここでは、効率化を考えてみよう。 最初に述べたように、iostream や streambuf は実装が極めて容易にできるように 設計されている。それも buffering することを基本にしてある。 前節に示した実装例は、むしろ邪道に近い。 ここでは buffering を使った具体例を示す。 ここで例と、前の例を比べてみれば、書き方も code size も ほとんど同じということがわかるだろう。つまり、1文字入出力と 同程度の簡単さで実装できる。 ** 修正 2010-01-13 [#wfb75b9c] - sync() の後、setp() を呼び出す必要がある。これを加えた。 ** 内部 buffer と基底 class [#la48b4da] send()/recv() call を減らすという意味では、内部 buffer を持てばよい。 実際、"[[C++ iostream 実装例 その 1]]" の実装でも underflow() の実装の ため、1 octet の buffer を持っていると見なせる(m_gpend_char)。 これを増やせばよい。 内部 buffer を持つことで、ある程度の効率化はもちろんできるが、 それだけでは underflow()/uflow()/overflow() の呼び出し数を減らすことはできない。ここまでの実装では 基底 class である streambuf 側で、どこに buffer があり、 どれだけ buffer に data が準備できているかなどが、わからないからである。 基底 class に buffer の開始位置や data の準備のできている位置を示す 手段が用意されている。この手段を使って、基底 class が必要と する pointer を知らせることにより、入力 buffer が 空になる、または出力 buffer が一杯になるまでは、underflow()/uflow()/overflow() はよばれずに、streambuf 基底 class の方で処理が行われる。 (私の[[C++のiostream]]ページに示してある実装資料参照)。 また、派生 class の方では buffer を詰めたり、出力したりするのに、 基底 class がどこまで buffer を使っているのか知る必要も出てくる。 そのための関数も用意されている。 ** 使われる buffer pointer と member 関数[#ne98476c] Buffer pointer は、読み出し(get)側、書き込み(put)側、それぞれに、 開始位置、現在位置、終端位置の3つが使われる。 派生 class からstreambuf class に対し読み出し buffer の各位置を与える 関数が void streambuf::setg(char* gbeg, char* gnext, char* gend); である。引数は左からそれぞれ開始位置、次に読み込まれる位置、終端位置 である(実は開始位置というのは正しくない。後で述べる)。 終端位置とは有効 data のある次の位置である。つまり、gbeg から n 文字有効 data が詰められているとすると、 gend = gbeg + n; である。 書き込み buffer に対しては void streambuf::setp(char* pbeg, char* pend); が用意されている。引数は左からそれぞれ開始位置、終端位置である。開始位置が 次に書き込まれる位置になる。終端位置は、この pointer の一つ前 までが書き込み可能であることを示す。 一方、streambuf が現在、各 pointer に対し、どのような値を持っているのかを 示す関数も用意されている。 char* streambuf::eback() const; char* streambuf::gptr() const; char* streambuf::egptr() const; は、それぞれ、読み出し buffer の開始位置、現在(次に読み込まれる)位置、 終端位置である。実は最初の関数を読み出し buffer の開始位置というのは 正しくなくて(実際名前が bgptr()とか gbase() とかにはなっていない)、 backup 列の終端位置である(だから eback())。 同様の機能を果たす書き込み buffer に対する関数は char* streambuf::pbase() const; char* streambuf::pptr() const; char* streambuf::epptr() const; である。現在位置(次に読み込まれる位置や次に書き込める位置)を 操作する関数も用意されている。 void streambuf::gbump(int n); void streambuf::pbump(int n); である。ここで n は負であってもよい。 ** backup 列 [#tb9d8266] さて、上で読み出し buffer の先頭位置は開始位置というのは 正しくないと述べた。ここでは、その理由を説明する。 gptr() が buffer の先頭位置より大きい時、buffer の先頭 位置から gptr() の一つ前までは、今まで読み込まれ、ユーザ側に 渡された文字である。この部分の範囲はまだ有効文字として残っているので gptr() を設定することが可能である。つまり、元に戻すことが 可能である。これを backup 列と呼ぶ。buffer の先頭位置は、 この backup 可能な最後の位置なので eback() というわけである。 この eback() に至るまでは、streambuf 側で sputbackc() や sungetc() が 処理される。eback() を越えてこれらが呼ばれた時には、pbackfail() が 呼ばれる。従って、もし double-buffering などを行っていて、更に元に戻す ことが可能ならば、pbackfail() が呼ばれた時に setg() を使って古い buffer を 使った設定に戻すことで、対応可能となる。 また、underflow() の時に、前の data の最後の何文字分かを残す ようにすることも可能であろう。いずれにせよ、十分な自由度がある。 ** header (宣言) [#sd090138] // tcpbuf.h #ifndef TCPBUFFER_H_INCLUDED #define TCPBUFFER_H_INCLUDED #include <streambuf> namespace mynetlibrary { class tcpbuffer : public std::streambuf { public: tcpbuffer(int sock); virtual ~tcpbuffer(void); int state(void) const; protected: virtual int underflow(void); virtual int overflow(int c = std::char_traits<char>::eof()); virtual int sync(void); protected: static const int MY_GBUFSIZE = 1024; static const int MY_PBUFSIZE = 1024; protected: int m_socket; unsigned char m_gbuf[(MY_GBUFSIZE + 1)]; unsigned char m_pbuf[(MY_PBUFSIZE + 1)]; }; } // namespace mynetlibrary #endif // TCPBUFFER_H_INCLUDED ここで、m_gbuf や m_pbuf は C の string でもないのに、わざわざ 一つ余分に確保しているか疑問に思うかも知れない(実際、使わない)。 これは、buffer の終端を示す pointer が、正しい pointer と なることを保証するためにこうしている。 unsigned char m_gbuf[MY_GBUFSIZE]; としてしまった時、setg() などで行っている (char*)(m_gbuf + MY_GBUFSIZE) が、正しい pointer になる保証はない。例えば MY_GBUFSIZE で確保したら (char*)(m_gbuf + MY_GBUFSIZE -1) が pointer として表せる限界一杯だったということだってあり得る。 この時、 (char*)(m_gbuf + MY_GBUFSIZE) は、pointer の格納 size を overflow して、とんでもない値が 入るだろう。その後、基底 class がこれを使って計算して access した 時に、何が起こるか予測できない(まあ、access violation で落ちる というのが一番ありそうではあるが)。しかも、これは compile でも link でも error にならず、実行してもたまたま偶然、そういう memory 配置になった時に だけ起こる現象なので、発見は非常に困難である。もちろん、そんな配置に なることは、滅多に無いというか、まず無いだろう。しかし、 原因究明を困難にする bug を引き起こす、そんな risk を少しでも負うぐらい なら1文字分余計に確保して(計算機の資源 cost に負わせて)おけと いうのが筆者の考え方である。 ** 本体(定義) [#r783147a] // tcpbuf.cpp #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include "tcpbuf.h" namespace mynetlibrary { tcpbuffer::tcpbuffer(int sock) { m_socket = ::dup(sock); setg((char*)m_gbuf, (char*)m_gbuf, (char*)m_gbuf); setp((char*)m_pbuf, (char*)(m_pbuf + MY_PBUFSIZE)); } tcpbuffer::~tcpbuffer(void) { if(m_socket != -1) ::close(m_socket); } int tcpbuffer::state(void) const { if(m_socket == -1) return -1; return 0; } int tcpbuffer::underflow(void) { if(m_socket == -1) return std::char_traits<char>::eof(); int c; if((gptr() >= (char*)m_gbuf) && (gptr() < egptr())) { // Why call me ? // Still we have enough data in the buffer. c = (int)*gptr(); c &= 255; return c; } int n; if((n = ::recv(m_socket, m_gbuf, MY_GBUFSIZE, 0)) <= 0) return std::char_traits<char>::eof(); setg((char*)m_gbuf, (char*)m_gbuf, (char*)(m_gbuf + n)); c = (int)m_gbuf[0]; c &= 255; return c; } int tcpbuffer::overflow(int c) { if(m_socket == -1) return std::char_traits<char>::eof(); // Try to make rooms to put a new character int npend = (int)(pptr() - (char*)m_pbuf); if(npend < 0) return std::char_traits<char>::eof(); if(npend > 0) { int nsend = ::send(m_socket, m_pbuf, npend, 0); if(nsend <= 0) return std::char_traits<char>::eof(); npend -= nsend; if(npend > 0) { // Oooops!! Still we have pending data. // Move the pending data to the beginning of // the buffer to send them in next time. ::memmove(m_pbuf, (m_pbuf + nsend), npend); } } setp((char*)(m_pbuf + npend), (char*)(m_pbuf + MY_PBUFSIZE)); if(c == std::char_traits<char>::eof()) return 0; *pptr() = (char)(c & 255); pbump(1); return c; } int tcpbuffer::sync(void) { if(m_socket == -1) return (-1); int nleft = (int)(pptr() - (char*)m_pbuf); int nsend; unsigned char* p = m_pbuf; while( nleft > 0 ) { nsend = ::send(m_socket, p, nleft, 0); if( nsend <= 0 ) return (-1); nleft -= nsend; p += nsend; } setp((char*)m_pbuf, (char*)(m_pbuf + MY_PBUFSIZE)); return 0; } } // namespace mynetlibrary