Edited at

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

More than 3 years have passed since last update.

前回前々回と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


実行してみる

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


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のセット用関数で指定します。

size_t callback(char *ptr, size_t size, size_t nmemb, void *stream) {

int block = size * nmemb;
// do somoting.
}

char *ptrが実際に取得されたデータです。

sizenmembはちょっとよく分かりませんが、おそらくデータのサイズを表しているんだと思います。(色々調べたけど具体的に言及してるのが見つからなかった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->dataNULLの場合は受信したデータ分のメモリを確保します。

すでにデータがある場合は既存のデータから、受信したデータ分のメモリを足した領域を再確保します。(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にリクエストを投げてレスポンスを受けることができます。