Skip to content

概述

TCP 是面向连接的可靠传输协议,广泛用于设备上报数据、接收服务器指令。Ai-WB2 通过 lwIP socket(lwIP 是专为嵌入式设备写的轻量级网络库,socket 是它提供的网络编程接口)实现 TCP,官方封装了 tcp_client_init/connect/send/receive 等易用 API。本教程演示:连接远程 TCP 服务器(官方演示服务器 tt.ai-thinker.com:7878),发送数据并循环接收回包。

用大白话讲:TCP 就像打电话——先拨号(connect),等对方接通(三次握手),再说话(收发数据),最后挂断(断开连接)。它可靠:话说没说清、对方听没听清,两边都会确认,丢字了会自动重说,所以设备上报、指令下发这类重要数据都用它。代价是要先「拨通」才能传,且一条电话线只能给一个人打。

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

🎯本页目标通过 TCP 客户端连接远程服务器并收发数据,掌握封装 API 的初始化、连接、发送、接收与资源释放全流程。
🧰前置条件① Ai-WB2 开发板一块(Type-C 数据线)② 2.4GHz 路由器 ③ 电脑一台(可开 TCP 测试工具或网络调试助手)④ 已按 [SDK 安装](../sdk/sdk_intro) 完成环境搭建,并完成 [连接 Wi-Fi](./wifi_connect)。
🔗相关章节服务端角色见 [TCP 服务器](./tcp_server);UDP 不可靠传输见 [UDP 客户端](./udp_client)。

进入示例工程

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

cd ~/Ai-Thinker-WB2/applications/protocols/socket/tcp_client

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

修改服务器参数

打开 tcp_client/main.c,修改开头的 SSID/密码(同 连接 Wi-Fi),并按需修改服务器地址:

#define ROUTER_SSID "your ssid"
#define ROUTER_PWD "your password"
//This is Ai-Thinker Remote TCP Server: http://tt.ai-thinker.com:8000/ttcloud
#define TCP_SERVER_IP "122.114.122.174"
#define TCP_SERVER_PORT 7878
默认值 说明
TCP_SERVER_IP 122.114.122.174 安信可远程 TCP 服务器;连它不需要自己搭服务器
TCP_SERVER_PORT 7878 服务器端口(服务器上的「房门号」),找对门才能连上

💡 无公网服务器时可用电脑局域网调试:电脑运行网络调试助手监听(如 192.168.x.x:8888),把 TCP_SERVER_IP/PORT 改成电脑 IP 与端口即可。

编写代码

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

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

代码要点:

代码 作用
tcp_client_init(ip, port) 创建 TCP socket 并初始化(tcp_example.h 封装);不创建就没法打电话
tcp_client_connect(socketfd) 连接服务器(返回 0 成功);相当于拨号,占线就失败
tcp_client_send(socketfd, "hell tcp server") 发送数据到服务器;打电话时的「说话」
tcp_client_receive(socketfd, buf) 接收服务器数据(阻塞等待,返回字节数);阻塞 = 没消息时就一直等
strstr(tcp_buff, "close") 收到 close 命令时断开连接;服务器说挂断就挂断
tcp_client_deinit(socketfd) 关闭 socket 并释放资源;不释放会一直占着内存
CODE_WIFI_ON_GOT_IP 回调 联网成功后创建 TCP 客户端任务;没拿到 IP 就打电话也打不通

💡 封装层 API 实现在工程内 src/tcp_example.c,底层即标准 lwIP socket:socket() / connect() / send() / recv() / close()

编译工程

在工程目录执行编译:

make -j8

说明:make 是「编译」命令,把代码变成开发板能运行的固件(烧进开发板的程序);-j8 表示用 8 个 CPU 核并行编译,更快。

编译成功后生成固件 build_out/tcp_client.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 5
tcp client task run
tcp_client_task:tcp client connect OK
tcp client send OK

连接官方服务器后,服务器会回发数据,串口打印接收内容:

tcp_client_task:tcp receive data:hello, WB2 !

出现 tcp client connect OKtcp client send OK 并能收到服务器回发的数据(tcp receive data:...)即成功。如果只看到 GOT IP 而没有 connect OK,先确认开发板已联网、目标服务器 IP/端口可达(可在电脑上先试试能不能连上这个 IP 和端口),见文末 FAQ。

💡 局域网自测:电脑开网络调试助手监听端口,手机热点或路由器同一局域网;修改 TCP_SERVER_IP/TCP_SERVER_PORT 为电脑 IP 与端口,烧录后助手收到 hell tcp server,向开发板发任意字符串,串口打印收到的内容;发送 close 可断开连接(串口停止收发)。

代码执行流程

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


本文 API 汇总

tcp_client_init(server_ip, port)

创建 TCP socket 并初始化(工程封装)。

参数

  • server_ip:服务器 IP 字符串
  • port:服务器端口

返回值:成功返回 socket 描述符;失败返回 -1

tcp_client_connect(socket_fd)

连接 TCP 服务器(工程封装)。

参数

  • socket_fdtcp_client_init 返回的 socket 描述符

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

tcp_client_send(socket_fd, data)

