C++でChat-GPTのAPIへリクエスト送ってみた（第２弾  Boost編）

# C++でChat-GPTのAPIへリクエスト送ってみた（第２弾 Boost編）


## はじめに
　初めての記事でC++のhttplibを使用したChat-GPTのAPIへのリクエストコードを投稿しました。しかし、httplibはブロッキング処理ということもあり、サーバのコードに組み込むにはパフォーマンス的に良くないです。（ただ、コードが簡単に書けるのでC++初心者にはいいかも！？）  
　そのため、今回はより効率の良い処理ができるBeastというネットワークライブラリで同じような処理を実現してみました。Beastはノンブロッキング処理であるため、サーバに同時アクセスする際に使用するなどに相性が良いです。HTTP通信のコードを詳細に制御、記載することができるのですが、コードを書くのが少し難しかったりします。慣れですね！


## 概要
　全体的な処理については、[初めての記事](https://qiita.com/Kenji0510/items/f8dece004f4c0ab99363)の内容とほとんど変わりません。タイトル通り、C++のBeastでChat-GPTのAPIへリクエスト送るだけです。ただ、コードレベルでは結構変化がありますので、解読するのに少し大変かもしれません。
　

## プログラムコードについて

### 使用するライブラリ
- Beast  
- asio
- openssl
- nlohmann

　インストール方法はネットにありますので、各自でよろしくお願いします。


### プログラムコードの処理順序
　処理順序については概要だけ説明します。
1. APIのKeyが保存されたテキストファイルからAPIのKeyを読む
1. リクエストするボディのJSONデータを作成
1. SSL通信の準備
1. サーバに接続
1. HTTPリクエストを送信
1. HTTPレスポンスを受信
1. レスポンス内容（Chat-GPTからの回答）を表示
1. 接続を切断


### 実際のコード
```c++
#include <boost/asio.hpp>
#include <boost/beast.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/error.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <iostream>
#include <string>
#include <cstdlib>
#include <nlohmann/json.hpp>
#include <openssl/ssl.h>
#include <fstream>


namespace asio = boost::asio;
namespace beast = boost::beast;
namespace http = beast::http;
namespace ssl = boost::asio::ssl;
using json = nlohmann::json;



// OpenAIのAPIKeyをファイルから読む
std::string readApiKey() {
    std::ifstream file("OpenAI_Key.txt");

    if (file.is_open()) {
        std::string line;

        while (std::getline(file, line)) {
            //std::cout << line << '\n';
        }

        file.close();
        // 読み込んだデータを返す
        return line;
    } else {
        std::cerr << "Unable to open file\n";
        return "error";
    }
}

// リクエスト用のJSONデータを作成する
std::string create_request_message() {
    // Body内容を作成
    nlohmann::json jsonBody;
    jsonBody["model"] = "gpt-3.5-turbo";
    jsonBody["messages"] = {
        {{"role", "user"}, {"content", "ジャパネットたかたとは？"}}
    };
    jsonBody["temperature"] = 0.7;

    // JSONデータを文字列に変換
    return jsonBody.dump();
}

int main() {
    try {
        // OpenAIのAPIKeyをファイルから読む
        std::string apiKey = readApiKey();
        if (apiKey == "error") {
            return 0;
        }

        // リクエスト用のJSONデータを作成する
        std::string body = create_request_message();


        // asio::io_contextオブジェクトを作成
        asio::io_context io_context;

        // SSLコンテキストを作成
        ssl::context ssl_context(ssl::context::tlsv13_client);

        // SSLコンテキストに証明書の検証を無効化するオプションを設定
        ssl_context.set_verify_mode(ssl::verify_none);

        // HTTPリクエスト先のホストとポートを設定
        std::string host = "api.openai.com";
        std::string port = "443";
        std::string target = "/v1/chat/completions";

        // asio::ip::tcp::resolverオブジェクトを作成し、ホスト名を解決
        asio::ip::tcp::resolver resolver(io_context);
        asio::ip::tcp::resolver::results_type endpoints = resolver.resolve(host, "https");

        // SSLストリームを作成し、サーバに接続
        ssl::stream<asio::ip::tcp::socket> stream(io_context, ssl_context);
        // Set SNI Hostname (many hosts need this to handshake successfully)
        // SSL_set_tlsext_host_name(..., host.c_str())の第２引数重要！
        if(!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str()))
        {
            boost::system::error_code ec{static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()};
            throw boost::system::system_error{ec};
        }

        asio::connect(stream.next_layer(), endpoints);
        stream.handshake(ssl::stream_base::client);

        // HTTPリクエストを作成
        http::request<http::string_body> request;
        request.method(http::verb::post);
        request.target(target);
        request.version(11);  // HTTP/1.1
        request.set(http::field::host, host);
        request.set(http::field::content_type, "application/json");
        request.set(http::field::authorization, "Bearer " + apiKey);
        request.body() = body;
        request.prepare_payload();
        request.keep_alive(true);

        // HTTPリクエストを送信
        http::write(stream, request);

        // HTTPレスポンスを受信
        beast::flat_buffer buffer;
        http::response<http::dynamic_body> response;
        http::async_read(stream, buffer, response, 
            [&](beast::error_code ec, std::size_t bytes_transferred) {
                if (!ec) {
                    // 受信したHTTPレスポンスを表示
                    std::cout << "Response code: " << response.result_int() << std::endl;
                    //std::cout << "Response body: " << beast::buffers_to_string(response.body().data()) << std::endl;
                    std::string response_data = beast::buffers_to_string(response.body().data());

                    // JSONデータの解析
                    try {
                        json data = json::parse(response_data);
                        // Chat-GPTからの回答を表示
                        std::cout << "Response of Chat-GPT: " << data["choices"][0]["message"]["content"] << std::endl;
                    } catch (const json::exception& e) {
                        std::cerr << "Failed to parse JSON: " << e.what() << std::endl;
                    }

                } else {
                    // エラーが発生した場合の処理
                    std::cerr << "Error: " << ec.message() << std::endl;
                }
            });

        // asio::io_contextを実行
        // async_read()の非同期処理が終わるまでここで処理止める
        io_context.run();

        // ソケットを閉じる
        //stream.shutdown();
        stream.next_layer().close();
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }

    return 0;
}

```

### 注意事項

#### １つ目
　Chat-GPTのAPIへのアクセスはSSL/TLSを使用するが、Beastを使用した場合にはひと手間必要です。詳しいことは分からないのですが、TLS通信の際に接続しようとしているホスト名をOpenSSLに教える必要があるらしいです。コードの場所は、
```c++
// SSL_set_tlsext_host_name(..., host.c_str())の第２引数重要！
        if(!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str()))
        {
            boost::system::error_code ec{static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()};
            throw boost::system::system_error{ec};
        }
```
　この処理を追加しないと「handshake: sslv3 alert handshake failure」が表示され、プログラムがうまく動作しません。（[参照元URL](https://stackoverflow.com/questions/59224873/boost-beast-handshake-sslv3-alert-handshake-failure-error)）
　また、どうやら上のコードはCの関数をインラインで挿入しています。そのため、「SSL_set_tlsext_host_name(..., host.c_str())」で第２引数はC用のデータに変換しないといけないので、「(host名).c_str()」でC++からCにデータを合わせています。

#### ２つ目
　APIからのデータを受信し、回答を表示する際に非同期処理で実装しないと通信処理とうまく連携できずにエラーが出ます。そのため、
```c++
http::async_read(stream, buffer, response, 
            [&](beast::error_code ec, std::size_t bytes_transferred) {
                ...
    });

// async_read()の非同期処理が終わるまでここで処理止める
io_context.run();
```
の「async_read()」で非同期処理でデータを読みとり、表示させます。
　また、「io_context.run()」で上記の処理が終わるまで処理を待ちます。


## 実行方法
　実行方法を以下に示します。MakeFileで実行しています。
```makefile
CC = g++
CFLAGS = -std=c++11 
LFLAGS = -lboost_system -lboost_thread -lssl -lcrypto

all: client

client: 
	$(CC) $(CFLAGS) -o chatgpt_req chatgpt_req.cpp $(LFLAGS) 

```


## 実行結果
　見やすいように「Response of Chat-GPT:」以下は改行コードを入れています。
```bash
Response code: 200
Response of Chat-GPT: "ジャパネットたかたは、日本を拠点とする家電通販企業であり、テレビショッピングやインターネット販売などで商品を販売しています。
元々は高田明氏が創業し、家電量販店「たかた」を展開していた企業でしたが、現在は主にテレビショッピング事業が中心となっています。
商品ラインナップは家電製品を中心に、健康食品や美容グッズ、生活雑貨など多岐にわたります。"
```

　問題なく、Chat-GPTのAPIから回答を受信できていると思います。


## 感想
　今回のコードはhttplibの実装よりも効率的に処理ができるようになったため、Chat-GPTのAPIへリクエストを送信するサーバを立てて、その上で実行するなどいいかなと思いました。自作スマートフォンアプリと連携し、アプリ上にChat-GPTからの回答を表示するのもいいかもしれません。（やってみようかな〜）  
　一番の難所は注意事項の１つ目であるSSL通信のひと手間でした。あれを解決するのに２時間近くかかったため、疲れましたが解決できてよかったです。