このエントリーをはてなブックマークに追加
スポンサーリンク

ソケット入門(2)

1 2 3

より実用的なプログラムを目指して

前ページではソケットについての簡単な説明をしました。しかし、実用性のあるプログラムにするためには、もう少し手を加えなければなりません。そこで、このページでは、実用性のあるソケットプログラミングにするためには、どうすれば良いか、もう少し追求してみましょう。

先の例ではソケットを読むために、read()という関数を使いました。しかし、この関数がくせものでして、第3引数である受信バイト数は、あくまで受信したい最大サイズを指定するものです。そのため、実際には指定バイトよりも少なく受信する事があります。

それでも、ただ端末からのメッセージを出すだけのサンプルプログラムであれば問題なく動いたようにも見えます。しかし、実用的なアプリを作る際に正しくバイト数分読み込めないのでは困ります。

そこでここでは、ソケットを期待したバイト数分まで読み込むための新しい関数、sockread() を作ってみましょう。

構造体の定義

ソケットをリードするための関数 sockreadを作るにあたり、関数へ渡す情報をセットするための構造体を定義します。

構造体というのは、COBOLでいうところの集団変数のことで(といってもCOBOLを知らない方の方が多いと思いますが)、C言語では一定の規則でまとまった変数群を「構造体」という新しい型として定義することができます。
struct SOCKR {
        int jusinsocket;    /* 受信するソケット */
        int jusinsize;      /* 受信したいサイズ */
        int jusinsumi;      /* 実際に受信したサイズ */
        char *jusinbuff;    /* 受信バッファ */
        int jusineofflg;    /* 正常終了フラグ */
        int jusintoutari;   /* タイムアウトを監視するか否か */
        int jusintoutflg;   /* タイムアウトフラグ */
        int jusintoutsec;   /* タイムアウト秒 */
        int jusintoutusec;  /* タイムアウト(ミリ秒)*/
        int jusinerror;     /* 受信エラーコード */
};
ここで、SOCKR型という変数の形を定義しました。しかし、COBOLと違うところは(しつこい?)、これは型を定義しただけで実体は確保されません。つまり、定義しただけでは、メモリーを確保しないわけです。

各変数の役割は、それぞれコメントを参考にしてもらう事にして、このように構造体を定義しておくと、呼び出し元と呼び出し先で多くの情報をやりとりする事ができて便利です。こうしないと、関数への引数がすごい量になってしまうでしょう。

ソケットを指定バイトまで読む関数sockread()の作成

いよいよsockreadを作ってみましょう。

