このエントリーをはてなブックマークに追加
<< 前のページ 1 2 3 次のページ >>

ソケットの基本

ソケットとの出会い

私がソケットプログラミングと出合ったのは、2000年でした。それまでDOSのプログラミングしか知らなかった私にとって、マルチタスクOS上で、複数の相手と同時に通信を行うソケットというものは衝撃的でした。当時、ソケットプログラミングを1から学習しなければならない状況になってしまった時には、すでに仕事を受注した後で、納期も迫っている状態でした。そこで、急いで文献をあさったり、ネットで検索したりしたのですが、なかなか初めての人にわかりやすく説明された文献に出会うことができません。

それでも、なんとか手探り状態でソフトを作り上げ納品したのですが、ソケットプログラミングでは常識的である事、たとえば、0バイト受信後に必須のソケットの切断処理や、ソケットからデーターを受信するバイト数が必ずしも一定ではない事を知らずに作っていたため、ひどく叱られた事を思い出します。それでもなんとか、発注先の担当者様に叱られながらも、なんとかソフトは完成に至りました。

今現在でも、なかなかソケットプログラミングについて初心者向けに説明している文献に出会うことができません。これでは、なかなか次世代の技術者が育たないと思い、今回、私の経験を元にソケットプログラミングについての基礎から説明をしたいと思います。

少しに間、私のつまらない講座にお付き合いいただければ幸いです。

ソケットとは

コンセントというのは、島国が生んだ和製大砲 和製英語で、英語では「ソケット」とか「プラグ」とかいいます。アニメ「カスミン」に出てくる、ソケットというキャラがコンセントの形をしていることからも、それが伺えるでしょう。(って、知らない??)

昔、「パソコン通信」なるものが主流だった頃、端末側(つまり、会員側のパソコン)はMS-DOSベース(もしくはWindows3.1)が主流でした。この際に、通信を行う相手は1箇所ずつで、しかもメールを読むときはメールだけ、ダウンロードする時はダウンロードだけ、掲示板を読むときは掲示板だけという風に、同時に1つずつの処理しかできませんでした。


ところで、今、皆さんがインターネットをお使いのWindows端末ですが、同時に色々なサイトを開く事ができると思います。当然、メールを受信中にサイトを開きながらFTPでファイルをアップするという事もできるはずです。(遅くなりますが)

これは、Windowsが「ソケット」に対応しているためです。

Windowsのソケット対応ですが、Windows3.1の頃は標準ではついてなくて「Tranpet Winsock」なる別のドライバーの導入が必要でした。Windows95からようやくソケットが標準装備になりました。

しかしながら、UNIXでは最初からソケット通信が標準装備でした。というより、UNIXはもともとマルチタスクで動作するOSであり、同時に複数の通信を行う必要があったわけです。

ソケットとは、通信ポートを、65535個のコンセントにたとえ、それぞれのさしこみ口で、別々の相手と通信をするという考えで作られています。

図のように、複数個の”さしこみ口”(以下ポート)があり、それぞれのポートに役目を与えておきます。

たとえば、FTPでサーバーにファイルをアップする時には20番と21番、TELNETでサーバーにアクセスする時には23番、メールを送受信する時は25番という具合にです。

こうする事によって、複数の通信を同時に行うことができ、複数の相手先と複数の処理を同時に行う事を可能としています。

このように、現在インターネット上で通信アプリを作成する際にソケットは不可欠なものです。というわけで、この章で少しずつですが、ソケットプログラミングをマスターしていきましょう。

ソケット通信のための主な手順(サーバー編)

ソケット通信をするためには、色々と手順を踏む必要があります。それらの手順について、これより図入りで説明したいと思います。

まず、ソケットを作らねばなりません。たとえるなら、壁にコンセントを設置するような事です。

図1)socket 関数


しかし、socket関数は壁にコンセントを設置するだけで、全然配線がつながっていません。このコンセントに、アドレスとポート番号をつけなくてはいけません。壁の内側の配線をするようなものです。(これを「バインドする」といいます。)

図2)bind関数


これで、ようやくコンセントに配線がつながりました。このコンセントは、IPアドレス:xxxx.xxxx.xxxx.xxxxの、ポート番号9000(例えばです。開いてるポートを任意に決めてください 。)で、使う事ができます。

しかし、このコンセントを使うためには、特殊なバッファ付きのタップを通さないと使う事ができません。というと、「何でだーー」と言われそうなので、話を比喩から現実に戻しますと、ソケット通信で使うための通信バッファを、OSが確保する必要があるのです。

通信バッファは、指定した個数作ることができ、このソケットで同時に通信することのできる数を指定することができます。

図3)listen関数


この図では5個ですが、実際には必要に応じた数のバッファを用意してください。

この後は、接続があるまで待つしかありません。この待つ命令がacceptです。

図4)accept関数


このまま、サーバープログラムは接続要求があるまで待機します。サーバープログラムとは、一般的にこのように接続があるまでずっと待っているわけです。

では、コネクトがあった場合どうすればいいでしょう。この後は通信処理を行うわけですが、その間に別の端末から接続の要求があるかもしれません。たとえば、もしメールソフトを起動して「受信」ボタンを押したときに、まったく違う誰かが1人でも受信中だとメールが受信できないようなサーバーだったら困ってしまいます。

こういう時こそマルチタスクOSの真価が発揮されます。つまり、複数のプロセスを同時に立ち上げ、並列処理を行わせるわけです。

図5)fork関数


この図のように、接続があった地点でサーバープログラムは子プロセスを作り、あとの処理を子プロセスに任せます。 そして、親プロセスは再び別の接続が来ないかどうかを待つわけです。

この後、端末との通信が終了しました。子プロセスはexit()関数により終了します。ところが、exit()で子プロセスが終了しても、終了ステータスを親プロセスが取得するまでは、子プロセスはプロセステーブルに残ってしまいます。これをゾンビプロセスと言います。 ゾンビプロセスをそのままにしておくと、 ps axコマンドで見たときにテーブルリストがゾンビだらけになってしまいます。 特にサーバープログラムでは、端末から接続要求があるたびに子プロセスが作られるため、 このまま24時間365日稼動させておくと、プロセステーブルを使い切ってしまうかもしれません。

