Skip to content

概述

HTTP 是物联网设备与服务器通信最常用的协议,GET 用于从服务器获取资源(网页、天气数据、固件版本号等)。开发板通过 socket(网络通信的「插座」,程序收发数据的通道)直接发送 HTTP 报文即可完成一次 GET 请求:域名解析 → TCP 连接 → 发送请求 → 读取响应。本教程演示:访问 example.com 根路径,把服务器返回的网页内容逐字符打印到串口,然后倒计时 10 秒重新请求。

用大白话讲:HTTP 就像去餐厅点菜——你(客户端)说「我要什么」(请求),服务员(服务器)把菜端上来(响应)。GET 就是「取」:跟服务器要一份资料(网页/数据),服务器把内容发回来。整个过程是:先把 example.com 这个「店名」翻译成「门牌号」(DNS 解析),拨通电话(TCP 连接),再说「给我菜单」(发送请求),最后收菜(读取响应)。

本教程基于安信可官方 SDKAi-Thinker-Open/Ai-Thinker-WB2,版本 release_bl_iot_sdk_1.6.40)的官方示例 applications/protocols/http_client_socket 编写,代码可在本地 SDK 中直接找到。

🎯本页目标通过原始 socket 完成一次 HTTP GET 请求,掌握 DNS 解析、请求报文构造、响应读取全流程。
🧰前置条件① Ai-WB2 开发板一块(Type-C 数据线)② 2.4GHz 路由器(能访问公网)③ 已按 [SDK 安装](../sdk/sdk_intro) 完成环境搭建,并完成 [连接 Wi-Fi](./wifi_connect)。
🔗相关章节TCP 基础见 [TCP 客户端](./tcp_client);向服务器提交数据见 [HTTP POST](./http_post)。

进入示例工程

打开终端,进入官方 http_client_socket 示例工程目录:

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

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

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

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

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

#define WEB_SERVER "example.com"
#define WEB_PORT "80"
#define WEB_PATH "/"
默认值 说明
WEB_SERVER example.com 服务器域名(如 example.com 这种好记的名字,代码会把它解析成 IP)
WEB_PORT 80 HTTP 端口(80 默认端口)
WEB_PATH / 请求路径(如 /api/weather

💡 也可换成任意 HTTP 站点(如 httpbin.orgapi.ipify.org)或局域网服务器(电脑跑一个 HTTP 服务)。注意仅支持 http://,https 站点请见官方 https_mbedtls 示例

编写代码

打开 demo.c,本步完整代码已移至文末,见:

📜 完整代码 — 位于本页「完整代码」章节,默认折叠,点击展开,与官方示例(applications/protocols/http_client_socket/http_client_socket/demo.c)完全一致。

代码要点:

代码 作用
REQUEST 静态字符串 拼好要发给服务器的「点菜单」(请求行+头+空行),格式错了服务器不认
getaddrinfo(WEB_SERVER, "80", &hints, &res) 把域名翻译成 IP,翻译失败就找不到服务器,只能重试
socket(res->ai_family, res->ai_socktype, 0) 创建「电话线」(socket),没有它没法通信
connect(s, res->ai_addr, res->ai_addrlen) 拨通服务器的 80 端口,不通说明地址或网络有问题
write(s, REQUEST, strlen(REQUEST)) 把「菜单」递给服务器,这一步才是真正发出请求
setsockopt(SO_RCVTIMEO, 5 秒) 设 5 秒超时,防止服务器没响应时程序永远卡住
read(s, recv_buf, ...) 循环 循环读响应直到读不到(服务器关闭连接),内容才完整
bl_putchar(recv_buf[i]) 把收到的内容一个字一个字打到串口,方便人眼查看
失败分支 continue + 倒计时 失败自动重试;成功等 10 秒再来一轮,模拟周期上报

💡 HTTP/1.0 请求报文由三部分组成:请求行 GET / HTTP/1.0(方法 + 路径 + 版本)、头部(Host 必填、User-Agent 可选)、空行结尾。服务器返回状态行(如 HTTP/1.1 200 OK)+ 响应头 + 响应体(网页 HTML)。

编译工程

在工程目录执行编译:

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,串口传数据的「语速」,两边必须一致)打印:

[APP] [EVT] GOT IP 5594
DNS lookup succeeded. IP=93.184.216.34
... allocated socket
... connected
... socket send success
... set socket receiving timeout success
<!doctype html>
<html>
<head>...(example.com 返回的完整网页 HTML,逐字符打印)
</html>
... done reading from socket. Last read return=0 errno=0
10... 9... 8... ...
0...
Starting again!

看到 DNS lookup succeededconnected 和网页 HTML 即成功;如果只看到 GOT IP 而没有 DNS lookup succeeded,说明域名解析失败(开发板没联网或路由器上不了公网),见文末 FAQ。验证前先确认开发板已 GOT IP(联网),且电脑浏览器能打开 http://example.com(目标服务器可达)。程序会循环执行(10 秒一轮)。

