13日目 15日目

14日目

目標

課題16

TCP 接続を行うクライアントプログラムの作成

前回見たように、TCP/IP による通信には、大きく分けて TCP と UDP が存在する。 課題16では、TCP によるクライアントプログラムを作成する。 もっとも簡単な例として、Web サーバに接続しデータを受領するプログラムとする。

はじめに、TCP による、通信を行う際の、手続きの流れをまとめると次のようになる。

TCP による通信の流れ
サーバプロセス   クライアントプロセス

1) 通信ポートの作成

2) 通信要求を待つ
3) 通信ポートの作成
4) 通信要求の送付
5) 通信要求の受領
通信回路(コネクション)の確立

通信(データ転送)

クライアントから送付されたデータ(サービス要求)がサーバで受理される。

サーバからのデータ(サービス)がクライアントに送付される。

上記を、繰り返しても良いし一度だけでも良い。

終了処理(どちらからはじめても良い)

サーバプロセスと、クライアントプロセスは同一のコンピュータ上にあっても 異なるコンピュータの上にあってもかまわない。

今回、TCP を用いる理由は、コネクション型のプロトコルであるため、 通信回路が確立したあとは、通常のストリームと同様の機能が提供され、 ファイルからの入出力と同様に利用できるためである。

UDP の場合は、非コネクション(コネクションレス connection less)型の プロトコルであるため、データの到着順序や漏れの有無等をプログラム内で 確認する必要がある。

簡単なソケット通信

先週のネットワークの構造の説明と対応を取るために、本当に必要な部分だけを抜き出した ネットワーク通信プログラムを次に示す。
ふつう、こんなプログラムは作成しないので注意すること。

/* socket_test.c */
#include <sys/types.h>
#include <sys/socket.h>

#include <netinet/in.h>

#include <errno.h>
#include <malloc.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
	int s, ch;
	long ip = htonl(167864066); /* IP アドレス (10.1.103.2) */
	struct sockaddr_in target;
	FILE *fp;

	/* TCP による接続のための準備 */
	if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
		fprintf(stderr, "socket: %s\n", strerror(errno));
		exit(1);
	}

	/* 通信相手の指定 */
	target.sin_family = AF_INET; /* インターネットプロトコル */
	target.sin_port   = htons(80);  /* ポート番号 */
	memcpy(&target.sin_addr, &ip, sizeof(ip));


	/* 接続 */
	if (connect(s, (struct sockaddr *)&target, sizeof(target)) == -1) {
		fprintf(stderr, "connect: %s\n", strerror(errno));
		exit(1);
	}

	/* 「送受信」 <-> 「読み込み書き込みモード」 */
	if (NULL == (fp = fdopen(s, "w+"))) {
		close(s);
		fprintf(stderr, "Cannot `fdopen' on the socket\n");
		exit(1);
	}

	/* 「送信」 */
	fprintf(fp, "GET /ogaito/OK.html HTTP/1.0\r\n");
	fprintf(fp, "Host: www.s.fpu.ac.jp\r\n\r\n");
	fflush(fp);

	/* 受信*/
	while (EOF != (ch = fgetc(fp))) {
		fputc(ch, stdout);
		fflush(stdout);
	}

}

バイトオーダ

前回見たように、他者と意思疏通を行うためには、なんらかのルールが必要であった。 TCP/IP もそのルールのひとつであるが、コンピュータのアーキテクチャによる違いも考慮する必要がある。 特に重要なもののひとつがバイトオーダ(エンディアン)の違いである。 バイトオーダとは、複数のバイトから構成されるデータ(すなわち、char 型以外のもの)をメモリ上に配置する方式の種類のことである。

試しに次のような int型の変数がどのように値を格納しているのか調べてみるプログラムを PowerPC で動作している Mac OS X と、 Pentium III で動作しているプログラミング E の実習を行っているマシンで実行してみよう。

/* byteorder.c */

/* バイトオーダーの違いを確かめる */
#include <stdio.h>

int main() {
	char *p;
	int ip = 0x12345678;
	int i;

	/* int 型の整数の先頭アドレスを代入。
	 * char 型でアクセスするので 1バイトずつ
	 * 中身を確認することができる
	 */
	p = (char *)&ip;
	printf("sizeof(ip) = %d\n", sizeof(ip));

	for (i = 0; i < sizeof(ip); ++i) {
		printf("| %02x ", *(p + i));
	}
	printf("|\n");

	return 0;
}