そのため、子プロセスが終了した時に親プロセスが終了状態を読み取る事で、プロセステーブルから削除してあげなければなりません。これを、ゾンビを埋葬するといいます。

図6)waitpid関数


図6のように、子プロセスの終了を感知した親プロセスは、ゾンビ化した子プロセスを適切に埋葬する必要があります。waitpid( ) 関数により子プロセスは埋葬され、プロセスリストから削除されます。
コラム waitpidノート
・この関数に書かれたゾンビプロセスは、死ぬ。
・プロセスを指定する時は、第一引数にプロセスIDが入ってないといけない。したがって、同名のプロセスに一遍の効果は得られない。
・第二引数に親プロセスの動作の状況を書くことができる。書かなければ子プロセスが死ぬまで待つ。
・waitpid関数に書かれたゾンビプロセスの死は、いかなる手段をもってしても取り消す事はできない。
・全てのプロセスはいつか必ず死ぬ。そして死の行き着く先は無である。
・ゾンビプロセスの死が他のプロセスの寿命を間接的に伸ばしたり縮めたりする場合があるが、それは一向にかまわない。

サーバープログラム

では、実際にサーバープログラムを作ってみましょう。

Cで作る

・ヘッダーの組み込み
まず基本的なヘッダーを組み込みます。この辺はどのプログラムにもデフォルトで組み込んでしまって良いでしょう。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
のように、ヘッダーを組み込みます。一般的なヘッダの他に、types.hとsocket.hはソケットで、また、signal.hとwait.hは子プロセスの埋葬で使います。
・ソケットの作成
socket関数を使ってソケットを作成します。

TCP/IPプロトコルを使うためには、第1引数にソケットファミリーAF_INET、第2引数にソケットタイプSOCK_STREAMを指定します。第3引数はプロトコルを指定しますが、0と書くことで「デフォルトのプロトコル」という意味になります。ソケットファミリーはPF_INETとかAF_INETとか言いますが、ヘッダsocket.hで同じ値に定義されています。

socket関数が成功すると、リターンコードにソケット番号が入ります。「ソケット番号」とは、このプログラム内で今作ったソケットを呼ぶための識別番号で、ファイルディスクリプタのようなものです。ソケット番号に-1が返った時はソケット作成に失敗したことを示します。この場合、呼び出し元に-1を返します。
/*ソケットを作成*/
if ((s_listen = socket(AF_INET, SOCK_STREAM,0)) < 0 ) {
        fprintf(stderr,"Socket for server failed.\n");
        return -1;
}
・自ホスト名の取得
ソケットに名前をつける(バインドする)ために、gethostnameで自ホスト名を取得しておきます。また、gethostbynameで自ホスト名をIPアドレスに変換します。この関数を使うためには、この環境で自ホスト名が逆引きされている必要があります。

myhostは、struct hostent *myhost;で、hostent型として宣言されている必要があります。
/*自ホスト名とIPアドレスの取得*/
gethostname(hostname,256);
if((myhost = gethostbyname(hostname)) == NULL) {
	fprintf(stderr,"bad hostname!\n");
	return -1;
}
・ソケットに名前をつける
ソケットに名前をつける(アドレスとポートをバインドする)ために、ホスト情報をあらかじめ構造体meにセットしておきます。meはあらかじめ、struct sockaddr_in me;として宣言しています。

me.sin_familyには、プロトコルを指定します。TCP/IPを使うために、AF_INETを指定します。me.sin_portに、この通信で使うポートを指定します。ポート番号はあらかじめ、#define PORT (u_short)9000で、宣言してあります。(ここでは9000番に定義してあります。)

自IPアドレスをme.sin_addrに代入します。現段階ではIPアドレスは4バイトに決まってるのですが、将来的にIPV6になった時もソースの修正が最小限で済むように、バイト長は「4」と固定するのではなく、myhost->h_lengthという風に指定します。

ホスト情報、ポート番号をme構造体にセットしたら、bind()関数でソケットにバインドします。引数として、ソケット・me構造体へのポインタ・me構造体の長さを渡します。結果として-1が返った場合はバインド失敗ですので、エラー処理を行います。
/*自ホスト情報を構造体にセット*/
bzero((char *)&me,sizeof(me));
me.sin_family = AF_INET;
me.sin_port = htons(PORT);
bcopy(myhost->h_addr, (char *)&me.sin_addr, myhost->h_length);
/*ソケットにアドレスとポート番号をバインドする*/
if (bind(s_listen, &me, sizeof(me)) == -1) {
	fprintf(stderr, "Server could not bind.\n");
	return -1;
}
・ポート番号を使う時の注意
1024未満のポート番号は、Linuxの既存のサービスで予約されていますので、独自にサービスを提供するのであれば、1024以上のポート番号にしなければいけません。また、1024以上でも、8080(squid)、5432(PostgreSQL)、5680(canna)、3306(MySQL)、6699(Winなんとかx)、7743(ウィなんとかー)など、既存のメジャーなアプリがデフォルトで使う番号は避けた方がいいです。(といっても、あまり避けすぎてもきりがないのですが。)

ポート番号は、符号なしで定数でme.sin_portに代入します。ここで、ポート番号は2バイトで表記するのですが、上位バイト・下位バイトの順で代入しなければなりません。ところが、インテル系CPUや、インテル互換CPU(80186、80286、80386、i486、Pentium、Pentium2、Pentium3、Pentium4、セレロン、アスロン、デュロン……などなど)では、2バイト以上の数値を、下位バイト・上位バイトの順に格納しています。

なので、ここでは上位バイトと下位バイトを逆にして代入しなければなりません。しかし、単に機械的に逆にしてしまうと、元々上位バイト・下位バイトの順で格納しているモトローラー系CPUのマシンで実行すると、そのままでいいのに逆になってしまいます。

そこで、ソース上で互換性を持たせるために、htons関数を使います。htons関数はインテル系CPU上のコンパイラでは上位バイトと下位バイトを逆にし、モトローラー系CPU上のコンパイラではそのままの値を返しています。これで、インテル系でもモトローラ系でも同じソースが使えるようになります。
コラム ビッグエンディアンとリトルエンディアン

