Skip to content

概述

PUT 用于整体更新服务器上的指定资源(RESTful 风格),与 POST 的区别在于幂等性(同一个操作重复做多少遍,结果都一样):对同一资源重复执行 PUT,结果一致(都是替换成相同内容);而 POST 每次都会新增一条记录。设备固件版本更新、配置下发常用 PUT。本教程演示:把 firmware=1.2.3 通过 PUT 提交到 httpbin.org/put,服务器回显收到的数据。

用大白话讲:PUT 就是把服务器上「旧版本的资料」整份换掉——像把你家墙上贴的旧海报整张撕下来换成新海报(整体替换,不是追加)。它和 POST「新增一条记录」不同:对同一个地址反复 PUT,结果都一样(幂等),所以特别适合「把固件版本改成 1.2.3」这种更新场景。

本教程基于安信可官方 SDKAi-Thinker-Open/Ai-Thinker-WB2,版本 release_bl_iot_sdk_1.6.40)。

⚠️ 官方 SDK 未提供独立的 PUT 示例工程。本教程以官方示例 applications/protocols/http_client_socket(GET 版)为基础改造,改动点仅请求报文(方法行与请求体),其余代码与官方一致,代码中已用注释标注。

🎯本页目标在官方 GET 示例基础上构造 PUT 请求报文并验证服务器回显,掌握幂等更新语义与报文构造。
🧰前置条件① Ai-WB2 开发板一块(Type-C 数据线)② 2.4GHz 路由器(能访问公网)③ 已按 [SDK 安装](../sdk/sdk_intro) 完成环境搭建,并完成 [连接 Wi-Fi](./wifi_connect) 与 [HTTP GET](./http_get)。
🔗相关章节提交新增数据见 [HTTP POST](./http_post);删除资源见 [HTTP DELETE](./http_delete)。

进入示例工程

官方没有 PUT 专用工程,直接使用官方 http_client_socket 工程改造:

cd ~/Ai-Thinker-WB2/applications/protocols/http_client_socket

说明:cd 是「进入目录」的命令,进入官方示例工程;后面所有 make 命令都要在这个目录下执行。

💡 复制一份目录(如 http_client_put)再修改,保留官方工程原样。

修改路由器参数与目标地址

打开 http_client_socket/main.c,修改开头的 SSID/密码(官方默认是 AIOT@FAE,需改成自己的路由器)。

打开 http_client_socket/demo.c,修改目标服务器:

#define WEB_SERVER "httpbin.org"
#define WEB_PORT "80"
#define WEB_PATH "/put"

💡 httpbin.org/put 是免费的 PUT 测试接口,收到数据会以 JSON 回显。也可换成自己的服务器。

编写代码(改造点:请求报文)

在官方 demo.c 基础上改造请求报文为 PUT,本步完整代码已移至文末,见:

📜 完整代码 — 位于本页「完整代码」章节,默认折叠,点击展开,与官方示例(applications/protocols/http_client_socket/http_client_socket/demo.c)一致,仅请求报文按本页改造为 PUT。

代码要点(与官方 GET 版的差异):

改动点 说明
PUT /put HTTP/1.0 方法行 GETPUT,路径指向资源更新接口
Content-Type: application/x-www-form-urlencoded 告诉服务器 body 是什么格式(表单 key=value),不说清服务器解析不了
Content-Length: 16 必须与实际 body 字节数一致firmware=1.2.3 为 16 字节),错了会挂起
firmware=1.2.3 请求体(要写入资源的新内容),字节数要和 Content-Length 对得上
其余代码 与官方 http_client_socket 完全一致

💡 PUT vs POST:PUT 是幂等的——对同一 URL 反复 PUT 结果相同("设置"语义,如更新设备固件版本);POST 非幂等——每次执行都会产生新效果("新增"语义,如上报一条新日志)。RESTful 接口中:POST /devices 创建设备、PUT /devices/{id} 更新设备。

编译工程

在工程目录执行编译:

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": "firmware=1.2.3",
  "form": {
    "firmware": "1.2.3"
  },
  ...
}
... done reading from socket. Last read return=0 errno=0

出现 HTTP/1.1 200 OK"data": "firmware=1.2.3" 即 PUT 成功——这表示服务器完整收到了数据。如果只看到 GOT IP 而没出现回显,先确认开发板已联网(GOT IP)、电脑浏览器能打开 httpbin.org(目标服务器可达),再排查报文问题,见文末 FAQ。

