C++でChat-GPTのAPIへリクエスト送ってみた(第2弾 Boost編)
はじめに
初めての記事でC++のhttplibを使用したChat-GPTのAPIへのリクエストコードを投稿しました。しかし、httplibはブロッキング処理ということもあり、サーバのコードに組み込むにはパフォーマンス的に良くないです。(ただ、コードが簡単に書けるのでC++初心者にはいいかも!?)
そのため、今回はより効率の良い処理ができるBeastというネットワークライブラリで同じような処理を実現してみました。Beastはノンブロッキング処理であるため、サーバに同時アクセスする際に使用するなどに相性が良いです。HTTP通信のコードを詳細に制御、記載することができるのですが、コードを書くのが少し難しかったりします。慣れですね!
概要
全体的な処理については、初めての記事の内容とほとんど変わりません。タイトル通り、C++のBeastでChat-GPTのAPIへリクエスト送るだけです。ただ、コードレベルでは結構変化がありますので、解読するのに少し大変かもしれません。
プログラムコードについて
使用するライブラリ
- Beast
- asio
- openssl
- nlohmann
インストール方法はネットにありますので、各自でよろしくお願いします。
プログラムコードの処理順序
処理順序については概要だけ説明します。
- APIのKeyが保存されたテキストファイルからAPIのKeyを読む
- リクエストするボディのJSONデータを作成
- SSL通信の準備
- サーバに接続
- HTTPリクエストを送信
- HTTPレスポンスを受信
- レスポンス内容(Chat-GPTからの回答)を表示
- 接続を切断
実際のコード
#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())の第2引数重要!
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;
}
注意事項
1つ目
Chat-GPTのAPIへのアクセスはSSL/TLSを使用するが、Beastを使用した場合にはひと手間必要です。詳しいことは分からないのですが、TLS通信の際に接続しようとしているホスト名をOpenSSLに教える必要があるらしいです。コードの場所は、
// SSL_set_tlsext_host_name(..., host.c_str())の第2引数重要!
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)
また、どうやら上のコードはCの関数をインラインで挿入しています。そのため、「SSL_set_tlsext_host_name(..., host.c_str())」で第2引数はC用のデータに変換しないといけないので、「(host名).c_str()」でC++からCにデータを合わせています。
2つ目
APIからのデータを受信し、回答を表示する際に非同期処理で実装しないと通信処理とうまく連携できずにエラーが出ます。そのため、
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で実行しています。
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:」以下は改行コードを入れています。
Response code: 200
Response of Chat-GPT: "ジャパネットたかたは、日本を拠点とする家電通販企業であり、テレビショッピングやインターネット販売などで商品を販売しています。
元々は高田明氏が創業し、家電量販店「たかた」を展開していた企業でしたが、現在は主にテレビショッピング事業が中心となっています。
商品ラインナップは家電製品を中心に、健康食品や美容グッズ、生活雑貨など多岐にわたります。"
問題なく、Chat-GPTのAPIから回答を受信できていると思います。
感想
今回のコードはhttplibの実装よりも効率的に処理ができるようになったため、Chat-GPTのAPIへリクエストを送信するサーバを立てて、その上で実行するなどいいかなと思いました。自作スマートフォンアプリと連携し、アプリ上にChat-GPTからの回答を表示するのもいいかもしれません。(やってみようかな〜)
一番の難所は注意事項の1つ目であるSSL通信のひと手間でした。あれを解決するのに2時間近くかかったため、疲れましたが解決できてよかったです。