2バイト以上の数値を格納する方式に、ビッグエンディアン方式とリトルエンディアン方式があります。ビッグエンディアンとは、上位バイトから順に格納する方式で、モトローラ系のCPUがこの方式を採用しています。一方インテル系のCPUはリトルエンディアン方式を採用していて、2バイト以上の数値は下位バイト~上位バイトの順で格納されます。

UNIXでは基本的に、ソースをハードに依存しないように作らねばなりませんので、上位バイト・下位バイトの変換は、htonsやhtonl関数を使うようにしてください。

ポート番号 16進数 ビッグエンディアン リトルエンディアン
4660 0x1234 12 34 34 12

・ソケットのバッファを用意する
listen関数を使って同時に通信できる数だけバッファを確保します。ここでは5を指定しました。同時に大量アクセスが見込まれるようなサーバーを作るのであれば、もっと多めに設定します。ただし、メモリーが足りないなどでバッファの確保に失敗した時は-1が返りますので、失敗した時はエラー処理をします。
/*バッファの確保*/
if ( (listen(s_listen, 5)) == -1) {
	fprintf(stderr, "Listen failed.\n");
	return -1;
}
・接続を待つ
クライアントから接続があるまで待ちます。接続があると、ソケットディスクリプタを返して、この関数を抜けます。ソケットディスクリプタとは、このプログラムで一時的に使う識別番号です。以後、通信はこの識別番号を使って行う事になります。
/* 接続待ち */
aiteaddrlen=sizeof(aite);
s = accept(s_listen, &aite , &aiteaddrlen);
第1引数は、socket関数で得たソケット番号を指定します。
第2引数は、相手の情報を格納するためのsockaddr_in型の構造体へのポインターを渡します。
第3引数は、sockaddr_in型のサイズを入れます・・・が、従来はint型でそのまま入れればよかったのですが、今はサイズがint型ではなく、socklen_t型となり、長さがコンパイラによって不定になりました。そのため、ここには値を直接いれず、別途sockten_t型で実体を宣言してから、そのポインターを渡すようになっています。

acceptに失敗した時は-1を返します。子プロセス終了直後に別の端末からの接続要求が来ると失敗する事があります。それなので、実際には-1が返ってもすぐには終了せず、数秒のウェイト後にリトライを何度か繰り返してからエラー処理をした方が良いでしょう。
while (s_retry_counter<5) {

        /* 接続待ち */
        aiteaddrlen=sizeof(aite);
        s = accept(s_listen, &aite , &aiteaddrlen);

        /*失敗ならリトライ、それ以外は抜ける*/
        if (s == -1) {
                s_retry_counter++;
        } else {
                break;
        }

        if (s_retry_counter<5) {
                /* リトライの前にしばし待つ */
                sleep(1);
        }
}

/*  accept失敗  */
if (s == -1) {
        fprintf(stderr,"Accept faild.\n");
        return -1;
}
・分身(子プロセス)を作る
acceptで接続が成功したら、後の処理は子プロセスに任せ、親プロセスは再び接続待ちに戻ります。こうする事によって、通信中に別の端末から接続があっても対応できるようになります。
if ( (pid=fork()) == 0 ) {
        close(s_listen);

        /* フォーク成功 */
        printf("fork success!!.\n");

        /* 子プロセスへ */
        if ( child_process(s) == -1) {
                fprintf(stderr,"Tusin failed.\n");
                return -1;
        } else {
                /*子プロセス終了*/
                printf("Clild process normal end.\n\n");
                break;
        }
}
forkが成功すると、親プロセスには0より大きい数(子プロセスのpid)、子プロセスには0が返ります。この返り値を判断して、親プロセスと子プロセスを判別します。

・forkの返り値
リターンコード プロセス 対処
0 親のソケットを閉じて、接続先との通信処理を行う。
-1 fork失敗。エラーメッセージを表示して終了。
0,-1以外 fork成功。端末との接続を閉じてacceptに戻る。
親プロセス、子プロセスともに、forkの帰り値以外はまったく同じ状態です。つまり、子プロセスには不要な、親ソケットもそのまま残っていますし、親プロセスには不要な端末との接続も開いたままになっています。そこで、子プロセスは、親の作成したソケットを閉じねばなりません。逆に親プロセスは、端末との接続を閉じねばなりません。

・ゾンビの埋葬
子プロセスは、mainの終了(や、return命令やexit関数)によって正常に終了します。しかし、そのままではメモリからは解放されず、ゾンビプロセスとして常駐しつづけます。そのため、親プロセスは子プロセスの終了を監視し、waitpid関数で埋葬する必要があります。 waitpid関数は、プロセスが終了するまで待ち、プロセスが終了したらゾンビを埋葬(メモリから解放)します。

子プロセスが終了したら、Wait関数を呼ぶ
/* signal監視 */
signal(SIGCHLD, Wait);

/* ゾンビの埋葬 */
void Wait(int sig) {
        while(waitpid(-1,NULL,WNOHANG)>0);
        signal(SIGCHLD, Wait);
}
子プロセス終了(SIGCHLDシグナル)を監視し、シグナルが来たらWaitという関数を割り込みで実行します。
waitpid関数 waitpid(プロセスID, 状態, オプション)

子プロセスをメモリから消去します。

第1引数・・・消去するプロセスIDを指定しますが、ここでは「最初に終了した子プロセス」を指示するために、-1をセットしています。
第2引数・・・子プロセスの状態を格納するポインタを指定しますが、ここでは不要なので、NULLをセットしています。
第3引数・・・WNOHANGを指定しています。これは、終了すべきプロセスがない場合は、待たずに終了するという意味です。この場合は、リターンコードとして0が返ります。
リターンコードは通常は終了させたプロセスIDを返します。while (waitpid (-1, NULL, WNOHANG) >0 );は、0が返る(終了すべき子プロセスがなくなる)までwaitpid関数でループする事をしめしています。この関数が呼ばれた地点でシグナル割り込みは自動的に解除されますので、最後にシグナル割り込みを復活させて終了します。

・通信処理
acceptに成功したら、リターンコードとしてソケットディスクリプタ(s)が返ってきます。このsを指定することで、ファイルへの読み書きと同様に、ソケットをread、writeする事ができます。ここで、通信処理は子プロセスに任せ、親プロセスはすぐにソケットディスクリプタ(s)を閉じて、accept待ちに戻ります。