同じプログラムでも出力結果が異なることがわかる。 データの下位バイトからメモリに「12 34 56 78」と並べる方式をビッグエンディアン(Big endian)、 下位バイトから「78 56 34 12」と並べる方式をリトルエンディアン(Little endian)と呼ぶ。 このような異なる機種間でデータをやり取りする際には、あらかじめどちらの方式でデータのやり取りを行うのかをきめておく必要がある。

TCP/IP ネットワークでは、このような通信を可能とするために、IP アドレスやポート番号を含む ネットワーク上のデータはビッグエンディアンを用いることと定められている。 そのため、ビッグエンディアンの事を「ネットワークオーダ」とも呼ぶ。

そのような変換(が必要な場合)に用いる関数として htonl, htons, ntohl, ntohs等が用意されている。 htonl 関数は long (4バイト)で示される値をホスト上のバイトオーダ(ホストオーダ)から ネットワークオーダへ変換する。 htons 関数は short (2バイト)に対する同様の関数である。 ntohl, ntohs 関数は、ネットワークオーダからホストオーダへの変換を行う。

少し考えればわかるように、実際には htonl と ntohl (htons と ntohs) は同じ操作である。 しかし、プログラム中で htonl(x) と書いてあれば、x はホストオーダーである事が、 ntohl(z) と書いてあれば z はネットワークオーダであることが読み取れ、プログラムの可読性が向上する。 そのため、適切に使い分けることが重要である。


ソケット

プログラムから「通信回路」を利用するには「ソケット」(socket)を用いる。 ソケットはファイルに対する open の様に、ネットワークトランスポート層を 利用した通信用のファイルディスクリプタを作成する。

socketは、3つの引数をとり、要求に応じたファイルディスクリプタ(これもソケットと呼ぶ)を作成し返す。

第1引数はドメインを指定する。ドメインによって通信に使用されるプロトコルファミリー(protocol family)を指定する。 TCP/IP (IPv4) 通信に置いては PF_INET を指定する。

第2引数は、通信方式を指定する。 前回の IPv4 ネットワーク概論で出てきた TCP と UDP の指定を行う部分である。 TCP の場合は SOCK_STREAMを、 UDP の場合は SOCK_DGRAM を指定する。

第3引数は、ソケットによって使用される固有のプロトコルの指定を行う。 通常、与えられたプロトコルファミリーの種類ごとに存在するプロトコルはただ一つであり、その場合には 0 を指定することができる。

したがって、TCPを用いた IP(v4)通信を行う場合には次のように指定することとなる

  s = socket(PF_INET, SOCK_STREAM, 0);    /* TCP */

作成されたソケット(s)に対する操作としては、様々なものがあるが、 TCP クライアントプログラムで必要な操作は、サーバ側のソケットへの接続要求を行う connect である。

接続要求を行うためには、サーバ側のソケットを指定する必要がある。 サーバ側のソケットは、IP(v4)の場合、アドレスファミリー(AF_INET)、 ポート番号、IP アドレスで指定する。 指定には sockaddr_in構造体を利用する。

struct sockaddr_in {
    sa_family_t    sin_family; /* アドレスファミリー AF_INET */
    u_int16_t      sin_port;   /* ポート番号 (ネットワークオーダ) */
    struct in_addr sin_addr;  /* IP アドレス */
};

struct in_addr {
    u_int32_t      s_addr;     /* アドレス (ネットワークオーダ) */
}

実は、上記のプログラムのように、直接ポート番号やアドレスを指定する事は少なく、 ホスト名から必要なアドレス情報を求める gethostbyname() や サービス名から必要なポート情報を求める getservbyname() 等が用いられることが多い。 可能ならばこれらの関数を用いるべきだが、やむをえず直接指定する必要がある場合、 htonX 関数を用いて、バイトオーダーをネットワークバイトオーダにすることが大切である。

sockaddr_in構造体に情報を指定したあと、connect を 用いてソケットに、その情報を設定する。

connectは、次の形式で呼び出す。

int connect(int s, const struct sockaddr *name, socklen_t namelen);

