C++でもHTTPクライアント側の通信実装が可能だったので試してみる

本稿ではC++でHTTP通信を行い, 取得したデータをユーザーに表示する方法について解説する．
docker-composeとGoのWebフレームワークであるEchoを用いてHTTPサーバーを起動した例は以前の記事で紹介したことがあると思う． 今回はGo言語のサーバーを実装し, C++側はVisual Studioを用いてlibcurlをリンクし, GETおよびPOSTリクエストを送信してレスポンスを表示するまでの流れを示す．

# 完成目標
今回は, "/"エンドポイントにアクセスすると"Hello, World"を返し, "/json"エンドポイントにアクセスすると{"message":"Hello from Echo server"}というJSONレスポンスを返すサーバーを実装する。  
さらに, "/json"エンドポイントへPOSTリクエストを送った場合は, 受け取ったJSONデータを加工してレスポンスを返す実装例を示す．
docker-composeを用いてこのEchoサーバーをコンテナ上で起動し, クライアント側でlibcurlを用いてGETとPOSTを試してみる．

# 1.Echoを用いたGoサーバーの実装例
Echoフレームワークは
```bash
go get github.com/labstack/echo/v4
go mod tidy
```
で取得可能である。  

```server.go
package main

import (
    "encoding/json"
    "net/http"
    "github.com/labstack/echo/v4"
    "io"
)

type RequestData struct {
    Data string `json:"data"`
}

type Response struct {
    Message string `json:"message"`
}

func main() {
    e := echo.New()
    
    e.GET("/", func(c echo.Context) error {
        return c.String(http.StatusOK, "Hello, World")
    })
    
    // GET時は{"message":"Hello from Echo server"}を返す
    // POST時は受け取った{"data":"..."}を元に{"message":"Received: ..."}と返す
    e.Any("/json", func(c echo.Context) error {
        if c.Request().Method == http.MethodGet {
            res := Response{Message: "Hello from Echo server"}
            return c.JSON(http.StatusOK, res)
        } else if c.Request().Method == http.MethodPost {
            body, err := io.ReadAll(c.Request().Body)
            if err != nil {
                return c.JSON(http.StatusBadRequest, Response{Message: "Failed to read request"})
            }
            var reqData RequestData
            if err := json.Unmarshal(body, &reqData); err != nil {
                return c.JSON(http.StatusBadRequest, Response{Message: "Invalid JSON"})
            }
            res := Response{Message: "Received: " + reqData.Data}
            return c.JSON(http.StatusOK, res)
        } else {
            return c.JSON(http.StatusMethodNotAllowed, Response{Message: "Method not allowed"})
        }
    })
    
    e.Logger.Fatal(e.Start(":8080"))
}
```
上記ではGET時とPOST時でレスポンスを分けている。GETで/jsonへアクセスすると{"message":"Hello from Echo server"}が返り, POSTで{"data":"test"}を送れば{"message":"Received: test"}というレスポンスを返す。

# 2.Dockerfileとdocker-compose.yml
docker-composeでGo+Echoのコンテナを起動するため, 以下のファイルを用意する。  
**Dockerfile**
goのバージョンは適宜変更してください
```Dockerfile
FROM golang:1.23-alpine 
WORKDIR /app
COPY . .
RUN go mod tidy && go build -o server server.go
EXPOSE 8080
CMD ["./server"]
```
**docker-compose.yml**
```yaml
version: '3'
services:
  go_server:
    build: .
    ports:
      - "8080:8080"
```

サーバー側の実行イメージとして，Dockerコンテナを起動したら `http://localhost:8080` および `http://localhost:8080/json` へアクセス可能となり，curlなどで確認すると, `/`エンドポイントでは`Hello, World`, `/json`エンドポイントではGET時`{"message":"Hello from Echo server"}`, POST時に`{"data":"test"}`を送れば`{"message":"Received: test"}`が返る。

# 3.Visual StudioでのlibcurlセットアップとC++クライアントからのHTTP通信
windowsマシンにvcpkgを使ってlibcurlをインストールする．コマンドプロンプトに以下のコマンドを入れる
```powershell
git clone https://github.com/microsoft/vcpkg.git
cd vcpkg
.\bootstrap-vcpkg.bat
.\vcpkg.exe install curl:x64-windows
```
インストールが成功すると，以下のディレクトリに curl ライブラリがインストールされる．
ライブラリファイル
```
C:\vcpkg\installed\x64-windows\lib\libcurl.lib
```
ヘッダーファイル
```
C:\vcpkg\installed\x64-windows\include\curl\curl.h
```
インストール後、Visual Studio プロジェクトに以下を設定する

