C言語のlibcurlをDockerで試すまで

# きっかけ
Linux環境でC言語のlibcurlの動作確認をしたかった。
調べたらWindows10 HomeでもDocker使えるようなので試してみる。

やったこと

* Dockerインストール
* Docker Hubへの登録
* ContOSコンテナを作成
* コンテナ内でC言語コンパイル＆実行できるようにする
* コンテナにlibcurlをインストールする
* POSTの動作確認

# Dockerの準備
## Dockerのインストール
https://docs.docker.com/desktop/windows/install/
からDocker Desktop Installer.exeをDLしてインストール。

### WSL2のインストール
ポップアップが出てWSL2をアップデートしてと言われるので
画面に従ってURLからインストールしてPC再起動。
https://aka.ms/wsl2kernel
→wsl_update_x64.msi

#### WSL2とは？
Windows Subsystem for Linux 2。
Windows 10上でLinuxを動作させるための仕組みのことらしい。それのバージョン2。


WSL2インストール後、Dockerのチュートリアルが始まるのでやってみる。

## Docker Hubのアカウント作成
DockerイメージをプルしたりするにはDocker Hubへの登録が必要なのでしておく。

Docker Hub
https://hub.docker.com/

## Dockerイメージの取得～コンテナ起動
今回はCentOS7の公式イメージを使うことにする。
Docker Hubで「Official Images」にチェックして「centos」で検索すると
公式のリポジトリが見つかる。
リポジトリを見ると以下の形式でCentOS7が取れそうなことがわかる。

```:Dockerイメージの取得
$ docker pull centos:7
```

ターミナルから試して無事取得できた。

```:Dockerイメージ一覧の取得
$ docker images
REPOSITORY               TAG       IMAGE ID       CREATED        SIZE
～省略～
centos                   7         eeb6ee3f44bd   4 months ago   204MB
```

取得したイメージからコンテナ作成&起動

```:コンテナを作成して起動する
$ docker run -it -d --name centos7c centos:7
```
-it：ログインして入出力可能にする
-d：コンテナから抜けてもバックグラウンドで起動したままにする
--name：コンテナに名前をつける

起動できた。

```:起動しているDockerコンテナを表示
$ docker ps
CONTAINER ID   IMAGE            COMMAND       CREATED          STATUS             PORTS     NAMES
df8c294e015f   centos:7         "/bin/bash"   13 seconds ago   Up 12 seconds                centos7c   
```

入ってみる。

```:Dockerコンテナに入る
$ docker exec -it centos7c /bin/bash
[root@df8c294e015f /]# ←コンテナに入った 
```
似たようなコマンドに```docker attach ～```があるが、
これはexitで終了時にコンテナも終了してしまうよう。

## その他のDockerコマンド

```:コンテナの一時停止
$ docker stop centos7c
```

```:一時停止中のコンテナ含めてプロセス確認
$ docker ps -a
```

```:コンテナ再起動
$ docker start centos7c
```


使用しなくなったコンテナは```docker rm centos7c```で削除できる。
削除すると```docker ps -a```でも表示されなくなる。

:::note alert
削除したらもちろん今までコンテナ内で行ったことはすべてなくなるので注意。
:::

# コンテナ内にC言語環境

## gccのインストール
gccが入っているか確認

```:
[root@df8c294e015f /]# rpm -qa gcc
[root@df8c294e015f /]# 
```
入ってなかったのでインストール

```:
[root@df8c294e015f /]# yum -y install gcc
～省略～
Dependency Updated:
  glibc.x86_64 0:2.17-325.el7_9                  glibc-common.x86_64 0:2.17-325.el7_9

Complete!
[root@df8c294e015f /]# 
```
インストールされた。

```
[root@df8c294e015f /]# rpm -qa gcc
gcc-4.8.5-44.el7.x86_64
```

## 作業用ユーザ作成

```:
[root@df8c294e015f /]# adduser user1
[root@df8c294e015f /]# su - user1
[user1@df8c294e015f ~]$ pwd
/home/user1
```

## GCCでコンパイル
コンパイルできるか試してみる。

