1.引言
网络通信中使用最多的就是广播、组播、单播几种通信方式了,今天我们抛开具体的标准和知识,简单聊聊单播、组播、广播的区别与使用。
2.单播、组播、广播区别与联系
单播:在同一网络内,两个设备点对点的通信就是单播通信。
组播:在同一网络可达范围内,一个网络设备与关心其数据的部分设备进行通信就是组播。
广播:在同一网络可达范围内,一个网络设备向本网络内所有设备进行通信就是广播。
具体如下:
简单地说,单播->组播->广播,是通信数量不断增加的通信方式。当然,通信数量的增多,带来的是通信设备的资源消耗更大,整体网络环境的复杂度更高。
通常,我们使用组播、广播完成两件事:
1)将同一份数据交互到多个目的地。比如,视频会议、新闻分发,都需要将一份数据同时传输到多个设备上,供大家使用。
2)通过客户端请求或发现服务器。有时,我们并不知道服务器的具体信息(如IP地址),这时,我们可以采取“盲发”的方式去广播或组播信息,等待服务器收到消息盲发的消息后,返回数据,如此找到对应目标设备。
众所周知,TCP是可靠传输(先与另一个通信端点建立可靠连接,再传输数据),因此TCP一般只支持单播这种通信方式,而DUP通信不需要建立连接就可以发送数据,因此,通常我们说的广播、组播,都是在UDP下概念。
此外,广播又可以分为两类:本地广播、定向广播。
1)本地广播:广播地址为255.255.255.255.
2)定向广播:广播地址类似192.168.4.255.
这两种广播功能类似,但具体区别说来话长,有感兴趣的可以留言,我再出篇帖子来介绍一下。
3.编程与测试
广播、单播在实现方式,以及使用方式上的区别不大,仅仅是目标IP,以及Socket属性的细微差别,我们先来看两者的区别与使用。
具体可参考以下代码:(开发板仍然是便宜且好用的ESP32开发板,开发环境是release V4.2,实际上,任何平台,以下代码都是可以参考滴,客官慢用)
#include <string.h>
#include <sys/param.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "protocol_examples_common.h"
#include "tcpip_adapter.h"
#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include <lwip/netdb.h>
#include "addr_from_stdin.h"
#if defined(CONFIG_EXAMPLE_IPV4)
#define HOST_IP_ADDR CONFIG_EXAMPLE_IPV4_ADDR
#elif defined(CONFIG_EXAMPLE_IPV6)
#define HOST_IP_ADDR CONFIG_EXAMPLE_IPV6_ADDR
#else
#define HOST_IP_ADDR ""
#endif
#define PORT CONFIG_EXAMPLE_PORT
static const char *TAG = "example";
static const char *payload = "Message from ESP32 ";
static int udp_creat(uint16_t port, char* bind_ip)
{
struct sockaddr_in6 unicast_dest_addr = {0};
struct sockaddr_in *unicast_dest_addr_ip4 = (struct sockaddr_in *)&unicast_dest_addr;
int ip_protocol = 0;
const int on = 1;
inet_aton(bind_ip, &unicast_dest_addr_ip4->sin_addr.s_addr);//bind sta ip
unicast_dest_addr_ip4->sin_family = AF_INET;
unicast_dest_addr_ip4->sin_port = htons(port);
ip_protocol = IPPROTO_IP;
int sock = socket(AF_INET, SOCK_DGRAM, ip_protocol);
if (sock < 0) {
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
return -1;
}
int err = bind(sock, (struct sockaddr *)&unicast_dest_addr, sizeof(unicast_dest_addr));
if (err < 0) {
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
shutdown(sock, 0);
close(sock);
return -1;
}
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) != 0) {
ESP_LOGE(TAG, "reuse addr fail");
close(sock);
return -1;
}
if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)) != 0) {
ESP_LOGE(TAG, "broadcast enable fail");
close(sock);
return -1;
}
return sock;
}
static void udp_client_task(void *pvParameters)
{
char rx_buffer[128];
char host_ip[] = HOST_IP_ADDR;
int addr_family = 0;
int ip_protocol = 0;
tcpip_adapter_ip_info_t sta_info;
while (1) {
#if defined(CONFIG_EXAMPLE_IPV4)
struct sockaddr_in unicast_dest_addr;
unicast_dest_addr.sin_addr.s_addr = inet_addr(HOST_IP_ADDR);
unicast_dest_addr.sin_family = AF_INET;
unicast_dest_addr.sin_port = htons(PORT);
addr_family = AF_INET;
ip_protocol = IPPROTO_IP;
#elif defined(CONFIG_EXAMPLE_IPV6)
struct sockaddr_in6 unicast_dest_addr = { 0 };
inet6_aton(HOST_IP_ADDR, &unicast_dest_addr.sin6_addr);
unicast_dest_addr.sin6_family = AF_INET6;
unicast_dest_addr.sin6_port = htons(PORT);
unicast_dest_addr.sin6_scope_id = esp_netif_get_netif_impl_index(EXAMPLE_INTERFACE);
addr_family = AF_INET6;
ip_protocol = IPPROTO_IPV6;
#elif defined(CONFIG_EXAMPLE_SOCKET_IP_INPUT_STDIN)
struct sockaddr_in6 unicast_dest_addr = { 0 };
ESP_ERROR_CHECK(get_addr_from_stdin(PORT, SOCK_DGRAM, &ip_protocol, &addr_family, &unicast_dest_addr));
#endif
char sta_ip_str[32] = {0};
esp_err_t ret = tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &sta_info);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "get sta ip fail");
}
printf(IPSTR, IP2STR(&sta_info.ip));
sprintf(sta_ip_str, IPSTR, IP2STR(&sta_info.ip));
int sock = udp_creat(3333,sta_ip_str);
if (sock < 0) {
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Socket created, sending to %s:%d", HOST_IP_ADDR, PORT);
struct sockaddr_in broadcast_dest_addr;//for broadcast
char sendline[32] = {"Hello"};
char temp_str[32] = "255.255.255.255";
bzero(&broadcast_dest_addr, sizeof(broadcast_dest_addr));
broadcast_dest_addr.sin_family = AF_INET;
broadcast_dest_addr.sin_addr.s_addr = inet_addr(temp_str); //广播地址(192.168.43.255 can be work, too.)
broadcast_dest_addr.sin_port = htons(3333);//target port
while (1) {
int err = sendto(sock, sendline, strlen(sendline), 0, (struct sockaddr*)&broadcast_dest_addr, sizeof(broadcast_dest_addr));//broadcast
if (err < 0) {
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Message sent");
err = sendto(sock, payload, strlen(payload), 0, (struct sockaddr *)&unicast_dest_addr, sizeof(unicast_dest_addr));//unicast
if (err < 0) {
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Message sent");
struct sockaddr_in source_addr; // Large enough for both IPv4 or IPv6
socklen_t socklen = sizeof(source_addr);
int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&source_addr, &socklen);
// Error occurred during receiving
if (len < 0) {
ESP_LOGE(TAG, "recvfrom failed: errno %d", errno);
break;
}
// Data received
else {
rx_buffer[len] = 0; // Null-terminate whatever we received and treat like a string
ESP_LOGI(TAG, "Received %s", rx_buffer);
if (strncmp(rx_buffer, "OK: ", 4) == 0) {
ESP_LOGI(TAG, "Received expected message, reconnecting");
break;
}
}
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
if (sock != -1) {
ESP_LOGE(TAG, "Shutting down socket and restarting...");
shutdown(sock, 0);
close(sock);
}
}
vTaskDelete(NULL);
}
void app_main(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(example_connect());
xTaskCreate(udp_client_task, "udp_client", 4096, NULL, 5, NULL);
}
上述代码,通过UDP,实现广播、单播一次,然后进入接收一次数据;依次循环的功能。
4.组播实现介绍
组播的实现就略微复杂了,要实现组播,至少要经过以下步骤:
1)建立socket_fd
2)socket_fd和指定本地端口绑定
3)加入一个组播组
4)通过sendto / recvfrom进行数据的收发
5)离开组播组
6)关闭socket
注意:服务器和客户端必须都要加入相同的组播地址才可以。涉及到的socket属性主要是以下三个:
感兴趣的小伙伴可以留言,我将再出一篇关于组播的博客,谢谢点赞或收藏。