・パケットが来るまで待つ
acceptに成功したら、端末からデーターが送信されてくるのを待ちます。ここで、select関数を使います。
/*受信待機*/
readok = mask;
if ( select(width, &readok, NULL, NULL, NULL) == -1) {
        fprintf(stderr,"Select error !!\n");
        return -1;
}
第1引数widthは、ソケット番号を何番まで監視するかを示すものです。

ソケット番号として取りうる最大値(1025)をセットしても良いのですが、そうすると常に1024個のソケットを監視する事になり、サーバーに無駄なメモリを消費してしまいます。そこで、現在読もうとするソケット番号まで監視するようにします。

ここで、第1引数の0は「どこもソケットを監視しない」という意味があり、1を入れると0番、2を入れると1番、3を入れると2番まで……(n)をいれると(n-1番)までのソケットを監視するという意味になります。そのため、widthの値は、監視したいソケット番号+1にしておかないといけません。

第1引数に0を入れると「どこもソケットを監視しない」という意味で、この場合、第5引数にタイムアウト時間を設定して、単なるウェイトとして使うことができます。sleep関数が秒単位でしか待てないのに対し、selectはマイクロ秒単位で待ち時間を設定することができ、さらにループで待ち時間を入れるとCPUに負荷がかかるため、ソケットと関係ないプログラムであっても、細かい待ち時間を入れる時にselect関数を使うという事がよくあります。

第2・第3、第4引数は、監視したいソケットを指定します。fd_set型で宣言した変数のアドレスを入れます
FD_ZERO(&mask);
FD_SET(s, &mask);
width = s+1;
FD_ZERO は、fd_set型変数の全ビットをクリアします。(つまり、128バイト全部を0にします。)FD_SETは、fd_set型変数のうちの、指定されたビット目を立てます。(つまり、1を指定ビット分だけ左シフトした後にorをとります。)

第2引数は、読み込み可能なソケット番号を指定します。実際にパケットが届くと、パケットが届いたソケット番号のビットがセットされます。
第3引数は、書き込み可能なソケット番号を指定しますが、ここでは読み込みの調査のみなのでNULLを指定します。
第4引数は、例外を監視するソケット番号を指定しますが、たいていはNULLにしておきます。
第5引数は、struct timeval timeout;でtimeval型の構造体を確保しておき、timeout.tv_sec=タイムアウト秒;timeout.tv_usec=タイムアウトマイクロ秒;をセットしておきます。ただし、NULLを指定するとタイムアウトなしで、延々に待ちます。

ここでは第5引数にNULLを指定しているため、データーが届くまで無限に待つようになっています。実用性を考えるなら、きちんとタイムアウトの処理を入れないといけないのですが、その辺は下巻で説明します。

select関数は、パケットが届くかタイムアウトになると終了します。select関数が終了すると、パケットが届いているソケットの数が返りますが、タイムアウトした場合は、「どこにもパケットが届いてない」という意味で、0が返ります。また、-1が返った時はエラーですので、エラー処理をします。

・パケットを読む
if (FD_ISSET(s,(fd_set *)&readok)){
        bzero(buff,257);
        jusin_size=read(s, buff, 256);
}
FD_ISSET関数は、fd_set型のメモリのうちの指定ビットが立ってるかどうかを調べます。ここでは、readok変数の中の、ソケット番号目のビットがたっているかどうか調べます、というと、なんか、わけのわかんない言葉になってしまったので、言い方を変えます。

例えば、01001001のビット1が立っているかどうかは、00000001とandをとって真か偽かを調べます。同様に、ビット2が立ってるかどうかは、00000010とandを取って調べます。このように、00000001を調べたいビット分だけ左シフトすればいいのですが、fd_set型はレングスが128バイトもあるので指定のビット目が立ってるかどうか調べる関数を作るのはいささか面倒です。

これを簡単にしたものがFD_ISSETです。FD_ISSETは第1引数に何ビット目を調べたいか、第2引数に調査対象を入れ、結果が0(偽)か、0以外(真)かで調べることができます。こうして、真が返ってきたらパケットが来たという意味ですので、read関数でパケットを読み込みます。

・パケットは何バイト来るかわからない
かつて、ウィなんとかというソフトがありました。著作権とか情報流出とかで色々と問題があったこのソフトですが、 ソケット通信を理解するために使うなら、とてもわかりやすいソフトでしょう。 そのなんとかニーを使った事がある人はおわかりと思いますが、パケットは一度に何バイトくるかわかりません。回線が空いてれば数キロバイトいっぺんにデーターが来たりしますし、回線が混んでいたり通信相手が送信幅をしぼっている場合は1バイトずつしか来ない事もあります。
コラム ソケット通信では、データーが一度に何バイト来るかわからない
「こんな画面を見たことのある方ならわかるはず」と友人が言ってましたが、私には何のことやらサッパリ・・・
jusin_size=read(s, buff, 256);というふうに256バイト指定してデーターを読むつもりでも、実際には最大256バイトまでという意味で、ちょうど256バイト来るとは限りません。第3パラメーターの値はあくまで最大受信サイズです。これを、256バイト来るまで待ってもらえると思うと思わぬバグになります。

したがって、「ちょうど256バイト受信する」というアルゴリズムにするためには、ループ命令を使ってそういう関数を自分で作る必要があります。(詳しくは、下巻で説明しています。)

・0バイト受信する
ソケットプログラミングについてGoogleで検索したり、書籍を色々と調べたりしましたが、「0バイトを受信した場合」について、あまり詳しく書かれている文献に出会うことはあまりありませんでした。

しかし、これはかなり大事なことです。

ソケット通信で0バイト受信した時は、相手がソケットを閉じたという事です。これを知らずにプログラムを作ると、相手がソケットを閉じているのに受信を待ってしまい、無限ループになるようなものを作ってしまいます。実際に、筆者もとある大手のプロジェクトに参加した時に、この事を知らずに怒られてしまいました。

ソケット通信では、EOFを受信すると、いったんEOFより前のバイトをバッファに入れます。これによりEOFよりも前のバイトがread関数で読み出されます。その後、EOFのみがバッファに残るため、select関数では「パケットが来ている」と判断されます。しかし、read関数でEOFは取り除かれるため、0バイトが受信されます。

つまり、read関数で0が返ってきた時は、相手側がソケットを閉じたという意味になります。

そういう、ややこしい理屈はどうでもいいという人は、とにかくread関数のリターンコードが0だったら、相手がソケットを閉じたとして処理してください。

・通信終了
read関数で0が返った場合、こちらもソケットを閉じて通信終了処理を行います。

Perlで作る

まず、ソケットモジュールを使う事を宣言します。
use Socket;
use POSIX ":sys_wait_h";
ここで、Socketは標準モジュールなので、そのまま宣言すれば使う事ができます。また、POSIX":sys_wait_h"モジュールは、ゾンビプロセスを埋葬する時に使いますので、これも宣言しておきます。
# ===================
#  ソケットオープン
# ===================
socket (P_SOCKET, PF_INET, SOCK_STREAM, 0) || die "[親]ソケットの作成に失敗しました $!";
PF_INET、SOCK_STREAMは共に通信プロトコルの名前です。インターネットプロトコル(IP)を使い、データーの順序や内容が保証されるプロトコルを使う・・・・・・と、要するにTCP/IPです。インターネット上で使う最も主流なプロトコルを使うわけです。

ソケットのオープンに成功すると、ハンドルがP_SOCKETに入ります。(ここでは、親(parent)のソケットをP_SOCKETと名づけます)
# ===================
#     BIND
# ===================
bind (P_SOCKET, pack_sockaddr_in(9000, INADDR_ANY)) || die "[親]BINDに失敗しました $!";
続いて、ソケットにアドレスとポートを関連付けます。C言語では、自サーバーのIPアドレスを取得して、ポート番号の上位バイト下位バイトをhtonsで変換して、それぞれを構造体にセットする、という手間が必要だったのですが、Perlではそれらの手間をかなり省くことが出来ます。

pack_sockaddr_in関数では、ポート番号をバイナリ化して、上位/下位バイトの変換をした後、IPアドレスと結合してくれます。第一引数にはポート番号を直接書きます。第二引数ではIPアドレスをバイナリに変換した変数を指定します。IPアドレスをバイナリに変換するためには、inet_aton関数を使います。

ただ、ほとんどのサーバーでは、LANカードは1枚で、IPアドレスも1個しか設定されてないでしょう。ですので、第二引数でINADDR_ANYと指定するだけで、関数内で自動的に自ホストのIPを調べてセットしてくれます。

このように、C言語でソケットを作った事のある方にとっては、Perlはだいぶ楽と感じると思います。
コラム バイナリのIPアドレス

Perlでは変数に「文字列」「数値」の区別はありませんが、「バイナリ」ははっきりと区別されます。

文字列で表されたIPアドレスは、"192.168.0.1" のように人間の目で見てわかりますが、バイナリで表されたIPアドレスは C0 A8 00 01 のように、そのままprint文などで表示させると文字化けしてしまいます。

しかし、ソケット関数ではIPアドレスとしてバイナリを要求します。そこで、バイナリと文字列を相互に変換する関数があると便利です。Perlのソケット関数では、IPアドレスの文字列とバイナリをそれぞれ変換する関数が用意されています。

inet_aton()・・・文字列で表されたIPアドレスをバイナリに変換します。
inet_ntoa()・・・バイナリのIPアドレスを文字列に変換します。

# ===================
#     LISTEN
# ===================
listen(P_SOCKET, SOMAXCONN) || die "[親]LISTENに失敗しました $!";
listen関数は、同時に処理できるソケット数を設定します。第二引数にソケット数を指定します。ただし、SOMAXCONNにはそのサーバーで同時に処理できる最大値がセットされているため、第二引数をSOMAXCONNと指定すれば最大数が指定された事になります。
printf("[親]接続を待っています.....\n");
$client=accept(C_SOCKET, P_SOCKET) || die "acceptに失敗しました $!";
acceptは端末からの接続があるまで待機します。サーバープログラムは大抵はここで止まっているわけです。接続があると、端末との情報を送受信するためのソケットを作り、C_SOCKETに返します。また、リターンコードとして、接続してきた端末のIPアドレスとポート番号をバイナリ値で返します。
($client_port, $client_ip_bin) = unpack_sockaddr_in($client);
$client_ip_str=inet_ntoa($client_ip_bin);
printf("[子] 端末[%s]のポート%s より接続しました\n", $client_ip_str, $client_port);
unpack_sockaddr_in関数は、IPアドレスとポート番号を分解し、ポート番号の上位バイトと下位バイトを元に戻します。IPアドレスはバイナリのままですので、inet_ntoa関数で文字列にします。

これらの流れを図にすると、このようになります。

図1)BINDに与える引数の作成

inet_atonでIPアドレスをバイナリ化して、ポート番号と結合させる。ポート番号は上位/下位バイトが反転される。

図2)acceptのリターンコードから端末情報を取得する

ポート番号とIPアドレスで分解した後、inet_ntoaで文字列に変換する。ポート番号は上位/下位バイトが元に戻る。
while(1) {
       :
     ここにacceptの処理
       :

        #子プロセス作成
        $pid=fork();

        # 親だったらaccept待ちに戻る
        if ($pid>0) {
                printf("[親]forkに成功しました。\n");
                next;
        }

        # フォーク失敗だったら終わり
        if ($pid!=0) {
                printf("[親]forkに失敗しました。\n");
                close(C_SOCKET);
                close(P_SOCKET);
                last;
        }

       :
    ここに子プロセスの処理
       :
}
acceptに成功したら、あとの処理は子プロセスに任せます。

fork関数で子プロセスを作成します。子プロセスもまったく同じプログラムがコピーされますが、作成された子プロセスは、fork関数を呼んだ次の行から続けて実行されます。

なので、fork関数の次の行は親プロセスか子プロセスかを判別する処理が必要となります。ここでは、リターンコードとしてプロセスIDが帰ってきたら親プロセスとしてループの頭に戻ります。(つまり、子プロセス用の処理を飛ばしてacceptに戻るわけです。)

また、リターンコードがプロセスIDでもなく0でもない場合は、forkが失敗した事を意味します。この時は、forkが失敗したとしてエラーメッセージを表示してプログラムを終了させます。実際サーバーとして使うのであれば、ダウンした時の日付や時刻、その時接続してきた端末の情報などをログに記録させると良いでしょう。
#端末との通信
while(<C_SOCKET>) {
        printf("%s",$_);
}

# 接続終了
close C_SOCKET;
printf("[子] [%s]との接続を終了しました\n" ,$client_ip_str );
last;
端末と通信している部分です。この辺が、C言語と比べると非常に簡単な所です。

C言語では、ソケットからデーターを受信するためには、まずselectで指定のポートにデーターがきているかどうか確認した後、データーがきていればバッファサイズを指定してリードするわけですが、一度に何バイト送信されてくるかわからないため、別途必要なバイト数分だけ読み込むまでループするような関数を作らねばなりませんでした。

ところが、Perlでは while(<C_SOCKET>) { 処理 } とするだけで、「ソケットから行単位で読み込んで結果を$_に入れる。ソケットが切断されれば偽を返す」という意味になります。

つまり、ソケットとファイルを同じ感覚で扱えるわけです。ソース上ではここだけ見ればファイルを読み込んでいるのと全く同じで、可読性は非常に高いといえます。SMTPやPOP3などの、電文がテキストベースで、行単位で通信するプロトコルでは、このやり方で十分サーバーソフトが作れるでしょう。

ただ、この方法では相手がデーターを送ってこないでフリーズしたままになると、こちらも相手がデーターを送ってくるまで延々と待ってしまいます。そのため、より実用的に作るのであれば、ソケットを読む前に、データーがきちんと届いているか確かめる必要があります。
#データーが来たか検査
$timeout=5;
$rin = "";
vec($rin,fileno(C_SOCKET),1) = 1;
($nfound,$timeleft) = select($rout=$rin, undef, undef, $timeout);
C言語でselectという関数を使い、ソケットにデーターが届いているかどうか検査する事ができるのと同様に、Perlでも select関数でソケットにデーターが届いているか検査する事ができます。
select(検査結果=検査ビット, undef, undef, タイムアウト)
C言語にも、select(検査幅, 検査ビット, NULL, NULL, タイムアウト) という同様の関数がありますが、若干の違いがあります。Perlの場合、検査幅を指定する必要はなく、検査ビットで指定したファイルディスクリプタのみが自動的に検査対象になります。また、Cでは検査ビットをFD_SET型、タイムアウト時間をTIME_T型で確保しましたが、高レベル言語のPerlでは変数型を意識する必要がない分、設定が楽になります。

検査ビットは、つまりどのファイルディスクリプタを検査するか、という意味です。1なら1ビット目、2なら2ビット目……を立てます。C言語では、FD_SETという関数を使って指定ビットを立てましたが、同様にPerlではvecという関数を使って指定ビットを立てます。
vec($rin,立てるビット,1)=1
第一引数$rinはあらかじめNULLでクリアしておきます。第二引数には何ビット目を立てるのかを指定します。第三引数には、何ビット連続して立てるのかを指定しますが、大抵は1でいいでしょう。右辺の「=1」は、つまり代入文の左辺として使う事で"代入"を表します。BASICのMID$文を"代入"で使った事のある方はよくわかると思います。

では、ここでは何ビット目を立てれば良いでしょう。C言語ではacceptで返ってきたソケット番号がそのままファイルディスクリプタなので、何もしなくても良かったのですが、Perlではファイルディスクリプタとファイルハンドルは別物です。なので、ファイルハンドルからファイルディスクリプタを取得しなければなりません。そこで、fileno関数を使います。
ファイルディスクリプタ=fileno(ファイルハンドル)
fileno関数に、引数としてファイルハンドルを与えることで、ファイルディスクリプタを取得する事ができます。
コラム ファイルハンドルとファイルディスクリプタ

C言語でもPerlでもファイルをオープンすると、そのファイルに関する情報を管理するためのエリア(構造体)がメモリに確保されます。ファイルを管理するためのエリアはポインタで示され、ファイルをリード/ライトする時に使われます。これをファイルハンドルといいます。

それとは別に、OS(Linux)の方でも、オープンされたファイルを番号で管理しています。0が標準入力、1が標準出力、2が標準エラー出力と決まっていますが、それ以降はオープンされた順にOS側で番号を割り振ります。これがファイルディスクリプタです。

ファイルディスクリプタもファイルハンドル内で管理され、通常はFILE->fdなどに格納されています。Perlの場合、fileno関数を使ってファイルハンドル内にあるファイルディスクリプタを取得する事ができます。
select関数では、「検査ビット」で指定したファイルディスクリプタにデーターが来るまで待ち、結果を「検査結果」に入れます。

データーが来ていれば$nfoundに1が入り、$timeleftに残り時間が入ります。タイムアウトが設定されていて、タイムアウト時間までにデーターが来なかった場合は、$nfoundと$timeleftにそれぞれ0が返ります。

これにより、タイムアウトまでにデーターが来なかった場合にタイムアウト処理を入れる事ができ、単にwhile(<C_SOCKET>) で読む場合と比べても、相手がフリーズした時にこちらの子プロセスを終了させる事ができ、それだけ安全性が高くなります。

タイムアウト時間はCではマイクロ秒で指定しましたが、Perlでは単純に秒数で指定します。そのかわり、1秒以下は小数点で指定する事ができます。

ソケットを、while(<C_SOCKET>)として読むと、「送信されてきたデーターを行単位読み$_に入れて、ソケットが切断されるまでループする」という意味になりましたが、この方法では行単位で(つまり改行コードが来るまで)読み込むため、改行コードが来る前に端末がフリーズしてしまった時にselect文の意味があまりなくなってしまいます。

同様に、$value=<C_SOCKET>として一気に全部を1つの変数に入れる事もできますが、この場合も、相手が送信の途中でフリーズしてしまった場合、やはりソケットが切断されるまでサーバーは延々と端末を待ってしまいます。

そこで、sysread関数を使います。これはC言語のread関数と同様の低レベルの読み込みができます。つまり、データーが現地点で来ている所まで読み込む事ができるわけです。さらに、バッファサイズも指定する事ができ、安全性が高くなります。ただし、その分可読性は低くなり、せっかくの高レベル言語を使っている意味は、あまりなくなってしまうかもしれません。
コラム 可読性を取るか安定を取るか!?