`「プロジェクトを右クリック > プロパティ > 構成プロパティ > C/C++ > 全般」` に移動。
「追加のインクルードディレクトリ」 に次を設定：
```
C:\vcpkg\installed\x64-windows\include
```
`「構成プロパティ > リンカー > 全般」 に移動。
「追加のライブラリディレクトリ」` に次を設定：
```
C:\vcpkg\installed\x64-windows\lib
```
`「構成プロパティ > リンカー > 入力」 に移動。
「追加の依存ファイル」` に次を設定：
```
libcurl.lib
```
プロジェクトにdllファイルを移動する必要もあるのでコマンドプロンプトで移動させる．
```powershell
copy C:\vcpkg\installed\x64-windows\bin\libcurl.dll C:\Path\To\Your\Project\x64\Debug
```

> 他のdllファイルも見つけられないようなエラーが出たら同様にそのファイルもcopyコマンドを実行すること

# 4.GETリクエスト例(client.cpp)
以下は`http://localhost:8080/json`へGETリクエストを送り, レスポンスを表示するコード例である。  
docker-composeでEchoサーバーが起動している状態で実行すると, `{"message":"Hello from Echo server"}`が表示される。
```client.cpp
#include <iostream>
#include <string>
#include "curl/curl.h"

size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
    size_t totalSize = size * nmemb;
    std::string* str = static_cast<std::string*>(userp);
    str->append(static_cast<char*>(contents), totalSize);
    return totalSize;
}

int main() {
    const std::string url = "http://localhost:8080/json";
    CURL* curl = curl_easy_init();
    if (!curl) {
        std::cerr << "Failed to init curl" << std::endl;
        return 1;
    }

    std::string responseBuffer;
    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBuffer);

    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        std::cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << std::endl;
    } else {
        std::cout << "Response from server:" << std::endl;
        std::cout << responseBuffer << std::endl;
    }

    curl_easy_cleanup(curl);
    return 0;
}
```
# 5.POSTリクエスト例(client_post.cpp)
次にPOSTリクエストを送る例である。  
以下は`http://localhost:8080/json`へ`{"data":"test"}`をPOSTで送り, `{"message":"Received: test"}`というレスポンスを取得する。
```client_post.cpp
#include <iostream>
#include <string>
#include "curl/curl.h"

size_t WriteCallback2(void* contents, size_t size, size_t nmemb, void* userp) {
    size_t totalSize = size * nmemb;
    std::string* str = static_cast<std::string*>(userp);
    str->append(static_cast<char*>(contents), totalSize);
    return totalSize;
}

int main() {
    const std::string url = "http://localhost:8080/json";
    const std::string postData = "{\"data\":\"test\"}";

    CURL* curl = curl_easy_init();
    if (!curl) {
        std::cerr << "Failed to init curl" << std::endl;
        return 1;
    }

    std::string responseBuffer;
    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
    curl_easy_setopt(curl, CURLOPT_POST, 1L);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str());
    curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)postData.size());
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback2);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBuffer);

    struct curl_slist* headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);

    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        std::cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << std::endl;
    } else {
        std::cout << "Response from server:" << std::endl;
        std::cout << responseBuffer << std::endl;
    }

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    return 0;
}
```
この後に`docker-compose.yml`があるディレクトリで以下のコマンドを実行してクライアント側のファイルを実行すればOK
```
docker compose up
```
> wslで上記のコマンドを実行するにはdocker desktopとwslを連携させる必要がある

https://qiita.com/JavaLangRuntimeException/items/d6942d269ca3140d5f3c

# 実行結果
`client_post.cpp`の実行結果
![スクリーンショット 2024-12-21 0.08.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757442/abbf7531-a3e0-ac15-9034-ec8f545776a3.png)
`client.cpp`の実行結果
![スクリーンショット 2024-12-21 0.09.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757442/065c087e-b9c5-bc2f-9ae4-5ed84f9c4a8a.png)


それではQiitaアドカレ.24企画の今日のクリスマスツリーです．

> 詳しくは[こちら](https://qiita.com/JavaLangRuntimeException/items/1f4a6febf957f522ba45)の記事から

![Wondershare Uniconverter 15_000010.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757442/7557df5f-4726-7b56-a2ff-1e7fa19a238b.gif)