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

前へ << C 言語で HTTP クライアントを作ってみよう (1) C 言語で echo サーバを作ってみよう (1) >> 次へ

入出力ライブラリ

前ページ のサンプルでは、ソケットに対する入力・出力に、 それぞれ read・write を使っていました。 しかし、できれば便利な標準入出力ライブラリ、つまり printf や fgets などを使いたいものです。

read/write で入出力を行うときには、socket で得られたソケットディスクリプタという 整数値をそのまま利用しました。一方、printf や fgets の入出力の際は、 FILE 構造体が必要になります。そこで fdopen という、 ディスクリプタから FILE 構造体を生成する ライブラリ関数を使います。使い方は

FILE *fp;
fp = fdopen(fd, "r");
です。fd は open や socket で得られたディスクリプタ (int 型) です。 fopen の第2引数と同じで、"r" や "w" で入力モードか出力モードかを 指定できます。

一度 fdopen で FILE 構造体を作ってしまえば、後は

fprintf(fp, "hoge %d %s\n", num,str);
fgets(buf, sizeof(buf), fp);
fputs("fuga", fp);
などと、おなじみの方法で入出力が可能になります (もちろん fread・fwrite も使えます)。

低水準入出力関数との比較

まず、前ページのサンプルプログラムの、WWW サーバとの 送受信部分を再掲します。

http-client.c

  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:     }
そして、これが標準入出力ライブラリを使った例です。 両者を見比べてみて下さい。

http-client-2.c

   21:     FILE *fp;                            /* ソケット用の FILE 構造体 */
   22:     char buf[BUF_LEN];                   /* 受信用バッファ */
  102:                                 /* FILE 構造体を作成 */
  103:     fp = fdopen(s, "r+");
  104:     if ( fp == NULL ){
  105:         fprintf(stderr, "fdopen に失敗しました。\n");
  106:         return 1;
  107:     }
  108:                                 /* バッファリング OFF */
  109:     setvbuf(fp, NULL, _IONBF, 0);
  110: 
  111:                                 /* サーバに送信 */
  112:     fprintf(fp, "GET %s HTTP/1.0\r\n", path);
  113:     fprintf(fp, "Host: %s:%d\r\n", host, port);
  114:     fprintf(fp, "\r\n");
  115: 
  116:                                 /* あとは受信して、表示するだけ */
  117:     while (1){
  118:         if ( fgets(buf, sizeof(buf), fp) == NULL ){
  119:             break;
  120:         }
  121:         printf("%s", buf);
  122:     }

  103:     fp = fdopen(s, "r+");
まず、入出力用の FILE 構造体を、ソケットディスクリプタから作成します。
ここでは "r+" で入出力用の FILE 構造体を作成しましたが、
FILE *fp_read;
FILE *fp_write;
fp_read  = fdopen(s, "r");
fp_write = fdopen(s, "w");
と、入力と出力を別々に作成する方法もあります。

  109:     setvbuf(fp, NULL, _IONBF, 0);
バッファリングを OFF にします。 printf・fprintf・fgets などの標準入出力ライブラリには、 バッファリング機能が備わっています。バッファリングには
  • 完全バッファリング (fully bufferd) … バッファが一杯になるまで実際の入出力を行わない
  • 行単位バッファリング (line bufferd) … バッファが一杯になるか、 データ中に改行コードを発見するまで、実際の入出力を行わない
  • バッファリングなし (unbufferd) … 標準入出力関数が実行されるたびに、実際に入出力を行う
という3種類のモードがあります。 デフォルトでは、
  • ターミナル (端末=tty、つまり画面上への出力) なら「行単位バッファリング」
  • 標準エラー出力は「バッファリングなし」
  • それ以外は「完全バッファリング」
となります。ソケットはターミナルでも標準エラー出力でもないので、 デフォルトでは「完全バッファリング」になります。 そのため、fprintf でデータを送ったつもりでもバッファリングされてしまい、 WWW サーバにはリクエストが届きません。 そこで setvbuf で _IONBF を指定し、「バッファリングなし」に変更します。
ここの setvbuf を書かずに、fprintf するたびに必ず fflush(fp) を 実行する、という手もあります。

  112:     fprintf(fp, "GET %s HTTP/1.0\r\n", path);
  113:     fprintf(fp, "Host: %s:%d\r\n", host, port);
  114:     fprintf(fp, "\r\n");
  115: 
  116:                                 /* あとは受信して、表示するだけ */
  117:     while (1){
  118:         if ( fgets(buf, sizeof(buf), fp) == NULL ){
  119:             break;
  120:         }
  121:         printf("%s", buf);
  122:     }
あとは、fprintf で送信用 FILE 構造体に送信し、 fgets でデータを読み込むだけです。 これは簡単に理解できますね。


以下は fgets・fprintf を使った HTTP クライアントの全ソースです。 ただし、上で説明した部分以外は、read・write 版との違いはありません。

