Skip to content

概述

UDP 组播(multicast)比广播更"聪明":设备主动加入某个组播组(D 类地址 224.0.0.0 ~ 239.255.255.255)后,只接收发往该组的数据,不打扰无关设备。发送端只需向组播地址发一份数据,组内所有成员都能收到。常用于局域网组网通信、固件批量升级、多媒体流分发。本教程演示:开发板加入组播组 224.0.1.0:7878,收到组播数据后打印来源并转发回组播组

用大白话讲:UDP 组播像微信群——广播是「小区大喇叭」(人人被迫听到),组播则是先进群才收得到:开发板主动「加群」(用 IP_ADD_MEMBERSHIP 加入组播组 224.0.1.0),之后群里发的消息它才收,没加群的设备完全不被打扰。群主只需发一份消息,所有群成员都能收到——比广播省带宽、更安静,是局域网组网的首选。

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

🎯本页目标通过 IP_ADD_MEMBERSHIP 加入组播组并收发组播数据,掌握组播初始化、收发与退出流程。
🧰前置条件① Ai-WB2 开发板一块(Type-C 数据线)② 2.4GHz 路由器 ③ 电脑一台(网络调试助手)④ 已按 [SDK 安装](../sdk/sdk_intro) 完成环境搭建,并完成 [连接 Wi-Fi](./wifi_connect)。
🔗相关章节全网广播见 [UDP 广播](./udp_broadcast);单播收发见 [UDP 服务器](./udp_server)。

进入示例工程

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

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

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

修改路由器参数与组播地址

打开 udp_multicast/main.c,修改开头的 SSID/密码(同 连接 Wi-Fi),并按需修改组播参数:

#define ROUTER_SSID "your ssid"
#define ROUTER_PWD "your password"
 // MULTICAST_ADDR: 224.0.0.0 ~ 239.255.255.255
#define MULTICAST_ADDR "224.0.1.0"
#define MULTICAST_PORT 7878
默认值 说明
MULTICAST_ADDR 224.0.1.0 组播组地址(D 类224.0.0.0 ~ 239.255.255.255);收发双方必须用同一个「群号」
MULTICAST_PORT 7878 组播端口(收发双方必须一致);「群号」加「房门号」都对了才收得到

💡 224.0.0.x 为链路本地保留段(不跨路由器);224.0.1.0 这类可自由使用。所有成员需在同一局域网。

编写代码

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

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

代码要点:

代码 作用
multicast_init("224.0.1.0", 7878) 初始化组播:绑定端口 + 加入组播组,返回 socket fd;一键「办入群手续」
setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, 1) 设置组播 TTL=1(仅本网段传播);TTL 像「包裹最多能走几站」,1 表示不出小区
setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &iaddr, ...) 指定组播发送出口为 station 接口(st1);不指定出口,消息不知从哪发出去
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, ...) 关键:加入组播组(imr_multiaddr 填组播地址);不「加群」就收不到组里消息
recvfrom(fd, buf, 512, MSG_DONTWAIT, &addr, &len) 非阻塞接收组播数据(50ms 轮询);没消息就继续干别的,不干等
sendto(fd, buf, len, 0, &addr, len) 转发数据回组播组(回填组播地址与端口);收到的消息再发回群里,全员可见
multicast_deinit(fd) 退出组播并关闭 socket;「退群」并释放资源

💡 组播 vs 广播:广播无需"申请",所有设备都被动接收;组播必须先用 IP_ADD_MEMBERSHIP 加入该组才接收——只有关心该组的设备会收到,省带宽、更安静,是局域网组网的首选。

编译工程

在工程目录执行编译:

make -j8

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

编译成功后生成固件 build_out/udp_multicast.bin

烧录固件

开发板保持 USB 连接,确认串口设备号后执行烧录:

make flash p=/dev/ttyUSB0 b=921600

说明:make flash 是「烧录」命令,把编译好的固件下载进开发板;p= 后面是串口设备号(Linux 下常为 /dev/ttyUSB0,Windows 下是 COM3 之类,以你电脑实际为准),b=921600 是烧录波特率(串口传数据的速度),保持默认即可。

⏳ 烧录过程中按提示长按开发板 EN 键进入下载模式,等待进度条完成即烧录成功。

运行验证

烧录完成后开发板自动重启运行,串口(波特率 921600,串口传数据的「语速」,两边必须一致)先打印 Wi-Fi 连接事件,随后打印:

[APP] [EVT] GOT IP 5594
<<<<<<<<<<<<<<<<<<udp multicast start<<<<<<<<<<<<<
multicast addr:224.0.1.0:7878

电脑网络调试助手(UDP 模式)向 224.0.1.0:7878 发送 Hello Ai-WB2,开发板串口打印:

192.168.1.111:Hello Ai-WB2
udp multicast data:Hello Ai-WB2