💡 注意 httpbin.org/put 返回的 form 字段中能看到解析后的键值对;PUT 与 POST 在报文层面几乎一样,区别完全在方法行语义——服务器据此决定"更新"还是"新增"。

代码执行流程

例程从启动到运行的完整流程如下(图中的循环箭头表示反复执行):


本文 API 汇总

getaddrinfo(name, port, &hints, &res)

域名解析为 IP 地址链表。

参数

  • name:域名或 IP 字符串,如 "httpbin.org"
  • port:端口字符串,如 "80"
  • hints:查询条件结构体(限定 IPv4 + TCP)
  • res:输出参数,解析结果链表指针

返回值:成功返回 0;失败返回负值错误码

freeaddrinfo(res)

释放解析结果。

参数

  • resgetaddrinfo 返回的结果链表

返回值:无

socket(af, type, proto)

创建 TCP socket。

参数

  • afAF_INET(IPv4)
  • typeSOCK_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:报文总长度

返回值:成功返回发送字节数;失败返回负值

read(fd, buf, len)

读取服务器响应。

参数

  • fd:socket 描述符
  • buf:接收缓冲区
  • len:缓冲区长度

返回值:返回读取字节数;0 = 对端关闭;负值 = 出错

close(fd)

关闭连接。

参数

  • fd:socket 描述符

返回值:成功返回 0;失败返回 -1

setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, len)

设置接收超时。

参数

  • fd:socket 描述符
  • levelSOL_SOCKET
  • optnameSO_RCVTIMEO
  • tvstruct timeval 指针
  • lensizeof(struct timeval)

返回值:成功返回 0;失败返回负值错误码

inet_ntoa(addr)

IP 地址转字符串。

参数

  • addr:网络字节序 IP

返回值:点分十进制字符串

bl_putchar(c)

逐字符输出到串口。

参数

  • c:要输出的字符

返回值:无

📌 PUT 报文体格式与 POST 完全一致(Content-Type + Content-Length + body),仅方法行不同。修改 body 后务必同步更新 Content-Length


完整代码

以下为 demo.c 完整源码,与官方示例(applications/protocols/http_client_socket/http_client_socket/demo.c)一致,仅请求报文按本页改造为 PUT:

📜 点击展开 demo.c 完整代码
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 "/put"

/* ★ 改造点 ①:方法 GET → PUT,增加 Content-Type / Content-Length 与请求体 */
static const char *REQUEST = "PUT " 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: 16\r\n"
                         "\r\n"
                         "firmware=1.2.3";  /* ★ 改造点 ②:请求体,16 字节 */

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
原因:目标路径不支持 PUT 方法
解决:改用支持 PUT 的接口(httpbin.org/puthttpbin.org/anything)或自己的服务器

⚠️ 服务器收到空 body / 请求挂起
原因Content-Length 与实际 body 字节数不一致
解决:修改 body 后逐个字节核对(firmware=1.2.3 为 16 字节),确保与 Content-Length 一致

⚠️ PUT 与 POST 分不清
原因:报文层面两者几乎相同,仅方法行与语义不同
解决:新增/上报用 POST(非幂等),更新/替换用 PUT(幂等);接口文档会明确标注各方法支持情况

⚠️ 其余问题(连不上路由器/DNS 失败/超时/栈溢出)
说明:与 HTTP GET 完全相同
解决:参照 HTTP GET 的踩坑提示处理(含连不上路由器、串口找不到、烧录失败等)

⚠️ 串口找不到设备 / 打不开
原因:USB 转串口驱动未装、权限不足,或数据线只能充电不能传数据
解决:Linux 用 lsusb/dmesg 查看设备,权限不足可 sudo chmod 666 /dev/ttyUSB0;Windows 装驱动后到设备管理器查 COM 口;换一根能传数据的线

⚠️ 烧录一直等待 / 失败
原因:未进入下载模式、波特率不对、或串口号填错
解决:烧录时按提示长按 EN 键进入下载模式;确认 p=/dev/ttyUSB0 换成你实际的串口;换 USB 口或数据线重试

运行自检

串口输出的 JSON 回显中出现 "data": "firmware=1.2.3",即 PUT 提交验证通过。

遇到问题?

如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

Released under the MIT License. Build Time 2026-09-11 14:52:23