前回、前々回とHTTP(S)クライアントについて書きました。
が、当然すでにこうしたクライアントはライブラリが存在し、その中でもcurl
はコマンドラインでも使う有名なライブラリです。
今回はこれを使ってみようと思います。
curlのインストール
curl
コマンドは入っていると思いますが、ライブラリはどうやらMacでは入っていないようなのでインストールを行います。
$ brew install curl
# // ... installing message ...
-L/usr/local/opt/curl/lib
-I/usr/local/opt/curl/include
という感じで、インストール先が表示されるのでこれをコンパイル時に指定します。
$ gcc test.c -I/usr/local/opt/curl/include -lcurl
実行してみる
まずは簡単に実行してみます。
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のセット用関数で指定します。
size_t callback(char *ptr, size_t size, size_t nmemb, void *stream) {
int block = size * nmemb;
// do somoting.
}
char *ptr
が実際に取得されたデータです。
size
とnmemb
はちょっとよく分かりませんが、おそらくデータのサイズを表しているんだと思います。(色々調べたけど具体的に言及してるのが見つからなかったorz)
そして最後のポインタが重要です。
ここに渡ってくるポインタは、上記の関数と同様に、セット用関数で設定したポインタが渡ってきます。
設定には自分で定義した構造体などのポインタを指定します。
それぞれ具体的には以下のように設定します。
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が呼ばれた回数をカウントアップするだけの簡単なサンプルコードを示します。
#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;
}
データを保持する
上記の例でなんとなく使い方が分かるかと思います。
しかしカウントアップだけできてもしょうがないので、最後は実際にデータを保持して出力するサンプルを作ります。
#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;
}
見てもらうと分かる通り、カウントアップのサンプルとほぼ同じです。
異なる点はバッファの保存部分です。
具体的には以下の部分です。
// バッファ用構造体
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;
}
バッファを取り扱うための構造体を宣言しています。
このバッファに、受信したデータを貯めていきます。
その処理がこの部分。
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
)
メモリの確保ができたらデータをメモリに書き込みます。
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データを付与する必要があります。
ヘッダを追加する
ヘッダの追加は以下のようにします。
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データは以下のように付与します。
// ... 中略
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にリクエストを投げてレスポンスを受けることができます。