概述
POST 用于向服务器提交数据(设备上报、表单提交、登录鉴权),数据放在请求报文 body(请求里装数据的「包裹」)中,比 GET 更适合传大量、敏感或结构化数据。本教程演示:把 hello=wb2v1 通过 POST 提交到 httpbin.org/post,服务器会回显收到的数据。
用大白话讲:HTTP 像去餐厅点菜,POST 就是「填一张订菜单交上去」——把数据(比如传感器读数)连同「这张单子是什么格式、多长」(Content-Type/Content-Length)一起交给服务器,服务器收下后会把单子内容念给你听(回显)确认收到。和 GET「要资料」不同,POST 是「交资料」。
本教程基于安信可官方 SDK(Ai-Thinker-Open/Ai-Thinker-WB2,版本
release_bl_iot_sdk_1.6.40)。⚠️ 官方 SDK 未提供独立的 POST 示例工程。本教程以官方示例
applications/protocols/http_client_socket(GET 版)为基础改造,改动点仅请求报文(方法行与请求体),其余代码与官方一致,代码中已用注释标注。
官方没有 POST 专用工程,直接使用官方 http_client_socket 工程改造:
cd ~/Ai-Thinker-WB2/applications/protocols/http_client_socket
说明:
cd是「进入目录」的命令,进入官方示例工程;后面所有make命令都要在这个目录下执行。
💡 复制一份目录(如
http_client_post)再修改,保留官方工程原样。
打开 http_client_socket/main.c,修改开头的 SSID/密码(官方默认是 AIOT@FAE,需改成自己的路由器)。
打开 http_client_socket/demo.c,修改目标服务器为支持 POST 回显的站点:
#define WEB_SERVER "httpbin.org"
#define WEB_PORT "80"
#define WEB_PATH "/post"
💡
httpbin.org/post是免费的 POST 测试接口,收到数据会以 JSON 回显。也可换成自己的服务器(局域网内电脑跑 Python/Node 服务即可)。
在官方 demo.c 基础上改造请求报文为 POST,本步完整代码已移至文末,见:
📜 完整代码 — 位于本页「完整代码」章节,默认折叠,点击展开,与官方示例(
applications/protocols/http_client_socket/http_client_socket/demo.c)一致,仅请求报文按本页改造为 POST。
代码要点(与官方 GET 版的差异):
| 改动点 | 说明 |
|---|---|
POST /post HTTP/1.0 |
方法行 GET → POST,路径指向服务器提交接口 |
Content-Type: application/x-www-form-urlencoded |
告诉服务器 body 是什么格式(表单 key=value&k2=v2),不说清服务器解析不了 |
Content-Length: 11 |
必须与实际 body 字节数一致,服务器靠它判定 body 边界,错了会挂起 |
hello=wb2v1(11 字节) |
请求体(真正要提交的数据),字节数要和 Content-Length 对得上 |
| 其余代码 | 与官方 http_client_socket 完全一致(DNS/socket/连接/收发/超时) |
💡 GET vs POST:GET 把参数拼在 URL 后面(
?a=1&b=2),无请求体;POST 把数据放在请求体中,并必须正确填写Content-Length。开发板write()一次发送整个报文(请求行 + 头 + body),服务器按行解析。
在工程目录执行编译:
make -j8
说明:
make是「编译」命令,把代码变成开发板能运行的固件(烧进开发板的程序);-j8表示用 8 个 CPU 核并行编译,更快。
编译成功后生成固件 build_out/http_client_socket.bin。
开发板保持 USB 连接,确认串口设备号后执行烧录:
make flash p=/dev/ttyUSB0 b=921600
说明:
make flash是「烧录」命令,把编译好的固件下载进开发板;p=后面是串口设备号(Linux 下常为/dev/ttyUSB0,Windows 下是COM3之类,以你电脑实际为准),b=921600是烧录波特率(串口传数据的速度),保持默认即可。
⏳ 烧录过程中按提示长按开发板 EN 键进入下载模式,等待进度条完成即烧录成功。
烧录完成后开发板自动重启运行,串口(波特率 921600,串口传数据的「语速」,两边必须一致)打印连接日志后,输出 httpbin.org 的 JSON 回显:
DNS lookup succeeded. IP=34.224.xxx.xxx
... connected
... socket send success
HTTP/1.1 200 OK
Content-Type: application/json
...
{
"args": {},
"data": "hello=wb2v1",
"form": {
"hello": "wb2v1"
},
...
}
... done reading from socket. Last read return=0 errno=0
出现 HTTP/1.1 200 OK 和 "data": "hello=wb2v1" 即 POST 成功——这表示服务器完整收到了请求体。如果只看到 GOT IP 而没出现回显,先确认开发板已联网(GOT IP)、电脑浏览器能打开 httpbin.org(目标服务器可达),再排查报文问题,见文末 FAQ。
💡 局域网自测:电脑 Python 起一个简单服务(
python3 -m http.server只支持 GET,可改用flask或 Node 写 POST 接口),把WEB_SERVER改为电脑 IP、WEB_PATH改为接口路径,即可用自己的服务器验证。
代码执行流程
例程从启动到运行的完整流程如下(图中的循环箭头表示反复执行):
本文 API 汇总
getaddrinfo(name, port, &hints, &res)
域名解析为 IP 地址链表。
参数:
name:域名或 IP 字符串,如"httpbin.org"port:端口字符串,如"80"hints:查询条件结构体(限定 IPv4 + TCP)res:输出参数,解析结果链表指针
返回值:成功返回 0;失败返回负值错误码
socket(af, type, proto)
创建 TCP socket。
参数:
af:AF_INET(IPv4)type:SOCK_STREAM(流式 TCP)proto:传0
返回值:成功返回 socket 描述符;失败返回 -1
connect(fd, addr, addrlen)
连接服务器。
参数:
fd:socket 描述符addr:服务器地址(struct sockaddr*)addrlen:地址长度
返回值:成功返回 0;失败返回负值错误码
write(fd, buf, len)
发送整个 HTTP 报文(含请求体)。
参数:
fd:socket 描述符buf:请求报文缓冲区(请求行 + 头 + 空行 + body)len:报文总长度
返回值:成功返回发送字节数;失败返回负值
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, len)
设置接收超时。
参数:
fd:socket 描述符level:SOL_SOCKEToptname:SO_RCVTIMEOtv:struct timeval指针len:sizeof(struct timeval)
返回值:成功返回 0;失败返回负值错误码
📌 请求体字段含义:
Content-Type声明数据格式(表单application/x-www-form-urlencoded、JSONapplication/json等);Content-Length声明 body 字节数——数值错误会导致服务器解析失败或请求挂起,修改 body 后务必同步更新。
完整代码
以下为 demo.c 完整源码,与官方示例(applications/protocols/http_client_socket/http_client_socket/demo.c)一致,仅请求报文按本页改造为 POST:
📜 点击展开 demo.c 完整代码
#include <stdio.h>
#include <FreeRTOS.h>
#include <task.h>
#include <lwip/sockets.h>
#include <lwip/netdb.h>
#include <lwip/tcp.h>
#include <lwip/err.h>
#include <http_client.h>
#include <cli.h>
#include "demo.h"
#include <blog.h>
#define WEB_SERVER "httpbin.org"
#define WEB_PORT "80"
#define WEB_PATH "/post"
/* ★ 改造点 ①:方法 GET → POST,增加 Content-Type / Content-Length 与请求体 */
static const char *REQUEST = "POST " WEB_PATH " HTTP/1.0\r\n"
"Host: " WEB_SERVER ":" WEB_PORT "\r\n"
"User-Agent: aithinker wb2\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: 11\r\n"
"\r\n"
"hello=wb2v1"; /* ★ 改造点 ②:请求体,11 字节 */
void http_get_task(void *pvParameters)
{
const struct addrinfo hints = {
.ai_family = AF_INET,
.ai_socktype = SOCK_STREAM,
};
struct addrinfo *res;
struct in_addr *addr;
int s, r;
char recv_buf[4096];
while (1)
{
int err = getaddrinfo(WEB_SERVER, "80", &hints, &res);
if (err != 0 || res == NULL)
{
blog_error("DNS lookup failed err=%d res=%p", err, res);
vTaskDelay(1000 / portTICK_PERIOD_MS);
continue;
}
addr = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
blog_info("DNS lookup succeeded. IP=%s", inet_ntoa(*addr));
s = socket(res->ai_family, res->ai_socktype, 0);
if (s < 0)
{
blog_error("... Failed to allocate socket.");
freeaddrinfo(res);
vTaskDelay(1000 / portTICK_PERIOD_MS);
continue;
}
blog_info("... allocated socket");
if (connect(s, res->ai_addr, res->ai_addrlen) != 0)
{
blog_error("... socket connect failed errno=%d", errno);
close(s);
freeaddrinfo(res);
vTaskDelay(4000 / portTICK_PERIOD_MS);
continue;
}
blog_info("... connected");
freeaddrinfo(res);
if (write(s, REQUEST, strlen(REQUEST)) < 0)
{
blog_error("... socket send failed");
close(s);
vTaskDelay(4000 / portTICK_PERIOD_MS);
continue;
}
blog_info("... socket send success");
struct timeval receiving_timeout;
receiving_timeout.tv_sec = 5;
receiving_timeout.tv_usec = 0;
if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &receiving_timeout,
sizeof(receiving_timeout)) < 0)
{
blog_error("... failed to set socket receiving timeout");
close(s);
vTaskDelay(4000 / portTICK_PERIOD_MS);
continue;
}
blog_info("... set socket receiving timeout success");
// FIXME fix putchar
extern int bl_putchar(int c);
/* Read HTTP response */
do
{
bzero(recv_buf, sizeof(recv_buf));
r = read(s, recv_buf, sizeof(recv_buf) - 1);
for (int i = 0; i < r; i++)
{
bl_putchar(recv_buf[i]);
}
} while (r > 0);
blog_info("... done reading from socket. Last read return=%d errno=%d\r\n", r, errno);
close(s);
for (int countdown = 10; countdown >= 0; countdown--)
{
blog_info("%d... ", countdown);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
blog_info("Starting again!");
}
}常见问题与踩坑提示
⚠️ 服务器返回 405 Method Not Allowed
原因:目标路径不支持 POST 方法(如静态网站根路径只允许 GET)
解决:改用支持 POST 的接口(httpbin.org/post、httpbin.org/anything)或自己的服务器
⚠️ 服务器收到空 body / 请求挂起
原因:Content-Length 与实际 body 字节数不一致(中文按 UTF-8 计 3 字节,空格也算)
解决:修改 body 后逐个字节核对(可用 strlen("hello=wb2v1") 验证),确保与 Content-Length 一致
⚠️ 提交中文乱码
原因:中文未经 URL 编码,且字节数与 Content-Length 对不上
解决:中文先做 URL 编码(如 %E4%BD%A0%E5%A5%BD),再以编码后的字节数填 Content-Length
⚠️ 串口找不到设备 / 打不开
原因:USB 转串口驱动未装、权限不足,或数据线只能充电不能传数据
解决:Linux 用 lsusb/dmesg 查看设备,权限不足可 sudo chmod 666 /dev/ttyUSB0;Windows 装驱动后到设备管理器查 COM 口;换一根能传数据的线
⚠️ 烧录一直等待 / 失败
原因:未进入下载模式、波特率不对、或串口号填错
解决:烧录时按提示长按 EN 键进入下载模式;确认 p=/dev/ttyUSB0 换成你实际的串口;换 USB 口或数据线重试
运行自检
串口输出的 JSON 回显中出现 "data": "hello=wb2v1",即 POST 提交验证通过。
遇到问题?
如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

