最近做项目用到了深度学习模型,模型是Python训练的,而算法是在C++里实现的。模型的调用方面,我思考了几种方式,第一种就是用pytorch训练好,用libtorch调用,这样是最快的方式,但是学习成本太高了,还需要统一模型的输入参数等比较不灵活的操作;第二种是将其封装为api进行调用,输入输出可以自己规定,而且如果部署在本地,一个Post请求似乎所用的响应时间很快。基于此,我使用Python的Flask搭建了一个模型调用api,想着怎么能够在C++平台调用这个api,基于此搜了一些资料,发现libcurl是一个比较流行的跨平台的网络协议库,能够完成上述需求。

什么是libcurl

  libcurl是一个跨平台的网络协议库,支持http、https、ftp、gopher、telnet、dict、file和ldap协议。libcurl同样支持HTTPS证书授权,HTTP POST、HTTP PUT、FTP上传、HTTP基本表单上传、代理、cookies用户认证。

编译Libcurl

  libcurl未提供编译好的版本,需要我们手动编译。

  • 下载最新版本的libcurl Source Code:curl - Download
    libcurl的下载
  • 解压后进入文件夹,运行buildconf.bat,现象是闪过一道黑框
  • 以编译X64 Source Code为例,我的Visual Studio版本是2022:在系统搜索栏中搜索x64 Native Tools Command Prompt for VS 2022,右键选择以管理员运行;同理,编译32位则搜索x86 Native Tools Command Prompt for VS 2022,右键选择以管理员运行。
    x64 Native Tools Command Prompt for VS 2022
  • 在x64 Native Tools Command Prompt for VS 2022控制台中输入以下命令,进入curl文件夹中的winbuild文件夹。
cd [你的libcurl源代码路径]winbuild

进入curl文件夹中的winbuild文件夹,如果无法切换目录,则使用

cd /d[你的libcurl源代码路径]winbuild

输入以下命令使用2019 + x64 + release + 静态编译,不推荐使用动态编译:

nmake /f Makefile.vc mode=static VC=17 MACHINE=x64 DEBUG=no

如果你不清除你的Visual Stuido与MSVC分别对应什么版本,请查阅下表:

MSVC版本_MSC_VER 宏值Visual Studio版本号MSVC toolset version
VC6.01200VS6.0-
VC7.01300VS2002-
VC7.11310VS2003-
VC8.01400VS200580
VC9.01500VS200890
VC10.01600VS2010100
VC11.01700VS2012110
VC12.01800VS2013120
VC14.01900VS2015140
VC15.0[1910, 1916]VS2017141
VC16.0[1920, 1929]VS2019152
VC17.0[1930, )VS2022143
  • 等待编译完成,编译输出路径为源码文件夹下的/builds/libcurl-vc15-x64-release-static-ipv6-sspi-schannel目录,将这个目录里的内容保存至你常放libs的目录中,比如我放在:E:/libs/libcurl中。
    libcurl编译后文件存放

Cmake工程中集成libcurl

  在Cmake工程中集成编译好的libcurl静态库非常简单,只需要让Cmake寻找到其头文件、lib文件及其所依赖lib,然后再链接即可。我提供一个简单的示例:

cmake_minimum_required(VERSION 3.10)
project(TestLibcurl)

# Manually set the paths to libcurl
set(CURL_INCLUDE_DIR "E:/libs/libcurl/include")
set(CURL_LIBRARY "E:/libs/libcurl/lib/libcurl_a.lib")
# Define CURL_STATICLIB for static linking
add_definitions(-DCURL_STATICLIB)

include_directories(${CURL_INCLUDE_DIR})

# Append required dependencies to CURL_LIBRARY
list(APPEND CURL_LIBRARY ws2_32.lib wldap32.lib Crypt32.lib Normaliz.lib)

# Set compiler flags
if (WIN32)
   set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHs")
else()
   set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread")
endif()

# Add executable
add_executable(${CMAKE_PROJECT_NAME} ${你的SOURCES文件})

# Set C++ standard
target_compile_features(${CMAKE_PROJECT_NAME} PRIVATE cxx_std_17)

# Link libraries
target_link_libraries(${CMAKE_PROJECT_NAME} ${CURL_LIBRARIES})

不仅仅需要添加libcurl_a.lib,还需要添加一些底层库,才能编译通过,不然会报Link2019的错误。

  • ws2_32.lib 是用于支持网络功能的基础库,尤其是与 libcurl 的网络请求功能相关。
  • wldap32.lib 是为支持 libcurl 的 LDAP 协议功能。
  • Crypt32.lib 用于支持 HTTPS 加密通信(SSL/TLS),保证通过 libcurl 进行安全的网络传输。
  • Normaliz.lib 是为支持 libcurl 处理国际化域名 (IDN) 的功能。