ここでは、この章の最後に示すサンプルプログラムの解説を中心に行いますので、まずサンプルプログラムをダウンした後、ソースを開きながら読むと良いでしょう。
/*
=================================================
         ソケットリード
         IN:  SOCKR型へのポインター
        OUT: 0 ソケットクローズ
          -1 エラー発生
          それ以外:受信したバイト数
=================================================
*/
関数を作る場合、自分が後でわかるようにするのはもちろんのこと、他の人がその関数を使う場合のことも考えて、必ず頭に入力情報とリターンコードの意味を書いておきます。
int sockread(struct SOCKR *sr) { 
頭のint は、リターンコードの型です。PHPやPerlでは変数に型というのはなく、リターンコードに型を宣言する必要はなかったのですが、C言語では厳密に型というのを宣言しなければなりません。

引数の、struct SOCKR *sr は、SOCKR型という自分の定義した型の構造体を引数として使う事を示しています。この構造体を、srというポインターを使って情報をやり取りします。
/* ローカル変数宣言 */
int nokori;
struct timeval toutval;
fd_set mask;
fd_set readok;
int width;
int retval;
int s;
ローカル変数とは、つまり、この関数内だけで使う変数です。つまり、この関数が呼ばれた間だけメモリに確保され、関数からリターンすればメモリは開放されます。ここで注目すべき点は、timeval型とfd_set型の変数です。これは先に説明した通り、この変数は構造体です。構造体は実体を宣言することで、はじめてメモリーに確保されます。

fd_set型は1024個のソケットを管理するための構造体で、1024ビット長あります。じゃあ、自分で1024ビット長の変数を定義すればいいか、というと、そうではありません。UNIXではさまざまな言語、機種で動作するOSですので、ソースレベルで互換性を保つため、ライブラリで使う変数はライブラリで定めた構造体を使う事になっています。

timeval型は時間を管理するための構造体です。秒とミリ秒がセットされています。これも何ビット長かは機種によって違いますので、timeval型という風に宣言して使います。

UNIXでは1970年1月1日からの経過秒で時間を管理していますが、32ビット長では2038年までしか計算できません。なので、ちょっと前まで2038年までしか動作しないUNIXが多かったのですが、最近のUNIXではほとんどが秒を64ビット長で計算するようになり、数千億年先まで計算できるようになりました。

このように、UNIXでは機種によって変数の長さがまちまちであるため、ソースレベルでの互換性を保つため、ライブラリで引数として使う関数は、ライブラリ内で定義された構造体を使うことになっています。
/* 変数初期化 */
nokori = sr->jusinsize;
sr->jusinsumi = 0;
sr->jusineofflg = 0;
sr->jusintoutflg=0;
s = sr->jusinsocket;
bzero(sr->jusinbuff,sr->jusinsize);

/* タイムアウト時間を設定 */
toutval.tv_sec  = sr->jusintoutsec;
toutval.tv_usec = sr->jusintoutusec;

/* 監視するポートを設定 */
FD_ZERO(&mask);
FD_SET(s, &mask);
width = s+1;
ここでは、構造体内の変数を初期化しています。ここで矢印演算子(−>)が出てきました。矢印演算子は、ポインターで示された構造体の中を読み書きする時に使います。

ポインター変数 *aのaが示すメモリーを書き換えたい場合、普通*a=1;と書きますが、構造体の場合*sr=1;としてしまうと、この構造体の先頭だけが変わってしまいます。ですので、構造体の先頭からjusinsumi目のメモリーを読み書きする場合、sr->jusinsumiという風に指示します。
if (sr->jusintoutari) {
	retval = select(width, &readok, NULL, NULL, &toutval);
} else {
	retval = select(width, &readok, NULL, NULL, NULL);
}
第1章ではソケットのリードにタイムアウトを設けず無限にデーターを待ちつづけました。しかし、実際にそんなプログラムを作ってしまうと、ソケットをコネクトした後に相手がデーターを送ってこないままフリーズしてしまった場合、こちらも子プロセスが永久に待機してしまいます。そうすると、子プロセスは永遠にメモリーに常駐しつづけ、それが増えていくことで、じきにサーバーはダウンしてしまうでしょう。

なので、今回はタイムアウトありのモードも設けることにします。sr->jusintimeoutariが1にセットされている場合、タイムアウトを有効にします。タイムアウトを有効にするには、第5引数にタイムアウトまでの秒数をセットすることになっています。

&toutvalとは、つまり、toutvalという構造体へのポインターをセットする、という意味です。toutvalはポインター変数ではなく実体そのものですが、実体そのもののポインターは&をつけて示すことができます。
/* エラー発生 */
if (retval == -1) {
	fprintf(stderr,"Select error !!\n");
	sr->jusinerror=1;
	return -1;
}

/* タイムアウト */
if (retval == 0) {
	sr->jusintoutflg=1;
	break;
}
select()関数は、エラーなら-1、タイムアウトなら0、正常終了ならソケット番号を返すことになっています。ですので、-1ならエラー、0ならタイムアウト処理をします。
/* --------- パケットが来た ------------ */
if(FD_ISSET(s,(fd_set *)&readok)) {

/* パケットを読む */
retval=read(s, (sr->jusinbuff)+(sr->jusinsumi), nokori);

/*ソケット切断*/
if (retval==0) {
	sr->jusineofflg=1;
	break;
}

/* エラー発生 */
if (retval == -1) {
	fprintf(stderr,"Jusin error !!\n");
	sr->jusinerror=2;
	return -1;
}
FD_ISSETは、指定したソケットの状態を検査するものです。fd_set型の変数の指定ビットが1かどうかを検査し、1ならば真を返します。つまり、自分が読みたいソケットが読み込み可能ならば1が返るわけです。

retval=read(s, (sr->jusinbuff)+(sr->jusinsumi), nokori);は、指定したソケットから、指定バイト数文読み込むわけですが、第1章との違いは、受信バイトが固定値ではなく、nokori(残り)という変数になっている点です。

また、受信バッファも固定の場所(s->jusinbuff)ではなく、受信バッファからsr->jusinsumi分だけ足した場所になっています。

これを図にすると、
こんな感じになります。受信バッファの先頭アドレスから、既に受信した分(青い部分)だけずらした場所から新たに受信します。受信すべき量は、受信したい量から既に受信した分を引いた残りになります。

これを、受信したいバイト数分だけ繰り返します。途中ソケットの切断やエラーがあった場合はループから抜けます。こうする事によって、エラーや途中切断がない限り、受信したいバイト数分だけ正確にソケットを読み込むことができます。
retval = sr->jusinsumi;
return retval;
受信が正常に終了したら、実際に受信したバイト数をリターンコードとして返し、この関数を終了します。

ソケットから行単位で読み込むsocklineread()の作成

これで、受信を期待するバイト数分だけソケットを読む関数はできました。これで、1パケットの長さが固定長になっている通信は可能になったと思います。また、パケットの長さが可変長であっても、その前にデーター長が送られてくるような通信なら問題はないと思います。

しかし、ソケット通信はしばしばデーター長が不定の通信をすることがあります。たとえば、SMTPのようなメールの受信に使われるプロトコルです。この場合、改行コード(\n)が区切りとして使われ、データーのやりとりは主にテキストで行われます。

例)SMTPプロトコルは、行単位で応答を行う
HELO xxxxx.com
250 OK
MAIL FROM: xxxx@xxxx.xxxx.com
250 OK
RCPT TO: xxxx@xxxx.xxxx.com
250 OK
DATA
250 OK
    :
    :
    :
    :
(注:↑かなりてきとーですので、ツッコミはナシね)

Perlのソケット入力ではこれに対応し、行単位で受信されるため、使い勝手が非常に良いといえます。そこで、ここでPerlライクな、行単位でソケットを読む関数を作ってみましょう。

これから作る関数は、図にすると、こうなります。
って、この図だけではわからないと思うので説明しますと、ソケットからある程度「先読みバッファ」にデーターを読み込んでおきます。そして、そこから1バイトずつデーターを取り出していき、「行バッファ」に入れます。

ここで、改行コードがあったら、いったん関数を抜け、呼び出し元に戻ります。呼び出し元では、行バッファを評価します。これはサンプルなので、単に画面に表示させるだけです。

先読みバッファが空になったら、また、ソケットから再度読み込んでバッファに補充します。ソケットを読もうとして0バイトが返ってきたら、ソケット切断と判断し、0を返します。

では、この処理に必要なワークメモリーを定義しましょう。
struct SOCKLINER {
        int linersocket;   /* 受信するソケット        */
        int linersize1;    /* ラインバッファのサイズ */
        int linersize2;    /* 受信バッファのサイズ    */
        int linersumi;     /* 受信バッファに入ってる量 */
        int linerpointer;  /* 受信バッファの既読地点 */
        char *linerbuff1;  /* 行バッファのアドレス */
        char *linerbuff2;  /* 受信バッファのアドレス */
        int linereofflg;   /* 正常終了フラグ */
        int linerempflg;   /* 受信バッファ空フラグ */
        char linermoji;    /* 1バイト単位で読んだ文字 */
        int linertoutari;  /* タイムアウトを監視するか否か */
        int linertoutflg;  /* タイムアウトフラグ */
        int linertoutsec;  /* タイムアウト秒 */
        int linertoutusec; /* タイムアウト(ミリ秒)*/
        int linererror;    /* 受信エラーコード */
};
先ほどのSOCKRと似てますが、今度はSOCKLINERと定義しましょう。これを、呼び出し元でメモリーを確保し、初期化しておきます。

これから作る関数の、引数とリターンコードを決めましょう。
/*
=================================================

       先読みソケットリード

   ソケットからある程度先読みしておいて、 
   1バイトずつ返す

   IN SOCKLINER型構造体へのポインター
   OUT  -1 エラー発生
         0  読み込みEOF
         1  正常終了
=================================================
*
C言語で関数を作る上で大切なことは、関数の仕様を明確にすることです。関数とは「AとBを渡せばCが返る」ものです。AとBを足し算した値を返す関数があった場合、1と2を渡せば3が返ることになります。

このように、関数を作る時は、渡した値がどう計算されて(どう処理されて)どういう結果が返ってくるのかを明確にしておくことで、あとあと別のプログラムからでも流用が利くようになります。実は、これまで当たり前のように使っていたsocket()やbind()、listen()、accept()のような関数群も、「ソケットライブラリ」という、他の誰かの作った関数を使っているわけです。

C言語は、結局はこのような「関数」の積み重ねと思ってください。

この関数では、IN(入力情報)として、先ほど定義しましたSOCKLINER型へのポインターを渡します。これは、引数として与える値が多すぎるため、構造体を別に確保しておいて、構造体へのポインターで渡しているからです。図にすると

のように、受け渡す「情報群」を構造体として定義しておき、そこのポインター(アセンブラでいうところの、ワークメモリの先頭アドレス)を関数に渡します。関数内では、この領域を書き換えます。呼び出し元と関数内とで、この領域を共有して、情報の伝達に使用します。

出力情報として、エラー情報を返します。「1バイトリードなので、読み込んだバイトを返した方がいいのでは?」という人もいるかもしれませんが、これは、絶対にアスキーコードしか返らない関数ならば、読み込んだ文字がリターンコードで、NULLが終了とすればいいのですが、今回はバイナリデーターも扱えるように、このような使用にしました。

ただし、エラーが発生したかどうか、ファイルがEOFになったかどうか、タイムアウトが発生したかどうかは、構造体内の各種フラグ(エラーコード、タイムアウトフラグ、正常終了フラグ)で判別できるようになっており、呼び出し元でそれらに基づいた処理を行うことができるようになっています。

なので、この関数が正常に終了すると、読み込んだ文字は、リターンコードではなく、slr->linermojiにセットされます。

では、いよいよ関数本体を作りましょう。基本的な部分は、先の固定長リードと同じですので、ここでは省略します。ここでは固定長リードとは違う部分を詳しく説明します。
/* 変数名を短くする */
s         = slr->linersocket;
sbuff     = slr->linerbuff2;
sbuffsize = slr->linersize2;
先の関数にはなかったので説明しますと、構造体へのポインターで示されたワークエリアは、いささか変数名が長くなってしまいます。なぜなら、矢印演算子(−>)を使い、【ポインター名−>要素名】で指定するために長くなるわけです。このまま使うと、矢印演算子と不等号の区別がつきにくく、ややこしいためできる限り短い変数に代入して使います。

ただし、関数内で書き換えなければならない値は短い変数名に代入すると、書き換えてもワークエリア内の値は変わりませんので、書き換えのない固定値や、ポインターのみを短い変数にします。

受信バッファ自体は内容が変化しますが、ポインター自体は変化がないので短くしても大丈夫です。なぜなら、
このように、たとえば受信バッファがメモリ内のA0000hにマッピングされた場合、(もっとも、こんなピッタリした所にマッピングされるわけありませんが)、slr->linebuff1にはただアドレスA0000が入ってるだけですので、lbという短い名前に代入してもバッファ自体は正しく操作できるわけです。
/*先読みバッファオーバーなら空とみなす*/
if (slr->linerpointer >= slr->linersumi) {
	slr->linerempflg=1;
}

/* 先読みバッファ空ならソケットから読む */
if (slr->linerempflg==1) {
	/* バッファ初期化 */
	bzero(sbuff,sbuffsize);
	slr->linerpointer=0;
	slr->linersumi=0;
}
まず、最初にデフォルトで「先読みバッファ空フラグ」に1をセットしておきます。(呼び出し元でセットします)。そして、先読みバッファが空なら、ソケットからデーターを読みます。ただし、もう先読みバッファのデーターをすべて既読してしまった場合は、同様に先読みバッファを空と同様に処理します。
/* パケットを読む */
retval=read(s, sbuff, sbuffsize);

/*ソケット切断*/
if (retval==0) {
        slr->linereofflg=1;
        return 0;
}

/* エラー発生 */
if (retval == -1) {
        fprintf(stderr,"Jusin error !!\n");
        slr->linererror=2;
        return -1;
}

/* 受信済みサイズ更新、空フラグおろす */
slr->linersumi=retval;
slr->linerempflg=0;
ここで、固定長リードと違うのは、先読みバッファに、バッファサイズめいっぱいまで読むことです。もちろん、ソケットはバッファサイズめいっぱいまで一気に読めませんので、この地点で来てる分だけデーターが先読みバッファに入ります。バッファサイズはあくまで、読み込む最大サイズと思ってください。

ここで、0バイト受信したらソケット切断ですので、EOFフラグをセットした後、0を返します。また、エラーが発生したらエラーコードをセットして、−1を返します。正しく読めたら、読めた量を記録し、先読みバッファ空フラグをクリアします。
/* バッファから現在のポインターのところにある文字を取得 */
slr->linermoji = *(sbuff + slr->linerpointer);
slr->linerpointer++;
return 1;
ここからはソケットを読んだ場合も、読まなかった場合も共通の処理になります。

先読みバッファ内の、現在の未読地点から1バイト読んで、構造体内のメモリにセットし、未読ポインターを1ずらします。正常に終了したことをしめす、1を呼び出し元に返します。これで、先読み処理ができました。今度は、先読みバッファから行バッファへコピーする処理を作りましょう。
/*
=================================================
        ソケットラインリード                     
                                                 
 IN       sr:  SOCKLINER型へのポインター         
 OUT      -1 エラー発生                          
           0 ソケット終了                        
          それ以外:読み込んだ文字数             
=================================================
*/
先ほどと同様に、SOCKLINER型へのポインターを入力情報として渡します。リターンコードも先ほどと同様に、-1ならエラー、0ならソケット切断という風にします。また、それ以外なら読み込んだ文字数を入れるようにします。

余談ながら、リターンコードとしての「0」はしばしば、falseとして扱われ、「異常終了」と定義する事があります。しかし、この関数では、ソケットが切断されて終了したのか、エラーによって終了したのかを明確にしたいため、「0」と「−1」という2種類の「通信終了」を示すリターンコードを設置しています。この関数が正常に終了すると、slr->linerbuff1で示された行バッファに、読み込んだ行が返ることになっています。

続いて関数本体を作ります。
/* 1バイトリード */
retval=sakiyomiread(slr);

/* 終了だったら抜ける */
if (retval<1) {
	break;
}

/* 改行コードを受信した!? */
moji = slr->linermoji;
if (moji==0x0d || moji==0x0a) {
	*(lb+buffpointer)=0x0a;
        buffpointer++;
	break;
}

/* バッファに追加 */
*(lb+buffpointer)=moji;
      buffpointer++;
先ほど作った、ソケットから先読みバッファを介して1バイトずつ返すルーチン、sakiyomiread()を呼びます。ここで、0(ソケット終了)や、−1(エラー発生)ならば、呼び出し元へ戻ります。

次に、読んだ文字を評価し、改行コード(0d 0a)があればいったん呼び出し元へ戻ります。本当は0d 0a があれば 0aのみに直せばいいのですが、普通UNIXでは改行コードは0aのみとなっているため、そこまで神経質には考えないことにします。改行コード以外ならば行バッファに今読んだデーターを足し、ポインターを1増やします。

行バッファがいっぱいになるか、改行コードを受信するか、通信が終了した場合は、呼び出し元に戻ります。行バッファを、ここでは4096バイト確保しましたが、1行の長さがこれ以上ある場合行が寸断されてしまうので、行バッファはなるべく多めに確保しておきます。

SMTP通信の場合、1行の長さはさほど大きくならないため、4096バイトあれば十分と思いますが、それ以上来ることもあるので、終端が改行でない場合の処理を追加するのも良いでしょう。

関数を呼ぶ側を作ろう

今度はこの関数を呼ぶ側を作ります。
struct SOCKLINER slr;
int retval;

/* ソケットを読むための構造体を初期化 */
slr.linersocket   = s;
slr.linersize1    = 4096;
slr.linersize2    = 4096;
slr.linersumi     = 0;
slr.linerpointer  = 0;
slr.linerbuff1    = linebuff;
slr.linerbuff2    = buff;
slr.linereofflg   = 0;
slr.linerempflg   = 1;
slr.linertoutari  = 1;
slr.linertoutflg  = 0;
slr.linertoutsec  = 30;
slr.linertoutusec = 0;
slr.linererror    = 0;

/* バッファ初期化 */
bzero(buff,4097);
bzero(linebuff,4097);
最初に、ソケットを行単位で読む関数のワークメモリを確保します。サイズが大きかったり可変長だったりする時は、malloc()で確保してポインターで操作するものですが、ここではサイズが固定長で小さいため、直接宣言して確保することにします。

ここで、slrを直接struct で宣言しましたので、slrはポインターではなく構造体そのものをさします。構造体をポインターで現している場合、構造体内の要素へは、【ポインター−>要素】みたく、矢印演算子で指定しますが、ポインターではなく構造体そのものを表す場合は、【構造体名.要素】という風にピリオドで指定します。

VisualBasicやJavaScriptがオブジェクト内の要素を【オブジェクト.要素】という風にピリオドで指定しますが、C言語では構造体の実体をオブジェクトと思えばわかりやすいと思います。(厳密にいうと違いますけど)

構造体をあらかじめ初期化します。余談ながら、筆者は「宣言時の初期化」という命令をほとんど使いません。宣言と初期化は別々に行います。これは、あとあと仕様変更があり、全体的をwhile() { }ループで囲って使うように仕様変更された場合に、思わぬバグになるからです。

筆者は、かつて変数の初期化忘れや、仕様変更によって再度の初期化が必要になった事に気づかず、何度もバグを出してしまった経緯があります。そのため、初期化は必ず宣言とは別に、明確にするようにしています。

ここで注意しなければならないのは、slr.linerempflgです。これは、先読みバッファが空である事を示すフラグですから、最初は1にしておきます。念のため、受信済み量や未読ポインターも0クリアしておきましょう。

同様に、受信バッファも0クリアしておきます。

受信バッファは4096バイトですが、どうして4097バイト確保してあるのでしょう??それは、もし何かバグがあってデバッグしたいという時に、printf文で受信バッファの内容を表示することでしょう。

この時、4097バイト目に0(ヌルコード)が入っていれば、たとえ4096文字ある文字列を読み込んだとしても、正しく表示されるわけです。

「じゃあ最後の1バイト目だけ0クリアすればいいのでは?」「4097バイトも0クリアしたら遅いんじゃないのか?」と思うのは昔の話で、今のCPUは、あるメモリ領域をまとめて0クリアする時間などはまったくかからないのと同じなので、全然問題ないです。
/* 1行ずつリードして表示する */
while(slr.linereofflg==0) {
	retval=socklineread(&slr);
	if (retval<1) {
		break;
	}
	printf("Reveived[%d] : %s",retval,linebuff);
}

/*  エラーがあったかどうか */
if (slr.linererror) {
	retval = -1;
} else {
	retval = 0;
}

return retval;
ここで、先に作った、行単位でソケットを読む関数を呼び、行ごとに画面に表示します。受信したサイズも表示します。

エラーがあれば−1、正常に終了すれば0を返します。

サンプルプログラム

これまでのサンプルをダウンすることができます。 サンプルの不具合報告は不要です。 たまにバグってるとか、コンパイルが通らないとか、stdlib.hを入れ忘れててワーニングが出るとか、変数のキャストを忘れててワーニングが出るとかいって文句を言ってくる人がいますが、 あくまでサンプルですので、参考程度にしてください。こんなのをコピペして業務とかに使わない方が身のためだと思います。

・サーバープログラム2
・これまでのサンプルをコンパイルするためのMakefile

さらに実用性を重視したプログラムは、次のページ で紹介します。
1 2 3
スポンサーリンク