```:
[user1@df8c294e015f ~]$ mkdir test01
[user1@df8c294e015f ~]$ cd test01/
[user1@df8c294e015f test01]$ cat test.c
#include<stdio.h>
int main(){
        printf("hello\n");
}
[user1@df8c294e015f test01]$ gcc test.c 
[user1@df8c294e015f test01]$ ./a.out
hello
```
無事コンパイルと実行ができた。



curl公式
https://curl.se/


### libcurl込みでコンパイルしてみる

```:とりあえずコンパイルしてみるとcurl.hが読み込めない
[user1@df8c294e015f test01]$ cat post.c 
#include<stdio.h>
#include <curl/curl.h>
～省略～
[user1@df8c294e015f test01]$ gcc test.c -lcurl
fatal error: curl/curl.h: No such file or directory
```
-lオプションでリンクしたいライブラリ名を指定している。




## libcurlを入れてみる

いろいろ調べてみるとC言語から```#include <curl/curl.h>```するには
```libcurl-devel```のインストールが必要のよう。
https://stackoverflow.com/questions/69633205/libcurl-c-how-to-correctly-install-and-use-on-centos-7

### ということでlibcurl-develを入れる

```:libcurl-develのインストール
# yum install libcurl-devel
～省略～
Downloading packages:
libcurl-devel-7.29.0-59.el7_9.1.x86_64.rpm                                      | 303 kB  00:00:00     
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Installing : libcurl-devel-7.29.0-59.el7_9.1.x86_64                                              1/1 
  Verifying  : libcurl-devel-7.29.0-59.el7_9.1.x86_64                                              1/1 

Installed:
  libcurl-devel.x86_64 0:7.29.0-59.el7_9.1

Complete!
```

インストール完了したら再度コンパイル。

```:
[root@df8c294e015f test01]# gcc test.c -lcurl
[root@df8c294e015f test01]# ./a.out
```
コンパイルが通った！


## モックサーバの用意
POSTのテストをしたいので何かよさそうなものはないか探してみると以下2つが簡単そうなのでそれぞれ試してみる。

* json-server
* httpbin.org

### json-server
npmでインストール。npm自体のインストールについては割愛。

```:インストール
$ npm install -g json-server
```
リクエストした際に返すレスポンスをjsonで定義しておく。

```json:db.json
{
  "user": {
    "name": "user1",
    "age": 30
  }
}
```
作成したjsonを指定して起動。

```:起動
$ json-server --watch db.json
```

ブラウザから以下にアクセスすると
http://localhost:3000/user
db.jsonで定義したuserオブジェクトの中身を返してくれる。
すごく簡単。

ただPOSTを試してみると、用意したdb.jsonを本当にPOSTしたデータで書き換えてしまう。
POSTで書き換えないようにするにはもうひと手間いるよう。(Javascriptでの実装が少し必要)

### httpbin.org
こちらは簡単なHTTPリクエストを試せるWebサービス。
http://httpbin.org
Dockerイメージも用意されているのでPullしてローカルで動かせば外部に通信する必要もない。

いったんDockerイメージではなくWebサービスをそのまま使ってみる。

```:GETの場合
$ curl -X GET "http://httpbin.org/get?name=user1&age=30"
{
  "args": {
    "age": "30",
    "name": "user1"
  },
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.79.1",
    "X-Amzn-Trace-Id": "Root=1-61ed8bac-11b4622d4e26172d4ab0b672"
  },
  "origin": "133.200.49.161",
  "url": "http://httpbin.org/get?name=user1&age=30"
}
```
GETの場合は"args"にリクエストパラメータを設定して返してくれる。

```:POSTの場合
$ curl -X POST "http://httpbin.org/post" -d "name=user1&age=30"
{
  "args": {},
  "data": "",
  "files": {},
  "form": {
    "age": "30",
    "name": "user1"
  },
  "headers": {
    "Accept": "*/*",
    "Content-Length": "17",
    "Content-Type": "application/x-www-form-urlencoded",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.79.1",
    "X-Amzn-Trace-Id": "Root=1-61ed8be5-4814397c319394335e8739a0"
  },
  "json": null,
  "origin": "133.200.49.161",
  "url": "http://httpbin.org/post"
}
```
POSTの場合は"form"にリクエストパラメータを設定して返してくれる。(おそらくformタグでSubmitしたイメージ)
```-H "Content-Type: application/json"```を指定すれば"data"にPOSTデータをそのまま設定して返してくれる。

