前へ << 低水準ファイル入出力関数を使おう | C 言語で HTTP クライアントを作ってみよう (2) >> 次へ |
% cc -o http-client http-client.cとすることで、http-client というバイナリが作成されます。 SunOS ではネットワーク関係のライブラリが libc に含まれていないので、
% cc -o http-client http-client.c -lresolv -lsocket -lnslとライブラリを指定しなければならないでしょう。プログラムの実行は
% ./http-clientとすることで、http://localhost/ の内容をヘッダも含めて標準出力に出力します。
% ./http-client http://host % ./http-client http://host/path/ % ./http-client http://host:8080/path/file.htmlなどと URL を指定することもできます。以下がC言語版 HTTP クライアントのソースです。
1: /* $Id: http-client.c,v 1.6 2013/01/23 06:57:19 68user Exp $ */ 2: 3: #include <stdio.h> 4: #include <string.h> 5: #include <stdlib.h> 6: #include <sys/types.h> 7: #include <sys/socket.h> 8: #include <netdb.h> 9: #include <netinet/in.h> 10: #include <sys/param.h> 11: #include <sys/uio.h> 12: #include <unistd.h> 13: 14: #define BUF_LEN 256 /* バッファのサイズ */ 15: 16: int main(int argc, char *argv[]){ 17: int s; /* ソケットのためのファイルディスクリプタ */ 18: struct hostent *servhost; /* ホスト名と IP アドレスを扱うための構造体 */ 19: struct sockaddr_in server; /* ソケットを扱うための構造体 */ 20: struct servent *service; /* サービス (http など) を扱うための構造体 */ 21: 22: char send_buf[BUF_LEN]; /* サーバに送る HTTP プロトコル用バッファ */ 23: char host[BUF_LEN] = "localhost"; /* 接続するホスト名 */ 24: char path[BUF_LEN] = "/"; /* 要求するパス */ 25: unsigned short port = 80; /* 接続するポート番号 */ 26: 27: if ( argc > 1 ){ /* URLが指定されていたら */ 28: char host_path[BUF_LEN]; 29: 30: if ( strlen(argv[1]) > BUF_LEN-1 ){ 31: fprintf(stderr, "URL が長すぎます。\n"); 32: return 1; 33: } 34: /* http:// から始まる文字列で */ 35: /* sscanf が成功して */ 36: /* http:// の後に何か文字列が存在するなら */ 37: if ( strstr(argv[1], "http://") && 38: sscanf(argv[1], "http://%s", host_path) && 39: strcmp(argv[1], "http://" ) ){ 40: char *p; 41: 42: p = strchr(host_path, '/'); /* ホストとパスの区切り "/" を調べる */ 43: if ( p != NULL ){ 44: strcpy(path, p); /* "/"以降の文字列を path にコピー */ 45: *p = '\0'; 46: strcpy(host, host_path); /* "/"より前の文字列を host にコピー */ 47: } else { /* "/"がないなら=http://host という引数なら */ 48: strcpy(host, host_path); /* 文字列全体を host にコピー */ 49: } 50: 51: p = strchr(host, ':'); /* ホスト名の部分に ":" が含まれていたら */ 52: if ( p != NULL ){ 53: port = atoi(p+1); /* ポート番号を取得 */ 54: if ( port <= 0 ){ /* 数字でない (atoi が失敗) か、0 だったら */ 55: port = 80; /* ポート番号は 80 に決め打ち */ 56: } 57: *p = '\0'; 58: } 59: } else { 60: fprintf(stderr, "URL は http://host/path の形式で指定してください。\n"); 61: return 1; 62: } 63: } 64: 65: printf("http://%s%s を取得します。\n\n", host, path); 66: 67: /* ホストの情報(IPアドレスなど)を取得 */ 68: servhost = gethostbyname(host); 69: if ( servhost == NULL ){ 70: fprintf(stderr, "[%s] から IP アドレスへの変換に失敗しました。\n", host); 71: return 0; 72: } 73: 74: bzero(&server, sizeof(server)); /* 構造体をゼロクリア */ 75: 76: server.sin_family = AF_INET; 77: 78: /* IPアドレスを示す構造体をコピー */ 79: bcopy(servhost->h_addr, &server.sin_addr, servhost->h_length); 80: 81: if ( port != 0 ){ /* 引数でポート番号が指定されていたら */ 82: server.sin_port = htons(port); 83: } else { /* そうでないなら getservbyname でポート番号を取得 */ 84: service = getservbyname("http", "tcp"); 85: if ( service != NULL ){ /* 成功したらポート番号をコピー */ 86: server.sin_port = service->s_port; 87: } else { /* 失敗したら 80 番に決め打ち */ 88: server.sin_port = htons(80); 89: } 90: } 91: /* ソケット生成 */ 92: if ( ( s = socket(AF_INET, SOCK_STREAM, 0) ) < 0 ){ 93: fprintf(stderr, "ソケットの生成に失敗しました。\n"); 94: return 1; 95: } 96: /* サーバに接続 */ 97: if ( connect(s, (struct sockaddr *)&server, sizeof(server)) == -1 ){ 98: fprintf(stderr, "connect に失敗しました。\n"); 99: return 1; 100: } 101: 102: /* HTTP プロトコル生成 & サーバに送信 */ 103: sprintf(send_buf, "GET %s HTTP/1.0\r\n", path); 104: write(s, send_buf, strlen(send_buf)); 105: 106: sprintf(send_buf, "Host: %s:%d\r\n", host, port); 107: write(s, send_buf, strlen(send_buf)); 108: 109: sprintf(send_buf, "\r\n"); 110: write(s, send_buf, strlen(send_buf)); 111: 112: /* あとは受信して、表示するだけ */ 113: while (1){ 114: char buf[BUF_LEN]; 115: int read_size; 116: read_size = read(s, buf, BUF_LEN); 117: if ( read_size > 0 ){ 118: write(1, buf, read_size); 119: } else { 120: break; 121: } 122: } 123: /* 後始末 */ 124: close(s); 125: 126: return 0; 127: }
($host,$port,$path) = m|http://([\-\_\.a-zA-Z0-9]+):?(\d+)?(/.*?)| $port = $port || getservbyname('http','tcp') || 80; $path = $path || '/';で済むんですが(うまく書けばもっと短くなりそう)、Cだと
27: if ( argc > 1 ){ /* URLが指定されていたら */ 28: char host_path[BUF_LEN]; 29: 30: if ( strlen(argv[1]) > BUF_LEN-1 ){ 31: fprintf(stderr, "URL が長すぎます。\n"); 32: return 1; 33: } 34: /* http:// から始まる文字列で */ 35: /* sscanf が成功して */ 36: /* http:// の後に何か文字列が存在するなら */ 37: if ( strstr(argv[1], "http://") && 38: sscanf(argv[1], "http://%s", host_path) && 39: strcmp(argv[1], "http://" ) ){ 40: char *p; 41: 42: p = strchr(host_path, '/'); /* ホストとパスの区切り "/" を調べる */ 43: if ( p != NULL ){ 44: strcpy(path, p); /* "/"以降の文字列を path にコピー */ 45: *p = '\0'; 46: strcpy(host, host_path); /* "/"より前の文字列を host にコピー */ 47: } else { /* "/"がないなら=http://host という引数なら */ 48: strcpy(host, host_path); /* 文字列全体を host にコピー */ 49: } 50: 51: p = strchr(host, ':'); /* ホスト名の部分に ":" が含まれていたら */ 52: if ( p != NULL ){ 53: port = atoi(p+1); /* ポート番号を取得 */ 54: if ( port <= 0 ){ /* 数字でない (atoi が失敗) か、0 だったら */ 55: port = 80; /* ポート番号は 80 に決め打ち */ 56: } 57: *p = '\0'; 58: } 59: } else { 60: fprintf(stderr, "URL は http://host/path の形式で指定してください。\n"); 61: return 1; 62: } 63: }となります。"http://host:port/path" という文字列から各要素を分解して、 host・port・path の各変数に代入します。 C言語の基礎まで書くつもりはないので いちいち説明はしませんが、わかりますよね?
% ./http-client http://X68000.startshop.co.jp/~68user/net/を実行したものとします。
まずホスト名を扱う構造体へのポインタ、servhost を宣言します。
18: struct hostent *servhost; /* ホスト名と IP アドレスを扱うための構造体 */struct hostent は、(FreeBSDでは) /usr/include/netdb.h で
struct hostent { char *h_name; /* official name of host */ char **h_aliases; /* alias list */ int h_addrtype; /* host address type */ int h_length; /* length of address */ char *h_addr; /* address from name server */ };と定義されています (ほんとはちょっと違うのですが、説明を簡単にするために書き換えました)。
68: servhost = gethostbyname(host);の行で、ホスト名の情報、IP アドレスを取得します。つまり
servhost = gethostbyname("X68000.startshop.co.jp");が実行されるわけです。これによって、IP アドレスが servhost (が指す構造体) に格納されます。 X68000.startshop.co.jp に対応する IP アドレスは 210.249.139.22 ですから、その結果
servhost->h_name = "X68000.startshop.co.jp" servhost->h_length = 4 (IP アドレスの長さは4バイト) servhost->h_addr[0] = 210 servhost->h_addr[1] = 249 servhost->h_addr[2] = 139 servhost->h_addr[3] = 22となります。
19: struct sockaddr_in server; /* ソケットを扱うための構造体 */(FreeBSDでは) /usr/include/netinet/in.h で
struct sockaddr_in { u_char sin_len; u_char sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; };と定義されています。
74: bzero(&server, sizeof(server)); /* 構造体をゼロクリア */まず構造体をゼロクリアして初期化します。
memset(&server, 0, sizeof(server));と書いた方がよいでしょう。
次に
76: server.sin_family = AF_INET;で、アドレスのタイプを設定します。
79: bcopy(servhost->h_addr, &server.sin_addr, servhost->h_length);そして構造体 (を指すポインタ) servhost から IP アドレスの情報をソケットにコピーします。
memcpy(&server.sin_addr, servhost->h_addr, servhost->h_length);と等価です。
先程の gethostbyname でIPアドレスの情報を servhost に格納しましたので、 それをそのまま server.sin_addr にコピーします。 具体的には、アドレス servhost->h_addr から &server.sin_addr へ、 4バイト (=servhost->h_length) コピーしたわけです。
81: if ( port != 0 ){ /* 引数でポート番号が指定されていたら */ 82: server.sin_port = htons(port); 83: } else { /* そうでないなら getservbyname でポート番号を取得 */ 84: service = getservbyname("http", "tcp"); 85: if ( service != NULL ){ /* 成功したらポート番号をコピー */ 86: server.sin_port = service->s_port; 87: } else { /* 失敗したら 80 番に決め打ち */ 88: server.sin_port = htons(80); 89: } 90: }ポート番号の設定をします。コマンドラインからポート番号を指定された場合は そのままその値を使います。 そうでなければまず getservbyname(3) を使い、もし getservbyname(3) で 失敗したら 80 を決め打ちします。
htons(3) というのは整数をネットワークバイトオーダーに変換する関数です。 Pentium や PowerPC などの CPU にはビッグエンディアンとリトルエンディアンというものがあり、 整数を上位バイトから先に格納するか、下位バイトから先に格納するかという違いがあります。
例えば 80 という 4 バイトの整数は、インテル系マシンでは「80,0,0,0」とメモリの中に格納されますが、 モトローラ系マシンでは「0,0,0,80」となります。データがそのマシンで完結しているなら この違いは問題にならないのですが、ネットワーク経由でデータのやりとりをする際は、 どちらかに統一しなければなりません。 そこで、ネットワーク上では「0,0,0,80」というふうに、上位バイトを先にすることが 決められています。これがネットワークバイトオーダーです。
server.sin_port = htons(80)の htons(3) は 2 バイトのデータをネットワークバイトオーダーに変換する関数です (4バイトなら htonl(3) を使います)。 Pentium のようなリトルエンディアンマシンでは htons(80) != 80 となりますが、PowerPC や Sparc のようなビッグエンディアンマシンでは htons(80) == 80 となります。
なお、ポート番号には 1〜65535 の範囲の値しか指定することができません。 そのため struct sockaddr_in ではポート番号を unsigned short (u_short) で扱っています。int ではないことに注意してください。
92: if ( ( s = socket(AF_INET, SOCK_STREAM, 0) ) < 0 ){次に connect(2) で接続します。
97: if ( connect(s, (struct sockaddr *)&server, sizeof(server)) == -1 ){これは perl で行ったことと同じですね。
(struct sockaddr *) というキャストについて説明します。 connect に限らず bind・accept・getsockname・getpeername などでも sockaddr_in 構造体のアドレスを渡す必要がありますが、 全て同様にキャストする必要があります。 なぜなら、これらの関数は sockaddr_in 構造体だけでなく、 sockaddr_un (UNIX ドメインプロトコル)、sockaddr_dl (データリンク) などへの ポインタも受け取ることができるようになっているからです。 struct sockaddr は (FreeBSD では) /usr/include/sys/socket.h で
struct sockaddr { u_char sa_len; /* total length */ u_char sa_family; /* address family */ char sa_data[14]; /* actually longer; address value */ };と定義されています。 この sockaddr 構造体はキャストの時だけに必要で、それ以外の場面では 使うことはありません。
103: sprintf(send_buf, "GET %s HTTP/1.0\r\n", path); 104: write(s, send_buf, strlen(send_buf)); 105: 106: sprintf(send_buf, "Host: %s:%d\r\n", host, port); 107: write(s, send_buf, strlen(send_buf)); 108: 109: sprintf(send_buf, "\r\n"); 110: write(s, send_buf, strlen(send_buf));あとは WWW サーバから返ってくる文字列を受け取り、それを標準出力に書き出すだけです。
113: while (1){ 114: char buf[BUF_LEN]; 115: int read_size; 116: read_size = read(s, buf, BUF_LEN); 117: if ( read_size > 0 ){ 118: write(1, buf, read_size); 119: } else { 120: break; 121: } 122: }read(2) は読み込んだ文字数を返しますので、read(2) の戻り値が 0 より 大きかったら文字列を出力します。 read(2) は文字列の最後に終端記号 ('\0') をつけてくれませんので、 出力したい文字数はプログラマが管理しなければいけません。そのため、 printf(3) や puts(3) を使わず、write(2) で 読み込んだ文字数だけ標準出力に書き出します。 write(1,buf,read_size) の第一引数の 1 というのは 標準出力を表すファイルディスクリプタの番号です。
最後にソケットをクローズして終了です。
C だと面倒な手続きが多いですね。やはり perl で書くのが手軽でしょうか。
前へ << 低水準ファイル入出力関数を使おう | C 言語で HTTP クライアントを作ってみよう (2) >> 次へ |
ご意見・ご指摘は Twitter: @68user までお願いします。