こちら

プログラミング演習 I V
−X-Window プログラミングとプロセス間通信−
情報工学科
はじめに
本稿ではX-Window システム上のプログラミングならびに、複数プロセスの間でデータの受け渡しを
行うプロセス間通信についての基本的なノウハウについて説明し、イベント駆動型のプログラミング作
法を習得していく.X-Window はビットマップディスプレイとマウス等を利用したGUI(Graphical
User Interface:画像や図形による情報のやり取りのこと)環境を実現するソフトウェアである.X端末に
見られるように、X-Window プログラミングを用いれば様々な大きさを持つ矩形の窓:ウィンドウに任
意の色の組(パレット)を使って任意の絵や図形を描いたり、量子化された画像を映し出したり、ある
いはマウスの動作を時々刻々探知し、ウィンドウ描画に反映することができる( e.g.,xeyes, xdvi, tgif).
加えてサーバ・クライアント方式であることから、離れたシステムどうしで画像や図形のやりとりがで
きるのも X-Window の大きな利点である。このように、独立に作動する複数のプログラム(UNIXで
はプロセスと呼ぶ)の間でデータを交換したり同期を取るなどの技法(プロセス間通信と呼ぶ)はシス
テム開発やネットワークプログラミングには不可欠である。以下ではこれらX-Windowならびにそ
のベースであるプロセス間通信を応用しネットワーク上で動作するプログラミング(ネットワークプロ
グラミングと呼ぶ)についての基礎を学ぶ.
第1部
1.X-Windowの基本的知識
X-Window はUNIXワークステーションやX端末上で動作するサーバ・クライアントシステムである(図1).
サーバ・クライアントシステムとはUNIXのリモート系のコマンド(例えば ftp
rlogin, telnet etc.)等と同様、
要求を発行する側(クライアント)とその要求を受け付けて処理する側(サーバ)の両者からなる.クライアン
トとサーバは同一のホストに位置しても、ネットワークで繋がれた別々のホスト上に位置しても良い.X−Wi
ndowは、図形や画像を生成する機能とそれらを表示する機能を各々分離独立させるためにこのような設計と
なっている.
X-Window におけるサーバプロセスの役割は以下のようなものである.
−キーボードのキータイプやマウスの移動、クリック等のイベントをクライアントプロセスに通知する.
−クライアントプロセスから発行された描画要求を処理する.
−クライアントプロセスとの通信路の維持・管理を行う.
これに対しクライアントプロセスでは
−サーバプロセスに種々の資源(後述)を確保してもらう.
−各々の資源を通して、サーバに対するウィンドウ生成や図形描画の要求を発行する.
−キーボードやマウスからのイベントをサーバに問い合わせ、通知してもらう.
等を行う.xeyes や xdvi,tgif 等はすべてXクライアントプロセスとして動いている.ここで注意すべき点は、キ
ーボードやマウスの管理、ならびに画面への出力操作はすべてサーバが一括して行い、クライアントはそれらの
要求を発行する機能だけを有し画面を直接制御することはない.
以上から知られるように、X端末はサーバの機能のみを持ったハードウェア装置である.
サーバ・クライアントシステムであることから両者の間にはXプロトコルと呼ばれる通信規約(OSI 参照モデ
ル最上位層プロトコルに位置する)がある.それらは以下の4種のメッセージに大別され、各メッセージ毎に独
立した通信路(チャンネル)を持つ。各通信路ごとに構成される F I F O バッファ(キュー)を通してメッセージ
が交信される。
−request:ウィンドウの生成や操作、テキスト、図形等のサーバへの要求メッセージ (クライアント -> サーバ)
−reply:
request に対する返答のためのメッセージ
(サーバ -> クライアント)
−event: サーバ側で発生したイベントをクライアント側に通知するメッセージ
−error:
(サーバ -> クライアント)
サーバ側で発生したエラーをクライアント側に通知するメッセージ (サーバ -> クライアント)
本稿で説明するX-Window プログラミングとは、上で述べたクライアントプロセスを設計・製作することであ
る.Xプロトコルの規約に基づいてXサーバを修正・改良することも原理的には可能ではあるが応用プログラマ
の範疇ではなく、本稿の目的から外れるのでここでは触れない.
X-Window は、UNIXのファイル構造と同様ツリーとして階層化されており、最上位のウィンドウをルート
ウィンドウ(root window)と呼ぶ.一つのウィンドウ上で以下のような仕方で kterm(漢字端末エミュレータ)
または xterm(端末エミュレータ)を起動すると
-−→
%kterm & (または %xterm &)
そのウィンドウの子供ウィンドウ(subwindow)が生成される.すると生成された個々のウィンドウ上では更に
一つのシェルが動作する.新たに生成されたシェルのカレントディレクトリは kterm または xterm を起動したデ
ィレクトリとなる.ここで、生成元である親ウィンドウ(parent window)を閉じると、その子供ウィンドウや孫ウ
ィンドウ等はすべて自動的に閉じてしまうので注意を要する.
X-Window の座標系は、個々のウィンドウ毎に左上端に原点が位置し、下方にy軸、右方にx軸が置かれ、各
軸の目盛りは画素(Pixel)単位となる.
なお、端末室のワークステーション群の場合、画面の水平方向が 1024 画素、垂直方向が 768 画素の大きさを持
ち、パレットの色数(同時に表現できる色数:色深度ともいう)は256色を持つ.グラフィクスワークステー
ションでは画面サイズは 1024 x 768 から 2048 x 1600 程度のものまで、色深度は24ビット(約1670万色)
や32ビット(約43億色)などが主流である。
さて、以下では X-Window プログラミングを行ううえで必要
となる、ワークステーションの環境設定について説明する.
2. ウィンドウマネージャとXクライアント
X-Window において、使い易さや機能を決定するもっとも重要なプログラムがウィンドウマネージャである.
ウィンドウマネージャはウィンドウサイズの変更や文字フォントの変更、マウスイベントなどの通知ならびにウ
ィンドウの切り替え制御等を行う.ウィンドウのレイアウト設定や各ウィンドウの属性の設定、ボタンやメニュ
ーなどの装飾は全てこのウィンドウマネージャの役割である.現在よく使われているウィンドウマネージャは、
twm、fvwm、ctwm、fvwm2、WindowMaker、Enlightment などがあるが、ここでは本学科でもっと
もよく使われているtwmの操作概要について触れよう.
UNIX操作環境の設定法において、X-Window 環境を設定するために起動する .xsessionというバッ
チファイルについて触れた.そのファイルにおいて最後に実行されていたのがtwm (Tab Window Manager)で
ある.twmが起動されると各ウィンドウの最上部に水平に伸びた細長い領域が出現する.これをタイトルバー
と呼ぶ.タイトルバー上の種々の箇所にマウスポインタを置きクリック(右ボタンを押す)することによってウ
ィンドウの操作(アイコン化、移動、リサイズ)を行うことができる.また、ルートウィンドウで左ボタンをク
リックするとウィンドウ操作メニューが現れる.メニュー上の項目は、
Iconify
:指定ウィンドウのアイコン化
Resize
:指定ウィンドウのリサイズ
Move
:指定ウィンドウの移動
Raize
:指定ウィンドウを最も手前に持っていく
Lower
:指定ウィンドウを最も奥に持っていく
Focus
:指定ウィンドウの入力フォーカスを設定
Unfocus
:指定ウィンドウの入力フォーカスを解除
Show Iconmgr
:アイコンマネージャを表示
Hide Iconmgr
:アイコンマネージャの非表示
Kill
:指定ウィンドウの消去
Delete
:指定ウィンドウの消去
Restart
:twmのリスタート
Exit
:twmの終了
等である.またtwmは起動時にホームディレクトリにある、.twmrcという環境設定ファイルを読み込む.
マウス操作の定義やメニューの内容等は、このファイルを変更することにより個人用にカスタマイズすることが
できる.
以下にXクライアントプロセスとして動作する重要なもののうちいくつかを挙げておく.
xinit
:Xを初期化してサーバプロセスを起動するプログラム、X端末では電源投入時にこれと同様の
機能が自動的に作動する.
xhost
:Xサーバへの接続を登録するホストの一覧にホストを登録するプログラム.サーバへのアクセ
スを許可しているホスト群がアクセスリストであり、サーバの動作するマシン上で( %xhost hostname )のよう
に hostname を引数として指定すると当該ホストからのアクセスが許可される.
xterm
kterm
:端末エミュレータ(端末機能を模擬する装置)プログラム、この上でシェルが動作する.
:漢字端末エミュレータプログラム、xterm にかな漢字フロントエンドプロセッサ機能を付加し
たもの.
3. X11プログラミングの基本的な考え方
次にXプログラミングについて説明していく.Xプログラミングはその version 番号を付けてX11プログラミ
ングとも呼ばれる.X11はUNIX上での汎用ユーザインタフェースを提供している.
まずは「習うより慣れろ」に従い、X11を用いた最も短いプログラムの例を図2に示す.
1:#include <X11/Xlib.h>
2:#include <X11/Xutil.h>
3:#include <stdio.h>
4:main()
5:{
6: Display *d;
/* Display 構造体 */
7: Window w;
/* Window 構造体 */
8:
9: d=XOpenDisplay(NULL);
/* Xサーバとの接続:ローカル X サーバ */
10: w=XCreateSimpleWindow(d,DefaultRootWindow(d),0,0,150,150,1,
11:
WhitePixel(d,DefaultScreen(d)),
12:
BlackPixel(d,DefaultScreen(d)));
/* Window の生成 */
/* 枠線色を白に設定 */
/* 背景色を黒に設定 */
13: XMapWindow(d,w);
14: XFlush(d);
15: getchar();
16:}
図2.X11プログラミングその1(ダミーウィンドウ)
このプログラムは起動すると小さな正方形の window を生成し、何らかのキーを押すとこれを消去し終了する.
以下このプログラムの内部構造について説明する.
プログラム中の1行目と2行目のインクルード文で、X11の基本部分であるXlibに関連するヘッダファ
イル Xlib.h と Xutil.h を参照している.またこれらのヘッダファイルで定義されている構造体 Display 型へのポ
インタdおよび Window 型を持つ変数wをそれぞれ6行目、7行目で定義している.
9行目の XOpenDisplay では、どのディスプレイ(Xサーバ)に対してこのX11プログラム(Xクライアント)
を接続するかを指定する(NULLを引数にした場合環境変数 DISPLAY で設定してあるディスプレイが接続先
となる).サーバとの接続に成功するとポインタdにその識別子(ID)が返される.
10行目の XCreateSimpleWindow において、第1引数dは XOpenDisplay で取得したディスプレイのID、
第2引数は開かれるウィンドウの親ウィンドウIDを表す構造体 Window 型変数(DefaultRootWindow(d)とした
場合は当該ディスプレイのルートウィンドウIDを返す)、第3引数、第4引数ははウィンドウの左上端のx座
標とy座標を表す int 型変数、第5引数、第6引数はそれぞれウィンドウの幅と高さを表す int 型変数、第7引数
はウィンドウの枠線の太さを表す int 型変数、第8引数、第9引数はそれぞれウィンドウの枠線と背景の色を指定
する int 型変数である(WhitePixel(d,DefaultScreen(d))と BlackPixel(d,DefaultScreen(d))は当該ディスプレイで
定義された白色と黒色を返すマクロ).この関数からの戻り値は Window 型の変数wに格納され、以降のウィン
ドウ操作のための識別子(ID)となる.この識別子をドロウワブルとも呼ぶが、本稿では統一性を持たせるた
めウィンドウIDと呼ぶことにする.
ここまででディスプレイへの接続とウィンドウの作成を終了しているが、これだけではウィンドウは表示されな
い.
そこで第13行目の XMapWindow は、XCreateSimpleWindow で作成したウィンドウをXサーバへ送信するた
めのバッファリングを行う関数であり、その第1引数dは XOpenDisplay で取得したディスプレイのID、第2
引数は XCreateSimpleWindow で取得したウィンドウのIDである.
バッファリングされたXサーバへのコマンドは14行目の XFlush でネットワーク上あるいは自分自身のXサー
バプログラムに掃き出され、実際のウィンドウが描画される.
そして最後に15行目の getchar でリターンキーを受け付けプログラムが終了する.
なお、図2のプログラムのコンパイルには
%gcc program.c -lX11 (gcc を使う場合)
のようなオプション(Xlib ライブラリのィンクオプション)を付けることに注意されたい.
また、使用済みのディスプレイやウィンドウの後始末として、XCreateSimpleWindow で生成したウィンドウは
XDestroyWindow(d,w)で消去、XOpenDisplay で接続したディスプレイサーバは XCloseDisplay(d)で切断するこ
とができる(ただしdはディスプレイID,wはウィンドウIDである).なお、これらの後処理を行わなくて
も、プログラム終了時にウィンドウは自動的に消滅、ディスプレイサーバは自動的に切断される。
4.イベント検出
次にXプログラミングの特徴的な機能の一つであるイベント処理について触れる.イベントとは、キー押下やマ
ウスカーソル、マウスボタンの動作という事象を指す.Xプログラミングではサーバ側で発生したこれらのイベ
ントを高速に検出し、それらに対して逐次適切な応答をとることにより実時間での「人と機械によるやり取り」
(マンマシンインタフェース)を実装することができる.このような方式によるプログラムをイベント駆動型プ
ログラムという.X11でこのイベント駆動型プログラムを作成するには、まずイベントの検出をX11システ
ムに依頼する動作を行い、更にイベント発生に備えて定常的に待機し、発生時に適切な処理を行う動作をループ
内に組み込む必要がある.図3にそのような簡単な例を示す.
ここで、10行目の XEvent はXサーバで検出可能なイベントの枠組みを定義する共用体で、event はこの型を
もつ変数である.
また18行目の XSelectInput は検知したいイベントの種別(これをイベントマスクと呼ぶ)を指定する関数で、
第1引数、第2引数はそれぞれ前述のディスプレイIDとウィンドウID、第3引数はXイベントの指定を行う
イベントマスクで、上記の例ではマウスボタンの押下を表す ButtonPressMask が指定されている.
1:#include <X11/Xlib.h>
2:#include <X11/Xutil.h>
3:#include <stdio.h>
4:
5:main()
6:{
7: Display *d;
8: Window w;
9:
10: XEvent event;
/** Event 共用体 **/
11: int i;
12:
13: d = XOpenDisplay( NULL );
14: w = XCreateSimpleWindow(d,DefaultRootWindow(d) ,
15:
0,0,150,150,1,
16:
WhitePixel(d,DefaultScreen(d)),
17:
BlackPixel(d,DefaultScreen(d)));
18: XSelectInput(d,w,ButtonPressMask);
/** 検知したいイベントタイプの指定 **/
19: XMapWindow(d,w);
/* ウィンドウのバッファリング */
20:
21: while(1) { XNextEvent (d, &event);
/** イベントキューからの取り込み **/
22:
switch(event.type) {
23:
case ButtonPress :
24:
printf("Mouse button pressed at x = %d, y = %d \n",
25:
event.xbutton.x,event.xbutton.y);
26:
} /* イベントが発生したウィンドウからみたx,y 座標*/
27:
}
28: }
図3.X11プログラミングその2(マウスボタン押下位置の検出)
21行目の while 文は無限ループを形成しており、ループ内では XNextEvent 関数でイベントキューの先頭にあ
るイベント情報が、XEvent 共用体で宣言された event 変数に取り込まれる.XNextEvent 関数の第1引数はディ
スプレイID、第2引数が XEvent 共用体変数へのポインタとなる.取り込まれたイベントはイベントキューから
外される.ループ内では更に、発生したイベントタイプを switch 文で判断している.その結果 event.type(イベ
ント種別)が ButtonPress である場合、すなわちマウスボタン押下イベントが検知された場合に、24行目、2
5行目で、押下された時点のイベントウィンドウ上でのマウスカーソルのx座標 event.xbutton.x とy座標
event.xbutton.y を printf 関数で書き出している.因みに、 XEvent 共用体のメンバ event.xbutton には上記 x,
y 以外にはルートウィンドウから見たマウスカーソル位置(int x_root , int y_root)、押下された時刻(構造体 Time
time) および押されたボタン番号(unsigned int button; ただし左が1,中が2,右が3)などが含まれ、ボタ
ン押下状態の詳細な情報が入手できる.
なお、上記で使用している ButtonPressMask 以外には、
−マウスボタンの開放を表す ButtonReleaseMask (event type : ButtonRelease)
−マウスの移動を表す PointerMotionMask (event type : MotionNotify)
− マ ウ ス の 各 ボ タ ン の ド ラ ッ グ を 表 す Button1MotionMask,Button2MotionMask,.. な ど (event
type:MotionNotify)
−ウィンドウ領域にマウスカーソルが入ったことを表す EnterWindowMask (event type: EnterNotify)
−マウスポインタが出たことを表す LeaveWindowMask (event type: LeaveNotify)
−隠蔽部分のあるウィンドウが露出したときなどを表す ExposureMask (event type: Expose)
−キーボード上のキーが押下されたことを示す KeyPressMask (event type: KeyPress)
など23種類のイベントマスクが利用できる.これらを利用すれば様々なイベント駆動型プログラムを作成する
ことが可能である.
5.ちょっと進んだイベント駆動型プログラム
ここでは、発生したイベントにあわせて図形を描画する「お絵描き」プログラムを作成してみる.このために
前節で触れたイベント検出後の動作を新たに付け加えることにする.そこで、ウィンドウ描画に必要な資源
(resource)という概念について触れておく.クライアントで動作しているプログラムが、サーバを介して制御の対
象とするウィンドウ(window)、ピックスマップ(PIXMAP)、グラフィックスコンテキスト(GC)等をX資源と
よぶ.X資源は、
1).クライアントからの要求にしたがってサーバ側に生成され、クライアントはこの資源を操作するための
識別子(ID)を受け取る.
2).X 資源に対するクライアントからの操作(指示や制御)は全てこのIDを通して行われる.
などの性質を持つ.以下、個々の資源についても触れておく.
<window 資源について>
window 資源は前述の XCreateSimpleWindow 関数により生成され、関数の戻り値として Window 構造体型の
変数にIDを受け取る.この資源の消去は XDestroyWindow にて行う.
<PIXMAP 資源について>
ウィンドウ内で図形や画像を描画・加工する際の作業領域が PIXMAP である.この資源を生成するには、
pixmap=XCreatePixmap(d,w,width,height,DefaultDepth(d,0));
なる関数を用いる.これにより新たに大きさ width x height の PIXMAP の生成をサーバに要求し、この資源に
対するIDがクライアントプログラム内の Pixmap 構造体型変数 pixmap に関数の戻り値として返される.
<GC資源について>
ウィンドウ上に直線や曲線を描画する際には、実線・破線の区別、線幅、色指定などの属性(グラフィックス
属性と呼ぶ)を指定する必要がある.X11ではクライアントプログラムからの要求によりサーバ側に生成され
るグラフィックスコンテキスト(GC)なるバッファにこれらの属性を設定しておき、描画の要求を行う際にこ
のGCの識別子(ID)を指定してグラフィックス属性を決定する.サーバ側にこの資源を生成するには
gc = XCreateGC(d,w,0,0);
なる関数を用いる.ここでd、wは各々ディスプレイID、ウィンドウIDである.第3、第4引数はGC生
成と同時に属性設定する場合に指定する、各々マスクビット名、XGCValues 構造体へのポインタである.ここで
はこれらの引数を0に設定しておく.生成されたGCへの識別子は構造体GC型の変数gcに、関数の戻り値と
して返される.
1:
2:
3:
4:
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#define N 500
main()
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58: }
{
Display
Window
GC
*d;
w;
gc;
unsigned long white,black;
XEvent event;
int i, j ,n;
XPoint points[N];
int ifstart[N];
d=XOpenDisplay(NULL);
white = WhitePixel(d,0);
black = BlackPixel(d,0);
w = XCreateSimpleWindow(d,RootWindow(d,0),50,100,320,200,
1,black,white);
/* Create Window resource (320 x 200) */
/* Make an appointment of the event type that should be detected (Four Types of Event) */
XSelectInput(d,w,ExposureMask|ButtonPressMask|ButtonMotionMask|KeyPressMask);
XMapWindow(d,w);
/* Map Window in X Server */
gc = XCreateGC(d,w,0,0);
/* Create Graphic Context Resource */
XSetLineAttributes(d,gc,5,LineSolid, CapRound,JoinRound); /* Set the Line Attributes */
XSetForeground( d, gc, BlackPixel(d,DefaultScreen(d)) );
n=0;
/* initialize the counter of points */
while (1) {
/* infinite loop */
XNextEvent(d,&event);
/* Pick up the next event from the event queue */
switch(event.type) {
/* branch to each case depending on four types */
case Expose:
/* When the hidden Window reappears */
for(i=0;i<n;i++)if(ifstart[i] != 1)
/* from 0 to (n-1)‘st point except for start mark */
XDrawLine(d,w,gc,points[i-1].x,points[i-1].y,points[i].x,points[i].y);
/* redraw all lines that had been drawn before they was hidden (except start point) */
break;
case ButtonPress:
/* When the Mouse Button Pressed */
ifstart[n] = 1;
/* n’th point is the start point to draw line */
points[n].x = event.xbutton.x; /* save the x,y coordinate of the mouse pointer */
points[n].y = event.xbutton.y;
n++;
/* increment n up to max */
break;
case MotionNotify :
/* When the Mouse pointer moves (Mouse Drag) */
points[n].x = event.xmotion.x; /* save the x,y coordinate of the mouse pointer */
points[n].y = event.xmotion.y;
XDrawLine(d,w,gc,points[n-1].x,points[n-1].y,points[n].x,points[n].y); /* to draw lines */
n++;
/* increment n up to max */
break;
case KeyPress : exit(0);
}
}
/* End of process when Any Key pressed */
/* End of Switch */
/* End of while */
/* End of Main */
図4.X11プログラミングその3(マウスボタン押下によるお絵描き)
このプログラムでは、8行目に構造体GC型の変数gcを定義しており、実際の識別子としては、25行目XC
reateGC関数の戻り値として初期化している.またイベント予約のための関数 XSelectInput では第1引数
ディスプレイ識別子d、第2引数ウィンドウ識別子 w に続き、第3引数イベントマスクには、4種類のイベント
マスク:ExposureMask、ButtonPressMask、ButtonMotionMask、KeyPressMask を指定している(4種類の
いずれかが発生したことを検知).更に26行目でコンビニエンス関数 XSetLineAttributes で線幅を5、種類を
実線(LineSolid)、線端形状を丸め( CapRound)、接続点形状を丸め(JoinRound)に設定する.
29行目からの無限ループではまず、XNextEvent 関数でイベントキューの先端にあるイベント情報を、XEvent
共用体型 event 変数に取り込み、そのメンバである event.type の値に応じてイベント処理を行う(31行目以降).
32行目から37行目までの Expose イベント処理では、遮蔽ウィンドウが再描画される際の処理を行う.従っ
てこれまでにマウスでトレースされた座標点の系列(ストローク毎に points 配列:point[0]∼point[n]に保持され
ている)を、順を追って再描画している.ここで34行目では各ストロークの始点(マウスボタンを押下した直
後の座標点)は描画対象から除外している。そうしないと前ストロークの終点と次のストロークの始点とが繋が
ってしまうからである。
次に39行目から45行目までの ButtonPress では、マウスボタンが押下された旨のイベントが発生したことを
受けて、第n番目の点(現在点)がストロークの始点であることをマークしておくと同時に point[n]にイベント発
生時のマウスポインター座標を保存し点の添字nをインクリメントしておく.続く47行目から53行目までの
処理では、イベントタイプ MotionNotify、すなわちマウスボタンが押下されながらマウスポインタが移動したと
いうイベント発生を受け、point[n]にイベント発生時のマウスポインター座標を保存し、point[n-1]から point[n]
までの線分を新たに描画すると同時に点の添字nをインクリメントしておく.
そして最後に KeyPress イベントすなわち window 上で任意のキーが押された場合に無限ループを抜け出し、プ
ログラムが終了する.
なお、Expose イベントならびに MotionNotify イベントの処理における線分の描画には XDrawLine 関数を用
い、XSetLineAttributes で設定した属性、XSetForeground で設定した色により描画する。
<中間課題 (碁石と碁盤の描画)>
はじめに縦16横16個の格子をもつ碁盤をウィンドウ上に描画しておき、そのウィンドウ上でユーザがマウス
クリックする度に、マウスポインタの場所から最も近い格子点上に黒丸と白丸を交互に描画していく(碁石を置
いていく)プログラムを作成せよ.なお、ウィンドウが他のウィンドウに遮蔽されたのち再び現れる場合も、そ
れまでの履歴に従って、置かれた碁石をすべて再描画すること.
<ヒント>
マウスボタン押下の際に黒丸を描画する場合、イベント ButtonPress 処理時に XFillArc 関数を用いて内部が塗
りつぶしてある円弧(黒丸)を描画すればよい. XFillArc 関数は、第1、2引数は各々ディスプレイID、ウィ
ンドウID、第3引数はXCreateGC関数で取得したGCのID、第4、5引数は描画する円弧に外接す
る矩形の左上端のx、y座標(int 型)を指定するが、ここでは押下された時点のマウスカーソル位置のx座標
event.xbutton.x とy座標 event.xbutton.y からもっとも近い格子点座標を計算し、それが丸の中央に来るように
各々の値に代入する.第6、7引数は描画する円弧の外接矩形の幅と高さ(unsigned int 型)、第8、9引数は各々
描画する円弧の開始角度と終了角度(単位はいずれも 1/64 度: int 型)である.白丸を描画する場合には同じく
XDrawArc 関数を用いる(引数は XFillArc と同じ)。
なお、X11が図形(プリミティブ)を描画するために用意している関数は、XDrawLine や XfillArc、XDrawArc
の他にも XDrawPoint(指定した座標に点を描画)、XDrawRectangle(矩形境界の線画)、XFillRectangle(矩
形内部の塗りつぶし)、XFillPolygon(任意の多角形内部の塗りつぶし)など多数存在するが、詳細な説明は紙面
の都合上省略する.
表1.代表的なコンビニエンス関数
関数名
意味
引数
XSetFunction
出力バッファ間論理演算
Display* d ,GC gc , int function_no
XSetPlainMask
出力バッファマスク属性
Display* d, GC gc, unsigned long color
XSetForeground
前景色の決定
Display* d, GC gc, unsigned long color
XSetBackground
背景色の設定
Display* d, GC gc, unsigned long color
XSetLineAttributes
直線描画属性の設定
Display* d, GC gc, unsigned int line_width,
(線幅,線種,端点処理,
int line_style, int cap_style,
交点処理)
int join_style
XSetFillStyle
プリミティブ塗りつぶしスタ
Display* d, GC gc, int fill_style
XSetArcMode
イル
Display* d, GC gc, int arc_mode
XSetFont
円弧塗りつぶし種別
(扇or 弦) Display* d, GC
文字書体(フォント)属性設
定
gc, Font
font
第2部
6.プロセス間通信
以下では、複数のプロセスどうしが互いにデータの交換を行う、プロセス間通信のプログラミング技法について
学んでいく。プロセス間通信では、同一ホストマシン上の複数プロセス間ならびに、ネットワークを介したプロ
セス間送受信を統一的にサポートしているため、ネットワークプログラミング、グループウェア等の開発に利用
できる。
6.1
ソケット
UNIXにおけるプロセス同士のコミュニケーション(データの送受や同期を取るなどの処理)の例としては、
X11や各種リモートコマンドで学んだようなサーバ・クライアントシステムが挙げられる.つまり、UNIX
におけるすべてのサーバ・クライアントモデルはサーバと呼ばれるプロセスとクライアントと呼ばれるプロセス
の対から構成されており、このおかげでネットワーク上の様々なホストコンピュータに対して均質な機能が保証
されている. そしてサーバプロセスとクライアントプロセスの両者を取り持つ代表的ツールがソケットと呼ばれ
る通信路である(図5、図6).
この方法を用いれば、サーバプロセスとクライアントプロセスの各対に独自のソケットを定義することで、ユ
ーザプログラムはネットワーク上の全てのUNIXホストに対して同一かつ均質の機能を実現することができ
る.
具体的には、以下の2種類の通信方式を使い分けることができる.
クライアントプロセス
socket()
connect()
サーバプロセス
socket()
bind()
listen()
accept()
socket 通信路の生成
socket 通信路の結合
read()
write()
write()
read()
close()
close()
socket 通信路の閉鎖
図5. ソケットを用いたコネクション型サーバクライアントモデル
クライアント
socket()
bind ()
サーバ
socket の生成
sendto()
socket()
bind()
recvfrom()
データ送受信
recvfrom()
sendto()
図6. ソケットを用いたコネクションレス型サーバクライアントモデル
(1) コネクション型(ヴァーチャルサーキットともいう):サーバ・クライアント間で個別の通信路を設定してお
いて自由にデータ送受を行う方式
=
電話による伝達方法に類似
(2) コネクションレス型(データグラムともいう):各プロセスはその都度送信データに相手のアドレスを付けて
送信する
=
はがきによる伝達方法に類似
ここで、図5に示したコネクション型方式は、プロセス間で独自の通信路を接続しなければならない(サーバ側
での listen, accept, クライアント側での connect により接続). しかしながらいったん通信路を確立してしまっ
た後はそれが閉鎖(close)されるまで、互いに自由に(相手のアドレスその他を再指定せずに)送信受信が可能
である. これに対して図6のコネクションレス型方式では socket 関数でソケットを生成し bind 関数で登録した
後(サーバの IP アドレスを知らない場合は gethostbyname 関数で取得した後)、すぐにデータ送受が可能とな
る. この場合送信側は毎回相手の IP アドレスを指定してデータを目的地に届けるよう指示しなければならない.
また、プロセスどうしの通信領域にも2種類の区別がある.サーバプロセスとクライアントプロセスがインター
ネットを介して通信する(サーバとクライアントがネットワークで接続している別々のホストに存在する)場合
と、同一のUNIXホスト上で通信する場合とで若干手順が異なる.前者をINETドメインといい、後者をU
NIXドメインという.INETドメイン上の通信の場合、互いの IP アドレスと共通のチャネル識別子(ポート
番号)の情報が必要である.
以下ではソケットを用いたプロセス間通信の基本的なプログラム作成を試みる. ここでは、通信方式として比
較的容易なコネクション型を、通信領域としては INET ドメインによる通信を取り上げる.
これまでと同様に
まず簡単なプログラム例を示す(ネットワークを介したチャットプログラム:図7(a)∼(c)).
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<netdb.h>
#include<fcntl.h>
#include<stdio.h>
#include<string.h>
#define NAMELENGTH 40
/* ホスト名+ドメイン名の格納される文字配列長 */
#define BUFMAX 40
/* バッファサイズ */
#define PORT_NO (u_short)10000 /* ポート番号 */
#define Err(x) {fprintf(stderr,"-"); perror(x); exit(0);} /* エラーメッセージのマクロ化 */
static char buf[BUFMAX], shostn[NAMELENGTH];
/* グローバル変数 */
static int sofd, nsofd, number;
/* バッファ、ホスト名の文字配列、ソケット ID */
static struct sockaddr_in svaddr, own;
/* ソケットアドレス構造体 */
static struct hostent *shost;
/* ホストのアドレス構造体 */
int servpro();
/* サーバプロセス関数のプロトタイプ */
int cliepro();
/* クライアントプロセス関数のプロトタイプ */
main()
{ number=0;
while(1) {
/* 無限ループ */
printf("待機する(サーバ):1 接続する(クライアント):2 \n");
scanf("%d",&number);
printf("\n");
if(number==1 || number==2) break; }
if(number==1) servpro();
/* サーバーとしての処理はサーバ関数 */
if(number==2) cliepro();
/* クライアントとしての処理はクライアント関数 */
}
図7(a).TCP/IP ソケットを用いた Chat プログラム(main 関数)
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
int servpro()
{
int n;
/* サーバプロセス */
/* サーバのホスト名を知る */
if(gethostname(shostn,sizeof(shostn))<0)
Err("gethostname");
printf("hostname is %s", shostn);
printf("\n");
/* サーバのホスト名から IPアドレスを取得する */
shost=gethostbyname(shostn);
if(shost==NULL) Err("gethostbyname");
/* ソケットに名前をつける */
bzero((char *)&own,sizeof(own));
own.sin_family = AF_INET;
own.sin_port = htons(PORT_NO);
bcopy((char *)shost->h_addr, (char *)&own.sin_addr, shost->h_length);
sofd=socket(AF_INET,SOCK_STREAM,0);
if(sofd<0) Err("socket");
if(bind(sofd,&own,sizeof(own))<0) Err("bind"); /* sofd と own の結合 (チャンネル公示)*/
/* 受信キューサイズ=1 */
listen(sofd,1);
/* クライアントからの接続要求の待ち */
nsofd=accept(sofd, NULL, NULL);
/* アクセプト(受理)するまでブロック */
if(nsofd<0) Err("accept");
/* 最初のソケット sofd を基に nsofd を新規作成し、通信路とする */
close(sofd);
/* sofd は不要となったので削除 */
write(1,"CHAT START\n",12);
/* 標準出力に“CHAT START”プロンプトを表示 */
printf("%d\n",nsofd);
/* 新たに取得したソケット ID:nsofd の番号を表示*/
while(1) {
/*** 無限ループ***/
n=read(0, buf, BUFMAX);
/* 標準入力から文字列の取得 −> バッファ*/
write(nsofd, buf, n);
/* ソケット宛てにバッファ −>文字列の送信 */
if(!strcmp(buf,"Q\n")) break;
/* バッファの中身が「Q」なら通信終了 */
bzero(buf,BUFMAX);
/* バッファのクリア */
n=read(nsofd, buf, BUFMAX);
/* ソケットから文字列の受信 −> バッファ*/
write(1,"client:",7);
/* 標準出力に、「client:受信文字列」の表示 */
write(1, buf, n);
if(!strcmp(buf,"Q\n")) break;
/* 受信バッファが「Q」なら通信終了 */
bzero(buf,BUFMAX);
/* バッファのクリア */
}
/*** 無限ループ終了 ***/
close(nsofd);
/*ソケット通信路の切断*/
}
図7(b).TCP/IP ソケットを用いた Chat プログラム(サーバー関数)
ここで、図7(a)の main 関数は、サーバとしての処理(相手からの接続を待つ側)を行うかまたはクライアン
トとしての処理(相手のコンピュータに接続要求する側)を行うかの選択をユーザに委ねる.前者なら図7(b)の
servpro 関数を呼び出し、後者なら図7(c)の clientpro 関数を呼び出す.
Servpro 関数ではまず6行目の gethostname 関数で「ホスト名.ドメイン名」からなる文字列を shostn 変数
に取り込み、それを8行目の printf 関数で標準出力に表示する.次に11行目の gethostbyname 関数でこのホス
ト名を IP アドレスに変換し、shost 変数に入れる.14行目から17行目までは own なるソケットアドレス変数
の初期化と設定( AF_INET ドメイン、ポート番号、IP アドレス)を行う.18行目のソケット関数でソケットを
生成し、20行目の bind 関数で生成したソケットの識別子 softd と own 変数をバインド(結合)し、自他ホスト
に存在するクライアントプロセスから参照可能にする.
22行目から26行目まででは、socket システムコールと bind システムコールで生成したソケット記述子 sofd
を用いて、クライアントからの接続要求に対して受け入れ態勢をととのえる。listen 関数は待機状態に入る前の準
備として、クライアントからの接続要求をいくつまで受け付けるかを指定する。24行目の accept 関数は、listen
関数で用意した待ち行列に、クライアントからの接続要求が到着するまで待ち状態(ブロック)になり、接続要
求が到着すると待ち状態が解除され、今まで使っていた接続用のソケット記述子 sofd と同じ特性の新しいソケッ
ト記述子 nsofd を作成する。以後の通信はこのソケット記述子を使用する。26行目の close 関数では、使用しな
いソケット sofd を解放している。接続用に使っていた最初のソケット sofd は、本来 accept 後も他からの接続要
求を受け取るために使われるが、ここでは接続要求は1つであると決めていることから接続用ソケットはもう使
用しない。
29行目からの、無限ループのなかの read 関数(データの読み込み)、write 関数(データの書き込み)によ
ってサーバとクライアントのプロセスの間でデータのやりとりが行われる。30行目の read で標準入力(キーボ
ード)から buf に読み込み、31行目の write でソケットにかきこむ(送信)。送信が済むと34行目の read で
ソケットから buf にデータをしまう(受信)。36行目の write で buf にあるデータを標準出力に書き出している。
33,38行目の bzero 関数では、buf の内容をクリアしている。プログラムは、“Q”を送信するか、または受
信したときに終了する。通信が終了したときは、40行目の close 関数で通信用ソケット記述子 nsofd を解放して
いる。
■ 接続の準備(listen)
サーバーは listen()システムコールにより、接続のための準備を行なう。
int listen(int s, int backlog)
s はソケット記述子。backlog は接続要求をいくつ受け付けるかを指定する。 最大値は通常 5。
■ 接続(accept)
サーバー側では、クライアントの connect 要求を accept によって受け入れる。
int accept(int s, struct sockaddr *addr, int *addrlen)
s はソケット記述子。 addr には空の構造体のポインタを渡す。accept()が成功すると、システムが 自動的に書き
込む。
接続要求が完了すると accept()はクライアントと接続された新しいソケット 記述子を返す。
サーバはクライアントとの通信にこの新しい記述子を用い、socket()コールの時に返された古い記述子はもう必要
なくなる。
■ 通信(write,read)
接続が完了した後は、write()、read() システムコールにより通信を行なう ことができます。
ssize_t write(int d, const void *buf, size_t nbytes)
d はファイル記述子。buf は送信したいデータ。nbytes は送信するデータのバイト数。返値は 実際に送信された
データのバイト数
ssize_t read(int d, void *buf, size_t nbytes)
d はファイル記述子。buf は受信したデータを置くバッファ。nbytes は受信するデータのバイト数。
返値は実際に送信されたデータのバイト数。
なおファイル記述子において、0はキーボードからの標準入力、1は端末画面への標準出力。ソケット記述子は
ソケットに対応している。
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
int cliepro()
/* クライアントプロセス */
{
int n;
printf("ホスト名?\n");
scanf("%s",shostn);
/* 「ホスト名.ドメイン名」 から IP アドレスに変換する */
shost = gethostbyname(shostn);
/* IP アドレス取得 */
if(shost==NULL) Err("gethostbyname");
/* ホスト名が無い? */
bzero((char *)&svaddr, sizeof(svaddr));
/* svaddr ソケット構造体を初期化 */
svaddr.sin_family = AF_INET;
/*
svaddr.sin_port = htons(PORT_NO);
bcopy((char *)shost->h_addr, (char *)&svaddr.sin_addr, shost->h_length);
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
/* TCP/IP ソケットの生成 */
sofd=socket(AF_INET,SOCK_STREAM,0);
if(sofd<0) Err("socket");
connect(sofd, &svaddr, sizeof(svaddr));
write(1,"お待ち下さい\n",13);
printf("%d\n",sofd);
/*無限ループ*/
while(1) {
n=read(sofd, buf, BUFMAX);
/* サーバからのメッセージ取り込み */
write(1,"server:",7);
/* サーバからのメッセージは・・・*/
write(1, buf, n);
/* #“$%‘&%@ です。
*/
if(!strcmp(buf,"Q\n")) break;
/* 受信文字が「Q」 ならチャット終了 */
bzero(buf,BUFMAX);
/* バッファを初期化 */
n=read(0, buf, BUFMAX);
/* キーボードからの文字列入力を読み込む */
write(sofd, buf, n);
/* 入力文字列をサーバに送信 */
if(!strcmp(buf,"Q\n")) break;
/* 送信文字列が 「Q」 ならチャット終了 */
bzero(buf,BUFMAX);
/* バッファを初期化 */
}
/*無限ループ終了 */
close(sofd);
/* 通信路を切断 */
}
図7(c).TCP/IP ソケットを用いた Chat プログラム(クライアント関数)
cliepro 関数ではまず5行目で、接続先ホストの「ホスト名.ドメイン名」を入力。7行目の gethostbyname で、
IP アドレスを得ている。15行目までやっていることはサーバプログラムと同じであるが、ソケットアドレス変
数 svaddr には自分のものではなく、接続先の相手の IP アドレスとポート番号を入れている。16行目の connect
関数では接続先ホストのアドレスを指定し、14行目で作成したソケット sofd を通して接続要求を行う。connect
コールが戻るとソケット記述子 sofd に対して read,write を行うことができる。クライアントプロセスがサーバプ
ロセスと違うところは、socket 関数で作成した記述子をそのまま使うところと、connect 関数によって相手サーバ
との間に接続が確立されるため bind 関数が使われないところである。
■ 接続要求(connect)
サーバに接続要求を出す。
int connect(int s, struct sockaddr *name, int namelen)
引数は、s はソケット記述子。 name はアドレス構造体へのポインタ。namelen はアドレス構造体のサイズ。
○同期と非同期
通信を行うのに双方がどういうタイミングでデータのやり取りをするのかきちんとスケジュールが決まっている
やり方を「同期通信」、それと違っていつデータが飛んでくるかわからない方式を「非同期通信」と呼ぶ。
非同期通信では送りたいときにデータを送り、送られてきたデータは否応なくすぐに受け取ることになる。そう
なると非同期の場合、オープンされている記述子すべてを監視してデータが来ていないかどうかチェックをし続
ける必要がある。記述子を監視し、チェックし続ける関数として select 関数がある。
上記で取り上げた「チャット」プログラムは、同期通信のものである。すなわち最初のメッセージはサーバから、
次はクライアント、次はサーバというようにお互いに順番を守ってメッセージをやりとりする必要がある。非同
期通信ではそれを行う必要がない。メッセージのやりとりを勝手に行うことができる。
注:今回の課題である“五目並べゲーム”は、データのやりとりの順番が決まっている。すなわち同期通信でじ
ゅうぶん達成できるものである。とくに select を使わなければできないというものではない。select を使い、非
同期通信方式で作ろうと思う人は下記を参照するとよい。
http://yonex1.cis.ibaraki.ac.jp/progai4/public_html/page04.htm
プログラミング概論および演習 I I レポート課題
− X11を用いたイベント駆動型プログラミング演習 −
上記で学んだX11プログラミングならびにソケット通信プログラミングを応用して以下のことを行う
プログラムを作成しなさい.
1)課題概要:
インターネットにおけるホスト名+ドメイン名の「ホストコンピュータ識別子」を使用して対
戦相手を指定する/されることにより、対戦相手どうしの間の通信路を確立したうえで「ネットワーク対
戦型五目並べゲーム」を行えるようなプログラムを UNIX 上で作成すること。この際、同プログラムはO
SIトランスポート層におけるTCP/IPプロトコル上で下記に指定されたアプリケーションプロトコ
ルを実装すること。従って X11および TCP/IP ソケットを用いて作成すること。また、ユーザが自ら対
戦相手を指定して接続要求を出すか(クライアント)、対戦相手から接続要求があるまで待機する(サー
バ)かを選択できるようにプログラムを設計すること。なおポート番号は十進数の20000を用いるこ
と。
2)課題仕様
レポート課題として作成するネットワーク対戦型五目並べは以下のような仕様を満たしている
必要がある。
① 動作環境
X Window(X11R?) の動作する UNIX ワークステーション
② 開発環境
TCP/IP ソケットライブラリ+X11 ライブラリ、GNU C コンパイラ
③ 碁盤サイズ 16 × 16
④ 通信方式
TCP/IP のソケット(コネクション型)を用いる。アプリケーションプロトコル(TCP/IP の
上位プロトコル)として、相手のコンピュータとの通信に以下の 3 つのメッセージを定義する。
これ以外のメッセージを受信した場合は無視する。
表2.アプリケーションプロトコル
メッセージタイプ
機能
(いずれも文字列)
(X,Y)に碁石を置く。X,Y はそれぞれ 16 進数"0 F"の 1 桁
1)”PLACE-XY”
2)”ERROR”
3)”YOU-WIN”
例) PLACE-7A--(7,A)に自石を置く( 接続後サーバ→クライアントの順で交互に送
信)
。
碁石の置ける条件が一致しないとき送られる。ERROR 受信側から接続を切断
(close)する。
相手が五目並べたとき送信。従って1)を受信した際に五目が成立したことを調
べ、成立した場合にこのメッセージを送信して「自分の負け」を通知する。
YOU-WIN を受信した側は接続を切断(close)する。
⑤ ルール
・碁石はサーバが先手(黒石)クライアントが後手(白石)で交互に 1 個ずつ置いていく。
・既に碁石が置かれている場所に新たに碁石を置くことはできない。
・縦、横、斜め、いずれかの方向に同色の碁石を先に五目並べた方が勝ち。
・盤が碁石で埋まっても勝負がつかない場合は引き分けとする(この場合最後の一手を打つ PLACE_XY
メッセージが送信され、これを受けた側が ERROR メッセージを送信し、これを受けた側から close(接続切断)
を発信し終局される)。
3)プログラム作成要領
プログラムのおおまかな流れを下図に示す。手続きを簡略化するため、ゲームはコネクショ
ンが確立すると同時に開始されるようにする。このときサーバ側は常に先手、クライアント側は
後手とし、1ゲームが終わるとコネクションは切れ、再度ゲームを行う場合には接続を行うか接
続待ち状態にする必要がある。また、メッセージを送信した後 read 関数で相手からのメッセージ
到着を待つ間、プログラムはブロックされてしまう。そこで、select 関数を用いて non-blocking
型の受信待ちをすることができる。
サーバプロセス
クライアントプロセス
ソケット生成
ソケット生成
socket()
socket()
アドレス/ソケット間結合
接続要求
bind() , listen()
connect()
接続許可
接続許可通知
accept()
ゲーム開始
ゲーム開始
マウス押下イベント取得
write(“PLACE-XY”)
read(buffer)
マウス押下イベント取得
read(buffer)
write(“PLACE-XY”)
終了判定
no
終了判定
close()
close()
no
図8.対戦型五目ならべプログラムの流れ図
なお、レポート作成にあたって以下の諸基準を厳守すること.
書式の基準
・用紙サイズはA4とする.
・上部二個所をステープラでとめること.
レポート書式
・レポートの書式は「技術文書の書き方」を参照のこと。この基準を満たさないものは受理しない。
レポートの提出
・プリンタにより出力したものを情報工学科レポート提出箱に提出すること.
詳細は追って掲示するので注意すること