后端工程师调用RESTful API完全指南(附C/C++实战)

后端工程师调用RESTful API完全指南(附C/C++实战) 第一部分RESTful API基础与设计规范1.1 什么是RESTful APIRESTRepresentational State Transfer表现层状态转移是一种软件架构风格它定义了一组用于创建 Web 服务的约束条件。RESTful API 则是基于这种风格设计的应用程序编程接口它利用 HTTP 协议的方法来操作资源使得 API 简洁、可缓存且易于理解。一个 RESTful API 的核心思想是将系统功能抽象为资源Resources每个资源通过唯一的 URI 进行标识。客户端通过 HTTP 方法如 GET、POST、PUT、DELETE对资源进行操作而服务器则根据操作结果返回相应的 HTTP 状态码和数据。1.2 RESTful 核心设计原则1.2.1 资源导向设计资源是 RESTful API 的核心。设计时应遵循单一职责原则将功能聚焦于“事物”名词而非“动作”动词。正确示例GET /api/users/123获取ID为123的用户错误示例GET /api/getUserById?id123将动作放入URI中1.2.2 无状态通信每个从客户端到服务器的请求必须包含理解该请求所必需的所有信息服务器不能利用任何存储在服务器上的上下文。这意味着所有的状态都应在客户端管理认证信息如 JWT需要在每次请求中通过 Header 传递。无状态设计极大地提升了系统的可扩展性。1.2.3 HTTP 方法的精准使用每个 HTTP 方法都有其特定的语义必须精准使用。方法语义幂等性安全性典型用途GET检索资源是是获取一个或多个资源的详细信息POST创建资源否否提交数据以创建新资源如新订单PUT完整更新是否替换整个资源客户端需提供资源的完整表示PATCH部分更新否否仅更新资源的特定字段更高效DELETE删除资源是否移除指定的资源1.2.4 HTTP 状态码的规范使用HTTP 状态码表示了请求的处理结果分类使用可以令客户端快速了解请求状态。2xx (成功类)200 OKGET、PUT、PATCH 请求成功响应体中包含结果。201 CreatedPOST 请求成功创建资源响应头中应包含Location字段指向新资源的URI。204 No Content请求成功如 DELETE但响应体为空。4xx (客户端错误类)400 Bad Request请求参数错误或格式不正确。401 Unauthorized请求未携带认证信息或认证失败。403 Forbidden服务器理解请求但拒绝执行无权限。404 Not Found请求的资源不存在。5xx (服务器错误类)500 Internal Server Error服务器内部错误。503 Service Unavailable服务器暂时无法处理请求如过载。1.3 数据格式与错误处理1.3.1 请求与响应格式现代 RESTful API 普遍采用 JSON 作为数据交换格式。请求体示例创建用户json{ username: john_doe, email: johnexample.com, age: 30 }成功响应体示例json{ data: { id: 123, username: john_doe, email: johnexample.com }, meta: { timestamp: 2024-05-20T10:30:00Z } }错误响应体示例json{ error: { code: USER.INVALID_EMAIL, message: The provided email format is invalid., details: [ { field: email, issue: must be a valid email address } ] } }统一且详细的错误响应对于调试 API 调用至关重要。1.4 API 版本控制策略随着业务发展API 不可避免地需要变更。良好的版本控制可以保证向后兼容性。常见的策略有三种URI 路径版本https://api.example.com/v1/users和https://api.example.com/v2/users。这种方式最直观适用于重大不兼容变更。请求头版本客户端在Accept头中指定版本如Accept: application/vnd.example.v1json。查询参数版本https://api.example.com/users?version1这种方式简单但可能被缓存系统忽略。第二部分后端调用RESTful API的基础在了解了 API 的设计规范后我们站在调用者的角度来看看一次成功的 API 调用需要关注哪些要素。2.1 调用过程的要素分析一个标准的 HTTP 请求由以下几个核心部分组成URL (统一资源定位符)标识要操作的资源。例如https://api.github.com/users/octocat。HTTP 方法定义操作类型GET, POST, PUT, DELETE 等。请求头 (Headers)提供关于请求的元数据。Content-Type指明请求体的格式如application/json。Authorization携带认证凭据如Bearer your-jwt-token或Basic base64-encoded-credentials。Accept告知服务器客户端期望接收的响应格式。请求体 (Body)在 POST、PUT、PATCH 请求中发送数据给服务器。超时设置为了防止客户端无限期等待必须设置连接超时和读取超时。2.2 认证与授权机制调用 API 时最常见的认证方式有以下几种API Key最简单的方式通常作为查询参数?api_keyxxx或放在 HeaderX-API-Key: xxx中传递。适用于服务间的简单通信。Basic Auth将用户名和密码用冒号连接并进行 Base64 编码后放在Authorization: Basic encoded-string头中。由于 Base64 可解码必须与 HTTPS 配合使用。Bearer Token / JWT是目前最流行的方式。客户端先通过登录接口获取一个令牌Token然后在后续请求的Authorization: Bearer token头中携带该令牌。JWT 本身包含用户信息和签名服务端无需存储 session非常适合无状态 API。OAuth 2.0用于第三方应用授权流程较复杂。客户端需要先向授权服务器获取一个访问令牌Access Token然后用这个令牌访问资源服务器。2.3 C/C 调用 RESTful API 的挑战与方案C/C 不像 Python、JavaScript 那样内置 HTTP 客户端库。在 C/C 中调用 RESTful API需要借助第三方库。主要有以下几种选择libcurl最古老、最强大、最底层的 C 语言网络库。它支持几乎所有协议功能极其丰富但需要开发者手动处理许多细节如响应缓冲区管理、内存分配等。RestClient-cpp一个对 libcurl 的轻量级 C 封装提供了极其简单直观的 API。非常适合快速实现 RESTful 调用。C REST SDK (cpprestsdk)微软开源的、现代的 C 异步编程库提供了完整的 HTTP 客户端/服务器、JSON 解析等功能。它使用 PPL 任务pplx::task进行异步操作性能强大但学习曲线稍陡。Chilkat C Library一个商业化的跨平台库功能全面但并非开源。本文将重点介绍libcurl (C语言)和RestClient-cpp (C)并在高级示例中涉及cpprestsdk。第三部分C语言调用RESTful API实战使用 libcurl3.1 libcurl 简介与安装libcurl 是一个免费、易用、支持跨平台的客户端 URL 传输库。它支持 DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, POP3, POP3S, RTMP, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET 和 TFTP 等众多协议。在 Ubuntu/Debian 系统上安装bashsudo apt-get update sudo apt-get install libcurl4-openssl-dev编译时需链接 curl 库bashgcc -o my_program my_program.c -lcurl3.2 libcurl 核心 API 与通用回调函数libcurl 的使用遵循“设置-执行-清理”的模式。一个关键的难点在于处理响应数据。libcurl 默认将响应数据输出到stdout我们必须通过设置回调函数来捕获它。c#include stdio.h #include stdlib.h #include string.h #include curl/curl.h // 回调函数用于处理接收到的数据块 // 参数: ptr - 指向数据块的指针, size * nmemb - 数据块的大小, userdata - 用户自定义数据指针 size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) { size_t realsize size * nmemb; // 将数据追加到动态字符串中。这里假设 userp 是一个指向可动态扩展内存的字符串指针。 // 在实际应用中需要更精细的内存管理例如使用结构体包含字符串指针和当前长度。 strncat((char*)userp, (char*)contents, realsize); return realsize; } int main(void) { CURL *curl; CURLcode res; char response_buffer[4096] {0}; // 静态缓冲区有溢出风险仅为示例 curl_global_init(CURL_GLOBAL_DEFAULT); // 全局初始化 curl curl_easy_init(); // 获取一个 easy 句柄 if(curl) { // 设置基础选项 curl_easy_setopt(curl, CURLOPT_URL, https://api.github.com/users/octocat); // 设置回调函数来处理响应数据 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, response_buffer); // 设置 User-AgentGitHub API 强制要求 curl_easy_setopt(curl, CURLOPT_USERAGENT, libcurl-agent/1.0); // 设置超时 curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); // 总超时10秒 // 执行请求 res curl_easy_perform(curl); // 检查执行结果 if(res ! CURLE_OK) { fprintf(stderr, curl_easy_perform() failed: %s\n, curl_easy_strerror(res)); } else { // 获取HTTP响应码 long http_code 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_code); printf(HTTP Response Code: %ld\n, http_code); printf(Response Body:\n%s\n, response_buffer); } // 清理 curl_easy_cleanup(curl); } curl_global_cleanup(); // 全局清理 return 0; }代码示例一个完整的 C 语言 GET 请求示例。3.3 实战发送 GET 请求GET 请求用于从服务器获取资源。核心是设置CURLOPT_HTTPGET选项默认就是 GET可不显式设置。c// ... 包含头文件、定义WriteCallback ... void perform_get_request(const char *url) { CURL *curl curl_easy_init(); if(!curl) return; char response[2048] {0}; struct curl_slist *headers NULL; // 设置请求头例如接受 JSON headers curl_slist_append(headers, Accept: application/json); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, response); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); // 跟随重定向 CURLcode res curl_easy_perform(curl); if(res CURLE_OK) { printf(GET Response:\n%s\n, response); } else { fprintf(stderr, GET Error: %s\n, curl_easy_strerror(res)); } curl_slist_free_all(headers); curl_easy_cleanup(curl); }3.4 实战发送 POST 请求提交 JSON 数据POST 请求通常用于创建资源需要向服务器发送数据。关键选项是CURLOPT_POST和CURLOPT_POSTFIELDS。同时必须设置Content-Type: application/json头部。cvoid perform_post_request(const char *url, const char *json_data) { CURL *curl curl_easy_init(); if(!curl) return; char response[2048] {0}; struct curl_slist *headers NULL; headers curl_slist_append(headers, Content-Type: application/json); headers curl_slist_append(headers, Accept: application/json); // 期望返回JSON curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); // 设置为POST请求 curl_easy_setopt(curl, CURLOPT_POST, 1L); // 设置POST数据 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_data); // 设置数据长度可选如果数据包含空字符则需要否则libcurl会自己计算长度 curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(json_data)); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, response); CURLcode res curl_easy_perform(curl); if(res CURLE_OK) { long http_code 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_code); printf(POST Response Code: %ld\n, http_code); printf(POST Response Body:\n%s\n, response); } else { fprintf(stderr, POST Error: %s\n, curl_easy_strerror(res)); } curl_slist_free_all(headers); curl_easy_cleanup(curl); } // 调用示例 // perform_post_request(https://jsonplaceholder.typicode.com/posts, // {\title\:\foo\, \body\:\bar\, \userId\:1});代码示例使用 libcurl 发送 JSON 格式的 POST 请求。3.5 实战PUT 与 DELETE 请求PUT 和 DELETE 请求可以通过设置自定义请求方法来实现。cvoid perform_put_request(const char *url, const char *json_data) { CURL *curl curl_easy_init(); if(!curl) return; char response[2048] {0}; struct curl_slist *headers NULL; headers curl_slist_append(headers, Content-Type: application/json); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); // 设置自定义请求方法为 PUT curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, PUT); // 设置要发送的数据 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_data); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, response); CURLcode res curl_easy_perform(curl); // ... 错误处理和清理 ... } void perform_delete_request(const char *url) { CURL *curl curl_easy_init(); if(!curl) return; char response[2048] {0}; curl_easy_setopt(curl, CURLOPT_URL, url); // 设置自定义请求方法为 DELETE curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, DELETE); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, response); CURLcode res curl_easy_perform(curl); // ... 错误处理和清理 ... }3.6 处理请求头与超时libcurl 提供了丰富的选项来控制请求头。可以使用curl_slist构建自定义头部并通过CURLOPT_HTTPHEADER设置。超时设置则通过CURLOPT_TIMEOUT总超时和CURLOPT_CONNECTTIMEOUT连接超时等选项实现。cstruct curl_slist *headers NULL; headers curl_slist_append(headers, Authorization: Bearer YOUR_ACCESS_TOKEN); headers curl_slist_append(headers, X-Custom-Header: FooBar); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L); // 连接超时5秒 curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); // 整个操作超时30秒3.7 完整示例调用 GitHub APIc#include stdio.h #include stdlib.h #include string.h #include curl/curl.h struct MemoryStruct { char *memory; size_t size; }; static size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp) { size_t realsize size * nmemb; struct MemoryStruct *mem (struct MemoryStruct *)userp; char *ptr realloc(mem-memory, mem-size realsize 1); if(!ptr) { printf(Not enough memory (realloc returned NULL)\n); return 0; } mem-memory ptr; memcpy((mem-memory[mem-size]), contents, realsize); mem-size realsize; mem-memory[mem-size] 0; return realsize; } int main(void) { CURL *curl_handle; CURLcode res; struct MemoryStruct chunk; chunk.memory malloc(1); chunk.size 0; curl_global_init(CURL_GLOBAL_ALL); curl_handle curl_easy_init(); if(curl_handle) { curl_easy_setopt(curl_handle, CURLOPT_URL, https://api.github.com/users/octocat/orgs); curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback); curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)chunk); curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, libcurl-agent/1.0); // GitHub API 可能需要 Token 来避免限流 struct curl_slist *headers NULL; headers curl_slist_append(headers, Authorization: token YOUR_GITHUB_TOKEN); curl_easy_setopt(curl_handle, CURLOPT_HTTPHEADER, headers); res curl_easy_perform(curl_handle); if(res ! CURLE_OK) { fprintf(stderr, curl_easy_perform() failed: %s\n, curl_easy_strerror(res)); } else { printf(%s\n, chunk.memory); } curl_slist_free_all(headers); curl_easy_cleanup(curl_handle); free(chunk.memory); } curl_global_cleanup(); return 0; }代码示例使用动态内存管理的完整 GitHub API 调用。第四部分C语言调用RESTful API实战C 调用 RESTful API 有多种优雅的方式。我们可以继续使用 libcurl 但封装进 C 类也可以使用更现代的库。本节将介绍RestClient-cpp和C REST SDK (cpprestsdk)。4.1 使用 RestClient-cpp 库RestClient-cpp 是一个对 libcurl 的轻量级 C 封装其 API 设计非常简洁是快速上手的理想选择。4.1.1 安装与配置bash# 从 GitHub 克隆并编译安装 git clone https://github.com/mrtazz/restclient-cpp.git cd restclient-cpp ./autogen.sh ./configure make sudo make install4.1.2 发送 GET 请求cpp#include iostream #include restclient-cpp/restclient.h int main() { RestClient::init(); // 可选用于全局初始化 RestClient::Response response RestClient::get(https://api.github.com/users/octocat); if (response.code 200) { std::cout GET successful! std::endl; std::cout Body: response.body std::endl; } else { std::cout GET failed with code: response.code std::endl; std::cout Error message: response.body std::endl; } RestClient::disable(); // 清理 return 0; }代码示例RestClient-cpp 的 GET 请求。4.1.3 发送 POST 请求cpp#include iostream #include restclient-cpp/restclient.h int main() { RestClient::init(); // 设置请求头 RestClient::HeaderFields headers; headers[Accept] application/json; headers[Content-Type] application/json; // POST 数据 std::string data {\title\:\foo\, \body\:\bar\, \userId\:1}; RestClient::Response response RestClient::post(https://jsonplaceholder.typicode.com/posts, data, headers); std::cout HTTP Code: response.code std::endl; std::cout Response Body: response.body std::endl; RestClient::disable(); return 0; }4.1.4 PUT 与 DELETE 操作RestClient-cpp 同样提供了put()和del()方法。cpp// PUT RestClient::Response put_resp RestClient::put(https://api.example.com/resource/1, {\key\:\new_value\}, headers); // DELETE RestClient::Response del_resp RestClient::del(https://api.example.com/resource/1);4.2 使用 C REST SDK (cpprestsdk)cpprestsdk 是微软开源的跨平台库专为异步编程设计提供了http_client,json::value等强大的类。4.2.1 安装与基本概念在 Ubuntu 上bashsudo apt-get install libcpprest-dev核心概念web::http::client::http_clientHTTP 客户端类。web::http::http_request代表一个 HTTP 请求。web::http::http_response代表一个 HTTP 响应。web::json::value用于构建和解析 JSON 数据。pplx::taskT用于处理异步操作的任务类。4.2.2 异步 GET 请求示例cpp#include iostream #include cpprest/http_client.h #include cpprest/filestream.h using namespace utility; // 通用工具如 string_t using namespace web; // Web 相关功能如 URI using namespace web::http; // HTTP 功能 using namespace web::http::client; // HTTP 客户端功能 using namespace concurrency::streams; // 异步流 int main() { // 创建一个 HTTP 客户端指定基础 URI http_client client(U(https://api.github.com)); // 构建一个 GET 请求 http_request request(methods::GET); request.set_request_uri(U(/users/octocat)); request.headers().add(U(User-Agent), U(cpprestsdk-agent)); // 异步发送请求并定义一个 lambda 表达式来处理响应 client.request(request).then([](http_response response) { if (response.status_code() status_codes::OK) { // 异步提取响应体为 JSON 字符串 return response.extract_json(); } throw std::runtime_error(Request failed); }).then([](json::value jsonData) { // 处理 JSON 数据 auto login jsonData.at(U(login)).as_string(); auto id jsonData.at(U(id)).as_integer(); std::cout Login: utility::conversions::to_utf8string(login) std::endl; std::cout ID: id std::endl; }).then([](pplx::taskvoid previousTask) { // 统一处理异常 try { previousTask.get(); } catch (const std::exception e) { std::cout Error: e.what() std::endl; } }).wait(); // 等待所有异步操作完成 return 0; }代码示例cpprestsdk 异步 GET 请求。4.2.3 异步 POST 请求示例发送 JSONcpp#include cpprest/http_client.h #include cpprest/json.h // ... using 声明 ... int main() { http_client client(U(https://jsonplaceholder.typicode.com)); // 构建 JSON 数据 json::value postData; postData[U(title)] json::value::string(U(foo)); postData[U(body)] json::value::string(U(bar)); postData[U(userId)] json::value::number(1); // 构建 POST 请求 http_request request(methods::POST); request.set_request_uri(U(/posts)); request.headers().set_content_type(U(application/json)); request.set_body(postData); client.request(request).then([](http_response response) { if (response.status_code() status_codes::Created) { return response.extract_json(); } throw std::runtime_error(POST failed); }).then([](json::value jsonData) { std::cout New post created with ID: jsonData.at(U(id)).as_integer() std::endl; std::cout jsonData.serialize() std::endl; }).then([](pplx::taskvoid previousTask) { try { previousTask.get(); } catch (const std::exception e) { std::cout Error: e.what() std::endl; } }).wait(); return 0; }第五部分高级话题与最佳实践5.1 JSON 的序列化与反序列化调用 API 不可避免要处理 JSON。在 C/C 中除了 cpprestsdk 自带的json::value还有很多优秀的 JSON 库可供选择。5.1.1 在 C 语言中使用 cJSONcJSON 是一个超轻量级、可移植、单文件的 JSON 解析器。c#include cJSON.h #include stdio.h #include stdlib.h #include string.h void parse_github_user(const char *json_string) { cJSON *root cJSON_Parse(json_string); if (root NULL) { printf(Error parsing JSON.\n); return; } cJSON *login cJSON_GetObjectItem(root, login); cJSON *id cJSON_GetObjectItem(root, id); cJSON *public_repos cJSON_GetObjectItem(root, public_repos); if (cJSON_IsString(login) cJSON_IsNumber(id)) { printf(Login: %s\n, login-valuestring); printf(ID: %d\n, id-valueint); if (cJSON_IsNumber(public_repos)) { printf(Public Repos: %d\n, public_repos-valueint); } } cJSON_Delete(root); // 释放内存 }5.1.2 在 C 中使用 nlohmann/json这是现代 C 中最受欢迎的 JSON 库语法像操作 STL 容器一样简单。cpp#include nlohmann/json.hpp #include iostream using json nlohmann::json; void parse_and_build() { // 解析 std::string response {\login\:\octocat\,\id\:1}; json j json::parse(response); std::string login j[login]; int id j[id]; // 构建 json postData; postData[title] foo; postData[body] bar; postData[userId] 1; std::string serialized postData.dump(); std::cout Serialized JSON: serialized std::endl; }5.2 错误处理与重试机制网络请求是不可靠的必须做好错误处理和重试。区分错误类型是网络错误超时、DNS 解析失败还是业务错误4xx, 5xx重试策略指数退避第一次等待 1 秒后重试第二次 2 秒第三次 4 秒... 避免加重服务器负担。幂等性检查只有幂等的请求GET、PUT、DELETE才能安全重试。POST 请求一般不能直接重试可能导致重复创建资源。最大重试次数设置一个上限如 3 次。5.3 连接池与性能优化在高并发场景下频繁创建和销毁 TCP 连接会消耗大量资源。HTTP Keep-Alivelibcurl 默认开启可以在一个 TCP 连接上发送多个 HTTP 请求。需要在请求头中添加Connection: Keep-Alive并在多个curl_easy_perform之间复用同一个CURL*句柄。连接池对于多线程程序可以使用curl_share_interface在不同CURL*句柄之间共享数据如 DNS 缓存和 SSL 会话 ID从而减少握手开销。5.4 日志与监控调用第三方 API 时必须记录详细日志以便排查问题。c// 伪代码示例 log_info(Calling external API: %s, Method: %s, url, method); long start_time get_current_ms(); CURLcode res curl_easy_perform(curl); long duration get_current_ms() - start_time; if (res CURLE_OK) { long http_code; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_code); log_info(API call succeeded. Status: %ld, Duration: %ldms, http_code, duration); if (http_code 400) { log_warn(API returned client/server error. Body: %s, response_body); } } else { log_error(API call failed. Error: %s, Duration: %ldms, curl_easy_strerror(res), duration); }5.5 安全最佳实践始终使用 HTTPS避免中间人攻击。证书验证不要禁用证书验证。libcurl 默认会验证服务器证书确保证书路径正确。敏感信息保护不要在日志中打印 Token 或密码。使用环境变量或安全的配置中心来管理凭据。输入验证即使调用自己的 API也要对发送的数据进行校验防止因程序 bug 导致错误数据被发送出去。