Help us understand the problem. What is going on with this article?

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

More than 5 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にリクエストを投げてレスポンスを受けることができます。

edo_m18
現在はUnity ARエンジニア。 主にARのコンテンツ制作をしています。 最近は機械学習にも興味が出て勉強中です。 Unityに関するブログは別で書いています↓ https://edom18.hateblo.jp/
http://edom18.hateblo.jp/
unity-game-dev-guild
趣味・仕事問わずUnityでゲームを作っている開発者のみで構成されるオンラインコミュニティです。Unityでゲームを開発・運用するにあたって必要なあらゆる知見を共有することを目的とします。
https://unity-game-dev-guild.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away