第1引数はソケットを、第2引数には情報が指定された構造体、第3引数には、その構造体の大きさを指定する。 ソケットでは、IPv4 以外のプロトコルを用いた通信も可能であるため、第3引数が必要である。 一般に sockaddr_AA構造体は、 先頭のアドレスファミリーを指定する部分は共通であるが、 それ以降は各プロトコルに応じた構造となっている。

connectが成功すれば、ソケットはサーバと接続された状態となり、 ソケットに書き込んだ情報はサーバに伝わり、サーバが書き込んだ情報はソケットを通して読み取ることができるようになる。


gethostbyname(), getservbyname()

gethostbynameは、 引数で与えられたホスト名に対する情報が格納された hostent 構造体を返す関数である。 ホスト名だけでなく、10.1.103.2のようなドットで区切られた IP アドレスでもかまわない。

hostenc 構造体は、次のような構造である。 ホストには、別名がついていたり、複数のアドレスを持つことが可能であるため、少し複雑に見えるかも知れない。

struct  hostent {
        char    *h_name;        /* ホストの正式名 */
        char    **h_aliases;    /* 別名のリスト */
        int     h_addrtype;     /* アドレスのタイプ */
        int     h_length;       /* アドレスの長さ */
        char    **h_addr_list;  /* 得られたアドレスのリスト */
};
#define h_addr  h_addr_list[0]  /* 最初のアドレス */

アドレスはネットワークバイトオーダで得られるため、構造体 sockaddr_inへ指定する際には 単に memcpy するだけで良い。 次のコードは、www.s.fpu.ac.jp に関する情報を得たあと、IP アドレスを設定している。

 struct sockaddr_in name;
 struct hostent *host_ent;
....
 host_ent = gethostbyname("www.s.fpu.ac.jp");
 memcpy(&name.sin_addr, host_ent->h_addr, host_ent->h_length);

同じように、getservbynameは、サービス名からポート番号等の情報を含んだ servent構造体を返す関数である。 サービス名と、プロトコル(tcp もしくは udp)を指定する。 得られた情報はすでにネットワークバイトオーダで得られるため、 構造体 sockaddr_inへ指定する際には単に値をコピーするだけで良い。 次のコードは、TCP 上の http サービスに関する情報を得たあと、ポート情報を設定している。

 struct sockaddr_in name;
 struct servent *serv_ent;
....
 host_ent = getservbyname("http", "tcp");
 name.sin_port = host_ent->s_port;
gethostbyname()関数は、エラーが発生した場合、返り値は NULL となり グローバル変数 h_errno にエラー内容がセットされる。 h_errno を引数として、hstrerror() を用いると人が理解できる文字列に変換することができる。

以上の関数を用いた、「もう少しまとも」なプログラムの例を次に示す。 このプログラムは tack.fukui-med.ac.jp の daytime というサービスを利用するための プログラムである。daytime というサービスはポート番号 13番で行われている サービスで、接続があった時点の時刻を返す。(昔は良く使われていたが、最近は あまり使われていない)

はじめに gethostbyname 関数を用いて「tack.fukui-med.ac.jp」という名前から IP アドレスへの変換を行う(番号案内)。

次に getservbyname 関数を用いて daytime サービスが行われているポート番号を得る(内線)。 daytime サービスは最近利用されていないため、番号が不明な場合は 13番という番号を用いることとした。

/* daytime.c */
#include <netinet/in.h>  /* sockaddr_in */
#include <sys/socket.h>  /* socket() */

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>      /* exit() */
#include <netdb.h>       /* gethostbyname(), getservbyname() */
#include <string.h>      /* memset(), memcpy() */

