[C言語] HTTPクライアントを作ってみる


[C 言語で HTTP クライアントを作ってみよう (1)](http://x68000.q-e-d.net/~68user/net/c-http-1.html)を参考にさせていただきました。

普通はHTTPを使ったコンテンツへのアクセスはそれようのメソッドなりが用意されていて、基本的には取得後のコンテンツをどうするか、というのが主な開発になると思います。

今回はさらにベーシックな部分を作ってみたいと思います。
*（深堀りしていけばさらにTCPなどのネットワーク層とか掘るとキリがないですがｗ）*

## ソースコード

```c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/param.h>
#include <unistd.h>

#define BUF_LEN 256

struct URL {
    char host[BUF_LEN];
    char path[BUF_LEN];
    char query[BUF_LEN];
    char fragment[BUF_LEN];
    unsigned short port;
};

/**
 * @param urlStr URLテキスト
 */
void parseURL(const char *urlStr, struct URL *url, char **error);

int main(int argc, char **argv) {

    // ソケットのためのファイルディスクリプタ
    int s;

    // IPアドレスの解決
    struct addrinfo hints, *res;
    struct in_addr addr;
    int err;

    // サーバに送るHTTPプロトコル用バッファ
    char send_buf[BUF_LEN];

    struct URL url = {
        "css-eblog.com", "/", 80
    };

    // URLが指定されていたら
    if (argc > 1) {
        char *error = NULL;
        parseURL(argv[1], &url, &error);

        if (error) {
            printf("%s\n", error);
            return 1;
        }
    }

    printf("http://%s%s%s を取得します。\n\n", url.host, url.path, url.query);

    // 0クリア
    memset(&hints, 0, sizeof(hints));
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_family   = AF_INET;

    char *serviceType = "http";

    if ((err = getaddrinfo(url.host, serviceType, &hints, &res)) != 0) {
        printf("error %d\n", err);
        return 1;
    }

    // ソケット生成
    if ((s = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) < 0) {
        fprintf(stderr, "ソケットの生成に失敗しました。\n");
        return 1;
    }

    // サーバに接続
    if (connect(s, res->ai_addr, res->ai_addrlen) != 0) {
        fprintf(stderr, "connectに失敗しました。\n");
        return 1;
    }

    // HTTPプロトコルの開始 ＆サーバに送信
    sprintf(send_buf, "GET %s%s HTTP/1.0\r\n", url.path, url.query);
    write(s, send_buf, strlen(send_buf));

    sprintf(send_buf, "Host: %s:%d\r\n", url.host, url.port);
    write(s, send_buf, strlen(send_buf));

    sprintf(send_buf, "\r\n");
    write(s, send_buf, strlen(send_buf));

    // 受信が終わるまでループ
    while(1) {
        char buf[BUF_LEN];
        int read_size;
        read_size = read(s, buf, BUF_LEN);

        if (read_size > 0) {
            write(1, buf, read_size);
        }
        else {
            break;
        }
    }

    // ソケットを閉じる
    close(s);

    return 0;
}


void parseURL(const char *urlStr, struct URL *url, char **error) {
    char host_path[BUF_LEN];

    if (strlen(urlStr) > BUF_LEN - 1) {
        *error = "URLが長過ぎます。\n";
        return;
    }

    // http://から始まる文字列で
    // sscanfが成功して
    // http://の後に何か文字列が存在するなら
    if (strstr(urlStr, "http://")              &&
        sscanf(urlStr, "http://%s", host_path) &&
        strcmp(urlStr, "http://")) {

        char *p = NULL;

        p = strchr(host_path, '#');
        if (p != NULL) {
            strcpy(url->fragment, p);
            *p = '\0';
        }

        p = strchr(host_path, '?');
        if (p != NULL) {
            strcpy(url->query, p);
            *p = '\0';
        }

        p = strchr(host_path, '/');
        if (p != NULL) {
            strcpy(url->path, p);
            *p = '\0';
        }

        strcpy(url->host, host_path);

        // ホスト名の部分に":"が含まれていたら
        p = strchr(url->host, ':');
        if (p != NULL) {
            // ポート番号を取得
            url->port = atoi(p + 1);

            // 数字ではない（atoiが失敗）か、0だったら
            // ポート番号は80に決め打ち
            if (url->port <= 0) {
                url->port = 80;
            }

            // 終端文字で空にする
            *p = '\0';
        }
        else {
            url->port = 80;
        }
    }
    else {
        *error = "URLはhttp://host/pathの形式で指定してください。\n";
        return;
    }
}
```

参考にした記事からいくつか変更しています。
まず、`gethostbyname()`関数を`getaddrinfo()`に変更しています。
また、URLのパース部分を関数化して構造体で受け取るようにしました。

ちなみに、ここで出てくる関数や構造体については[こちら](http://qiita.com/edo_m18/items/7414028fd91269e5427d)にまとめたのでそっちを参照ください。


## IPアドレスの解決

ちょっと前に[[C言語] IPアドレスを解決する](http://qiita.com/edo_m18/items/2cf4e538bf8ba7e82caa)という記事で書いたのでそちらを。
HTTPクライアントも、↑の`getaddrinfo()`関数でIPアドレスの解決を行っています。


## サーバに接続

サーバに接続するにはソケットの生成とコネクションを張る必要があります。


### ソケットの生成

```c
// ソケットの生成
if ((s = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) < 0) {
    fprintf(stderr, "ソケットの生成に失敗しました。\n");
    return 1;
}
```

ソケットの生成には`socket()`関数を使います。
引数にはその直前で取得した`addrinfo`構造体の値を使います。

### コネクション

```c
// サーバに接続
if (connect(s, res->ai_addr, res->ai_addrlen) != 0) {
    fprintf(stderr, "connectに失敗しました。\n");
    return 1;
}
```

サーバへの接続は`connect()`関数で行います。
引数にはその前に生成したソケットと、該当サーバのIPアドレス、IPアドレスの長さを渡して実行します。

接続が成功したら、以降はファイルの読み書きと同じように操作できます。
（ファイルもソケットもどちらもストリームという概念で抽象化され、同じ操作で実行できるように実装されています）


## HTTPプロトコル

よく聞くプロトコル。Objective-CだとJavaのinterfaceのような感じで使われています。
<del>HTTPのPはプロトコルだからプロトコルプロトコルになるよね</del>

[Wikipediaから引用](http://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%83%88%E3%82%B3%E3%83%AB)すると以下の意味。

>プロトコルまたはプロトコール（英語 protocol 英語発音: [ˈproutəˌkɔːl] プロウタコール、[ˈproutəˌkɔl] プロウタコル、フランス語 protocole フランス語発音: [prɔtɔkɔl] プロトコル）とは、複数の者が対象となる事項を確実に実行するための手順等について定めたもの。日本語では「規定」「議定書」「儀典」などと意訳される。もともとは人間同士のやりとりに関する用語としてのみ用いられていたが、近年では派生的に、情報工学分野でマシンやソフトウェア同士のやりとりに関する概念を指すためにも用いられるようになった。

ものすごくざっくり言うと、「 **山と言ったら川と答える** 」っていうベタなやりとりを見ることがあるけど（あれ、歳バレる？）、これもいわばプロトコル。
つまり、ある決まった手順でやりとりをすることで確実に処理を遂行するための規定のこと。

なので、HTTPの場合は決まったフォーマットに命令などを書き込んでサーバに送信することで、サーバ側はそれを解釈して応答してくれる、というわけです。

その処理を行っているのが以下の部分。

```c
// HTTPプロトコルの開始 ＆サーバに送信
sprintf(send_buf, "GET %s%s HTTP/1.0\r\n", url.path, url.query);
write(s, send_buf, strlen(send_buf));

sprintf(send_buf, "Host: %s:%d\r\n", url.host, url.port);
write(s, send_buf, strlen(send_buf));

sprintf(send_buf, "\r\n");
write(s, send_buf, strlen(send_buf));
```

簡単に書くと、`GET...`と命令を書いて、`Host:...`と指定し、空行を入れる、という規定。
telnetを使うとこれを手動で行うことができます。

```console
└> telnet qiita.com 80
Trying 54.248.79.191...
Connected to qiita.com.
Escape character is '^]'.
GET /index.html HTTP/1.0⏎
Host: qiita.com:80⏎
⏎
```

最後の空行を入力したあとはダーっとHTMLが表示されます。
これはサーバ側がプロトコルを解釈して結果を返してくれたからです。
ちなみに、`Host: hoge.com:80`とかすると結果が出ずに終了します。
（「山　滝」と言ったようなもんですね）