第一行是收到的组播数据(含来源 IP),第二行是开发板把数据转发回组播组。若电脑端工具支持加入组播组(如 Wireshark 过滤器),也能收到转发的回包。

出现 udp multicast start 和「来源 IP:Hello Ai-WB2」两行打印即成功。如果电脑发组播后串口没反应,先确认开发板已联网(先看到 GOT IP)、电脑发送的目标地址和端口是 224.0.1.0:7878、开发板已打印 multicast addr:224.0.1.0:7878(加入组播组成功),见文末 FAQ。

💡 双板互测:两块开发板烧录同一固件,A 板通过串口发送组播消息(或都用电脑触发),两板都能打印彼此的消息——这是验证组播最直接的方式。

代码执行流程

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


本文 API 汇总

multicast_init(mutc_addr, mutc_port)

创建 UDP socket、绑定端口、加入组播组(工程封装 src/)。

参数

  • mutc_addr:组播地址字符串,如 "224.0.1.0"
  • mutc_port:组播端口(官方示例 7878

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

multicast_deinit(socke_fd)

退出组播并关闭 socket(工程封装 src/)。

参数

  • socke_fdmulticast_init 返回的 socket 描述符

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

setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL, ...)

设置组播 TTL(1 = 仅本网段传播)。

参数

  • fd:socket 描述符
  • levelIPPROTO_IP
  • optnameIP_MULTICAST_TTL
  • ttl:TTL 值(uint8_t,官方 1
  • lensizeof(uint8_t)

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

setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, ...)

指定组播发送出口网络接口(station 接口 st1)。

参数

  • fd:socket 描述符
  • levelIPPROTO_IP
  • optnameIP_MULTICAST_IF
  • iaddr:出口 IP(struct in_addr,取 sta_addr->ip_addr
  • lensizeof(struct in_addr)

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

setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, ...)

加入组播组(组播收发的关键)。

参数

  • fd:socket 描述符
  • levelIPPROTO_IP
  • optnameIP_ADD_MEMBERSHIP
  • imreqstruct ip_mreqimr_multiaddr 填组播地址)
  • lensizeof(struct ip_mreq)

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

struct ip_mreq

组播成员结构体。

参数

  • imr_multiaddr:组播组地址
  • imr_interface:本机接口地址

返回值:无(结构体,非函数)

IP_MULTICAST(addr)

判断地址是否为合法 D 类组播地址。

