TCP/IP通信による同期描画
Last update: <2004/03/13 17:29:53 +0900>
ネットワーク通信の基礎に関する話をいまさらここでしようとは思いませんが,最低限,この勉強会で身につけておく(思い出して欲しい)事柄について触れます.
コンピュータとコンピュータが互いにやりとりするためには,その手順を決めておかなければなりません.これをプロトコル(protocol)といいます.現在のコンピュータ通信において,もっとも普及しているのはTCP/IPです.IPというのはInternet Protocolの略で,コンピュータ間のデータ転送をおこなうためのプロトコルです.またTCPはTransport Control Protocolで,IPを利用しアプリケーション間のデータ転送をおこなうためのプロトコルです.
TCP/IPでは,コンピュータの識別子としてIPアドレスを使用します.ご存知の通り,IPアドレスとは'.'で区切られた4つの整数のことで,150.89.238.131というように表されます.IPアドレスは,TCP/IPでネットワーク接続されているコンピュータに固有に割り当てられています.*22
自分が今使っているマシンのIPアドレスを調べてみましょう.スタート=>プログラム=>コマンドプロンプトで,
c:\>ipconfig
と入力します.
さて,普段何気なく使用している電子メールやウェブなどですが,これらの通信はすべてTCP/IPにより実現されています.同じコンピュータで同時に電子メール,ウェブ,チャットなどをおこなうことができますが,これはIPアドレスの他にポート番号によって,処理内容に応じて切り替えて使っているからです.例えば電子メール(SMTP)はポート番号が25番,ウェブ(HTTP)は80番というようなルールになっています.
Unix系やWindowsNT系のマシンでは,ポート番号がどんなアプリケーションに割り当てられているか調べることができます.Unix系では/etc/services,WindowsNT系ではc:\winnt\system32\servicesにファイルがあります.lessやnotepadなどで開き,見てみましょう.
通信を利用したアプリケーションでは,サービスを提供する側のサーバと,サービスを受ける側のクライアントがあります.サーバは常に自分のポート番号を見張っていて,クライアントがサーバのIPとポート番号を指定してアクセスすると,通信が成立します.
アプリケーションで通信をするためには,ソケットという概念を用います.サーバ用のソケット,クライアントのソケットを両方のマシンで用意します.サーバ用のソケットにはポート番号がついており,クライアントの接続があるまで待ち続けます.クライアントがサーバのIPとポート番号を指定したソケットを作製し,指定されたIPのサーバに問い合わせます.ソケットとソケットが通い合えば通信が開始になります.
さて,実際の通信プログラムではどのような手順で通信が確立するのでしょうか.
以下の表にまとめました.
サーバ側 | | クライアント側 |
起動 | |
|
ソケット作成(socket) | |
|
ポート番号の割り当て(bind) | |
|
接続待ち(listen) | |
|
| | | 起動 |
| | | ソケット作成(socket) |
| | | ポート番号,通信先の設定 |
接続受付け(accept) | ← | 通信先に接続(connect) |
接続完了 |
通信の接続が完了すると,あとはお互いにデータの読み書きをすることになります.サーバとクライアントでは,当然ですが送受信が対になります.
サーバ側 | | クライアント側 |
接続完了 |
... | | ... |
送信(send) | → | 受信(recv) |
... | | ... |
受信(recv) | ← | 送信(send) |
... | | ... |
受信のコマンド(recv)は,データを受け取るまで待ちます.つまりデータが来なければ次の処理に進めません(blocking).データのやり取りを手順を間違えると,受信時にプログラムがフリーズすることも珍しくありません.フリーズを回避するには,データやりとりの手順をしっかり決めておくことが重要です.
接続を終了するには,クライアント側から切るのが流儀です.
サーバ側 | | クライアント側 |
| ← | 終了(close) |
終了または接続待ち | |
|
クライアント,サーバの通信プログラムを作成してみましょう.
- サーバプログラム,クライアントプログラム用に,プロジェクト2個を用意します.プロジェクトの作成方法は第1回演習,管理方法は第4回演習を見て下さい.
- 今回はサーバとクライアント2つのプログラムを同時に開発しますので,ワークスペースの中に2つプロジェクトを置くと便利です.画面のようにワークスペースの部分で右クリックをして追加することができます.
- 通信ライブラリプログラム
をダウンロードし,両方のプロジェクトに追加します.
-
プロジェクト=>設定のリンクで,ライブラリモジュールにws2_32.libを追加します.両方のプロジェクトに設定が必要です.
-
次のプログラムを見て下さい.
/* サーバサンプルプログラム */
#include
#include "socket.h"
void main(void)
{
int netd, fd;
unsigned char data;
InitSocket(); /* ソケットの初期化 */
OpenServer(&netd, 60001); /* ソケット作成,設定 */
WaitServer(netd, &fd); /* 接続待ち */
/* 接続完了 */
while(1){
ReadSocket(fd,1,&data); /* データ受信 */
if(data=='q')break; /* 'q'キーが押されたら終了 */
printf("%c", data); /* 受信データの表示 */
WriteSocket(fd,1,&data);/* 同じデータを送信しなおす */
}
/* 接続終了 */
CloseServer(fd);
QuitServer();
}
このプログラムはポート番号60001番を使用したサーバ側プログラムです.1バイト受信すると,その受信データを表示した後,同じデータを送り返すものです.'q'キーのアスキーコードがデータとして送られてくると,無限ループを抜け終了します.
netdはソケット記述子,fdは現在接続されているソケットの記述子です.サーバはソケット記述子で待ち受けますが,現在接続されているソケットとは区別します.サーバが複数のクライアントを待ち受けることもあるからです.
このプログラムをサーバ側のプロジェクトに追加してコンパイルします.ファイル名は何でも構いません.
-
次のプログラムを見て下さい.
/* クライアントサンプルプログラム */
#include
#include
#include "socket.h"
void main(void)
{
int netd;
unsigned char data=0;
unsigned char recv_data;
InitSocket(); /* ソケットの初期化 */
if(!OpenClient(&netd, 60001, "150.89.238.131")){
fprintf(stderr,"open error\n");
exit(1);
}
/* 接続完了 */
while(1){
data=getch();
WriteSocket(netd,1,&data);/* データ送信 */
if(data=='q')break;
ReadSocket(netd,1,&recv_data); /* データ受信 */
printf("%c", recv_data); /* 受信データの表示 */
}
CloseClient(netd);
}
このプログラムは,IPアドレスが"150.89.238.131",ポート番号60001番のサーバと接続するためのクライアントプログラムです.接続が完了した時点でキーボードを押すと,そのアスキーコードが1文字送信されます.また,サーバ側から受け取った1バイトのデータを表示しています.'q'キーを押すと接続を終了します.
netdはソケット記述子です.
このプログラムをクライアント側のプロジェクトに追加してコンパイルします.ファイル名は何でも構いません.コンパイルや実行するプロジェクトを切りかえる場合には,ワークスペースでプロジェクトを右クリック選択しアクティブに設定すればOKです.
プログラム中のIPアドレスは,現在使用しているマシンのアドレスに変更することを忘れないように.
- 両方のプログラムがコンパイルできたら,サーバ側から順に実行してみましょう.
-
周りの誰かとペアを組み,他のマシンのサーバと通信してみましょう.クライアント側プログラムのIP設定を忘れずに.
共用体
-
ReadSocket,WriteSocketは引数に送信(受信)バイト数を指定できますが,配列の型がunsigned charへのポインタになっています.floatやint型のデータを送信したい場合には,型変換が必要になります.がむしゃらに型変換をすると情報が落ちるので,このような場合には共用体を使用します.以下のような宣言をします.
union data_vali{
unsigned int iidata;
unsigned char cidata[4];
}data_i;
これはunsigned int 1個をunsigned char 4個に分けるための共用体の宣言です.実際にデータの変換は
unsigned char buff[4];
...
data_i.iidata=henkan_shitai_data;
buff[0]=data_i.cidata[0]
buff[1]=data_i.cidata[1]
buff[2]=data_i.cidata[2]
buff[3]=data_i.cidata[3]
のようにやります.共用体を使用した通信は次節で詳しくやります.
TCP/IPによる通信が出来るようになれば,TCP/IPで接続されているマシンどうしでコミュニケーションをとることが原理的に可能となります.上でやってきたことは,記号レベルでの通信でしたが,これをつきつめていくと文字にとどまらず画像メディアや触覚メディアの伝達もできることでしょう.
さて,勉強会の最後のテーマは,OpenGLにおける映像メディアの伝送(同期)です.複数のコンピュータで,OpenGLの映像をシンクロさせる技法について述べます.なぜ映像の伝送の必要があるのか簡単に触れておきますと,
- 離れた人どうしで同じ画面を共有したい
- 多面スクリーンに連続した映像を出したいときや,右目用と左目用に立体映像を出したい
というケースを想定しています.
- 太陽,地球,月の例題を使用して,2台のマシンで動きの同期を取ってみましょう.
実際に映像の動く部分は地球と月で,これらの空間的位置を実質的に表しているのは,theta_mk,theta_ekという角度パラメータです(ここでは地球の自転(theta_e)はないものとしてください).このパラメータをサーバからクライアントに常に送信することにより,サーバとクライアントの表示映像を一致させようとするのがここのねらいです.通信のプロトコルを一応決めておきましょう.ここでは下記のようにしてみました.
サーバ側 | | クライアント側 |
1フレーム描画開始 |
| ← | '0xFFFF'(2byte) |
theta_ek(4byte)
| → |
|
| ← | '0xFFFF'(2byte) |
theta_mk(4byte)
| → |
|
1フレーム描画 |
次の描画へ |
- 太陽,地球,月がそれぞれ回転するプログラムを使います.第3回演習課題の動作確認をしましょう.忘れた人はサンプルプログラムをコンパイルし,動作を確認します.
- 動作確認したプログラムをサーバ用,クライアント用として,2つコピーを作成します.サーバとクライアントがわかるように命名しておきましょう.それぞれのファイルを,前節の演習課題ででてきたサーバのプロジェクト,クライアントのプロジェクトに追加します.
- サーバ側プログラムを作成します.以下に例を示します.
TCP/IP通信を使用するには,ソケットのライブラリ
#include "socket.h"
を追加します.
ソケット通信のため,ソケット記述子をグローバル変数に宣言します.すなわち,
int netd, fd;
ソケットの初期化,設定をおこなうため,init関数に
InitSocket(); /* ソケットの初期化 */
OpenServer(&netd, 60001); /* ソケット作成,設定 */
WaitServer(netd, &fd); /* 接続待ち */
を追加します.
さて,送受信したいデータはfloat型のtheta_ekとtheta_mkです.これを共用体を使用して,unsinged char型に変換します.この変換における共用体は,
union data_valf{
float fdata;
unsigned char cdata[4];
}data;
で構いません.これをグローバルに宣言します.
続いてデータ送受信です.データ送受信は,描画が発生するたびに実行されれば映像として同期がとれます.この場合はdisplay関数内がよいでしょう.サーバ側のデータ送信手順は前述の表より,2バイト受信,4バイト送信,2バイト受信,4バイト送信の順です.これに従うと
void display(void)
{
static float theta_ek;
static float theta_mk;
unsigned char buff[20];
theta_mk+=3.0;
theta_ek+=1.0;
ReadSocket(fd,2,buff); /* 2バイト受信 */
data.fdata=theta_ek; /* 共用体によるデータ変換 */
buff[0]=data.cdata[0];
buff[1]=data.cdata[1];
buff[2]=data.cdata[2];
buff[3]=data.cdata[3];
WriteSocket(fd,4,buff); /* 4バイト送信(theta_ek) */
ReadSocket(fd,2,buff); /* 2バイト受信 */
data.fdata=theta_mk; /* 共用体によるデータ変換 */
buff[0]=data.cdata[0];
buff[1]=data.cdata[1];
buff[2]=data.cdata[2];
buff[3]=data.cdata[3];
WriteSocket(fd,4,buff); /* 4バイト送信(theta_mk) */
...
- クライアント側プログラムを作成します.以下に例を示します.
#include "socket.h"
を追加します.
ソケット記述子をグローバル変数に宣言します.
int netd;
ソケットの初期化,設定をおこなうため,init関数に
InitSocket(); /* ソケットの初期化 */
if(!OpenClient(&netd, 60001, "150.89.238.131")){
fprintf(stderr,"open error\n");
exit(1);
}
を追加します.IPには自分のマシンのものを指定して下さい.
共用体を宣言します.サーバと同じです.
union data_valf{
float fdata;
unsigned char cdata[4];
}data;
をグローバルに宣言します.
続いてデータの送受信です.サーバと手順が入れ違いになることに注意します.
void display(void)
{
static float theta_ek;
static float theta_mk;
unsigned char buff1[2]={0xff, 0xff};
unsigned char buff2[10];
WriteSocket(netd,2,buff1);/* 2バイト送信 */
ReadSocket(netd,4,buff2); /* 4バイト受信 */
/* データ変換 */
data.cdata[0]=buff2[0];
data.cdata[1]=buff2[1];
data.cdata[2]=buff2[2];
data.cdata[3]=buff2[3];
theta_ek=data.fdata;
WriteSocket(netd,2,buff1);/* 2バイト送信 */
ReadSocket(netd,4,buff2); /* 4バイト受信 */
/* データ変換 */
data.cdata[0]=buff2[0];
data.cdata[1]=buff2[1];
data.cdata[2]=buff2[2];
data.cdata[3]=buff2[3];
theta_mk=data.fdata;
- サーバ,クライアントの両プログラムが完成したら,サーバ側から起動します.下図画面のように,同期がとれた映像が2枚表示されれば成功です.
-
周りの誰かとペアを組み,他のマシンのサーバ,クライアントと通信してみましょう.クライアント側プログラムのIP設定を忘れずに.
本勉強会は,OpenGLを映像生成ツールとして使用する目的で実施しました.ツールというのは結局は使う人次第です.この勉強会がきっかけで,ツールを少しでも使えるようになれば幸いです.
お気づきの点がありましたら,ご意見を賜りたく思います.次年度勉強会の参考にします.
戻る