13日目 15日目

14日目

課題14

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

前回見たように、TCP/IP による通信には、大きく分けて TCP と UDP が存在する。 ProgrammingE の最終回にあたり、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(2634423298); /* IP アドレス (157.6.28.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 はネットワークオーダであることが読み取れ、プログラムの可読性が向上する。 そのため、適切に使い分けることが重要である。


ファイルディスクリプタ (低水準入出力)

ここまでで、プロセスと外部とのやり取りには FILE 構造体を用いて fputc, fgetc, fprintf, fscanf などを用いてきた。 (標準入出力を利用する printf等も同様である。) これらの関数は「高水準入出力関数」ともよばれ、入出力はいったん OS 等が 管理するメモリ領域におかれたあと、プログラムによって指定されたメモリ領域にコピーされる。 これらの関数は OS 等が異なっても同様に使えるよう互換性に配慮されたものとなっている。

一方、実際のデータの入出力には「低水準入出力関数」が用いられる。 低水準入出力関数ではプロセスと外部とのやり取りには 「ファイルディスクリプタ」(file descriptor)とよばれるものを通じて行われる。 ファイルディスクリプタは通常int型の0以上の整数で表される。 プログラムが起動した際には 0 が標準入力、1 が標準出力、2 が標準エラー出力である。 明示、非明示を問わず、オープンしたファイルに対しては、 すでに割り当てられていない0以上の整数のうち、最小のものがディスクリプタとして割り当てられる。 低水準入出力関数における、ファイルのオープン/クローズには open/closeを用いる。

fdopen関数を用いると、すでに存在するファイルディスクリプタから 高水準入出力関数で用いられるファイル構造体を得る事ができる。


ソケット

プログラムから「通信回路」を利用するには「ソケット」(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 構造体を返す関数である。 ホスト名だけでなく、157.6.28.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;

問題 14

Web 情報を得るため指定する URL (例 http://www.s.fpu.ac.jp/ogaito/OK.html)は、 サービス名(http)、ホスト名(ww.s.fpu.ac.jp)、そのホスト内部でのパス(/ogaito/OK.html) に 分解することができる。 上記の情報にアクセスするための実際のプログラムを次に示す。 (O:\examples/14/get.c としてアクセスできる)

このプログラムを元に、起動時に渡される任意の URL (http:// だけでよい)で 示される情報を取得し、ファイルに保存するようなプログラムを作成せよ。 具体的には

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

と実行すると、現在見ているページのソースが 14.html という名前の ファイルに格納されるようなプログラムに変更せよ。

学外の URL については、うまく動作しない可能性があるので、 テストには、http://prog2.s.fpu.ac.jp/%7Etacha/OK.html や、 情報基礎演習で作成した自分のページを指定してみること。

/* get.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>
#include <stdlib.h>
#include <unistd.h>

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 = "www.s.fpu.ac.jp";
	char *service = "http";
	char *socktype = "tcp";
	char *path     = "/ogaito/OK.html";

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

	/* getservbyname 関数
	 * service (この場合は "http") から、そのサービスに
	 * 関する情報(ポート番号を含む)を検索
	 */
	if (NULL == (serv_ent = getservbyname(service, socktype))) {
		fprintf(stderr, "getservbyname: Unknown service \"%s\" on %s.\n",
			service, socktype);
		return -1;
	}

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

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

	/* getservbyname や gethostbyname の返り値は
	 * すでに network byte order になっている */

	/* ポート番号 */
	target.sin_port = serv_ent->s_port;

	/* アドレス */
	memcpy(&target.sin_addr, host_ent->h_addr, host_ent->h_length);


	/* ソケットの作成 */
	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);
	}

	/* 情報の送付を要求 */
	fprintf(fp, "GET %s HTTP/1.0\r\n", path);
	fprintf(fp, "Host: %s\r\n\r\n", host);

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

	return (0);
}

13日目 表紙 15日目

tacha@tack.fukui-med.ac.jp
$Id: 14.html,v 1.1 2007/02/15 14:43:31 tacha Exp $