参数

  • addr:32 位 IP 地址(网络序,需先 ntohl

返回值:合法组播地址返回非 0;否则返回 0

inet_aton / inet_addr(str, &addr / str)

IP 字符串转 in_addr(网络序)。

参数

  • str:IP 字符串,如 "224.0.1.0"
  • addr:输出参数,struct in_addr

返回值inet_aton:成功返回非 0,失败返回 0inet_addr:失败返回 INADDR_NONE

recvfrom / sendto(fd, buf, len, flags, addr, addrlen)

接收 / 发送组播数据报。

参数

  • fd:socket 描述符
  • buf:数据缓冲区
  • len:缓冲区长度
  • flags:可选 MSG_DONTWAIT(非阻塞)
  • addr:来源 / 目标地址
  • addrlen:地址长度

返回值:成功返回收发字节数;非阻塞无数据时 <= 0;失败返回负值

netif_find(name)

查找 station 网络接口(组播出口)。

参数

  • name:网卡名称,如 "st1"

返回值:成功返回 struct netif*;失败返回 NULL

close(fd)

关闭 socket。

参数

  • fd:socket 描述符

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

📌 组播地址段:224.0.0.0 ~ 239.255.255.255(D 类);224.0.0.x 为链路本地保留(路由器不转发),自定义组请用 224.0.1.0 起。


完整代码

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

📜 点击展开 udp_multicast/main.c 完整代码
c
/**
 * @file main.c
 * @author your name (you@domain.com)
 * @brief
 * @version 0.1
 * @date 2022-10-13
 *
 * @copyright Copyright (c) 2022
 *
 */
#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 "udp_multicast.h"

#define ROUTER_SSID "ssid"
#define ROUTER_PWD "password"
 // multicast_ADDR: 224.0.0.0 ~ 239.255.255.255
#define MULTICAST_ADDR "224.0.1.0"
#define MULTICAST_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 udp_multicast_task
 *
 * @param arg
 */
static void udp_multicast_task(void* arg)
{
    int socket_fd = 0;
    struct sockaddr_in send_addr;
    char* udp_buf = pvPortMalloc(512);
    int socklen = sizeof(send_addr);

    socket_fd = multicast_init(MULTICAST_ADDR, MULTICAST_PORT);
    if (socket_fd<0) goto __exit;
    blog_info("<<<<<<<<<<<<<<<<<<udp multicast start<<<<<<<<<<<<<\r\n");
    blog_info("multicast addr:%s:%d\r\n", MULTICAST_ADDR, MULTICAST_PORT);
    while (1) {
        memset(udp_buf, 0, 512);
        //Read multicast data
        if (recvfrom(socket_fd, udp_buf, 512, MSG_DONTWAIT, (struct sockaddr*)&send_addr, (socklen_t*)&socklen)>0) {
            blog_info("%s:%s\r\n", inet_ntoa(send_addr.sin_addr.s_addr), udp_buf);
            //Forward the read data to multicast
            send_addr.sin_port = htons(MULTICAST_PORT);
            send_addr.sin_addr.s_addr = inet_addr(MULTICAST_ADDR);
            if (sendto(socket_fd, udp_buf, strlen(udp_buf), 0, (struct sockaddr*)&send_addr, socklen)>0) {
                blog_info("udp multicast data:%s\r\n", udp_buf);
            }
        }
        vTaskDelay(50/portTICK_PERIOD_MS);
    }
__exit:
    blog_info("multicast close\r\n");
    vPortFree(udp_buf);
    multicast_deinit(socket_fd);
    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 udp multicast task
            xTaskCreate(udp_multicast_task, "udp_multicast_task", 2048, 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);
}

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

📜 点击展开 udp_multicast/src/udp_multicast.c 完整代码
c
/**
 * @file udp_multicast.c
 * @author your name (you@domain.com)
 * @brief
 * @version 0.1
 * @date 2022-10-18
 *
 * @copyright Copyright (c) 2022
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <FreeRTOS.h>
#include <task.h>
#include <lwip/sockets.h>
#include <blog.h>
#include "lwip/udp.h"
#include "lwip/inet.h"
#include "lwip/netdb.h"

#define MAX_CLIENT_NUM 4
static struct sockaddr_in s_dest;
/**
 * @brief multicast_receiver_init
 *      UDP multicast initialization
 * @param mutc_addr multicast addr
 * @param mutc_port multicast port
 * @return success: socket fd  fail:-1
 */
int multicast_init(char* mutc_addr, int mutc_port)
{
    int socke_fd = socket(PF_INET, SOCK_DGRAM, 0);
    if (socke_fd<0) return -1;

    s_dest.sin_addr.s_addr = htonl(INADDR_ANY);
    s_dest.sin_family = PF_INET;
    s_dest.sin_port = htons(mutc_port);
    int ret = bind(socke_fd, (struct sockaddr*)&s_dest, sizeof(s_dest));
    if (ret<0) return -1;

    struct netif* sta_addr = netif_find("st1");
    struct in_addr iaddr = { 0 };

    uint8_t ttl = 1;
    ret = setsockopt(socke_fd, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(uint8_t));
    if (ret<0) return -1;

    struct ip_mreq imreq = { 0 };
    if (mutc_addr!=NULL) {
        ret = inet_aton(mutc_addr, &imreq.imr_multiaddr.s_addr);
        if (ret<0) return -1;
    }
    else {
        inet_addr_from_ip4addr(&iaddr, &sta_addr->ip_addr);
    }
    if (!IP_MULTICAST(ntohl(imreq.imr_multiaddr.s_addr))) {
        printf("Configured IPV4 multicast address '%s' is not a valid multicast address. This will probably not work.\r\n", mutc_addr);
    }
    //set ip addr
    ret = setsockopt(socke_fd, IPPROTO_IP, IP_MULTICAST_IF, &iaddr, sizeof(iaddr));
    if (ret<0) return -1;
    ret = setsockopt(socke_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq));
    return socke_fd;
}
/**
 * @brief muliticast_deinit
 *      UDP multicast de initialization
 * @param socke_fd socket fd
 * @return success:0  fail:-1
 */
int multicast_deinit(int socke_fd)
{
    memset(&s_dest, 0, sizeof(s_dest));
    return close(socke_fd);
}

常见问题与踩坑提示

⚠️ 电脑发组播,开发板收不到
原因:开发板未成功加入组播组、端口不一致、或路由器不支持组播(IGMP)
解决:确认串口已打印 multicast start;双方端口一致(7878);部分家用路由器默认丢弃组播,可换路由器或在 AP 设置中开启组播(IGMP Snooping 相关选项)

⚠️ 使用 224.0.0.x 段地址收不到数据
原因224.0.0.0/24链路本地保留段,部分驱动/路由器不转发该段组播
解决:改用 224.0.1.0 等非保留地址(官方默认值)

⚠️ 组播消息无法跨路由器/跨网段
原因:组播默认仅限本网段(官方 TTL=1),路由器不转发
解决:组播仅用于局域网;跨网段通信改用单播(TCP/UDP)或配置三层组播路由

⚠️ 网络调试助手不支持组播发送
原因:部分调试工具只能填单播 IP
解决:用两块开发板互测(都烧录本固件,串口观察);或使用支持组播的工具(如 Wireshark、socat

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

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

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

运行自检

串口打印 udp multicast startmulticast addr:224.0.1.0:7878,且能收到并转发组播数据,即组播验证通过。

遇到问题?

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

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