```
curl -X POST "http://httpbin.org/post" -H "Content-Type: application/json" -d '{"name":"user1","age":30}'
{
  "args": {},
  "data": "'{name:user1,age:30}'",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "*/*",
    "Content-Length": "21",
    "Content-Type": "application/json",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.79.1",
    "X-Amzn-Trace-Id": "Root=1-61ed8dbd-672e05e12b0709182676ec4d"
  },
  "json": null,
  "origin": "133.200.49.161",
  "url": "http://httpbin.org/post"
}
```

↑ちなみに"data"はWindowsコマンドプロンプトだと
```"data": "'{name:user1,age:30}'",```で
DockerコンテナのLinuxだと
```"data": "{\"name\":\"user1\",\"age\":30}", ```
だった。

## POST処理の実装
公式ドキュメントを参考に実装。
https://curl.se/libcurl/c/getinmemory.html

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

#define MAX_BUF 65535

typedef struct
{
	char *memory;
	int size;
} Chunk;

// Callback function
static size_t write_data(void *buffer, size_t size, size_t nmemb, void *userp)
{
	size_t realsize = size * nmemb;
	Chunk *chunk = (Chunk *)userp;

	char *ptr = realloc(chunk->memory, chunk->size + realsize + 1);
	if (!ptr)
	{
		/* out of memory! */
		printf("not enough memory (realloc returned NULL)\n");
		return 0;
	}

	chunk->memory = ptr;
	memcpy(&(chunk->memory[chunk->size]), buffer, realsize);
	chunk->size += realsize;
	chunk->memory[chunk->size] = 0;

	return realsize;
}

int main()
{
	printf("### POST START ###\n");
	CURL *curl;
	CURLcode ret;
	int wr_error = 0;
	curl = curl_easy_init();
	char *post_data = "name=user2&age=31";
	Chunk chunk;
	memset(&chunk, 0, sizeof(Chunk));

	if (curl == NULL)
	{
		fprintf(stderr, "curl_easy_init failed.\n");
		return 1;
	}
	curl_easy_setopt(curl, CURLOPT_URL, "http://httpbin.org/post");
	curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);
	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);

	// post option
	curl_easy_setopt(curl, CURLOPT_POST, 1);
	curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data);
	curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, strlen(post_data));

	ret = curl_easy_perform(curl);
	printf("ret : %d\n", ret);

	if (ret == 0)
	{
		printf("size : %d\n", chunk.size);
		printf("response :\n%s\n", chunk.memory);
	}
	curl_easy_cleanup(curl);

	printf("### POST END ###\n");
	return 0;
}
```

```:実行
[user1@df8c294e015f test04]$ ./a.out 
### POST START ###
ret : 0
size : 413
response :
{
  "args": {},
  "data": "",
  "files": {},
  "form": {
    "age": "31",
    "name": "user2"
  },
～省略～
### POST END ###
```


## その他
### Dockerで実装したファイルをホストに渡す

```:コマンド実行したカレントディレクトリにコピー
docker cp centos7c:/home/user1/test01/test.c .
```
```docker cp ＜コンテナ名＞:＜コンテナ内ファイル絶対パス＞ ＜コピー先ホストディレクトリ＞```
の形式


:::note alert
すでにtest.cがコピー先ホストディレクトリにある場合は上書きされるので注意。
:::

### ホストからコンテナにファイルを渡す

```:カレントディレクトリのファイルをコピー
docker cp test.c centos7c:/home/user1/test02
```
```docker cp ＜コピーするファイル＞ ＜コンテナ名＞:＜コンテナ内ファイル絶対パス＞```
の形式


# あとがき
実は最初VMWareで試そうとしたけど、
CentOS7のISOイメージDLしてVMWareで仮想マシン作成して起動したら
以下のエラーが発生してよくわからなかったのでDocker使ってみました。

```:
Linux install error "dracut Warning: Can't mount root filesystem."
```

あとでlibcurlインストールまでの作業をDockerfileにまとめてDockerイメージ化したい。