13日目 15日目

14日目

目標


課題13

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(167864066);
	struct sockaddr_in target;
	FILE *fp;

	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));

	printf("%d\n", s);

	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);
	}

}

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

ここまでで、プロセスと外部とのやり取りには 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);

同じように、サービス名からポート番号等の情報を含んだ 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;

バイトオーダ

上記の解説で「バイトオーダー」という言葉が出てきた。 これは、2バイト以上の大きさをもつデータをどのような順番で格納するかを意味する。 たとえば、int型の整数 i に 0x12345678 という数字を格納したとしよう。 int型のサイズを4バイトだとすると i という変数で4バイトの領域を使用することになる。 メモリの番地は1バイトごとについているので、メモリを1バイトづつ調べるような次のプログラムを使用すると どのようにデータが格納されているかを調べることができる。

/* chk_byteorder.c */
#include <stdio.h>

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

	p = (char *)&i;
	for (j = 0; j < sizeof(i); ++j) {
		printf("%p: %02x\n", p+j, *(p+j));
	}
	return 0;
}

同じプログラムを Intel系の CPU のマシンと、Motorola系の CPU のマシンで実行すると 異なる結果が得られる。

このように、世の中には異なるバイトオーダーのコンピュータが存在するため、 それらのコンピュータ間であっても正しく通信を行えるよう、TCP/IP では IP アドレスやポート番号などのデータを表現するバイトオーダを定めている。

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

問題 13

Web 情報を得るため指定する URL (例 http://www.s.fpu.ac.jp/ogaito/OK.html)は、 サービス名(http)、ホスト名(ww.s.fpu.ac.jp)、そのホスト内部でのパス(/ogaito/OK.html) に 分解することができる。 上記の情報にアクセスするための実際のプログラムを次に示す。 (~tacha/kadai13/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 name;
	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;
	}

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

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

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

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

	/* アドレス */
	memcpy(&name.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 *)&name, sizeof(name)) == -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 2006/02/10 02:53:59 tacha Exp $