int main(int argc, char *argv[])
{
	int s;
	FILE *fp;
	struct servent *serv_ent;
	struct hostent *host_ent;
	struct sockaddr_in target;
	char buf[256];

	char *host = "tack.fukui-med.ac.jp";
	char *service = "daytime";
	char *socktype = "tcp";


	/* target の初期化 */
	memset(&target, 0, sizeof(target));

	/* アドレスファミリー ... IPv4*/
	target.sin_family = AF_INET;

	/* gethostbyname 関数
	 * host (この場合は "tack.fukui-med.ac.jp") から、そのホストに
	 * 関する情報(アドレスを含む)を検索
	 */
	if (NULL == (host_ent = gethostbyname(host))) {
		fprintf(stderr, "gethostbyname: %s\n", hstrerror(h_errno));
		return -1;
	}

	/* gethostbyname の結果はすでに、「ネットワークーオーダー」に
	 * なっているので、結果をコピーするだけでよい */
	memcpy(&target.sin_addr, host_ent->h_addr, host_ent->h_length);

	/* getservbyname 関数
	 * service (この場合は "daytime") から、そのサービスに
	 * 関する情報(ポート番号を含む)を検索
	 */
	if (NULL != (serv_ent = getservbyname(service, socktype))) {
		/* 検索結果があった場合、結果はネットワークオーダーである*/

		/* ポート番号 */
		target.sin_port = serv_ent->s_port;
	} else {
		/*
		 * 検索結果がなければ 13番を使う。
		 * 「ネットワークオーダー」に変換するために
		 * htons を利用
		 */
		target.sin_port = htons(13);
	}


	/* ソケットの作成 */
	if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
		fprintf(stderr, "socket: %s\n", strerror(errno));
		return -1;
	}

	/* 接続 (コネクションの確立) */
	if (connect(s, (struct sockaddr *)&target, sizeof(target)) == -1) {
		fprintf(stderr, "connect: %s\n", strerror(errno));
		return -1;
	}

	/* ディスクリプタから FILE 構造体へ */
	if (NULL == (fp = fdopen(s, "w+"))) {
		close(s);
		fprintf(stderr, "Cannot `fdopen' on the socket\n");
		exit(1);
	}

	/* 送られてきた情報の取得 & 表示 */
	while (NULL != fgets(buf, sizeof(buf), fp)) {
		printf("%s", buf);
	}
	fclose(fp);

	return (0);
}
  [user99@proge1]~/kadai16% ./daytime
  Thu Feb  7 23:26:19 2008

上記のプログラムで、ソケットから生成した FILE 構造体 fp に対して読み込みしか 行っていないのに、 fdopen() の引数が "w+" になっている事に疑問を持ったかも知れない。 上の daytime の場合は、相手先のマシンの時刻を得るというサービスであり、 クライアント側で指定する要素がないため、ソケットへの書き込みは存在しなかった。 しかし、サーバが「サービスを提供する」ためには、クライアントが「サービスを要求し」なければならない。 要求を行うためにはソケットに対して、書き込みを行うのである。

その例として、daytime.c をもとに Web 情報を得るためのプログラムを作成してみる事にする。 Web (HTTP) は、tcp の 80番ポートで行われているサービスである。

Web 情報は URL(Uniform Resource Locator)と呼ばれるインターネット上での 所在を記述するための方法で指定される。 Web 情報を表現する URL は次のように分解することができる。

http://tack.fukui-med.ac.jp/%7Etacha/fpu/ProgrammingE/14.html

この URL は、ホスト tack.fukui-med.ac.jp に対して HTTP(Hyper Text Transform Protocol) にしたがって接続し、 /%7Etacha/fpu/ProgrammingE/14.html を要求する 事を意味している。

上記のルールの理解ができれば、strchr 関数を使って次のように、URL を分解することができる。

/* url_split.c */
/*
 * 引数として、http:// から始まる URL を受取り、
 * scheme, ホスト名, 内部パスに分解する
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
	char *work;
	char *scheme, *hostname, *url_path;
	char *full_url;

	if (argc != 2) {
		fprintf(stderr, "使用法: %s URL\n", argv[0]);
		exit(1);
	}

	/* 引数を複製する */
	full_url = strdup(argv[1]);
	if (full_url == NULL) {
		fprintf(stderr, "メモリが確保できませんでした。\n");
		exit(1);
	}

	scheme = full_url;
	work = strchr(full_url, ':'); /* 左端から ':' を探索 */
	if (work != NULL) {
		*work = '\0';
		work++;
		work++;
		work++;
	}

	/*  :// の後ろまで来ている。ここはホスト名の始まる部分である。 */
	hostname = work;
	work = strchr(hostname, '/');  /* 最初の / を探索する */
	if (work != NULL) {
		*work = '\0';
		url_path = work+1;
	} else {
		url_path = strdup("");
	}

	printf("scheme:   <%s>\n", scheme);
	printf("hostname: <%s>\n", hostname);
        /* 欠ている先頭の / を補ってあることに注意 */
	printf("path:     </%s>\n", url_path);
	exit(0);
}

