| 前へ << 低水準ファイル入出力関数を使おう | 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 までお願いします。