[[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

トップ   編集 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS