システム奮闘記:その45
C言語で足し算サーバープログラムの作成
(2006年1月7日に掲載)
クライアント・サーバーとはプロセス間通信
クライアントとサーバーの違い。
教科書的には、クライアントは要求する側、サーバーは提供する側となる。
教科書的な説明 |
|
クライアントはサーバーに対して情報やデータの要求を行い、
サーバーはクライアントからの要求に応じて、返答として、
該当する情報やデータを送る。
|
だが、一般的に言われているクライアント・サーバーを、
もっと突き詰めて考えて行くと以下のようになる。
クライアント・サーバー間の通信の本質 |
|
異なるコンピューター同士のプロセス間の通信の事を指している場合が多い。
TCP/IPでの通信を実現させるのには、Linuxの場合、ソケットという
システムコールを使って実現させる。
そこで今回は、ソケットを使って足し算サーバープログラムを作るまでの
経緯を書く事にしました。
クライアント・サーバーとの出会い
クライアントとサーバーという言葉。
その出会いは古い。12年前の1994年に遡る。大学2年の時だった。
大学のUNIXが置いてある部屋で、メールとWebにハマっていたのだ。
サーバー機があり、学生が使うのはX端末だった。通称、ペケ端だ。
クライアントとサーバーの違いは、サーバー機か端末の違いという認識を
持っていた。
だが、大学3年の時に研究室配属で、Windows95が入ったパソコンでも、
FreeBSDやLinuxを使えば、サーバーが構築できる事を知る。
この時、サーバー機能は、マシンといったハードではなく、
OSで実現させる物だと思い込んだ。
FreeBSDやLinuxはサーバーOSで、Windows95はクライアントOS。
サーバーOSを使えば、パソコンでもサーバーができる。そんな認識だった。
だが、だいぶ後になり、FrontPageを使えば、Windows98でもWebサーバーが
構築できる事を知った。この時は
クライアントでもサーバーができるのか!
だった。
何せ、Winows98はクライアントOSであり、サーバーOSではないので、
サーバー機能を使う事ができるという発見は、驚きだったのだ。
ここで初めてサーバー機能は、OSではなく、サーバーソフトで
実現させる物だというのを知った。
その後、システムを触るのが好きな私は、実験的に、Windows2000Proで、
メールサーバーを構築したり。IISでWebサーバーを構築したりして、
クライアントOSのサーバー利用を試したりしていた。
Windows2000Proのメールサーバーの話は、「システム奮闘記:その32」を
ご覧ください。
そんな感じでクライアントとサーバーの違いを、紆余曲折しながらも
理解(?)していったのだった。
クライアント・サーバーのプログラムに挑戦だが
クライアント・サーバーのプログラミング。実は、結構前から
プログラミングしてみたい!
と思っていた。
2001年に「新版 応用C言語」(三田 典玄:オーム社)を購入した時、
TCP/IPプログラミングの話を触れている。
当時、「これでサーバープログラムを書けるように」と思ったのだが、
ここはいつも如く・・・
難しすぎて、わからへん (TT)
だった。
今にして覚えば、無謀な話だった。
ポインタを全く理解していないのに、返り値が「**のポインタ」という
記述を見ても、わかるわけがない。
それに、inetdを使ったプログラムだったので、当時の私に
inetdなんぞ知るわけもないのらー!!
という感じだった (^^;;
ちなみに、inetdが何なのかを知ったのは、2005年になってからだった。
詳しくは「システム奮闘記:その38」をご覧ください。
(スーパーデーモン xinetdの設定)を
そのため見事に撃沈してしまったのだった。
再びクライアント・サーバーのプログラムに挑戦
2005年になり、C言語の壁だったポインタが理解する事ができた。
詳しくは「システム奮闘記:その36」をご覧ください。
(C言語入門:ポインタ、構造体)
だが、ポインタを理解したからといって、簡単にソースが読めるわけではない。
そこで、C言語のお勉強をする事にした。
そこで眠っていた「新版 応用C言語」(三田 典玄:オーム社)を取り出した。
だが、読みはじめてみるが・・・
よぅ、わからへん (TT)
だった。
確かに説明不足が多いのだ。
そこで「こんな事もあろうかと」と言わんばかりに次の本を取り出した。
取り出したのは「Linuxプログラミング」(葛西重夫訳:ソフトバンク出版)だ。
通称、「目隠し本」だ。
2004年に、ソースを読めるようになろうと夢見て買ったのだが、
ちょこっと読んでも内容が理解できずに、眠っていた本だったのだ。
結構、引用されている「こんな事もあろうかと」 |
「こんな事もあろうかと」と言って、ドラえもんのポケットの如く
何でも発明品を出す時のセリフ。宇宙戦艦ヤマトの技師長・真田が起源らしい。
その後、アニメなどで発明好きのキャラのセリフで引用されているようだ。
赤ずきんチャチャの、やっこちゃんも使っている。
|
さて、読み進めると大きな壁が立ちはだかっていた。
シェルがわからへん (TT)
本の内容は、シェルで組んだソフトを、C言語に置き換えて行きながら
C言語を学ぶ型式になっているので、両方の言語の比較ができないと
読んでもチンプンカンプンだ。
一応、シェルの章があるのだが・・・
よくわからへん (TT)
だった。
「シェルがわからずとも、強行突破してやる!」と意気込んだものの・・・
見事にコケてしまった (TT)
何が何でも早く通信プログラムの部分を読みたいという焦りがあった。
だが、ここは急げば回れ。焦る気持ちを必死にこらえ、次の本を取り出した。
「Linux & UNIX Shellプログラミング」
(デイビッドタンズリー著:服部 由実子訳:ピアソンエデゥケーション)
分厚い本の内容を詰め込んで行く作業。受験の頃を思い出す。
第二次ベビーブームに生まれたため、受験戦争まっただ中だった。
だが、あの時は、まだ若かった。
だが、30を越えると・・・
記憶力が落ちるのらー!! (--;;
来る日も来る日も詰め込み学習。
うんざりしながら覚えていく日々。
しかも、覚えてもすぐに忘れるため、何度も同じページを見たりする。
要点をまとめてノートの書き写す。
一見、効率の悪い勉強のようだが、視覚だけの学習よりも効果があると言われる。
手を使うため、効果があるというのだ。しかし、長時間続けると・・・
腕が痺れてくる (--;;
詰め込み、詰め込み。うんざり、うんざり。
先が見えない。分厚い本との格闘に終わりが見えない。
次へ進みたいという焦りだけが募る。
「巨人の星」の星飛馬のように、「思い込んだら試練の道を〜♪」に
なっている。私のようなアンチ体育会系の人間にとって、精神論を振りかざす
スポ根は好きでないのだが、前に進むため、泣く泣く、スポ根に走る。
ついに精神科へ行くハメに (--;; |
実は、シェルの本を読む前に、アルゴリズムの本を読んだりなど、
詰め込み作業だけでなく、頭を最大限に稼働させる日が何ヶ月も続いていた。
暗記だけでなく、頭の回転も要求されるとなると、かなり頭が疲れる。
だんだんと寝つきが悪くなるは、夜中に目が覚めるわ、
精神的に不安定になるわで、ノイローゼ気味になり、
ついには精神科へ通院する事になった。
オープンソースの開発者のような凄い実力の持ち主なら大丈夫な事でも
ただの事務員が無理をすると、精神的にやられてしまうのらー!!
通院のため医療費が1万円以上かかったのが痛い (TT)
|
600ページにも及ぶシェルの本。ようやく読み終える。
そして、C言語の勉強のために目隠し本を取り出すが、これも分厚い・・・。
だが、シェルの話がわかるようになり、読みやすくなった。
それでも覚える事が多いので、詰め込み、詰め込み。うんざり、うんざり。
C言語でプロセス間通信のプログラム
ようやくプロセス間通信の話にたどり着く。
プロセス同士が情報のやりとりを行うための通信だ。
プロセス間通信 |
|
プロセス同士でデータの交換などを行うために通信を行う場合がある。
|
だが、クライアント・サーバー間の通信の話に辿り着かない。
パイプなどを使ったプロセス間通信の話だった。
うんざりした心境で、分厚い目隠し本を読んでいたため、
「もう、ええやん。はよ、マシン間の通信の話に進みたい」という心境だった。
だが、後になってわかった話だが、クライアント・サーバー間の通信は
異なるマシン同士間のプロセス間通信ならのー!!
クライアント・サーバー間の通信 |
|
いくつかのC言語の本を見ると、クライアント・サーバー間の通信の前に、
パイプなどを使ったプロセス間通信の話をとり上げている。
プロセス間通信が何なのかの学習順序として、まずは同じマシン上での、
プロセス間の通信を取り上げていると思う。
というわけで、以下のように本の内容に沿って学習した話を書いていきます (^^)
プログラムの学習の流れ |
(1) |
名前なしパイプの話 |
(2) |
名前付きパイプの話 |
(3) |
共有メモリの話 |
(4) |
ソケット通信(TCP)の話 (真打ち登場!) |
(5) |
ソケット通信(UDP)の話 (おまけ) |
C言語:パイプでプロセス間通信
さて、同じマシン上でのプロセス間通信。
プロセス同士が情報のやりとりを行うための通信なのだが、
どうやって通信を行うかが説明されている。
通信の方法として、パイプを使ったプロセス通信の説明がある。
パイプとは、コマンドの出力結果を、次のコマンドで使う場合に使う
「|」のパイプと同じ意味だ。
「 ls | grep test 」は、lsコマンドで出したファイルのリストの出力結果を
grep コマンドに渡している。
ところで、パイプを使ったプロセス間通信には2種類ある。
パイプを使った2種類のプロセス間通信 |
(1) |
名前なしパイプ |
(2) |
名前つきパイプ |
まずは、名前なしパイプの話をします。
これは親子間のプロセスで通信を行う場合に使ったりする。
名前なしパイプによるプロセス間通信 |
|
パイプを使って文字列を相手のプロセスへ送る事ができます。
パイプは単方向です。
|
さて、どんな風にプログラムを実現させていくのか、見てみる事にする。
名前なしパイプによるプロセス間通信の仕組み |
|
(1) |
親プロセスが通信を行うためのパイプを作成する。 |
(2) |
fork()を使って、子プロセスを生成。
この時、親プロセスが作成したパイプは子プロセスにも引き継がれる。
|
(3) |
子プロセスから親プロセスへデータの送信を行うため
子プロセス側の読み込みディスクリプタは不要なので閉じる。
親プロセス側の書き込みディスクリプタは不要なので閉じる。
|
(4) |
そして子プロセスから親プロセスへデータの送信を行う。
書き込みの場合は write()を使い、読み込みは read() を使う。
|
上の説明だと、不十分なので、実際にプログラムにすると次のようになる。
子プロセスから親プロセスへ文字列を送るプログラム |
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
pid_t pid ;
char buffer[256];
int pp[2];
/* パイプの作成 */
pipe(pp);
pid = fork(); /* 子のプロセスの生成 */
/* 子プロセスの動き */
if ( pid == 0 )
{
close(pp[0]);
fgets(buffer,256,stdin);
write(pp[1],buffer,strlen(buffer)+1); /* パイプへ書き込み */
close(pp[1]);
}
/* 親プロセスの動き */
else
{
close(pp[1]);
read(pp[0],buffer,256); /* パイプから読み込み */
printf("Message From Child : %s\n",buffer);
close(pp[0]);
}
return(0);
}
|
さて、実行結果は
実行結果 |
suga@jitaku[~/pipe]% ./pipe1
test message ← 子プロセスで文字列を入力
Message From Child : test message ← 子プロセスから送られた文字列を親プロセスで出力
suga@jitaku[~/pipe]%
|
親子間でプロセス間通信ができた (^^)
本の丸写しなので、できて当然なのだが、うまく動いた時は、うれしいのだ。
エラー処理について |
プログラムを見て「エラー処理をしていないではないか」という
ツッコミをいただくかもしれません。
本来ならエラー処理をいれる必要があると思いますが、悩みました。
エラー処理を省いた方がソースが見やすくなるのではないかと考え、
敢えて、省く事にしました。
|
ところで、上の例だと、子プロセスから親プロセスにしか文字列を
送れないように思える。確かに、パイプは一方通行だ。
だが、パイプを、もう一本増やす事で、親プロセスから子プロセスへ
文字列を送信する事もできる。
パイプを2本にする事で双方向のデータ送信が可能になる |
|
これで親子プロセス間で、クライアント・サーバー通信が可能になる。
さて、上図の事を応用したら親子プロセス間で通信ができる。
そこで、親子のプロセス間で、クライアント・サーバーにみたてて
以下の事をプログラムしてみる事にした。
足し算サーバープログラム |
|
子プロセスから足し合わせたい数字を文字列の形にして親プロセスに送る。
親プロセスは、足し合わせた結果を文字列にして、子プロセスに送り
子プロセスは結果を表示させる。
|
以下のプログラムソースを書いてみた。
足し算サーバープログラム (pipe2.c) |
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
pid_t pid ;
char buffer[10],result[10];
int pp[2] , qq[2];
int a , b , c ;
pipe(pp); /* 親へデータを送るためのパイプ */
pipe(qq); /* 計算結果を子に送るためのパイプ */
pid = fork(); /* 親子のプロセスの生成 */
/* 子プロセスの動き */
if ( pid == 0 )
{
close(pp[0]);
close(qq[1]);
/* 1つ目の変数の代入とパイプへの書き込み */
printf("a = ");
fgets(buffer,10,stdin);
write(pp[1],buffer,strlen(buffer)+1);
/* 2つ目の変数の代入とパイプへの書き込み */
printf("b = ");
fgets(buffer,10,stdin);
write(pp[1],buffer,strlen(buffer)+1);
/* 計算結果を読み込む */
read(qq[0],result,10);
printf("a + b = %s\n",result);
close(qq[0]);
close(pp[1]);
}
/* 親プロセスの動き */
else
{
close(qq[0]);
close(pp[1]);
/* 1つ目の変数の読み込み */
read(pp1[0],buffer,256);
a = atoi(buffer);
/* 2つ目の変数の読み込み */
read(pp2[0],buffer,256);
b = atoi(buffer);
c = a + b ; /* 計算 */
sprintf(buffer,"%d",c);
/* 計算結果をパイプへ書き込む */
write(qq[1],buffer,strlen(buffer)+1);
close(pp[0]);
close(qq[1]);
}
return(0);
}
|
さて、実行結果は
実行結果 |
suga@jitaku[~/pipe]% ./pipe2
a = 5 ← 子プロセスで文字列を入力
b = 3 ← 子プロセスで文字列を入力
suga@jitaku[~/pipe]% a + b = 8 ← 親プロセスでの計算結果を子プロセスが表示
|
見事、成功! (^^)
読者の中で、勘の鋭い人は、ちょっとパイプの勉強をしただけで
足し算サーバープログラムが書けるのかと疑問に思う方がおられると思います。
実は、正直な事を書きますと・・・
足し算サーバープログラムは編集中に書きました (^^;;
そうなのです。C言語の勉強の過程ではなく、これを編集している時に
知識の整理を兼ねて書いたプログラムなのです。
ミエを張ってカッコ良く「勉強中にスラスラとソースを書いた」と書きたいが
そんなウソを書いても、すぐにバレる (^^;;
実際の所、名前なしパイプの勉強していた時点では、子プロセスから
親プロセスへ文字列を送る事ができたのを見て「へぇ〜」と思ったぐらいなのです。
C言語:名前付きパイプでプロセス間通信
名前なしのパイプの次は、名前付きのパイプの話になる。
実は、名前なしのパイプの場合、親子プロセス間しか通信できない。
それも親プロセスがパイプを作成して、子プロセスに引き継いだ場合のみだ。
というと限定されたケースになってくる。
だが、親子関係のないプロセスでの通信を行う事もある。
どっちかというと、そういう場合が大半だと思う。
その場合に対処するため、FIFOという特別なファイルを経由した方法がある。
それを「名前付きのパイプ」という方法だ。
名前付きパイプによるプロセス間通信 |
|
上図のように、プロセスAはFIFOファイルに送りたいデータを書き込み
プロセスBは、受信データを取得するため、FIFOファイルを読み込む
という形をとる。
|
要するに、FIFOファイルを仲介させて、データのやりとりを行うという。
さて、FIFOファイルの作成方法だが、以下のように作成する。
FIFOファイルの作成方法 |
|
mkfifo()関数を使って、FIFOファイルを作成する。
|
さて、データを送信する側の設定を見てみる。
データを送る側の設定 |
|
FIFOファイルに書き込むために、まずはファイルオープンを行う。
この場合、低水準関数のopen()を使う。
書き込む際には、低水準のwrite()関数を使うのだが、
既に、FIFOファイルにデータがある場合は、他のプロセスによって
FIFOのデータが読み込まれ、データがなくなるまで
書き込みを待つ仕組みになっている。
|
そして、データを受信する側の設定なのだが
データの受信側の設定 |
|
FIFOファイルに読み込むために、まずはファイルオープンを行う。
この場合も、低水準関数のopen()を使う。
読み込む際には、低水準のwrite()関数を使うのだが、
まだ、FIFOファイルにデータが書き込まれていない場合は、
FIFOファイルにデータが書き込まれるまで待つ仕組みになっている。
|
以上、FIFOファイルの作成、書き込み、読み込みの方法を使って
実際にプログラムを作ってみる事にした。
送信側のプログラム (fifo-cli1.c) |
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
int fifodes ;
char buffer[256];
/* FIFO ファイルをオープンにする */
fifodes = open("my_fifo",O_WRONLY);
fgets(buffer,256,stdin);
/* データを送信する */
write(fifodes,buffer,strlen(buffer)+1);
close(fifodes);
return(0);
}
|
受信側のプログラム (fifo-ser1.c) |
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
int fifodes ;
char buffer[256];
/* 「my_fifo」という名のFIFOファイルの作成 */
mkfifo("my_fifo",0777);
/* FIFO ファイルをオープンにする */
fifodes = open("my_fifo",O_RDONLY);
/* データを受信する */
read(fifodes,buffer,256);
printf("Message: %s",buffer);
close(fifodes);
return(0);
}
|
さて、プログラムの結果は以下のようになった。
プログラムの実行結果 |
送信側の様子 |
suga@jitaku[~/pipe]% ./fifo-cli1
Test data! ← 送信した文字列
suga@jitaku[~/pipe]%
|
受信側の様子 |
suga@jitaku[~/pipe]% ./fifo-ser1
Message: Test data! ← 受信した文字列
suga@jitaku[~/pipe]%
|
見事、送信ができた (^^)
さて、FIFOファイルを介して通信を行うのだが、実際に
どんなファイルが生成されたのかを目で確かめる事にした。
FIFOファイルが生成された様子 |
prwxr-xr-x 1 suga users 0 1月 2日 13:05 my_fifo|
|
パーミッションの部分の「p」はパイプファイル(FIFO)を意味する。
そして、ファイル名の後ろに「|」もパイプファイル(FIFO)を意味する。
|
ところで、お互いのプロセス同士の通信を行う場合は、これだとダメだ。
名前なしパイプの時と同様、FIFOファイルの場合も一方通行だからだ。
そこで双方向の通信を行うのには、以下のように、もう1つのFIFOファイルを
用意する必要がある。
名前付きパイプによるプロセス間通信 |
|
さて、お勉強の時は、ここで終わったのだが、編集している時に、
FIFOファイルを使った足し算サーバーのプログラムを書いてみようと思った。
足し算サーバープログラム |
|
プロセスAから足し合わせたい数字を文字列の形にして、プロセスBに送る。
プロセスBは、足し合わせた結果を文字列にして、プロセスAに送り返し
プロセスAは結果を表示させる。
|
さて、上図のような事を行うプログラムを書いてみた。
足し算サーバープログラム |
クライアント側のプログラム (fifo-cli2.c) |
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
int des1,des2 ;
int a , b ;
char buffer[256], result[10];
des1 = open("fifo1.dat",O_WRONLY);
des2 = open("fifo2.dat",O_RDONLY);
/* サーバーへ値を送信:その1 */
printf("a = ");
fgets(buffer,256,stdin);
write(des1,buffer,strlen(buffer)+1);
/* サーバーへ値を送信:その2 */
printf("b = ");
fgets(buffer,256,stdin);
write(des1,buffer,strlen(buffer)+1);
/* サーバーから計算結果を受信 */
read(des2,result,10);
printf("a + b = %s\n",result);
close(des1);
close(des2);
return(0);
}
|
サーバー側のプログラム (fifo-ser2.c) |
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
int des1,des2 ;
int a , b, c ;
char buffer[256] , result[10] ;
mkfifo("fifo1.dat",0777); /* サーバーへ送信用のFIFO */
mkfifo("fifo2.dat",0777); /* クライアントへ送信用のFIFO */
des1 = open("fifo1.dat",O_RDONLY);
des2 = open("fifo2.dat",O_WRONLY);
/* クライアントからの受信:その1 */
read(des1,buffer,256);
a = atoi(buffer);
/* クライアントからの受信:その2 */
read(des1,buffer,256);
b = atoi(buffer);
c = a + b ;
sprintf(result,"%d",c);
/* クライアントへ計算結果を送信 */
write(des2,result,strlen(result)+1);
close(des1);
close(des2);
return(0);
}
|
さて、プログラムの実行結果は・・・
足し算サーバープログラムの実行結果 |
クライアント側の様子 |
suga@jitaku[~/pipe]% ./fifo-cli11
a = 5 ← 送信した文字列
b = 3 ← 送信した文字列
a + b = 8 ← サーバーから返ってきた結果
suga@jitaku[~/pipe]%
|
サーバー側の様子 |
suga@jitaku[~/pipe]% ./fifo-ser12
suga@jitaku[~/pipe]%
|
見事、プログラムが動いた (^^)V
さて、他にも編集中にわかった話なのだが、FIFOファイルをopen()関数で
オープンさせる時、ノンブロック(O_NONBLOCK)というオプションを付ける
場合がある。
名前つきパイプのお勉強の際、このオプションの意味がわからなかった。
そのため「難しい話やなぁ。一体、何が違うねん」と思ったのだが、
実は、難しい話ではなかった。
読み込み専用にノンブロック(O_NONBLOCK)を付けた場合 |
|
ノンブロック(O_NONBLOCK)を付けると、FIFOファイルの中身がない場合、
データの取得を諦めて終わらせる働きをする。
ノンブロックなので、FIFOファイルに中身がなければ
FIFOファイルをブロックしないで読み込みを諦めるという話だ。
|
書き込みの場合は以下のようになる。
書き込み専用にノンブロック(O_NONBLOCK)を付けた場合 |
|
ノンブロック(O_NONBLOCK)を付けると、FIFOファイルの中身がある場合、
書き込みを諦めて終わらせる働きがある。
ノンブロックなので、FIFOファイルに中身があれば、
FIFOファイルをブロックしないで書き込みを諦めるという話だ。
|
C言語:共有メモリでプロセス間通信
名前なしパイプ、名前付きパイプのプロセス間通信の話を勉強した。
だが、まだ、異なるパソコン同士の通信の話に辿り着かない。
勉強する身としては、気が遠くなり、疲れてくるのだが、我慢の子。
本にしたがって、IPCセマフォと、共有メモリを読む。
IPCセマフォは省略して、共有メモリの話を書きます。
共有メモリとは、読んで字の如く共有されたメモリ領域なのだ。
共有メモリによるプロセス間通信 |
|
プロセス同士で共有できるメモリ領域を確保できる。
この共有メモリは、カーネルが確保しているメモリ領域の部分を使っている。
|
さて、実際のプログラムの流れですが、以下のように共有メモリを
確保します。
プログラムで共有メモリを確保するためには |
|
まず最初に、共有メモリにアクセスするための共通キーを決める。
そして、その共通キーをshmget()関数に代入し、識別子を得る。
次に、その識別子をshmat()関数に代入して、確保した共有メモリの
先頭のアドレスを得る。
共有メモリを使い終えると、shmdt()関数で、使い終えたのを知らせる。
|
共通キーを決めながら、なんでわざわざ識別子をを求めるのか、
最初から共通キーを代入すれば、アドレスが得られれば、わかりやすいのにと
思いながらも、共有メモリ確保のための実装がどうなっているのか
全く知らないので、深い訳があるのだなぁで、終わらせます。
もし、調べろというのであれば、こう反論します。
事務員に難しい話は、わからないもーん (^^)V
さて、実際に関数の使い方を見ていく事にします。
shmget()関数の使い方 |
|
shmget()関数には、キー、確保するメモリの量、パーミッションの
3つの引数がある。
パーミッションの設定だが、chmodコマンドの時に使う数字の設定と同じだ。
ただ、新しく共有メモリを生成する場合は、「IPC_CREAT」を付ける必要がある。
もし、既に共有メモリがあった場合は、「IPC_CREAT」は無視されるため、
「IPC_CREAT」は、おまじないという感じで付けておくのが無難かも。
|
shmat()関数の使い方 |
|
shmat()関数には、識別子と、残り2つの引数がある。
通常は、2つ目の引数の所はヌルポインタ、3つ目は「0」をいれる。
なぜそうなのかと聞かれても、私も、わからない (^^;;
プログラマーでも何でもない、タダの事務員なので、許してね。
この関数の返り値は、共有メモリの先頭アドレス(void型)だ。
|
shmdt()関数の使い方 |
|
shmdt()関数は、共有メモリを使い終えた時に使う関数。
|
そこで、共有メモリのプログラムを作ってみる事にした。
共有メモリを使ったデータ受け渡しのプログラム |
クライアント側のプログラム (mem-cli1.c) |
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct transfer_data {
int no ;
char name[10];
int flag ; /* 0:未入力 1:入力済み 2:サーバーで出力済み */
}; /* 共有メモリに渡すデータの構造 */
int main(void)
{
char no[10] ;
int shmid ;
void *shared_mem ;
/* 共有メモリのアドレスを格納する変数の宣言 */
struct transfer_data *mem ;
shmid = shmget( (key_t)12345 , 100 , 0777 | IPC_CREAT );
shared_mem = shmat(shmid,(void *)0 ,0 );
/* 取得した共有メモリのアドレスを変数 mem へ代入 */
mem = (struct transfer_data *)shared_mem ;
mem->flag = 0 ;
printf("No = ");
fgets(no,10,stdin);
mem->no = atoi(no);
printf("name = ");
fgets(mem->name,10,stdin);
mem->flag = 1 ;
while(1)
{
if ( mem->flag == 2 ) break ;
}
shmdt(shared_mem);
return(0);
}
|
サーバー側のプログラム (mem-ser1.c) |
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct transfer_data {
int no ;
char name[10];
int flag ; /* 0:未入力 1:入力済み 2:サーバーで出力済み */
}; /* 共有メモリに渡すデータの構造 */
int main(void)
{
int shmid ;
void *shared_mem ;
/* 共有メモリのアドレスを格納する変数の宣言 */
struct transfer_data *mem ;
shmid = shmget( (key_t)12345 , 4096 , 0777 | IPC_CREAT );
shared_mem = shmat(shmid,(void *)0 ,0 );
/* 取得した共有メモリのアドレスを変数 mem へ代入 */
mem = (struct transfer_data *)shared_mem ;
while(1)
{
if ( mem->flag == 1 )
{
printf("No : %d\n",mem->no);
printf("name : %s\n",mem->name);
mem->flag = 2 ;
}
}
return(0);
}
|
さて、プログラムの実行結果は・・・
プログラムの実行結果 |
クライアント側の様子 |
suga@jitaku[~/pipe]% ./mem-cli1
No = 10 ← 送信した数値
name = ono-mayumi ← 送信した文字列
suga@jitaku[~/pipe]%
|
サーバー側の様子 |
suga@jitaku[~/pipe]% ./mem-ser1
No : 10 ← 受信した数値
name : ono-mayum ← 受信した文字列
|
プログラム成功 (^^)V
プログラムのお勉強中の時は、ここで終わった。
だが、共有メモリを使って「足し算サーバー」もできる。
そこで、またまた編集中に知識の整理も兼ねて、共有メモリを使った
「足し算サーバー」を作ってみた。
共有メモリを使った足し算サーバープログラム |
|
共有メモリを通じて、データの受け渡しを行う。
サーバー側で計算結果を代入して、クライアントへ送り返す。
|
プログラムソースは以下のようになった。
プログラムソース |
クライアント側のプログラム (mem-cli2.c) |
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct transfer_data {
int a ;
int b ;
int c ;
int flag ; /* 0:未入力 1:入力済み 2:サーバーで計算済み */
}; /* 共有メモリに渡すデータの構造 */
int main(void)
{
char a[10] , b[10] ;
int shmid ;
void *shared_mem ;
struct transfer_data *mem ;
shmid = shmget( (key_t)12345 , 100 , 0777 | IPC_CREAT );
shared_mem = shmat(shmid,(void *)0 ,0 );
mem = (struct transfer_data *)shared_mem ;
mem->flag = 0 ;
printf("a = ");
fgets(a,10,stdin);
mem->a = atoi(a);
printf("b = ");
fgets(b,10,stdin);
mem->b = atoi(b);
mem->c = 0 ;
mem->flag = 1 ;
while(1)
{
if ( mem->flag == 2 ) break ;
}
printf("a + b = %d\n",mem->c);
shmdt(shared_mem);
return(0);
}
|
サーバー側のプログラム (mem-ser2.c) |
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct transfer_data {
int a ;
int b ;
int c ;
int flag ; /* 0:未入力 1:入力済み 2:サーバーで計算済み */
}; /* 共有メモリに渡すデータの構造 */
int main(void)
{
int shmid ;
void *shared_mem ;
struct transfer_data *mem ;
shmid = shmget( (key_t)12345 , 4096 , 0777 | IPC_CREAT );
shared_mem = shmat(shmid,(void *)0 ,0 );
mem = (struct transfer_data *)shared_mem ;
while(1)
{
if ( mem->flag == 1 )
{
printf("a = %d\n",mem->a);
printf("b = %d\n",mem->b);
printf("只今 %d + %d を計算中\n",mem->a,mem->b);
mem->c = mem->a + mem->b ;
mem->flag = 2 ;
}
}
return(0);
}
|
さて、プログラムの実行結果は・・・
プログラムの実行結果 |
クライアント側の様子 |
suga@jitaku[~/pipe]% ./mem-cli2
a = 5 ← 送信した数値
b = 3 ← 送信した数値
a + b = 8 ← 受信した計算結果
suga@jitaku[~/pipe]%
|
サーバー側の様子 |
suga@jitaku[~/pipe]% ./mem-ser2
a = 5
b = 3
只今 5 + 3 を計算中
|
プログラム成功 (^^)V
C言語:ソケットを使ったネットワークプログラム
七転八倒の末、ソケット通信、即ち、異なるマシン間の通信の章に到着した。
ボーイング747のエンジンの4つのうち、3つが停止して、フラフラの状態で
不時着した感じだった。
だが待っていたのは・・・
ソケット通信の説明がわからへん (TT)
唯一わかったのは、ソケットを使った通信でも、同じマシン内のプロセス間通信と
異なるマシン同士のプロセス間通信の2つある事だ。
本の説明では、最初のプログラムの部分は、同じマシン内のプロセス間通信が
書いてあった。
さて、意味はわからずとも、本の丸写しでプログラムを入力していくと
内容が理解できるだろうと考え、本のプログラムを入力していった。
ソケットを使った通信プログラム。本の丸写し |
クライアント側のプログラム (client1.c) |
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
int main(void)
{
int sockfd ;
int len ;
struct sockaddr_un address ;
int result ;
char ch = 'A' ;
sockfd = socket(AF_UNIX,SOCK_STREAM,0);
address.sun_family = AF_UNIX ;
strcpy(address.sun_path , "server_socket");
len = sizeof(address);
result = connect(sockfd , (struct sockaddr *)&address , len);
if ( result == -1 ) {
perror("oops: client1");
exit(1);
}
write(sockfd,&ch,1);
read(sockfd,&ch,1);
printf("char from server = %c \n",ch);
close(sockfd);
exit(0);
}
|
サーバー側のプログラム (server1.c) |
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
int main(void)
{
int server_sockfd , client_sockfd ;
int server_len , client_len ;
struct sockaddr_un server_address ;
struct sockaddr_un client_address ;
unlink("server_socket");
server_sockfd = socket(AF_UNIX,SOCK_STREAM,0);
server_address.sun_family = AF_UNIX ;
strcpy(server_address.sun_path , "server_socket");
server_len = sizeof(server_address);
bind(server_sockfd , (struct sockaddr *)&server_address , server_len);
listen(server_sockfd , 5);
while(1) {
char ch ;
printf("server waiting\n");
client_sockfd = accept(server_sockfd ,
(struct sockaddr *)&client_address , &client_len);
read(client_sockfd,&ch,1);
ch++;
write(client_sockfd,&ch,1);
close(client_sockfd);
}
}
|
使っている関数など、わけもわからず、ただ本の丸写しを行った。
プログラムの内容はクライアントが「A」という文字列を送ると
サーバー側は隣りの文字コードも文字列(この場合「B」)を送り返すという物だ。
さて、いざ動かしてみると・・・
プログラムの実行結果 |
クライアント側の様子 |
suga@jitaku[~/pipe]% ./client1
char from server = B ← サーバーから受信した文字列
suga@jitaku[~/pipe]%
|
サーバー側の様子 |
suga@jitaku[~/pipe]% ./server1
server waiting
server waiting
|
プログラム成功 (^^)V
まぁ、本の丸写しなので、動いて当り前なのだが (^^;;
だが、これだと異なるマシン同士の通信ができない。
なぜなら、同じマシンでのプロセス通信だからだ。
そこで、本の内容を見よう見まねで、異なるマシン間の通信のプログラムに
書き換えてみる事にした。これこそ試行錯誤。(単なる「当てずっぽ」とも言う)
まずは、socket()関数の設定を変えるという。
socket() ソケット関数の設定 |
同じマシンのプロセス間通信の場合 |
sockfd = socket(AF_UNIX,SOCK_STREAM,0);
|
異なるマシン同士のプロセス間通信の場合 |
sockfd = socket(AF_INET,SOCK_STREAM,0);
|
実は、AF_INETでも、同じマシン内のプロセス間通信にも使える。
AF_UNIXの場合、通信を行うのにソケットファイルを生成する。
まるで、名前付きパイプの時の通信の際に生成されるFIFOファイルのようだ。
AF_UNIXの場合、通信方法 |
|
FIFOファイルと同様に、通信を仲介するためのファイルが生成される。
FIFOファイルとの違いは、FIFOの場合、一方通行だったのに対して
ソケットファイルは、1つのファイルで双方向の通信が可能だ。
|
さて、実際にソケットファイルが生成された様子を見てみる。
ソケットファイルが生成された様子 |
srwxr-xr-x 1 suga users 0 1月 2日 11:55 server_socket=
|
パーミッションの部分の「s」はソケットファイルを意味する。
そして、ファイル名の後ろに「=」もソケットファイルを意味する。
|
他の部分は、適当に、本のプログラムを参考に、プログラムを触ってみた。
その結果が次のソースになった。
本を参考にして書いたプログラム |
クライアント側のプログラム (client2.c) |
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(void)
{
int sockfd ;
int len ;
struct sockaddr_in address ;
int result ;
char ch = 'A' ;
sockfd = socket(AF_INET,SOCK_STREAM,0);
address.sin_family = AF_INET ;
address.sin_addr.s_addr = inet_addr("192.168.1.10");
address.sin_port = 9734 ;
len = sizeof(address);
result = connect(sockfd , (struct sockaddr *)&address , len);
if ( result == -1 ) {
perror("oops: client2");
exit(1);
}
write(sockfd,&ch,1);
read(sockfd,&ch,1);
printf("char from server = %c \n",ch);
close(sockfd);
exit(0);
}
|
サーバー側のプログラム (server2.c) |
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(void)
{
int server_sockfd , client_sockfd ;
int server_len , client_len ;
struct sockaddr_in server_address ;
struct sockaddr_in client_address ;
server_sockfd = socket(AF_INET,SOCK_STREAM,0);
server_address.sin_family = AF_INET ;
server_address.sin_addr.s_addr = inet_addr("127.0.0.1");
server_address.sin_port = 9734 ;
server_len = sizeof(server_address);
bind(server_sockfd , (struct sockaddr *)&server_address , server_len);
listen(server_sockfd , 5);
while(1) {
char ch ;
printf("server waiting\n");
client_sockfd = accept(server_sockfd ,
(struct sockaddr *)&client_address , &client_len);
read(client_sockfd,&ch,1);
ch++;
write(client_sockfd,&ch,1);
close(client_sockfd);
}
}
|
さて、実行結果は・・・
見事、成功! (^^)V
プログラムの実行結果 |
クライアント側の様子 |
suga@jitaku[~/pipe]% ./client2
char from server = B ← サーバーから受信した文字列
suga@jitaku[~/pipe]%
|
サーバー側の様子 |
suga@jitaku[~/pipe]% ./server2
server waiting
server waiting
|
ようやく辿り着いた通信プログラムだ。
だが、ここで喜ぶのは、まだ早い。
なぜなら、プログラムの丸写しと、適当に書き換えしただけであって、
関数の使い方などを理解したわけではないからだ。
しかし、目隠し本を見ても、よくわからんし、理解するために
ソースの丸写しをした物の、ぼんやりとした感じにすぎなかった。
単に、こんな関数を使ったなぁという程度しか見えてこない。
そこで「こんな事もあろうかと」という感じで、次の本を取り出した。
「オペレーティングシステム」(野口健一郎:オーム社)
すると、ソケットを使った通信の流れが、わかりやすく書いた図が載っていた。
ソケットを使った通信プログラムの流れ (コネクション型の場合) |
|
コネクション型(TCP方式)の時の、プログラムの流れです。
コネクションレス型(UPD方式)の場合は、違う方法になります。
こういう図があると、全体の流れが見えるので、わかりやすい。
|
なんだか、すっきりした感じだ (^^)
さて、実際に自由自在に関数を使えるようになるには、
関数の使い方を理解しないといけない。
そこで、それぞれの関数について、見ていく事にした。
まずはソケット作成から見ていく事にします。
ソケットの概念図 |
|
クライアント、サーバー共に通信を行うための窓口を作成します。
|
では、実際にソケットを作成する、socket()関数を見てみる。
ソケットを生成するsocket()関数について |
|
socket()は、ドメイン、タイプ、プロトコルの3つの引数を使って
ソケットディスクリプタを返す。
|
さて、ドメイン、タイプ、プロトコルの3つが出てきましたが、
その意味は以下の通りです。
socket()関数の引数について |
ドメイン |
通常、使うのは、「AF_UNIX」、「AF_INET」の2種類。
AF_UNIXは、ソケットファイルを作成して
同じマシン同士での通信の時に選ぶ。
AF_INETは、異なるマシン同士の通信を行う時に選ぶ。
|
タイプ (通信手段) |
ドメインで「AF_INET」を選ばれた場合、
TCP通信の場合、SOCK_STREAMを指定する。
UDP通信の場合、SOCK_DGRAMを指定する。
ドメインで「AF_UNIX」を選ばれた場合は、
無条件でSOCK_STREAMを指定する。
|
プロトコル |
複数のプロトコルを使う場合に、該当のプロトコルを指定する。
しかし、通常は、複数のプロトコルを使わないので「0」を代入する。
|
という事で、もし、異なるマシン同士の通信で、TCP通信にする場合は
socket_d = socket(AF_INET,SOCK_STREAM,0) ;
とします。
int型の引数なのに、なんで文字列があるのと思われた方、鋭いです。
私も、同じ疑問を持ちました (^^)
実は、マクロ定義しているのです。
/usr/inclue/sys/socket.hのヘッダーを追いかけましたら、ありました。
/usr/include/bits/socket.hのヘッダーファイルに!
最初からマクロでなく数字でも、ええやんと思いたくなったのだが、
バージョンが変わるたびに、数字が変わったりすると互換性の問題もある。
それを吸収するのだと思う。人間にわかりやすくする配慮もあると思う。
さて、socket()関数だけでは、ソケットを生成しただけであって、
ソケットに名前をつけないと、他のプロセスが認識できません。
さて、次にサーバー側の設定で、ソケットの名前をつける関数として、
bind()の紹介をします。
まずは、bind()が何を行うかの概念を図にしてみた。
bind()の概念 |
|
クライアントのプロセスからサーバー側のソケットに接続するために
目印になるような名前をつけます。
この時、クライアントには何も設定しません。
|
bind()関数の話。ここで暴露。
bind()関数はDNSの関連の関数だと思い込んでいました (^^)
そうなのです。「bind」という文字を見れば、DNSだと思いたくなるのです。
最初、システムコールに関する本を見た時、ネットワークの部分で
bind()関数が書いているだけに、勘違いをしてしまったのらー!!
閑話休題。
さてさて、bind()関数の使い方を見ていきます。
bind()関数について |
|
引数に、ソケットディスクリプタ、アドレス構造体のポインタ、
アドレス構造体の長さがあります。
そして、ソケットに名前をつけるのに成功したら「0」を返します。
失敗の場合は「-1」を返します。
|
さて、ソケットディスクリプタは、socket()関数の返り値なのだが、
残り2つのアドレス構造体のポインタと、その長さは、一体、何だろうか。
そこで目隠し本を見てみる事にした。
通信を行うために、ソケットの特徴などを記述した物なのだ。
その記述するデータの型(構造体)を、アドレス構造体というのだ。
アドレス構造体の中身は、bind()関数以外にも、この後に出てきます
クライアントがサーバーに接続要求するための、connect()関数にも使われるし、
サーバー側で接続受け入れのための、accept()関数にも使われる。
ややこしいのは、AF_UNIXとAF_INETでは、構造体の中身が違う
そこでまずはAF_UNIXのケースを見ていく。
AF_UNIXの場合のアドレス構造体 |
struct sockaddr_un
{
sa_family_t sun_family ;
char sun_path[]; /* Path name. */
};
|
sun_familyのメンバには「AF_UNIX」を入れる
sun_path[]のメンバには、ソケットファイルのファイル名を入れる
この構造体は、sys/un.h のヘッダーで定義されているため、
AF_UNIXのソケット通信を行う際にはプログラムに
#include <sys/un.h> をいれて、ヘッダーを取り込む必要がある。
|
次にAF_INETの場合を見ていく。
AF_INETの場合のアドレス構造体 |
struct sockaddr_in
{
short int sin_family ;
int sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
};
|
sin_familyのメンバには「AF_INET」を入れる。
sin_portのメンバには、通信ポート番号を入れる。
sin_addrのメンバには、IPアドレス(32ビット)を入れる
IPを32ビットの数値に変換させる関数があります(後述しています)
ちなみに、このIPアドレスは、クライアント側の場合は接続先のIPを入れ
サーバー側の場合は、サーバー自身のIPを入れる。
サーバー側のIPについては、詳しい事は後述しています。
この構造体は、netinet/in.h のヘッダーで定義されているため、
AF_INETのソケット通信を行う際にはプログラムに
#include <netinet/in.h> をいれて、ヘッダーを取り込む必要がある。
|
ちなみに、2種類のアドレス構造体ですが、ここで取り上げた以外にも
あるかもしれません。バージョン等の関係で。
でも、ここで取り上げたので問題なく使えます。
さて、具体的に、アドレス構造体への代入と、bind()関数の利用法を
見てみる事にします。
まずは、ソケットファイルを使った同じマシン同士の通信の場合です。
bind()関数の使い方(ソケットファイルを使った通信の場合) |
/* アドレス構造体の定義したヘッダー */
#include <sys/un.h>
/* アドレス構造体の変数を宣言 */
struct sockaddr_un server_address ;
/* アドレス構造体に代入 */
server_address.sun_family = AF_UNIX ;
strcpy(server_address.sun_path , "server_socket"); /* ソケットファイル名 */
/* アドレス構造体の長さ */
server_len = sizeof(server_address);
/* bind()関数の使い方 */
bind(server_sockfd , (struct sockaddr *)&server_address , server_len);
|
次に、異なるマシン同士の通信の場合を見てみます。
bind()関数の使い方(異なるマシン同士の通信の場合) |
/* アドレス構造体の定義したヘッダー */
#include <netinet/in.h>
/* アドレス構造体の変数を宣言 */
struct sockaddr_in server_address ;
/* アドレス構造体に代入 */
server_address.sin_family = AF_INET ;
server_address.sin_addr.s_addr = inet_addr("127.0.0.1"); /* IPアドレス */
server_address.sin_port = 9734 ; /* ポート番号 */
/* アドレス構造体の長さ */
server_len = sizeof(server_address);
/* bind()関数の使い方 */
bind(server_sockfd , (struct sockaddr *)&server_address , server_len);
|
さて、IPアドレスの32ビットに置き換える関数がでてきました。
inet_addr()関数です。これを使えば、IPアドレスを32ビットの数値に
変換してくれます。
次に、サーバー側で行う設定で、クライアント側からの接続があり、
処理待ち(接続待ち)を何個にするかの設定です。
listen()関数を使います。
listen()関数について |
|
最大処理待ちの数を代入します。
本では「5」が一般的と書いていますので、ここではとりあえず「5」にします。
返り値は成功した場合は「0」、失敗の場合は「-1」です。
使い方は、そのまま代入するだけです。
listen(socket_d , 5);
|
ここまでで、接続の準備完了です。ふぅ、結構、大変だ。
だが、接続を行うのに、2つの関数が残っています。
クライアント側のconnect()関数とサーバー側のaccept()関数です。
connect()関数について |
|
さきほどから出ている、アドレス構造体と、その長さを代入します。
返り値は「0」は成功、「-1」は失敗です。
|
これでクライアントは、サーバーへ接続要求を行います。
次に、接続要求受け入るサーバー側の設定です。accept()関数を使います。
accept()関数について |
|
2番目のアドレス構造体ですが、サーバー側のアドレス構造体ではありません。
connect()で接続要求してきたクライアントの情報が入ったアドレス構造体です。
アドレス構造体の長さも、クライアントのアドレス構造体の長さです。
返り値は、クライアントとの通信のためのディスクリプタです。
|
クライアントがconnect()を使って接続要求した時と、
サーバーがaccept()で接続を受け入れた様子を図にしてみますと
次のようになります。
接続要求の様子 |
|
connetc()を使ってクライアントが接続要求を行います。
そして、サーバーがaccept()で受け入れを行った時、
サーバー側で通信用のソケットが生成され、通信用のソケットを使い
実際のデータ通信が行われます。
そして、前からあったソケットは、次の接続要求の待ち受けを行います。
|
これで、ソケット通信のプログラムの方法がわかった。
すっかり気を良くした私は、次のように考えた。
サーバーソフトを作成してみよう!
その時、思い付いたのが「足し算サーバープログラム」だった。
足し算サーバープログラム |
|
異なるマシン同士の通信を使って、足し算の計算をさせる。
|
ちなみに、パイプ、共有メモリの時と違い、編集中に知識の整理で
プログラムの作成したのではなく、実際の、お勉強の時に作成しました。
足し算サーバープログラム |
クライアント側のプログラム (client3.c) |
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(void)
{
int sockfd ;
int len ;
struct sockaddr_in address ;
int result ;
int ch[3] ;
sockfd = socket(AF_INET,SOCK_STREAM,0);
address.sin_family = AF_INET ;
address.sin_addr.s_addr = inet_addr("192.168.1.10");
address.sin_port = htons(9734) ;
len = sizeof(address);
result = connect(sockfd , (struct sockaddr *)&address , len);
if ( result == -1 ) {
perror("oops: client3");
exit(1);
}
printf("A = ");
scanf("%d",&ch[0]);
printf("B = ");
scanf("%d",&ch[1]);
write(sockfd,ch,sizeof(ch));
read(sockfd,ch,sizeof(ch));
printf("A + B = %d \n",ch[2]);
close(sockfd);
exit(0);
}
|
サーバー側のプログラム (server3.c) |
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(void)
{
int server_sockfd , client_sockfd ;
int server_len , client_len ;
struct sockaddr_in server_address ;
struct sockaddr_in client_address ;
server_sockfd = socket(AF_INET,SOCK_STREAM,0);
server_address.sin_family = AF_INET ;
server_address.sin_addr.s_addr = INADDR_ANY ;
server_address.sin_port = htons(9734) ;
server_len = sizeof(server_address);
bind(server_sockfd , (struct sockaddr *)&server_address , server_len);
listen(server_sockfd , 5);
while(1) {
int ch[3] ;
printf("server waiting\n");
client_sockfd = accept(server_sockfd ,
(struct sockaddr *)&client_address , &client_len);
read(client_sockfd,ch,sizeof(ch));
ch[2] = ch[0] + ch[1] ;
write(client_sockfd,ch,sizeof(ch));
close(client_sockfd);
}
}
|
さて、プログラムを実行させてみた。
足し算サーバープログラムの実行結果 |
クライアント側の様子 |
suga@client[~/pro]% ./client3
A = 5 ← 送信した数値
B = 3 ← 送信した数値
A + B = 8 ← 受信した計算結果
suga@client[~/pro]%
|
サーバー側の様子 |
suga@server[~/pipe]% ./server3
server waiting
server waiting
|
見事、プログラム成功 !!!
そして・・・
これが動いた時は、感激だった (TT)
たかだか「足し算サーバー」なのだが、生まれて初めて作った
サーバーソフトなのだ。それも異なるマシン同士の通信ができているのだ!
ここまで辿り着くだけでも苦難の道程だっただけに、喜びも大きい!!
ところで、上のソースを見て「おやっ?」と思われた方も
おられると思います。
アドレス構造体のメンバーに値を代入する部分だ。
アドレス構造体のメンバーに代入する部分 (server3.c) |
server_address.sin_addr.s_addr = INADDR_ANY ;
server_address.sin_port = htons(9734) ;
|
まずは下のポート番号を代入している部分で、htons()関数を使っています。
リトルエンディアンからビッグエンディアンに変換するための関数で、
標準的な通信では、ビッグエンディアンで行っているため、
インテルのCPUで扱う数値は、リトルエンディアンなので、
変換する必要があるためです。
次に、自分自身のIPアドレスの指定が「INADDR_ANY」になってます。
これは、自分自身が持っている、どのIP宛へも接続を許可するという意味です。
ここでは、IPアドレスは、32ビットの数値を代入します。
ちなみに、このIPアドレスも、実は、ビッグエンディアンなので、
本来ならhtons()関数を使って、htons(INADDR_ANY)が正しいのですが、
マクロを見ると「0」の事なので、htons()関数は省略しました。
|
「自分自身が持っている、どのIP宛へも接続を許可する」
わかったような、わからんような書き方なので、図で説明します。
「自分自身が持っている、どのIP宛へも接続を許可する」
とはどういう事? |
|
上図のように、サーバー側で2枚指しのNIC(2つIPを持っている)状態で
接続許可のIPを、eth1側の「192.168.200.10」の許可だけとします。
クライアントが「192.168.1.10」宛に接続要求を行っても、
許可していないIP宛なので、クライアントの接続要求は拒否されます。
|
同じマシンでも、複数枚、NICをつけていて、片方のIPしか
接続許可をしていないと、もう片方のIP宛に接続要求があっても、
接続要求は拒否されます。
そこで、サーバーが持っている全てのIPを接続許可する方法として、
INADDR_ANYを使います。
INADDR_ANYを使った場合 |
|
クライアントが、サーバーが持っている、任意のIP宛に接続要求を行うと
接続が許可される仕組みになる。
|
「説明だけで、実験はないんかい!」というツッコミがあるかもしれません。
ご心配なく。既に実験済みです。今回は、実験結果だけを載せました。
これに気づいたのは、実は、本のプログラムでは、
サーバー側のアドレス構造体のIPの指定が「127.0.0.1」になっていました。
最初、このIPは、自分自身のIPだから問題なしと思い、異なるマシン間で
通信を行ってみたら、接続拒否されました。
なぜかなぁと思い、本をよく見てみると、自分宛のIPで接続許可を行う物を
代入する事がわかりました。
さて、ここで終わらず、一歩踏み込んでいきたいと思います。
今までの方法だと、1台づつクライアントの処理を順番に行っているため
同時に複数台のクライアントの接続があっても、最初の1台をのぞけば
他のクライアントは、処理待ち状態にあります。
しかし、実際のサーバーソフトは、同時に複数台の処理を行います。
もし、複数のクライアントから同時接続があった場合 |
|
この問題の解消方法も、目隠し本にあった。fork()を使う方法だという。
以下のような仕組みにするという。
サーバー側のプロセスで、fork()を使い、子プロセスを生成し
クライアントの処理を子プロセスに任せる |
|
クライアントの接続があった場合、接続ソケットが生成され
今までのソケットは、他の接続の待ち受けを続けます。
そこで、接続ソケットが生成されたと同時に、fork()で親子を作り
クライアントの処理を子プロセスに任せるという。
|
早速、サーバープログラムの部分を書き換えてみることにした。
サーバー側のプログラム (server3-1.c) |
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(void)
{
int server_sockfd , client_sockfd ;
int server_len , client_len ;
struct sockaddr_in server_address ;
struct sockaddr_in client_address ;
server_sockfd = socket(AF_INET,SOCK_STREAM,0);
server_address.sin_family = AF_INET ;
server_address.sin_addr.s_addr = INADDR_ANY ;
server_address.sin_port = htons(9734) ;
server_len = sizeof(server_address);
bind(server_sockfd , (struct sockaddr *)&server_address , server_len);
listen(server_sockfd , 5);
while(1) {
int ch[3] ;
printf("server waiting\n");
client_sockfd = accept(server_sockfd ,
(struct sockaddr *)&client_address , &client_len);
if ( fork() == 0 )
{
/* 子プロセス */
read(client_sockfd,ch,sizeof(ch));
ch[2] = ch[0] + ch[1] ;
write(client_sockfd,ch,sizeof(ch));
close(client_sockfd);
exit(0);
}
else /* 親プロセス */
{
close(client_sockfd);
}
}
}
|
見事に動いた!!
さて、同時接続でも接続数が少ないと問題はないのだが、短時間の間に
大量の接続があると、接続の度に、fork()で子プロセスを
生成していたのでは、反応が悪くなると言われる。
目隠し本では、select()関数を使うと、fork()を使わなくて済む方法が
書かれていますし、Apacheでは、最初に待ち受けのための子プロセスを
fork()で生成してから、接続が来たら対応する形をとっている話だ。
だが、この話を書き出すと、分量が多くなるので、機会があれば、
書いて行きたいと思います。
正直な事を書きますと・・・
select()関数の使い方を理解してないもーん (^^;
でも、ここは「事務員なので良いのだ!」と逃げ切りたいと思います (^^)
今まで取り上げましたソケット通信は、コネクション通信(TCP)の場合でした。
最後に、DNSやNFSなどで使われていますコネクションレス通信(UDP)の
プログラムについて触れたいと思います。
目隠し本には、コネクションレス通信が載っていない。
だが、「こんな事もあろうかと」という感じで、以下の本を取り出した。
「UNIXネットワークプログラミング」(羽山博 監:金内典充、今安正和 書)
コネクションレス通信の全体を見た場合、コネクション通信よりも
プログラムは簡素化される。
コネクションレス通信の場合のプログラムの流れ |
|
コネクション通信の場合と違い、簡素化された感じがする。
コネクション通信の場合、接続要求を行い、接続が確立される。
お互いの通信が正常にできているか確かめながらデータ送信を行う。
しかし、コネクションレスの場合は、相手にデータを投げたら
投げっ放しなので、connect()、listen()、accept()といった関数が不要になる。
コネクション通信の場合、送信にはwrite()、受信にはread()を使ったが、
コネクションレスの場合、送信にはsendto()、受信にはrecvfrom()を使う。
ちなみに、sendto()、recvfrom()は、コネクション通信でも使える。
|
さて特徴的なのは、コネクションレスの場合、クライアント側もbind()を使う。
そこでコネクションレスの場合のbind()の概念を見てみる。
コネクションレスの場合のbind()関数について |
|
クライアントとサーバーの両方にソケットの名前をつける。
コネクション通信の場合、クライアントから接続要求を行うため、
サーバー側のソケットに名前をつければ、クライアントが認識でき、
しかも、サーバーは、接続確立をしているため、クライアント側のソケットの
名前を知らなくても通信ができる。
しかし、コネクションレス通信の場合、接続確立を行わなず、
お互いがデータを投げっ放しにするため、クライアント側にも名前をつけないと
サーバー側は、相手先がわからず、データが送信できないという。
|
さて、次にデータの送信、受信の関数の説明を行いたいと思います。
sendto()関数について |
|
コネクションレス通信でのデータ送信を行う際に使うsendto()関数の仕様だ。
|
次に送信されたデータを受信するためのrecvfrom()関数を見てみる。
recvfrom()関数について |
|
アドレス構造体は、接続してきた所の情報を得るためにあるようだ。
アドレス構造体の長さのポインタだが、長さの値を入れる変数の
ポインタを意味する。
|
さて、これでコネクションレス版の「足し算プログラム」を作成してみた。
コネクションレス版の「足し算プログラム」 |
クライアント側のプログラム (udp-cli1.c) |
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(void)
{
int sockfd ;
int tolen , fromlen ;
struct sockaddr_in to_address , from_address ;
int result ;
int ch[3] ;
sockfd = socket(AF_INET,SOCK_DGRAM,0);
to_address.sin_family = AF_INET ;
to_address.sin_addr.s_addr = INADDR_ANY ;
to_address.sin_port = htons(9734) ;
tolen = sizeof(to_address);
bind(sockfd , (struct sockaddr *)&to_address , tolen);
printf("A = ");
scanf("%d",&ch[0]);
printf("B = ");
scanf("%d",&ch[1]);
sendto(sockfd , ch , sizeof(ch) , 0 , (struct sockaddr *)&to_address , tolen);
recvfrom(sockfd , ch , sizeof(ch) , 0 , (struct sockaddr *)&from_address , &fromlen);
printf("A + B = %d \n",ch[2]);
close(sockfd);
exit(0);
}
|
サーバー側のプログラム (udp-ser1.c) |
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(void)
{
int ch[3] ;
int sockfd ;
int tolen , fromlen ;
struct sockaddr_in to_address ;
struct sockaddr_in from_address ;
sockfd = socket(AF_INET,SOCK_DGRAM,0);
to_address.sin_family = AF_INET ;
to_address.sin_addr.s_addr = INADDR_ANY ;
to_address.sin_port = htons(9734) ;
tolen = sizeof(to_address);
bind(sockfd , (struct sockaddr *)&to_address , tolen);
while(1) {
printf("server waiting\n");
recvfrom(sockfd , ch , sizeof(ch) , 0 ,
(struct sockaddr *)&from_address , &fromlen);
ch[2] = ch[0] + ch[1] ;
sendto(sockfd , ch , sizeof(ch) , 0 , (struct sockaddr *)&from_address , fromlen);
}
}
|
さて、実際にプログラムを実行してみます。
コネクションレス版の「足し算プログラム」の実行結果 |
クライアント側の様子 |
suga@client[~/pro]% ./udp-cli1
A = 5 ← 送信した数値
B = 3 ← 送信した数値
(結果が出ないので、CTRL+Dで一度、終わらせる)
suga@client[~/pro]% ./udp-cli1
A = 5 ← 送信した数値
B = 3 ← 送信した数値
A + B = 8 ← 受信した計算結果
suga@client[~/pro]%
|
サーバー側の様子 |
suga@server[~/pipe]% ./udp-ser1
server waiting
server waiting
server waiting
|
うぬぬ、なぜ、一度目の計算だけできぬのだ (--;;
だが、一度、CTRL+Dで終わらせると、2回目以降は、計算をしてくれる。
なぜか、わからぬ・・・。原因がわからないだけに気持ち悪いのらー!!
もうひと踏ん張りして頑張ってみたいのだが、私は、技術者ではなく、
タダの事務員なので力尽きて断念。
まとめ
今回は、プログラムの話を書いてみました。
プログラムとしては、ツッコミ所が多いかもしれませんが、
とりあえずはプロセス間通信のプログラムの概要は理解できました。
プログラムとしては参考例になるかどうかは、わかりませんが、
概要を知る内容をして、将来、プログラマーを目指す方々の
少しでもお役に立てれば幸いです (^^)
次章:「OSが固まる原因。CPUの特権レベルの話」を読む
前章:「バッファフロー攻撃の手口。メモリ違反を悪用し管理者権限奪取」を読む
目次:Linux、オープンソースで「システム奮闘記」戻る