Perlのソケット入出力はソケットさえオープンしてしまえば、ファイルとまったく同じに読み書きする事ができます。一見すると単にファイルの読み書きをしているだけのソースが、ケット通信だったりするわけです。

なので可読性からいけば最高なのですが、そこが高レベル言語の欠点でもあります。もし悪意を持った端末が、数百メガバイトのデーターを送りつけてきたらどうなるでしょう?Perlでは、そのまま改行コードが来るまでひたすら$_に送信されてきた情報を格納します。そのため、送信されてくるデーターがいくら大きくても、その分メモリを確保してしまいます。結果、悪意を持った端末からサーバーをダウンさせる事ができてしまうわけです。

また、端末が送信の途中でフリーズしてしまったらどうなるでしょう。普通に<C_SOCKET>からデーターを読もうとすると、データーが来るまで半永久的に待ってしまいます。それでも1件や2件ならば良いのですが、100件、200件と途中でフリーズしてしまう端末が発生すると、それらは全部ゾンビプロセスと化してしまい、メモリがその分だけ無駄に確保されたままになってしまうのです。

このように高レベルでのファイルの読み書きは、自社のサーバー同士でデーターをやりとりするだけなら十分なのですが、インターネット上で不特定多数に公開するサーバーでは実用には向かないと言っていいでしょう。
一通り、端末とのデーターのやりとりをする部分が完成し、デバッグしても特に問題は起きないのであれば、プログラムは完成です。といいた所なんですが、1つ大事な事を忘れてはいけません。

先に説明した通り、子プロセスをそのままexit関数で終わらせたままでは、そのままゾンビプロセスとなり、メモリに残ったままになってしまいます。なので、親プロセスでは子プロセスの終了を監視し、随時埋葬しなければなりません。

そこでシグナルハンドラを使います。
# ===================
#    ゾンビの埋葬
# ===================
sub killzombie() {
        waitpid(-1,&WNOHANG);
        printf("[親] 子プロセスを開放しました\n");
        $SIG{'CHLD'} = \&killzombie;
}
$SIG{'CHLD'} = \&killzombie;
$SIG{'シグナル名'}=¥&サブルーチン名;

これをプログラムの頭の方で宣言しておきます。これで、「シグナル名」で指定されたシグナルが発生した時に、「サブルーチン名」で指定した処理を割り込ませる事ができます。ここで、「CHLD」は「子プロセスの終了シグナル」を意味します。

waitpid(-1,&WNOHANG)

waitpidは指定の子プロセスを埋葬する関数です。第一引数の「-1」は「最初に状態が変化した子プロセス」を意味します。つまり、終了した子プロセスです。&WNOHANGは「終了させる子プロセスがなくても待たない」という意味です。

実のところ、Perl5では子プロセスが終了した際にPerl側の方で埋葬してくれるようで、実際に試してみたところwaitpidを抜いてもゾンビプロセスを確認する事はできませんでした。なので、無理に子プロセスの終了を待つようにすると逆に親プロセスの方がここで止まってしまいゾンビになってしまいます。なので、ここでは終了させる子プロセスがなくても待たないようにしています。

ただ、Perlの方で必ずしも子プロセスを埋葬してくれるとは限らないので、互換性も考えてこの処理は必ずいれておきましょう。

クライアント編

これまでは、サーバー用ソフトについて解説してきました。今度は、接続を待つ側ではなく、接続を要求する、クライアント側のソフトを作ってみましょう。

まずソケットを作成します。しかし、今回は差し込み口ではなくプラグの方を作ります。とはいっても、使う関数は同じsocket関数です。socket関数で、コンセントの差込口とプラグのどちらも作ることができます。

socket関数


しかし、クライアントではサーバーのようなソケットに名前をつけたり、バッファを用意したりする作業は必要ありません。このプラグをサーバーに差し込めばいいわけです。

connect関数


「ミームいろいろ夢の旅」では、ミームがパソコン~パソコン間を自由に移動することができましたが、ソケットも同じように、ルーター同士が正しくコネクトしていれば、インターネットに接続されたサーバー内で接続する事ができます。ソケットがつながれば、相手サーバー機とデーターを送受信する事ができます。もしつながらない時は、以下の点を確認してください。

(1)相手サーバー機の指定ポートが空いていること
相手のサーバー機のサーバーソフトが正しくacceptで受信待ちになっている必要があります。(これを「ポートが空いている」といいます)

(2)ファイアウォールがないこと
プロバイダーによっては、(というか、大抵は)そのプロバイダーが使わないポートはファイアウォールでふさいであります。ユーザーがCGIで好き勝手にポートを空けてしまうとセキュリティーホールになってしまいます。

ソケットに立ちはだかる火壁!?
ひかべがあらわれた!HP100 MP50

そこで、ソケット通信をする際は、その通信ポートのみを開けておく必要がありあす。なので、ソケット通信を行うためにはサーバーのセキュリティーの設定を変更できる立場の人間の協力が必要になります。
コラム ポートスキャンにご注意

新しいサービスを提供する時は、インターネットで公開されているサーバーの特定のポートをaccept待ちにしておく必要があります。しかし、インターネット上には、常に全IPアドレスの、全ポートを調べて、新しくサーバーが立ったか、どこのポートから侵入できるか、を探している人がいます。

これは、特定の企業や政府のサーバーを攻撃したり、SPAMメールを大量に送付するための「踏み台」を探しているためです。日本はもちろんですが、インターネットにサーバーを設置するだけで世界中からポートをスキャンするパケットが飛んできます。

したがって、むやみにポートを空けておくと、とたんに外部からの攻撃対象になってしまいます。なので、ソケット通信で新たなサービスを提供する時は、接続元IPアドレスを限定したり、認証機能を厳密に作るなど、セキュリティーには十分気を使います。

Cで作る