http-client-2.c

    1: /* $Id: http-client-2.c,v 1.3 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:     FILE *fp;                            /* ソケット用の FILE 構造体 */
   22:     char buf[BUF_LEN];                   /* 受信用バッファ */
   23: 
   24:     char host[BUF_LEN] = "localhost";    /* 接続するホスト名 */
   25:     char path[BUF_LEN] = "/";            /* 要求するパス */
   26:     unsigned short port = 80;            /* 接続するポート番号 */
   27: 
   28:     if ( argc > 1 ){                     /* URLが指定されていたら */
   29:         char host_path[BUF_LEN];
   30: 
   31:         if ( strlen(argv[1]) > BUF_LEN-1 ){
   32:             fprintf(stderr, "URL が長すぎます。\n");
   33:             return 1;
   34:         }
   35:                                          /* http:// から始まる文字列で */
   36:                                          /* sscanf が成功して */
   37:                                          /* http:// の後に何か文字列が存在するなら */
   38:         if ( strstr(argv[1], "http://") &&
   39:              sscanf(argv[1], "http://%s", host_path) &&
   40:              strcmp(argv[1], "http://" ) ){
   41:             char *p;
   42: 
   43:             p = strchr(host_path, '/');  /* ホストとパスの区切り "/" を調べる */
   44:             if ( p != NULL ){
   45:                 strcpy(path, p);        /* "/"以降の文字列を path にコピー */
   46:                 *p = '\0';
   47:                 strcpy(host, host_path); /* "/"より前の文字列を host にコピー */
   48:             } else {                     /* "/"がないなら=http://host という引数なら */
   49:                 strcpy(host, host_path); /* 文字列全体を host にコピー */
   50:             }
   51: 
   52:             p = strchr(host, ':');       /* ホスト名の部分に ":" が含まれていたら */
   53:             if ( p != NULL ){
   54:                 port = atoi(p+1);        /* ポート番号を取得 */
   55:                 if ( port <= 0 ){        /* 数字でない (atoi が失敗) か、0 だったら */
   56:                     port = 80;           /* ポート番号は 80 に決め打ち */
   57:                 }
   58:                 *p = '\0';
   59:             }
   60:         } else {
   61:             fprintf(stderr, "URL は http://host/path の形式で指定してください。\n");
   62:             return 1;
   63:         }
   64:     }
   65: 
   66:     printf("http://%s%s を取得します。\n\n",host, path);
   67: 
   68:                                 /* ホストの情報(IPアドレスなど)を取得 */
   69:     servhost = gethostbyname(host);
   70:     if ( servhost == NULL ){
   71:         fprintf(stderr, "[%s] から IP アドレスへの変換に失敗しました。\n", host);
   72:         return 0;
   73:     }
   74: 
   75:     bzero((char *)&server, sizeof(server));     /* 構造体をゼロクリア */
   76: 
   77:     server.sin_family = AF_INET;
   78: 
   79:                                                /* IPアドレスを示す構造体をコピー */
   80:     bcopy(servhost->h_addr, (char *)&server.sin_addr, servhost->h_length);
   81: 
   82:     if ( port != 0 ){                          /* 引数でポート番号が指定されていたら */
   83:         server.sin_port = htons(port);
   84:     } else {                                   /* そうでないなら getservbyname でポート番号を取得 */
   85:         service = getservbyname("http", "tcp");
   86:         if ( service != NULL ){                /* 成功したらポート番号をコピー */
   87:             server.sin_port = service->s_port;
   88:         } else {                               /* 失敗したら 80 番に決め打ち */
   89:             server.sin_port = htons(80);
   90:         }
   91:     }
   92:                                 /* ソケット生成 */
   93:     if ( ( s = socket(AF_INET, SOCK_STREAM, 0) ) < 0 ){
   94:         fprintf(stderr, "ソケットの生成に失敗しました。\n");
   95:         return 1;
   96:     }
   97:                                 /* サーバに接続 */
   98:     if ( connect(s, (struct sockaddr *)&server, sizeof(server)) == -1 ){
   99:         fprintf(stderr, "connect に失敗しました。\n");
  100:         return 1;
  101:     }
  102:                                 /* FILE 構造体を作成 */
  103:     fp = fdopen(s, "r+");
  104:     if ( fp == NULL ){
  105:         fprintf(stderr, "fdopen に失敗しました。\n");
  106:         return 1;
  107:     }
  108:                                 /* バッファリング OFF */
  109:     setvbuf(fp, NULL, _IONBF, 0);
  110: 
  111:                                 /* サーバに送信 */
  112:     fprintf(fp, "GET %s HTTP/1.0\r\n", path);
  113:     fprintf(fp, "Host: %s:%d\r\n", host, port);
  114:     fprintf(fp, "\r\n");
  115: 
  116:                                 /* あとは受信して、表示するだけ */
  117:     while (1){
  118:         if ( fgets(buf, sizeof(buf), fp) == NULL ){
  119:             break;
  120:         }
  121:         printf("%s", buf);
  122:     }
  123:                                 /* 後始末 */
  124:     fclose(fp);
  125:     close(s);
  126: 
  127:     return 0;
  128: }
前へ << C 言語で HTTP クライアントを作ってみよう (1) C 言語で echo サーバを作ってみよう (1) >> 次へ

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