发送数据(工程封装)。

参数

  • socket_fd:socket 描述符
  • data:待发送数据字符串

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

tcp_client_receive(socket_fd, data)

接收数据(阻塞,工程封装)。

参数

  • socket_fd:socket 描述符
  • data:接收缓冲区

返回值:成功返回接收字节数;失败返回负值

tcp_client_deinit(socket_fd)

关闭 socket 释放资源(工程封装)。

参数

  • socket_fd:socket 描述符

返回值:无

socket(domain, type, proto)

创建 socket(封装层底层)。

参数

  • domainAF_INET(IPv4)
  • typeSOCK_STREAM(流式 TCP)
  • proto:传 0

返回值:成功返回 socket 描述符;失败返回 -1

connect(fd, addr, len)

连接服务器(封装层底层)。

参数

  • fd:socket 描述符
  • addr:服务器地址(struct sockaddr*
  • len:地址长度

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

send / recv(fd, buf, len, flags)

发送 / 接收数据(封装层底层)。

参数

  • fd:socket 描述符
  • buf:数据缓冲区
  • len:缓冲区长度
  • flags:标志,传 0

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

pvPortMalloc / vPortFree(size / ptr)

FreeRTOS 动态内存分配 / 释放。

参数

  • size:分配字节数
  • ptr:待释放指针(pvPortMalloc 返回值)

返回值pvPortMalloc:成功返回指针,失败返回 NULLvPortFree:无

📌 服务端地址为 IP 时直接传字符串;域名服务器需先用 DNS 解析为 IP(见官方 dns 示例)。


完整代码

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

📜 点击展开 tcp_client/main.c 完整代码
c
#include <FreeRTOS.h>
#include <task.h>
#include <stdio.h>
#include <string.h>
#include <blog.h>
#include <aos/yloop.h>
#include <aos/kernel.h>
#include <lwip/sockets.h>
#include <lwip/tcpip.h>
#include <wifi_mgmr_ext.h>
#include <cli.h>
#include <hal_wifi.h>
#include <lwip/init.h>
#include "tcp_example.h"

#define ROUTER_SSID "your ssid"
#define ROUTER_PWD "your password"
//This is Ai-Thinker Remote TCP Server: http://tt.ai-thinker.com:8000/ttcloud
#define TCP_SERVER_IP "122.114.122.174"
#define TCP_SERVER_PORT 7878

static wifi_conf_t conf = {
    .country_code = "CN",
};

/**
 * @brief wifi_sta_connect
 *        wifi station mode connect start
 * @param ssid
 * @param password
 */
static void wifi_sta_connect(char* ssid, char* password)
{
    wifi_interface_t wifi_interface;

    wifi_interface = wifi_mgmr_sta_enable();
    wifi_mgmr_sta_connect(wifi_interface, ssid, password, NULL, NULL, 0, 0);
}
/**
 * @brief tcp_client_task
 *
 * @param arg
 */
static void tcp_client_task(void* arg)
{
    blog_info("tcp client task run\r\n");
    int socketfd;
    int ret = 0;
    char* tcp_buff = pvPortMalloc(512);
    memset(tcp_buff, 0, 512);
    socketfd = tcp_client_init(TCP_SERVER_IP, TCP_SERVER_PORT);
    if (!tcp_client_connect(socketfd)) {
        blog_info("%s:tcp client connect OK\r\n", __func__);
    }
    else goto __exit;
    if (tcp_client_send(socketfd, "hell tcp server")<0) {
        printf("tcp client send fail\r\n");
        goto __exit;
    }
    else
        blog_info("tcp client send OK\r\n");
    while (1) {

        ret = tcp_client_receive(socketfd, tcp_buff);

        if (ret>0) {
            blog_info("%s:tcp receive data:%s \r\n", __func__, tcp_buff);
            if (strstr(tcp_buff, "close")) goto __exit;
            memset(tcp_buff, 0, 512);
        }

        vTaskDelay(100/portTICK_PERIOD_MS);
    }
__exit:
    vPortFree(tcp_buff);
    tcp_client_deinit(socketfd);
    vTaskDelete(NULL);
}
/**
 * @brief event_cb_wifi_event
 *      wifi connet ap event Callback function
 * @param event
 * @param private_data
 */
static void event_cb_wifi_event(input_event_t* event, void* private_data)
{
    static char* ssid;
    static char* password;

    switch (event->code)
    {
        case CODE_WIFI_ON_INIT_DONE:
        {
            printf("[APP] [EVT] INIT DONE %lld\r\n", aos_now_ms());
            wifi_mgmr_start_background(&conf);
        }
        break;
        case CODE_WIFI_ON_MGMR_DONE:
        {
            printf("[APP] [EVT] MGMR DONE %lld\r\n", aos_now_ms());
            //_connect_wifi();

            wifi_sta_connect(ROUTER_SSID, ROUTER_PWD);
        }
        break;
        case CODE_WIFI_ON_SCAN_DONE:
        {
            printf("[APP] [EVT] SCAN Done %lld\r\n", aos_now_ms());
            // wifi_mgmr_cli_scanlist();
        }
        break;
        case CODE_WIFI_ON_DISCONNECT:
        {
            printf("[APP] [EVT] disconnect %lld\r\n", aos_now_ms());
        }
        break;
        case CODE_WIFI_ON_CONNECTING:
        {
            printf("[APP] [EVT] Connecting %lld\r\n", aos_now_ms());
        }
        break;
        case CODE_WIFI_CMD_RECONNECT:
        {
            printf("[APP] [EVT] Reconnect %lld\r\n", aos_now_ms());
        }
        break;
        case CODE_WIFI_ON_CONNECTED:
        {
            printf("[APP] [EVT] connected %lld\r\n", aos_now_ms());

        }
        break;
        case CODE_WIFI_ON_PRE_GOT_IP:
        {
            printf("[APP] [EVT] connected %lld\r\n", aos_now_ms());

        }
        break;
        case CODE_WIFI_ON_GOT_IP:
        {
            printf("[APP] [EVT] GOT IP %lld\r\n", aos_now_ms());
            printf("[SYS] Memory left is %d Bytes\r\n", xPortGetFreeHeapSize());
            //WiFi connection succeeded, create TCP client task
            xTaskCreate(tcp_client_task, (char*)"tcp_client_task", 1024*2, NULL, 16, NULL);
        }
        break;
        case CODE_WIFI_ON_PROV_SSID:
        {
            printf("[APP] [EVT] [PROV] [SSID] %lld: %s\r\n",
                   aos_now_ms(),
                   event->value ? (const char*)event->value : "UNKNOWN");
            if (ssid)
            {
                vPortFree(ssid);
                ssid = NULL;
            }
            ssid = (char*)event->value;
        }
        break;
        case CODE_WIFI_ON_PROV_BSSID:
        {
            printf("[APP] [EVT] [PROV] [BSSID] %lld: %s\r\n",
                   aos_now_ms(),
                   event->value ? (const char*)event->value : "UNKNOWN");
            if (event->value)
            {
                vPortFree((void*)event->value);
            }
        }
        break;
        case CODE_WIFI_ON_PROV_PASSWD:
        {
            printf("[APP] [EVT] [PROV] [PASSWD] %lld: %s\r\n", aos_now_ms(),
                   event->value ? (const char*)event->value : "UNKNOWN");
            if (password)
            {
                vPortFree(password);
                password = NULL;
            }
            password = (char*)event->value;
        }
        break;
        case CODE_WIFI_ON_PROV_CONNECT:
        {
            printf("[APP] [EVT] [PROV] [CONNECT] %lld\r\n", aos_now_ms());
            printf("connecting to %s:%s...\r\n", ssid, password);
            wifi_sta_connect(ssid, password);
        }
        break;
        case CODE_WIFI_ON_PROV_DISCONNECT:
        {
            printf("[APP] [EVT] [PROV] [DISCONNECT] %lld\r\n", aos_now_ms());
        }
        break;
        default:
        {
            printf("[APP] [EVT] Unknown code %u, %lld\r\n", event->code, aos_now_ms());
            /*nothing*/
        }
    }
}

static void proc_main_entry(void* pvParameters)
{

    aos_register_event_filter(EV_WIFI, event_cb_wifi_event, NULL);
    hal_wifi_start_firmware_task();
    aos_post_event(EV_WIFI, CODE_WIFI_ON_INIT_DONE, 0);
    vTaskDelete(NULL);
}


void main()
{
    puts("[OS] Starting TCP/IP Stack...\r\n");
    tcpip_init(NULL, NULL);
    puts("[OS] proc_main_entry task...\r\n");
    xTaskCreate(proc_main_entry, (char*)"main_entry", 1024, NULL, 15, NULL);
}

常见问题与踩坑提示

⚠️ connect OK 但 send 失败
原因:服务器主动断开、网络中断,或发送数据超长(TCP 缓冲有限)
解决:发送前检查 socket 状态;单次发送控制在 1KB 内;失败后 tcp_client_deinit 重连

⚠️ receive 一直收不到数据
原因:服务器未回发、或对端防火墙拦截
解决:先确认服务器确实会回发数据;局域网测试时关闭电脑防火墙,确认端口放行

⚠️ 局域网 IP 写在代码里,路由器重启后失效
原因:电脑 IP 由 DHCP 动态分配
解决:电脑设置静态 IP;或服务端用域名(先 DNS 解析,见官方 dns 示例)

⚠️ 内存不足(malloc 失败)
原因:任务栈与缓冲区占用过大
解决:任务栈用官方 1024*2;接收缓冲 512B 够用则不要加大;用 xPortGetFreeHeapSize() 监控

⚠️ 一直打印 Connecting,始终没有 GOT IP(连不上路由器)
原因:SSID/密码填错、路由器是 5GHz、或信号太弱
解决:核对 tcp_client/main.cROUTER_SSID/ROUTER_PWD 与路由器完全一致;确认路由器是 2.4GHz(开发板不支持 5GHz);把开发板靠近路由器再试

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

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

运行自检

串口打印 tcp client connect OKtcp client send OK,并能收到服务器回发数据,即 TCP 客户端验证通过。

遇到问题?

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

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