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