C++ iostream 実装例 その 3 †前節までは、効率を無視して、1 octet 入出力だけで TCP/IP の streambuf化を 行ってみた。 さすがに 1 octet 入出力のたびに send()/recv() call を 実行するのは、あまりにも非効率だろう。 そこで、ここでは、効率化を考えてみよう。 最初に述べたように、iostream や streambuf は実装が極めて容易にできるように 設計されている。それも buffering することを基本にしてある。 前節に示した実装例は、むしろ邪道に近い。 ここでは buffering を使った具体例を示す。 ここで例と、前の例を比べてみれば、書き方も code size も ほとんど同じということがわかるだろう。つまり、1文字入出力と 同程度の簡単さで実装できる。 修正 2010-01-13 †
内部 buffer と基底 class †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 関数 †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 列 †さて、上で読み出し buffer の先頭位置は開始位置というのは 正しくないと述べた。ここでは、その理由を説明する。 gptr() が buffer の先頭位置より大きい時、buffer の先頭 位置から gptr() の一つ前までは、今まで読み込まれ、ユーザ側に 渡された文字である。この部分の範囲はまだ有効文字として残っているので gptr() を設定することが可能である。つまり、元に戻すことが 可能である。これを backup 列と呼ぶ。buffer の先頭位置は、 この backup 可能な最後の位置なので eback() というわけである。 この eback() に至るまでは、streambuf 側で sputbackc() や sungetc() が 処理される。eback() を越えてこれらが呼ばれた時には、pbackfail() が 呼ばれる。従って、もし double-buffering などを行っていて、更に元に戻す ことが可能ならば、pbackfail() が呼ばれた時に setg() を使って古い buffer を 使った設定に戻すことで、対応可能となる。 また、underflow() の時に、前の data の最後の何文字分かを残す ようにすることも可能であろう。いずれにせよ、十分な自由度がある。 header (宣言) †// 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 に負わせて)おけと いうのが筆者の考え方である。 本体(定義) †// 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 |