27、UDP组播收发:多播发送端实现、多播接收端实现、TTL设置

组播这个东西,说实话,我第一次接触是在做一个视频监控项目的时候。那时候要在局域网内把摄像头画面分发给几十个客户端,如果用单播,服务器得开几十个socket,CPU直接飙到80%。后来改用组播,一个socket搞定所有客户端,CPU降到15%。嗯,这就是组播的魅力。

今天我们就来聊聊UDP组播。说白了,组播就是“一对多”的通信方式。发送端只发一份数据,网络设备会自动复制给所有加入组播组的接收端。这比单播省带宽,比广播更精准。

组播的基本概念

组播使用D类IP地址,范围是224.0.0.0到239.255.255.255。其中224.0.0.0到224.0.0.255是保留地址,不能用于用户程序。我一般用239.0.0.0到239.255.255.255这个范围,这是管理本地范围的组播地址,不会往外扩散。

组播通信需要两个角色:

  • 发送端:只管往组播地址发数据,不需要加入组播组
  • 接收端:必须加入组播组,才能收到数据

这里有个容易搞混的点:发送端不需要加入组播组,但接收端必须加入。我刚开始做的时候,以为发送端也要加入,结果代码写复杂了,其实完全没必要。

多播发送端实现

发送端的实现其实和普通UDP发送差不多,区别在于目标地址是组播地址。来看代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MULTICAST_IP "239.0.0.1"
#define MULTICAST_PORT 8888
#define TTL_VALUE 4

int main() {
    int sockfd;
    struct sockaddr_in addr;
    char buf[] = "Hello, multicast!";
    
    // 创建UDP socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }
    
    // 设置TTL
    int ttl = TTL_VALUE;
    if (setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_TTL, 
                   &ttl, sizeof(ttl)) < 0) {
        perror("setsockopt TTL");
        close(sockfd);
        exit(1);
    }
    
    // 设置目标地址
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(MULTICAST_IP);
    addr.sin_port = htons(MULTICAST_PORT);
    
    // 发送数据
    while (1) {
        int ret = sendto(sockfd, buf, strlen(buf), 0,
                        (struct sockaddr*)&addr, sizeof(addr));
        if (ret < 0) {
            perror("sendto");
            break;
        }
        printf("Sent: %s\n", buf);
        sleep(1);
    }
    
    close(sockfd);
    return 0;
}

这段代码里,关键点就两个:

  • IP_MULTICAST_TTL:设置组播包的生存时间,默认是1,只能在本网段内传播
  • sendto:目标地址填组播地址就行,和普通UDP发送一模一样
我的习惯:发送端我一般会设置TTL为4,这样数据包能跨3个路由器。如果只在局域网内用,设成1就够了。设太大反而浪费带宽。

多播接收端实现

接收端比发送端复杂一点。你需要先创建一个UDP socket,然后绑定到本地端口,最后加入组播组。来看代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MULTICAST_IP "239.0.0.1"
#define MULTICAST_PORT 8888

int main() {
    int sockfd;
    struct sockaddr_in addr;
    struct ip_mreq mreq;
    char buf[1024];
    
    // 创建UDP socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }
    
    // 允许地址重用
    int reuse = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, 
                   &reuse, sizeof(reuse)) < 0) {
        perror("setsockopt REUSEADDR");
        close(sockfd);
        exit(1);
    }
    
    // 绑定到本地端口
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(MULTICAST_PORT);
    
    if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("bind");
        close(sockfd);
        exit(1);
    }
    
    // 加入组播组
    mreq.imr_multiaddr.s_addr = inet_addr(MULTICAST_IP);
    mreq.imr_interface.s_addr = htonl(INADDR_ANY);
    
    if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
                   &mreq, sizeof(mreq)) < 0) {
        perror("setsockopt ADD_MEMBERSHIP");
        close(sockfd);
        exit(1);
    }
    
    printf("Joined multicast group %s:%d\n", MULTICAST_IP, MULTICAST_PORT);
    
    // 接收数据
    while (1) {
        int len = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, NULL, NULL);
        if (len < 0) {
            perror("recvfrom");
            break;
        }
        buf[len] = '\0';
        printf("Received: %s\n", buf);
    }
    
    close(sockfd);
    return 0;
}

接收端有几个关键点:

  • SO_REUSEADDR:允许多个程序绑定到同一个端口。我遇到过一个问题,如果不加这个选项,程序崩溃重启后端口会被占用,得等几分钟才能重新绑定
  • IP_ADD_MEMBERSHIP:加入组播组。这个操作告诉网卡,我要收这个组播地址的数据
  • 绑定到INADDR_ANY:绑定到所有网卡,这样不管数据从哪个网卡进来都能收到
注意:我曾经在Linux上调试组播接收端,发现收不到数据。折腾了半天,原来是防火墙把组播包给过滤了。如果你也遇到类似问题,先检查防火墙规则,或者直接关掉防火墙试试。

TTL设置详解

TTL(Time To Live)这个东西,很多人以为只是防环的。其实在组播里,TTL还有一个重要作用:控制组播包的传播范围。

TTL值和传播范围的关系:

TTL值 传播范围 典型场景
0 本机 调试用,数据不出网卡
1 同一网段 局域网内组播
2 跨1个路由器 小范围跨网段
4 跨3个路由器 中等规模网络
32 站点本地范围 企业内网
64 区域范围 城域网
128 全球范围 互联网组播(很少用)

设置TTL的代码很简单,就一行:

int ttl = 4;
setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl));

你想想看,如果TTL设成1,数据包只能在同一个交换机下传播。如果设成4,就能跨3个路由器。我一般建议局域网内用1,跨网段用4,别设太大,否则网络里到处是你的组播包,影响别人。

组播通信流程图

下面这张图展示了组播通信的完整流程,从发送端到接收端,数据包是怎么走的:

UDP组播通信流程图 发送端 创建UDP socket 设置TTL sendto 组播地址 239.0.0.1:8888 TTL控制传播范围 网络复制 接收端1 加入组播组 接收端2 加入组播组 接收端3 未加入组播组 收不到数据 图例: 发送数据 网络复制 未加入组播组 说明:发送端只发一份数据,网络设备自动复制给所有加入组播组的接收端

从图上可以看得很清楚:发送端把数据发到组播地址,网络设备(交换机/路由器)负责把数据复制给所有加入组播组的接收端。接收端3因为没有加入组播组,所以收不到数据。

避坑指南

做组播开发,有几个坑我踩过,分享给你:

  • 防火墙拦截:Linux的iptables默认会拦截组播包。我建议先关掉防火墙测试,确认程序没问题再配置防火墙规则
  • 多网卡问题:如果机器有多个网卡,需要指定用哪个网卡发送组播。用setsockopt设置IP_MULTICAST_IF可以指定出口网卡
  • Windows和Linux差异:Windows上加入组播组用的是WSAJoinLeaf,Linux用setsockopt。跨平台开发时要注意
  • 组播地址冲突:同一个网段内,不同应用不要用同一个组播地址,否则会互相干扰

核心要点总结:

  • 发送端:创建UDP socket,设置TTL,往组播地址发数据
  • 接收端:创建UDP socket,绑定端口,加入组播组,收数据
  • TTL控制传播范围:1局域网,4跨网段,别设太大
  • 接收端必须加入组播组才能收到数据

组播这个东西,说白了就是“一次发送,多点接收”。用好了能大幅降低服务器负载,用不好就是网络灾难。我建议你先在虚拟机里搭个环境测试,确认流程走通了再上生产环境。


公众号:蓝海资料掘金营,微信deep3321