まずソケットを作成します。これは、サーバーもクライアントも同じです。
/*ソケットを作成*/
if ((s_listen = socket(AF_INET, SOCK_STREAM,0)) < 0 ) {
	fprintf(stderr,"Socket for client failed.\n");
	return -1;
}
次に、サーバーにコネクトします。
/*接続先ホスト名設定*/
bzero(hostname,257);
strcpy(hostname,"*サーバー名*");
/*接続先IPアドレス取得*/
if((connect_host = gethostbyname(hostname)) == NULL) {
        fprintf(stderr,"bad hostname!\n");
        return -1;
}
/*接続先情報をソケット型構造体にセット*/
bzero((char *)&desthost,sizeof(desthost));
desthost.sin_family  = AF_INET;
desthost.sin_port    = htons(PORT);
bcopy(connect_host->h_addr, (char *)&desthost.sin_addr, connect_host->h_length);
ここでは、接続先をgethostbyname()関数を使って*サーバー名*をIPアドレスに変換していますが、接続先が引けてないといけません。/etc/resolv.confを正しく設定しておくか、仮のアドレスで実験するのであれば、/etc/hostsにIPとアドレスの対応を記述しておきます。

次に、connect()関数を使ってサーバーにコネクトします。
/*コネクト*/
if (connect(s_listen, &desthost , sizeof(desthost) ) == -1) {
	fprintf(stderr,"Connect failed.\n");
	return -1;
}
第1引数にはソケットを指定します。第2引数には、ソケットプログラミングでよく登場する、sockaddr_in型構造体へのポインタを指定します。ここに使用プロトコル(AF_INET)、ポート番号、接続先ホスト情報をセットしておきます。コネクトに失敗した時は-1が返りますので、エラー処理をします。サーバーのポートが空いてない(サーバーがacceptで待機してない)場合は失敗になります。

接続が確立したら、read()、write()関数でデーターを送受信する事ができます。
/*文字列送信*/
bzero(buff,257);
strcpy(buff,"This program is Socket test.");
if (write(s_listen, buff, strlen(buff) ) == -1 ) {
	fprintf(stderr,"Send failed.\n");
	return -1;
}
write()関数は第1引数で指定したソケットに、第2引数のバッファから、第3引数で指定した文字数分送信します。

相手にこのパケットがいっぺんに届くとは限らず、途中経路の関係で細切れに届くことも考えられます。応答を待つ場合は、サーバープログラムでも使ったように、select()関数で応答が来るのを待ってからread()関数を使います。

通信終了は、ソケットをcloseします。
/*ソケット切断*/
close(s_listen);
通信は、接続をした側から切るのが普通だと思います。(電話と同じですね)close()関数でソケットを閉じるとEOF信号がサーバー側に伝わり、サーバープログラムはread()で0が受信されます。

Perlで作る

Cと同様に、まずはサーバーのIPアドレスを調べるルーチンを作成します。
#IPアドレスの取得方法その1
$ipaddr_bin = gethostbyname($host);
@ipaddr_arr = unpack("C4",$ipaddr_bin);
$ipaddr_str = sprintf("%u.%u.%u.%u",@ipaddr_arr);
printf("IPアドレスは[%s] \n",$ipaddr_str);
IPアドレスを調べる方法その1です。ここで、$hostnameには接続先サーバー名が文字列で入っているものとします。(例:$hostname="localhost")

gethostname関数でホスト情報をバイナリ値で取得し、unpack関数で文字列へ変換しています。この方法はPerl標準に関数だけで構成されていて、互換性に優れています。その反面、unpack関数をsprintfするなど、少しややこしかったり、ipv6になった時にsprintf文を直さないといけなかったりします。
#IPアドレス取得方法その2
$ipaddr_bin = inet_aton($host);
$ipaddr_str = inet_ntoa($ipaddr_bin);
printf("IPアドレスは[%s] \n",$ipaddr_str);
こちらの場合、Socketモジュール内の関数を使っています。inet_aton関数は、IPアドレスをバイナリ化してくれる関数なのですが、ホスト情報としてホスト名そのものを指定した場合、自動的にDNSサーバーからIPアドレスを引いてくれます。(/etc/resolv.confは正しく設定しておきましょう。)

バイナリ化したアドレスを文字列に変換するために、inet_ntoa関数を使います。「その1」のunpackしてsprintfするのと同じですが、将来的にipv6になったときに6桁化してくれる事が期待できます。

なので、Socketモジュールを使う場合は、こちらの関数を使った方が可読性はあきらかに良くなります。この章ではSocketモジュールを使う事を前提にしているため、こちらを推奨する事にします。
# ソケットオープン
socket (SOCKET, PF_INET, SOCK_STREAM, 0) || die "ソケットの作成に失敗しました $!";
connect(SOCKET, pack_sockaddr_in(9000,$ipaddr_bin)) || die "接続に失敗しました $!";
ソケットの作成は、サーバーの時と同じです。コネクトは、先に取得した接続先サーバーのIP(バイナリ化されたもの)とポート番号を合わせたものを使用します。

サーバーの所で説明した通り、pack_sockaddr_inはポート番号の上位/下位バイトの変換とIPアドレスの合成をしてくれる関数です。socket関数、connect関数共に、失敗するとundefを返すので、dieコマンドでエラーの処理を行います。
print SOCKET "テスト送信 \n";
ソケットに送信するのは簡単です。ファイルに書き込むのと同まったく同じです。printやprintf文でファイルハンドルを指定すれば良いのです。

送信は簡単ですが、サーバーからレスポンスを受信する場合、サーバープログラムと同様の処理が必要となります。
$res=<SOCKET>;
としてもいいでしょう。また、安全性を考えるのであれば、サーバーの所で説明したように、selectでタイムアウトの管理をしても良いでしょう。
close(SOCKET);
ソケットを閉じます。電話はかけた側が先に切るのがエチケットとされていますが、ソケット通信も同様、通信が終了したらコネクトした側から切断しましょう。

ソケットを閉じるとサーバー側にEOFが送信されます。サーバー側の while(<SOCKET>)関数は偽となりwhileから抜けます。また、sysread関数を使用した場合は、読み込んだバイト数として0が返ります。

サンプルプログラム

これまでのサンプル(C言語版)をダウンすることができます。

・サーバープログラム
・クライアントプログラム

このサンプルでは、ソケットの入門ということで、単純に256バイト固定で受信していますが、実用性を考えるなら、指定バイト数、または改行コードが来るまでread関数でループするように作らねばなりません。

より実用性を重視したプログラムは、次のページで紹介します。
<< 前のページ 1 2 3 次のページ >>
このページの先頭へ
  広告