这些库共同为libcurl提供底层支持,使其能够执行跨网络的多种操作,包括加密通信、目录服务访问以及处理特殊字符的域名。

libcurl的使用

前置条件

  网络请求传参一般使用JSON格式的数据进行传参,所以离不开处理JSON数据的处理,比如从格式化数据转换为JSON数据,从JSON数据解析出格式化数据。nlohmann是一个用于解析 JSON 的开源C++库,口碑一流,使用非常方便直观,是很多C++程序员的首选。
  配置nlohmann/json的方式很简单,nlohmann/json是一个纯头文件库(.hpp),hpp初衷就是不希望通过头文件、动静态库的方式来屏蔽代码实现细节,省去不必要的链接装载以及库版本维护成本,我们可以从GitHub(nlohmann/json: JSON for Modern C++ (github.com))中clone最新版本的nlohmann/json,打开single_include/nlohmann目录,将json.hpp直接集成在项目中即可。
nlohmann/json的hpp文件

libcurl的使用方法

  为了方便调用libcurl,参考https://www.cnblogs.com/RioTian/p/17563205.html后,我添加一个工具类:HttpConnection,提供构造、析构、Get请求、Post请求。

class HttpConnection {
public:
    HttpConnection() {
        curl_global_init(CURL_GLOBAL_ALL);
        curl_ = curl_easy_init();
    }

    ~HttpConnection() {
        curl_easy_cleanup(curl_);
    }

    bool Post(const std::string& url, const std::string& data, std::string& response) {
        if (!curl_) {
            return false;
        }

        // set params
        // set curl header
        struct curl_slist* header_list = NULL;
        // der_list = curl_slist_append(header_list, "User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko");
        header_list = curl_slist_append(header_list, "Content-Type:application/json; charset = UTF-8");
        curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, header_list);

        curl_easy_setopt(curl_, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl_, CURLOPT_POST, 1L);
        curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, data.c_str());
        curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, &WriteCallback);
        curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response);

        CURLcode res = curl_easy_perform(curl_);
        return (res == CURLE_OK);
    }

    bool Get(const std::string& url, std::string& response) {
        if (!curl_) {
            return false;
        }

        curl_easy_setopt(curl_, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl_, CURLOPT_POST, 0L);
        curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, &WriteCallback);
        curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response);

        CURLcode res = curl_easy_perform(curl_);
        return (res == CURLE_OK);
    }

private:
    CURL* curl_ = nullptr;

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

使用上述工具类对接口进行调用,这里举例请求Post接口,拿我自己使用Flask写的Api接口为例,以下是我封装的Post接口,接口要求多个参数,pre_seq_length、aft_seq_length、gsize、dimension、input_data,返回预测结果{'pred': pred.tolist()},pred是一个二维数组。

@app.route('/predict', methods=['POST'])
def predict_api():
    data = request.get_json()
    pre_seq_length = data['pre_seq_length']
    aft_seq_length = data['aft_seq_length']
    gsize = data['gsize']
    dimension = data['dimension']
    input_data = data['input_data']
    print('input_data:', input_data)
    pred = predict(pre_seq_length, gsize, dimension, input_data)
    return jsonify({'pred': pred.tolist()})

请求这个POST接口的使用方法如下,可以仿照下面的方法进行调用:

#include "<json/json.hpp>"

// 在全局注册(比如类的构造函数中)HttpConnection类的实例m_http_connection
std::unique_ptr<HttpConnection> m_http_connection = std::make_unique<HttpConnection>();

// 调用方法
std::vector<std::vector<double>> fitnessPredict(const std::vector<std::vector<std::vector<Real>>> &data) {
    json request_data;
    request_data["pre_seq_length"] = pre_seq_length;
    request_data["aft_seq_length"] = aft_seq_length;
    request_data["gsize"] = m_pop_size;
    request_data["dimension"] = GET_PRO(m_id_pro).numVariables();
    request_data["input_data"] = input_validated;
    std::string request_string = request_data.dump();
    // api地址,例如http://127.0.0.1:5000
    std::string url = m_api;

    std::string response;
    bool success = m_http_connection->Post(url, request_string, response);
    if (success) {
        try {
            json response_json = json::parse(response);
            std::vector<std::vector<double>> predictions = response_json["pred"].get<std::vector<std::vector<double>>>();
            return predictions;
        } catch (json::exception& e) {
            std::cerr << "JSON parsing error: " << e.what() << std::endl;
        }
    } else {
        std::cerr << "HTTP request failed" << std::endl;
    }
    return {};
}

调用的结果如下所示,大功告成:

"{"pred":[[23.676162719726562,23.671985626220703,23.682897567749023,23.66766357421875,23.025022506713867]]}\n"
最后修改:2024 年 10 月 03 日
如果觉得我的文章对你有用,请随意赞赏