「HTTPにしたがって何かを要求する」ためには、接続後に、次のような形式にしたがって要求を送信する。 例として上記の URL を要求する場合を取り上げる。

  GET /%7Etacha/fpu/ProgrammingE/14.html HTTP/1.1[改行]
  Host: tack.fukui-med.ac.jp[改行]
  [改行]

ただし、[改行]の部分は、"\r\n" で表現する。

問題 16

url_split.c や daytime.c を参考にして、プログラム起動時に指定された URL (http:// で始まると仮定して良い)にアクセスし、内容をファイルに保存するようなプログラムを作成せよ。 保存するファイル名は固定でもかまわないが、URL の最後の / より右側 (上の例だと 14.html) になっていると実用的である。 この場合は、URL が / で終了している場合には "index.html"とせよ。


課題 17

複数の入出力をうまくさばく

課題15でとりあげた低水準入出力関数はその説明のなかに 「あまり使うべきではない」などと書いたが、ファイルディスクリプタという概念は必要となる場合もある。 課題17では、その例として、同時に複数の入出力が発生するような場合の処理について解説する。

トランシーバ等のように、同時に一人しか話せない「半二重通信」であれば、 たとえば、次のようなロジックを用いれば今までの学習内容で、プログラムが作成できる。

while (1) {

        /* A さんが喋っている */
        while (fgets(buf, sizeof(buf), fp1)) {
                if (strcmp(buf, "どうぞ"))
                        break;
                /* B さんに伝えるのに必要な処理をここに */
        }

        /* B さんが喋っている */
        while (fgets(buf, sizeof(buf), fp2)) {
                if (strcmp(buf, "どうぞ"))
                        break;
                /* A さんに伝えるのに必要な処理をここに */
        }
}

しかし、電話などに代表される「二重通信方式」の通信においては、どちらがどのタイミングで「話す」のかは不明なため、 上のロジックでは、動作しないことがある。
たとえば、A さんが「どうぞ」と言う前に、B さんが話した場合、どうなるか?

このような場合に、用いられる関数が selectである。 select に対して、複数のファイルディスクリプタのリストを引数として渡すと、 そのいずれかのファイルディスクリプタの状態に変化が起きるまで待機し、 入出力が可能になったファイルディスクリプタの情報を得ることが可能である。

待機する時間の最大値を設定することもできるため、決して変化しないファイルディスクリプタを 引数として渡すことで、こまかなプログラムの待ち状態を作成することも可能である。

select は、次のような書式をとる。

  #include <sys/select.h>

  int select(int nfds, fd_set * readfds, fd_set * writefds,
         fd_set * exceptfds, struct timeval *  timeout);

第1引数は、監視するファイルディスクリプタの内、(最大のもの + 1)である。 select は 0 から nfds-1 までのファイルディスクリプタを監視対象とする。

2から 4番目までの引数は、それぞれ、入力可能の監視対象、出力可能の監視対象、例外発生の監視対象を 指定する。監視が不要な場合は NULL を指定する。

5番目の引数は、状態変化がなかった場合、どれだけの時間待つかを指定する。 構造体 timeval は <sys/time.h> で指定されており次のような形式である。

     struct timeval {
             long    tv_sec;         /* 秒数 */
             long    tv_usec;        /* マイクロ秒数 */
     };

いつまでも待つ場合は NULL を指定する。

監視対象を指定する fd_setという型に対して値を設定するのは「ややこしい」ので つぎのようなマクロを用いる。表の中の fdsetは fd_set 型のポインタである。

名前役割
FD_ZERO(fdset) fdset をクリアする
FD_SET(fd, fdset) ファイルディスクリプタ fd を fdset に加える
FD_CLR(fd, fdset) ファイルディスクリプタ fd を fdset から除く
FD_ISSET(fd, fdset) ファイルディスクリプタ fd が fdset に含まれているかどうかを調べる。 返り値は含まれていなければ 0 となり、含まれていれば 0 以外となる。 すなわち if 文の条件節にそのまま書くことが可能である。

この講義の初回で実行した chat4.cでは、 関数 do_chatの中で select を使用して、ソケットとキーボードを監視し、 ソケットに入力(他人からのメッセージ)がある場合はその内容を画面に表示し、 キーボードに入力(他人へのメッセージ)がある場合はサーバへの送信処理を行っている。


13日目 表紙 15日目

tacha@tack.fukui-med.ac.jp
$Id$