C 言語で HTTP クライアントを作ってみよう (1)

前へ << 低水準ファイル入出力関数を使おう C 言語で HTTP クライアントを作ってみよう (2) >> 次へ

HTTPクライアント C言語版

HTTP プロトコルについては既に解説しましたので、 いきなりソースの解説に入ります。 その前に一応コンパイル方法を説明しておきましょう。
% 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 クライアントのソースです。

http-client.c

    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: }

引数解析

perl に比べて、C言語で一番面倒なのが引数の解析部分ですね。perl だと
($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" という文字列から各要素を分解して、 hostportpath の各変数に代入します。 C言語の基礎まで書くつもりはないので いちいち説明はしませんが、わかりますよね?

struct hostent

以下の説明では、
% ./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
となります。

struct sociaddr_in

次にソケットの構造体 struct sockaddr_in を見てみましょう。
   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));            /* 構造体をゼロクリア */
まず構造体をゼロクリアして初期化します。
bzero(3) というのはメモリをゼロクリアする関数です。 発祥は BSD 系ですが、Linux や Solaris でも使用可能です。 ただし POSIX には含まれていませんので Windows 環境などには bzero(3) はありません。移植性を考慮するなら、
memset(&server, 0, sizeof(server));
と書いた方がよいでしょう。

次に

   76:     server.sin_family = AF_INET;
で、アドレスのタイプを設定します。
   79:     bcopy(servhost->h_addr, &server.sin_addr, servhost->h_length);
そして構造体 (を指すポインタ) servhost から IP アドレスの情報をソケットにコピーします。
bcopy もまた BSD なライブラリ関数です。
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 となります。
ですからビッグエンディアンマシンでは htons(3) は何も変換しないようになっているはずです。 その場合インクルードファイルには #define htons(x) (x) などと書いてあるかもしれません。
「うちのマシンでは htons(3) を使わなくても正しいポートに接続できる」などと思って htons を省いてはいけません。そのソースを他のマシンに持っていった途端に 動かなくなるでしょう。
getservbyname(3) によって設定した service->s_port の値に対しては htons(3) を使う必要はありません。既にネットワークバイトオーダーに変換されているからです。
ですから、リトルエンディアンマシンで printf("%d",service->s_port); を実行すると、80 ではなく 20480 という 値が表示されるはずです。メモリ中には「80,0」ではなく「0,80」という順序で 格納されているので、80×256=20480 という値になるのです。

なお、ポート番号には 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 構造体はキャストの時だけに必要で、それ以外の場面では 使うことはありません。

接続後

サーバに送信するプロトコル文字列を生成し、write(2) で送信します。
  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 というのは 標準出力を表すファイルディスクリプタの番号です。
0 が標準入力 (STDIN)、1 が標準出力 (STDOUT)、2 が標準エラー出力 (STDERR) です。

最後にソケットをクローズして終了です。

C だと面倒な手続きが多いですね。やはり perl で書くのが手軽でしょうか。

前へ << 低水準ファイル入出力関数を使おう C 言語で HTTP クライアントを作ってみよう (2) >> 次へ

ご意見・ご指摘は Twitter: @68user までお願いします。