ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RESTful API 设计规范与 VC++ 客户端接入实战指南

RESTful API 设计规范与 VC++ 客户端接入实战指南 之前带一个新人写接口他很快给我端上来一个 URL/getUserInfo?userId123。我说能用但等接口数量上了两位数、客户端有三端要联调、每次改版还要往前兼容的时候这种命名方式就会变成事故现场。这篇东西不是教科书式的名词解释是我自己从「接口能跑」到「接口能维护」之后对 RESTful API 的一次系统整理。如果你正准备做后端开发、如果你是个客户端或桌面端工程师需要对接 RESTful API、或者你写的是 VC 程序但需要访问 HTTP 服务端的接口这篇文章帮你把规范、案例、坑一次性捋顺。文里的代码都是可以直接抄下来跑的你只需要有个 Python 环境和终端。1. RESTful API 到底在规范什么事情1.1 把 URL 当名词把 HTTP 方法当动词很多人第一次接触 RESTful 会觉得它玄乎术语一堆表述、状态转移、资源定位、无状态……其实落到代码上RESTful 做的最重要的一件事就是统一约定把每一个 URL 看作一个「资源」名词用 HTTP 方法动词来表达对这个资源的操作。你不用再写getUserInfo、deleteUserById、updateUserInfo2这种让人猜不透的接口名了。资源是用户那就叫/users操作用方法区分GET /users是查列表POST /users是新增GET /users/123是查单个PUT /users/123是整体更新DELETE /users/123是删除。这种设计的本质是让「动词」和「名词」分离。名词是内容动词是动作分离之后无论是前端、移动端还是桌面客户端对接接口时只需要记住资源路径再配合 HTTP 方法字典就能猜出 90% 接口的含义。这比翻接口文档去找「updateUserInfo2」背后的真实用途靠谱得多。1.2 用快递柜来理解这套设计把 RESTful API 想成快递柜。资源就是柜子上的编号001 柜、002 柜方法就是你对柜子执行的操作投放POST、查看有没有快递GET、拿走DELETE、把柜子里的东西换成别的PUT。你不需要在每个柜子上贴「投递操作柜 001」「查询操作柜 001」这种标签你只需要知道 001 号柜存在然后决定要对它做什么。接口设计也是同一个道理路径只表达「我是谁」方法负责表达「要干吗」。两者解耦之后整个接口体系的扩张变得非常有秩序。今天加一个订单资源那就建/orders五个方法一套客户端的代码结构也天然跟着资源走。1.3 说句实话并不是所有接口都适合 RESTful必须承认RESTful 不是银弹。像「订单结算并同时通知库存系统」这种复杂的业务动作强行拆成资源反而增加理解成本。早期我踩过的坑就是照本宣科把功能全拆成 REST 资源结果业务方看不懂、客户端也嫌绕。后来我学到的原则是CRUD 型的资源操作用 RESTful 非常舒服真正复杂的业务动作保留一个 POST 动作端点反而更清晰。比如POST /orders/123/confirm大家一看就懂。规范是为人服务的不是反过来。2. 写 RESTful 接口前先把这几个开发规范背下来这一节是「restful 接口开发规范」的浓缩版也是我实际评审代码时固定会检查的内容。2.1 URL 设计名词复数、层级、过滤参数URL 里不要出现动词用名词复数。用户资源是/users不是/user也不是/getUser。资源之间如果有从属关系用层级表达/users/123/orders表示用户 123 的订单列表。层级不要超过两层超过之后建议拆开否则 URL 会很难读。过滤、排序、分页这类「查询需求」不要塞进路径里用 Query 参数。比如GET /users?statusactivepage1page_size20sort-created_at。资源路径保持干净参数只表达查询条件客户端封装起来也方便。还有一点我特别想强调URL 里的id这类标识符生产环境多用 uuid 或 hashid别直接暴露数据库自增主键。真实主键暴露在 URL 里爬虫和恶意用户沿着id1、id2一路遍历很容易把整个表的数据全拖走。这个坑我见过不止一次。2.2 HTTP 方法的分工与幂等性常用方法有五个GET、POST、PUT、PATCH、DELETE。方法职责是否幂等GET查询不改变服务器状态是POST新增或触发不幂等的业务动作否PUT全量替换更新是PATCH局部更新只传需要改的字段是DELETE删除是幂等性这个概念值得单独展开。幂等的意思是「执行一次和执行多次的结果一致」。GET 查一百次结果一样PUT 把字段 a 改成 b改一百次还是 bDELETE 删一个不存在的资源服务端也返回成功。POST 不幂等——你点一次发布就会多一篇文章。所以涉及支付、下单这类关键业务时客户端的重复请求要谨慎后端要通过幂等键Idempotency-Key来兜底后面第 5 章会细说。2.3 状态码别乱用200 不能包打天下HTTP 状态码是 RESTful API 里最容易敷衍、也最影响排查效率的部分。很多团队无论什么错误都返回 200然后在响应体里塞一个code字段表示业务错误。坏处很明显监控系统无法通过状态码快速发现 5xx 风暴客户端的统一错误处理也没法做。我的建议是这张表打底场景状态码说明查询成功200响应体是结果数据创建成功201通常带 Location 头指向新资源删除成功204无响应体请求参数错误400响应体说明哪个字段错未认证401没登录、没 token 或 token 失效已登录但无权限403登录了但没权限操作资源不存在404注意不要泄露敏感信息数据冲突409比如用户名已存在服务器内部错误500必须配合日志和 request_id业务层面自定义的错误码可以放响应体里但 HTTP 状态码不能全用 200 糊弄。状态码是监控、告警、排查的地基地基歪了后面全歪。2.4 分页、筛选、排序先统一约定再写代码分页最常用的两种方案页码分页和游标分页。页码分页是page/page_size适合后台管理列表游标分页是cursor/limit适合移动端无限滚动因为新增数据不会导致上一页和下一页出现重复或漏数据。筛选参数避免发明新词。userName、username、user_name这三种写法都要禁止统一成user_name或小驼峰userName定好后写进代码模板。时间筛选用start_time/end_time排序用sortfield或sort-field负号表示倒序。这些约定一旦统一客户端 SDK 的封装几乎可以自动生成。3. 第一版能跑的 RESTful API用户增删改查实操前面都是规范这章直接上案例。目标很明确用一个用户资源的主流程把 GET、POST、PUT、DELETE 全部跑通。3.1 选型为什么用 Flask 做入门案例RESTful 入门案例用 Flask 最合适原因有三条框架轻写一个接口只要几行能把「路由 方法」这个核心逻辑看得清清楚楚周边生态全网上资料多。生产环境你当然可以换成 FastAPI、Spring Boot、Go 的 gin 或者 Node 的 Express但入门阶段 Flask 把噪音降到最低让你专注理解 RESTful 骨架本身。3.2 服务端代码最小可用版本一个最小可用的用户管理接口Python 3.8装一下依赖pip install flask flask-restful代码直接贴存储用内存 list 演示正式项目要接数据库from flask import Flask, request, jsonify from flask_restful import Api, Resource app Flask(__name__) api Api(app) # 内存存储仅用于演示 users [] next_id 1 class UserListResource(Resource): def get(self): # GET /users - 200 返回用户列表 return jsonify({code: 0, message: ok, data: users}) def post(self): # POST /users - 201 创建用户 global next_id data request.get_json(forceTrue) if not data or not data.get(name): return jsonify({code: 40001, message: name is required}), 400 user {id: next_id, name: data[name], email: data.get(email, )} users.append(user) next_id 1 return jsonify({code: 0, message: ok, data: user}), 201 class UserResource(Resource): def get(self, user_id): # GET /users/id - 200 返回单个用户 user next((u for u in users if u[id] user_id), None) if not user: return jsonify({code: 40401, message: user not found}), 404 return jsonify({code: 0, message: ok, data: user}) def put(self, user_id): # PUT /users/id - 200 全量更新 user next((u for u in users if u[id] user_id), None) if not user: return jsonify({code: 40401, message: user not found}), 404 data request.get_json(forceTrue) user[name] data.get(name, user[name]) user[email] data.get(email, user[email]) return jsonify({code: 0, message: ok, data: user}) def delete(self, user_id): # DELETE /users/id - 204 删除 global users before len(users) users [u for u in users if u[id] ! user_id] if len(users) before: return jsonify({code: 40401, message: user not found}), 404 return , 204 api.add_resource(UserListResource, /users) api.add_resource(UserResource, /users/int:user_id) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)这段代码的逻辑非常直白每个方法都对应一个 HTTP 方法URL 保持不变。GET /users和POST /users是两个完全不同的操作这在传统接口风格里必须靠两个不同的 URL 实现而 RESTful 用方法区分URL 就少了一半。3.3 用 curl 逐个验证请求服务跑起来之后开个新终端用 curl 验证。创建用户curl -X POST http://127.0.0.1:5000/users \ -H Content-Type: application/json \ -d {name: Alice, email: aliceexample.com}返回201响应体会带一个id。查询列表curl http://127.0.0.1:5000/users查询单个curl http://127.0.0.1:5000/users/1更新用户curl -X PUT http://127.0.0.1:5000/users/1 \ -H Content-Type: application/json \ -d {name: Alice2, email: alice2example.com}删除用户curl -X DELETE http://127.0.0.1:5000/users/1 -v注意删除返回204curl 默认不显示响应体你看到HTTP/1.1 204 No Content就对了。3.4 统一返回体与参数校验再强调一个我强烈推荐的做法所有接口的返回体统一格式。成功是{code:0,message:ok,data:...}失败时data放错误详情。虽然 HTTP 状态码已经表达了成功失败但统一返回体能让客户端解析逻辑更简洁尤其配合分页数据时data字段里再挂pagination对象一套解析模板走天下。参数校验放在路由的第一道关卡。手动 if 判断可以但更推荐用 marshmallow 或 pydantic 这类声明式校验。校验错误返回 400并在 message 里说明是哪个字段错了、期望什么格式。这个细节能省掉大量「接口报 500 但不知道入参错在哪」的后端排查时间。4. 客户端视角用 VC 访问服务端 RESTful API这一节写给桌面端和 Windows 工具链的开发者。热搜里频繁出现「vc 访问 http 的服务端 restful api 接口」确实是很多老 C 项目里常见的需求。4.1 三种常规武器WinHTTP、libcurl、C REST SDKWindows 平台做 C HTTP 客户端方案主要有三个先给结论方案优点缺点WinHTTP系统自带、零依赖、TLS 支持好风格偏 C代码略啰嗦libcurl跨平台、功能全Windows 下要管依赖和证书C REST SDK面向 RESTful、内置 JSON项目维护状态看团队接受度如果你只是要在 Windows 上做一个访问 RESTful API 的 VC 程序我第一选择是 WinHTTP因为发布时最省心不需要给客户额外装 DLL。4.2 用 WinHTTP 封装一个 GET 和一个 POST直接给一个能跑的片段。封装逻辑参考了很多老项目核心是用WinHttpOpen、WinHttpConnect、WinHttpOpenRequest、WinHttpSendRequest四件套。#include windows.h #include winhttp.h #include string #include vector #pragma comment(lib, winhttp.lib) struct HttpResponse { int status_code 0; std::string body; }; std::string Utf8FromWide(const std::wstring wstr) { if (wstr.empty()) return ; int size WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int)wstr.size(), nullptr, 0, nullptr, nullptr); std::string result(size, 0); WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int)wstr.size(), result[0], size, nullptr, nullptr); return result; } HttpResponse SendHttpRequest(const std::wstring method, const std::wstring host, const std::wstring path, const std::string json_body ) { HINTERNET hSession WinHttpOpen(LRESTClient/1.0, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); HINTERNET hConnect WinHttpConnect(hSession, host.c_str(), INTERNET_DEFAULT_HTTP_PORT, 0); HINTERNET hRequest WinHttpOpenRequest(hConnect, method.c_str(), path.c_str(), nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_REFRESH); std::vectorwchar_t headers LContent-Type: application/json\r\n; BOOL sent WinHttpSendRequest(hRequest, headers.c_str(), (DWORD)headers.size(), (LPVOID)json_body.c_str(), (DWORD)json_body.size(), (DWORD)json_body.size(), 0); if (!sent) { WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return {}; } BOOL received WinHttpReceiveResponse(hRequest, nullptr); if (!received) { WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return {}; } DWORD status 0, statusSize sizeof(status); WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, status, statusSize, WINHTTP_NO_HEADER_INDEX); std::string body; DWORD available 0, read 0; do { if (!WinHttpQueryDataAvailable(hRequest, available)) break; std::vectorchar buffer(available); if (!WinHttpReadData(hRequest, buffer.data(), available, read)) break; body.append(buffer.data(), read); } while (available 0); WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return { (int)status, body }; }调用就很简单了HttpResponse resp SendHttpRequest(LGET, L127.0.0.1, L/users); // resp.status_code 200 // resp.body 是 UTF-8 的 JSON 字符串 HttpResponse create SendHttpRequest(LPOST, L127.0.0.1, L/users, {\name\: \Bob\, \email\: \bobexample.com\}); // create.status_code 201这段代码处理了 HTTP 请求的基本链路但还缺 JSON 解析、超时和编码处理继续往下看。4.3 JSON 解析与编码GB2312/UTF-8 的坑VC 程序最大的坑是字符编码。RESTful API 返回的大多是 UTF-8 的 JSON但你在 Windows 控制台和 MFC 界面里拿到的是宽字符或者 GBK。我写过封装HTTP 响应拿到的是 UTF-8 字节流用 nlohmann/json 或 rapidjson 解析 JSON 没问题但要展示到界面上时需要先做转换用MultiByteToWideChar转成 UTF-16 的wstring再交给控件显示。反向发送时也容易错。中文用户名在std::string里直接拼接发出去很可能变成问号。正确做法是把宽字符用WideCharToMultiByte转成 UTF-8再放进 JSON 的字符串值里。代码里建议单独封装一个ToUtf8()和FromUtf8()所有边界都走这两个函数不要在业务代码里裸调 Windows API。另一个容易被忽略的细节Content-Type头里除了application/json最好显式带上charsetutf-8。虽然 JSON 规范默认是 UTF-8但不少老服务端解析逻辑不严谨没有 charset 就可能按 ISO-8859-1 去解中文直接乱码。4.4 超时、重试、TLS 这些工程问题用WinHttpSetTimeouts可以分别设置解析超时、连接超时、发送超时和接收超时。我第一次只配置了接收超时结果 DNS 解析卡住整个界面线程跟着假死。正确做法是给所有超时都设一个明确的值比如连接 5 秒、发送 30 秒、接收 60 秒。另一个重点是 HTTPS。自签名证书在测试环境会让请求直接失败调试期间可以用WinHttpSetOption加SECURITY_FLAG_IGNORE_UNKNOWN_CA临时跳过证书校验但生产环境千万不要这么干。证书校验是安全底线线上绕过证书校验等于把数据裸奔在公网上。重试策略上建议只对 GET 和幂等操作做自动重试POST 要谨慎。重试间隔用指数退避比如 1 秒、2 秒、4 秒每次加上随机抖动避免服务端故障时所有客户端同时重试压垮服务。我在一个内部工具里就遇到过这种场景服务端重启的那一刻几百个客户端同时补发请求直接把服务又打挂了。加了抖动之后才稳住。5. 真正上线前需要解决的四个问题到这里一个 RESTful API 从服务端到客户端的链路就通了。但真实项目上线前还有四个问题绕不开。5.1 接口鉴权Bearer Token 从何而来RESTful API 最常见的鉴权方式是Authorization请求头里放 Bearer Token。Token 通常由认证接口换取比如POST /auth/token拿到 Token 后客户端每次请求带上。服务端用中间件统一校验。Token 失效时间要短刷新机制要设计好不要为了省事把有效期设成一个月——泄露后风险很大。我在 C 客户端里踩过的一个典型问题Token 过期后WinHTTP 请求返回 401当时代码没有处理 401 重新去拉 Token 的逻辑导致用户第二天打开软件「白屏」。后来加了 401 拦截 自动刷新 重放请求的链路才算真正可用。这个链路看起来简单但把它做进客户端架构里比每次手动处理要省太多事。5.2 写操作要防重复提交幂等键的意义我见过最典型的线上事故用户点击支付按钮后网络卡顿客户端自动重试了一次结果在支付平台生成了两笔订单。RESTful 里 POST 天生不幂等所以需要在请求头或参数里带一个幂等键Idempotency-Key服务端用「用户 ID 幂等键 首次请求时间」做唯一约束重复请求直接返回第一次的执行结果而不是重新执行。设计成幂等键后客户端重试、用户手抖连点都不会再造成重复数据。这个机制在订单、支付、消息发送类接口里是标配越早设计越省心。5.3 服务端日志与客户端排查request_id 是个好东西接口联调最怕两边各查各的。强烈建议服务端给每个请求生成一个request_id响应里也带上。这样客户端报「你接口挂了」你把request_id甩给后端后端一条日志就能定位整个链路。我早期做接口时没有这个意识线上排查全靠人肉对时间戳一场事故能折腾半天。日志里除了 request_id我建议至少记这三个维度入参全文脱敏后、出参状态码和耗时、异常堆栈。用这套日志排查问题速度能快一个量级。5.4 接口文档人话比好看更重要不管用 OpenAPI/Swagger 还是 Apifox文档要写清楚三件事第一每个字段的含义、取值范围、是否必填第二错误码和触发场景第三示例请求和示例响应。好的接口文档不是写给你自己看的是让三个月后的你和客户端同学都能不问人就能完成对接。我见过很多团队把接口文档写成「只有自己看得懂的代码注释」字段含义全靠猜错误码只有 0 和 1。等到新人接手每个接口都要来问你一遍。文档写得好不好本质上决定了团队的沟通效率。最后说点个人体会RESTful API 不是什么高深理论它更像是团队之间的「普通话」。规范定得越早、越一致后面吃的亏就越少。我现在带人写接口最常强调一句话你们写的每一个 URL都是三个月后自己和别人要读的代码命名和语义的一致性远比实现某个炫技特性重要。先把这个原则内化再去记具体方法和状态码整件事就顺了。这篇文章给出的案例代码是基础版但链路是完整的从服务端 RESTful API 的设计规范到实战的增删改查再到 VC 客户端的接入与工程踩坑。希望对刚开始接触 RESTful 的你有一点帮助。如果你也在做 C 客户端对接 HTTP 服务欢迎在评论区聊聊你踩过的编码和超时的坑这些细节多交流一次后面的同行就能少走一次弯路。
返回列表