💡 打开串口助手的自动换行/时间戳更易观察;网页内容较大时耐心等待打印完成。

代码执行流程

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


本文 API 汇总

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

域名解析为 IP 地址链表(struct addrinfo)。

参数

  • name:域名或 IP 字符串,如 "example.com"
  • port:端口字符串,如 "80"
  • hints:查询条件结构体(限定只返回 IPv4 + TCP,避免解析出 IPv6/其他类型)
  • res:输出参数,解析结果链表指针

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

freeaddrinfo(res)

释放 getaddrinfo 的结果。

参数

  • resgetaddrinfo 返回的结果链表

返回值:无

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*,指向 sockaddr_in
  • addrlen:地址长度 sizeof(struct sockaddr_in)

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

write(fd, buf, len)

发送请求报文。

参数

  • fd:socket 描述符
  • buf:请求报文缓冲区
  • len:报文长度

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

read(fd, buf, len)

读取服务器响应。

参数

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

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

close(fd)

关闭连接。

参数

  • fd:socket 描述符

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

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

设置接收超时(struct timeval:秒 + 微秒)。

参数

  • fd:socket 描述符
  • level:选项层,SOL_SOCKET
  • optname:选项名,SO_RCVTIMEO(接收超时)
  • tvstruct timeval 指针,如 {5, 0} 表示 5 秒
  • lensizeof(struct timeval)

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

inet_ntoa(addr)

IP 地址转点分十进制字符串(非线程安全)。

参数

  • addr:网络字节序的 IP 地址(in_addr

返回值:点分十进制字符串,如 "93.184.216.34"

bzero(buf, len)

清零缓冲区(同 memset(buf, 0, len))。

参数

  • buf:目标缓冲区
  • len:清零长度

返回值:无

bl_putchar(c)

逐字符输出到串口(demo.c 中 extern 声明,SDK 平台层实现)。

参数

  • c:要输出的字符

返回值:无

📌 struct addrinfo 关键字段:ai_family(AF_INET)、ai_socktype(SOCK_STREAM)、ai_addr(服务器地址)、ai_addrlen(地址长度)。hints 限定只返回 IPv4 + TCP,避免同时解析出 IPv6/其他类型。


完整代码

以下为 demo.c 完整源码,与官方示例(applications/protocols/http_client_socket/http_client_socket/demo.c)完全一致:

📜 点击展开 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 "example.com"
#define WEB_PORT "80"
#define WEB_PATH "/"

static const char *REQUEST = "GET " WEB_PATH " HTTP/1.0\r\n"
                             "Host: " WEB_SERVER ":" WEB_PORT "\r\n"
                             "User-Agent: aithinker wb2\r\n"
                             "\r\n";

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;
        }

        /* Code to print the resolved IP.
           Note: inet_ntoa is non-reentrant, look at ipaddr_ntoa_r for "real" code */
        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!");
    }
}

常见问题与踩坑提示

⚠️ 一直打印 DNS lookup failed
原因:路由器未联网、DNS 服务器不可达、或域名拼写错误
解决:确认开发板能上公网(其他示例可 GOT IP 并联网);检查 WEB_SERVER 域名正确;重启路由器

⚠️ connect failed 后反复重试
原因:服务器端口错误(非 80)、目标不可达、或路由器防火墙拦截
解决:确认 WEB_PORT "80" 与服务器一致;局域网服务器先电脑自测能否访问;注意仅支持 http://

⚠️ 打印网页到一半停止
原因:5 秒接收超时到期(服务器响应慢或大页面)
解决:调大 receiving_timeout.tv_sec(如 10);或改用响应更快的站点

⚠️ 修改代码后栈溢出死机
原因recv_buf[4096]任务栈上,任务栈必须足够大
解决:保持官方任务栈 16384xTaskCreate(&http_get_task, "http_get_task", 16384, NULL, 5, NULL)),勿缩小

⚠️ 访问 https:// 站点失败
原因:本示例是明文 HTTP,HTTPS 需要 TLS 加密握手
解决:用 http:// 站点;HTTPS 请参考官方 https_mbedtls 示例(后续章节 MQTTS 也涉及 TLS)

⚠️ 一直打印 Connecting 不 GOT IP(连不上路由器)
原因:SSID/密码写错、路由器是 5GHz 频段、或信号过弱
解决:核对 main.c 里的 SSID/密码;确认路由器开 2.4GHz;开发板靠近路由器;可先单独跑 连接 Wi-Fi 验证联网

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

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

运行自检

串口出现 DNS lookup succeededconnected → 网页 HTML 全文打印,即 HTTP GET 验证通过。

遇到问题?

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

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