[C言語] curl libを使ってみる


[前回](http://qiita.com/edo_m18/items/41770cba5c166f276a83)、[前々回](http://qiita.com/edo_m18/items/cef278d0c14d1371db3b)とHTTP(S)クライアントについて書きました。
が、当然すでにこうしたクライアントはライブラリが存在し、その中でも`curl`はコマンドラインでも使う有名なライブラリです。

今回はこれを使ってみようと思います。

## curlのインストール

`curl`コマンドは入っていると思いますが、ライブラリはどうやらMacでは入っていないようなのでインストールを行います。

```console
$ brew install curl
# // ... installing message ...
 -L/usr/local/opt/curl/lib
 -I/usr/local/opt/curl/include
```

という感じで、インストール先が表示されるのでこれをコンパイル時に指定します。

```console
$ gcc test.c -I/usr/local/opt/curl/include -lcurl
```

## 実行してみる

まずは簡単に実行してみます。

```c:test.c
int main(void) {
    CURL *curl;

	// curlのセットアップ
    curl = curl_easy_init();
    curl_easy_setopt(curl, CURLOPT_URL, "https://qiita.com/");
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
    
    // 実行
    curl_easy_perform(curl);
    
    // 後始末
    curl_easy_cleanup(curl);

    return EXIT_SUCCESS;
}
```

これを実行すると、qiita.comの情報が標準出力に表示されます。
curlはデフォルトの出力先は標準出力になっています。

しかし、ただ表示されるだけではまったく使い物になりません。
例えばAPIにアクセスした場合はJSONなりでデータが返ってくると思いますが、これを保持できなければなりませんね。


## callback関数を定義する

curlではcallback関数を指定することで、サーバから届いたデータの塊を伴ってその関数を呼び出してくれます。

### callback関数の定義

以下のような形で関数を作り、curlのセット用関数で指定します。

```c
size_t callback(char *ptr, size_t size, size_t nmemb, void *stream) {
	int block = size * nmemb;
	// do somoting.
}
```

`char *ptr`が実際に取得されたデータです。
`size`と`nmemb`はちょっとよく分かりませんが、おそらくデータのサイズを表しているんだと思います。（色々調べたけど具体的に言及してるのが見つからなかったorz）

そして最後のポインタが重要です。
ここに渡ってくるポインタは、上記の関数と同様に、セット用関数で設定したポインタが渡ってきます。
設定には自分で定義した構造体などのポインタを指定します。

それぞれ具体的には以下のように設定します。

```c
CURL *curl;
curl = curl_easy_init();

// callback関数の最後の引数に渡ってくるポインタを指定
curl_easy_setopt(curl, CURLOPT_WRITEDATA, anyPointer);

// callback関数を設定
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, funcPointer);
```


### CURLOPT_WRITEDATAを定義する

`CURLOPT_WRITEDATA`で指定したポインタが、上記で書いた「最後のポインタ」のに渡ってくる部分です。
ここのポインタは、関数の宣言を見てもらうとわかりますが`void *`型になっています。
つまりなんでもいいわけですね。

文章だけだと分かりづらいので、curlが呼ばれた回数をカウントアップするだけの簡単なサンプルコードを示します。

```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>

struct counter {
    int count;
};

size_t countup(char *ptr, size_t size, size_t nmemb, void *stream) {
    struct counter *counter = (struct counter *)stream;
    int block = size * nmemb;

    if (!counter) {
        return block;
    }

    counter->count++;

    return block;
}

int main(void) {
    CURL *curl;
    struct counter *counter = (struct counter *)malloc(sizeof(struct counter));

    counter->count = 0;

    curl = curl_easy_init();
    curl_easy_setopt(curl, CURLOPT_URL, "https://qiita.com/");
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, counter);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, countup);

    curl_easy_perform(curl);
    curl_easy_cleanup(curl);

    printf("Total count: %d\n", counter->count);

    free(counter);

    return EXIT_SUCCESS;
}
```


## データを保持する

上記の例でなんとなく使い方が分かるかと思います。
しかしカウントアップだけできてもしょうがないので、最後は実際にデータを保持して出力するサンプルを作ります。

```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>

struct Buffer {
    char *data;
    int data_size;
};

size_t buffer_writer(char *ptr, size_t size, size_t nmemb, void *stream) {
    struct Buffer *buf = (struct Buffer *)stream;
    int block = size * nmemb;
    if (!buf) {
        return block;
    }

    if (!buf->data) {
        buf->data = (char *)malloc(block);
    }
    else {
        buf->data = (char *)realloc(buf->data, buf->data_size + block);
    }

    if (buf->data) {
        memcpy(buf->data + buf->data_size, ptr, block);
        buf->data_size += block;
    }

    return block;
}

int main(void) {

    CURL *curl;
    struct Buffer *buf;

    buf = (struct Buffer *)malloc(sizeof(struct Buffer));
    buf->data = NULL;
    buf->data_size = 0;

    curl = curl_easy_init();
    curl_easy_setopt(curl, CURLOPT_URL, "https://qiita.com/");
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, buf);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, buffer_writer);

    curl_easy_perform(curl);
    curl_easy_cleanup(curl);


    printf("%s\n", buf->data);

    free(buf->data);
    free(buf);

    return EXIT_SUCCESS;
}
```

見てもらうと分かる通り、カウントアップのサンプルとほぼ同じです。
異なる点はバッファの保存部分です。

具体的には以下の部分です。

```c
// バッファ用構造体
struct Buffer {
    char *data;
    int data_size;
};

size_t buffer_writer(char *ptr, size_t size, size_t nmemb, void *stream) {
    struct Buffer *buf = (struct Buffer *)stream;
    int block = size * nmemb;
    if (!buf) {
        return block;
    }

    if (!buf->data) {
        buf->data = (char *)malloc(block);
    }
    else {
        buf->data = (char *)realloc(buf->data, buf->data_size + block);
    }

    if (buf->data) {
        memcpy(buf->data + buf->data_size, ptr, block);
        buf->data_size += block;
    }

    return block;
}
```

バッファを取り扱うための構造体を宣言しています。
このバッファに、受信したデータを貯めていきます。
その処理がこの部分。

```c
if (!buf->data) {
    buf->data = (char *)malloc(block);
}
else {
    buf->data = (char *)realloc(buf->data, buf->data_size + block);
}

if (buf->data) {
    memcpy(buf->data + buf->data_size, ptr, block);
    buf->data_size += block;
}
```

最初の受信時、つまり`buf->data`が`NULL`の場合は受信したデータ分のメモリを確保します。

すでにデータがある場合は既存のデータから、受信したデータ分のメモリを足した領域を再確保します。（`realloc`）

メモリの確保ができたらデータをメモリに書き込みます。

```c
if (buf->data) {
    memcpy(buf->data + buf->data_size, ptr, block);
    buf->data_size += block;
}
```

やっていることは、受信したデータを新しく確保した場所にコピーするだけです。

`buf->data + buf->data_size`の部分はポインタのアドレスを、現在受信済のデータ分だけ進めています。その場所から、今回受信したデータを追加で書き込みます。
最後に、受信したデータサイズを更新して終わりです。

受信が終わったら、バッファに保存したデータを表示しています。
コネクションに成功すれば`qiita.com`のHTMLがだーっと表示されます。

HTMLを表示してもあまり面白みはありませんが、例えばAPIなどにアクセスしてJSONを取得し、それを使用する、という場合も同じように処理することで可能になります。
これでAPIからデータを取ってきてなにかする、ということができるようになりますね。


[追記]

## リクエストにヘッダとPOSTデータを含める

上記でリクエストすることができました。
が、APIからレスポンスを得るためにはヘッダを操作したり、POSTデータを付与する必要があります。

### ヘッダを追加する

ヘッダの追加は以下のようにします。

```c
struct curl_slist *headers = NULL;

headers = curl_slist_append(headers, "Hoge: Fuga");

// ... 中略

curl_easy_setopt(curl, CURLOPT_HEADER, headers);

// ... 中略

curl_easy_perform(curl);
```

ヘッダの追加には`struct curl_slist`構造体を使います。


### POSTデータを付与する

POSTデータは以下のように付与します。

```c
// ... 中略
char *post_data = "name=edo&age=20";

curl_easy_setopt(curl, CURLOPT_POST, 1);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data);
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, strlen(post_data));
```

データの受信については同じです。
これでAPIにリクエストを投げてレスポンスを受けることができます。