背景
有些時候我們在網絡通信中也需要用到 組播(多播)、廣播。現在我們來介紹如何實現。
建議:在此之前,關閉防火牆。
ubuntu: service ufw stop
windows: 控制面板關閉
有關知識
基本概念
- 單播:兩個主機間單對單的通信
- 廣播:一個主機對整個局域網上所有主機上的數據通信(網絡地址全1)
單播和廣播是兩個極端,要么對一個主機進行通信,要么對整個局域網的主機進行通信
- 組播:實際情況下,經常需要對一組特定的主機進行通信,而不是所有局域網上的主機
-
IP組播(也稱多址廣播或多播),是一種允許一台或多台主機發送數據包到多台主機的TCP/IP網路技術。
-
多播是 IPv6 數據包的 3 種基本目的地址類型之一,多播是一點對多點的通信, IPv6 沒有采用 IPv4 中的組播術語,而是將廣播看成是多播的一個特殊例子。
多播組只能用UDP 或者原始套接字實現,不能用TCP。
廣播地址
在使用TCP/IP 協議的網絡中,主機標識段host ID 為全1 的IP 地址為廣播地址,廣播的分組傳送給host ID段所涉及的所有計算機。
傳輸層只有UDP可以廣播 。
組播地址
IP 組播通信必須依賴於 IP 多播地址,在 IPv4 中它是一個 D 類 IP 地址,范圍從 224.0.0.0 到 239.255.255.255,並被划分為局部鏈接多播地址、預留多播地址和管理權限多播地址3類:
-
局部鏈接多播地址范圍在 224.0.0.0~224.0.0.255,這是為路由協議和其它用途保留的地址,路由器並不轉發屬於此范圍的IP包;
-
預留多播地址為 224.0.1.0~238.255.255.255,可用於全球范圍(如Internet)或網絡協議;
-
管理權限多播地址為 239.0.0.0~239.255.255.255,可供組織內部使用,類似於私有 IP 地址,不能用於 Internet,可限制多播范圍。
組播地址與MAC地址的映射
使用同一個 IP 多播地址接收多播數據包的所有主機構成了一個主機組,也稱為多播組。一個多播組的成員是隨時變動的,一台主機可以隨時加入或離開多播組,多播組成員的數目和所在的地理位置也不受限制,一台主機也可以屬於幾個多播組。
這個我們可以這樣理解,多播地址就類似於 QQ 群號,多播組相當於 QQ 群,一個個的主機就相當於群里面的成員。
設備驅動程序就必須接收所有多播數據幀,然后對它們進行過濾,這個過濾過程是網絡驅動或IP層自動完成。(設備驅動程序會對多播數據進行過濾,將其發到相應的位置)
組播應用
- 單點對多點應用
點對多點應用是指一個發送者,多個接收者的應用形式,這是最常見的多播應用形式。典型的應用包括:媒體廣播、媒體推送、信息緩存、事件通知和狀態監視等。
- 多點對單點應用
多點對點應用是指多個發送者,一個接收者的應用形式。通常是雙向請求響應應用,任何一端(多點或點)都有可能發起請求。典型應用包括:資源查找、數據收集、網絡競拍、信息詢問等。
- 多點對多點應用
多點對多點應用是指多個發送者和多個接收者的應用形式。通常,每個接收者可以接收多個發送者發送的數據,同時,每個發送者可以把數據發送給多個接收者。典型應用包括:多點會議、資源同步、並行處理、協同處理、遠程學習、討論組、分布式交互模擬(DIS)、多人游戲等。
組播編程
多播程序框架主要包含套接字初始化、設置多播超時時間、加入多播組、發送數據、接收數據以及從多播組中離開幾個方面。其步驟如下:
1)建立一個socket。
2)然后設置接收方多播的參數,例如超時時間TTL、本地回環許可LOOP等。
3)設置接收方加入多播組。
4)發送和接收數據。
5)從多播組離開。
我們需要用到 setsocket 函數 ,使用這些參數:
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
struct ip_mreq
{
struct in_addr imn_multiaddr; // 多播組 IP,類似於 群號
struct in_addr imr_interface; // 將要添加到多播組的 IP,類似於 成員號
};
struct in_addr
{
in_addr_t s_addr;
}
// 當imr_interface 為 INADDR_ANY 時,選擇的是默認組播接口。
level :
- IPPROTO_IP
optname:
- IP_MULTICAST_LOOP 支持多播數據回送
- IP_ADD_MEMBERSHIP 加入多播組
- IP_DROP_MEMBERSHIP 離開多播組
默認情況下,當本機發送組播數據到某個網絡接口時,在IP層,數據會回送到本地的回環接口,選項IP_MULTICAST_LOOP用於控制數據是否回送到本地的回環接口。
使用IP_ADD_MEMBERSHIP選項每次只能加入一個網絡接口的IP地址到多播組,但並不是一個多播組僅允許一個主機IP地址加入,可以多次調用IP_ADD_MEMBERSHIP選項來實現多個IP地址加入同一個廣播組,或者同一個IP地址加入多個廣播組。
optval:
- IP_MULTICAST_LOOP 選項對應傳入 unsigned int 來確認是否支持多播數據回送
- IP_ADD_MEMBERSHIP 傳入 ip_mreq
- IP_DROP_MEMBERSHIP 傳入 ip_mreq
組播例程
group_client.c
/*
# Copyright By Schips, All Rights Reserved
# https://gitee.com/schips/
#
# File Name: group_client.c
# Created : Mon 23 Mar 2020 04:00:49 PM CST
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#define IP_FOUND "IP_FOUND"
#define IP_FOUND_ACK "IP_FOUND_ACK"
/*
廣播與多播只支持UDP協議,因為TCP協議是端到端,這與廣播與多播的理念相沖突
廣播是局域網中一個主機對所有主機的數據通信,而多播是一個主機對一組特定的主機進行通信.多播可以是因特網,而廣播只能是局域網。多播常用於視頻電話,網上會議等。
setsockopt設置套接字選項可以設置多播的一些相關信息
IP_MULTICAST_TTL //設置多播的跳數值
IP_ADD_MEMBERSHIP //將主機的指定接口加入多播組,以后就從這個指定的接口發送與接收數據
IP_DROP_MEMBERSHIP //主機退出多播組
IP_MULTICAST_IF //獲取默認的接口或設置多播接口
IP_MULTICAST_LOOP //設置或禁止多播數據回送,即多播的數據是否回送到本地回環接口
例子:
int ttl=255;
setsockopt(socket,IPPROTO_IP,IP_MULTICAST_TTL,&ttl,sizeof(ttl));//設置跳數
socket -套接字描述符
PROTO_IP -選項所在的協議層
IP_MULTICAST_TTL -選項名
&ttl -設置的內存緩沖區
sizeof(ttl) -設置的內存緩沖區長度
struct in_addr in;
setsockopt(socket,IPPROTO_IP,IP_MUTLICAST_IF,&in,sizeof(in));//設置組播接口
int yes=1;
setsockopt(socket,IPPROTO_IP,IP_MULTICAST_LOOP,&yes,sizeof(yes));//設置數據回送到本地回環接口
struct ip_mreq addreq;
setsockopt(socket,IPPROTO_IP,IP_ADD_MEMBERSHIP,&req,sizeof(req));//加入組播組
struct ip_mreq dropreq;
setsockopt(socket,IPPROTO_IP,IP_DROP_MEMBERSHIP,&dropreq,sizeof(dropreq));//離開組播組
*/
#define MCAST_ADDR "224.0.0.88"
int main(int argc ,char **argv)
{
int ret,count;
int sock_fd;
char send_buf[20];
char recv_buf[20];
struct sockaddr_in server_addr; //多播地址
struct sockaddr_in our_addr;
struct sockaddr_in recvaddr;
int so_broadcast=1;
socklen_t socklen;
sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
//server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_addr.s_addr=inet_addr(MCAST_ADDR); //多播地址
server_addr.sin_port = htons(6666);
//客戶端綁定通信端口,否則系統自動分配
memset(&our_addr,0,sizeof(our_addr));
our_addr.sin_family = AF_INET;
our_addr.sin_port = htons(7777);
our_addr.sin_addr.s_addr = htonl(INADDR_ANY); //MCAST_ADDR
//自定義地址如果為有效地址
//則協議棧將自定義地址與端口信息發送到接收方
//否則協議棧將使用默認的回環地址與自動端口
//our_addr.sin_addr.s_addr = inet_addr("127.0.0.10");
ret = bind(sock_fd, (struct sockaddr *)&our_addr, sizeof(our_addr) );
if(ret == -1)
{
perror("bind !");
}
socklen = sizeof(struct sockaddr);
strncpy(send_buf,IP_FOUND,strlen(IP_FOUND)+1);
for(count=0;count<1;count++)
{
ret = sendto(sock_fd, send_buf, strlen(send_buf)+1, 0,(struct sockaddr *)&server_addr, socklen);
if(ret != strlen(send_buf)+1)
{
perror(" send to !");
}
ret = recvfrom(sock_fd, recv_buf, sizeof(recv_buf), 0,(struct sockaddr *)&recvaddr, &socklen);
if(ret < 0 )
{
perror(" recv! ");
}
printf(" recv server addr : %s \n", (char *)inet_ntoa(recvaddr.sin_addr));
printf(" recv server port : %d \n", ntohs(recvaddr.sin_port) );
printf(" recv server msg :%s \n", recv_buf);
}
close(sock_fd);
return 0;
}
group_server.c
/*
# Copyright By Schips, All Rights Reserved
# https://gitee.com/schips/
#
# File Name: group_server.c
# Created : Mon 23 Mar 2020 04:02:12 PM CST
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
//#include <linux/in.h>
#include <arpa/inet.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#define IP_FOUND "IP_FOUND"
#define IP_FOUND_ACK "IP_FOUND_ACK"
#define MCAST "224.0.0.88"
//說明:設置主機的TTL值,是否允許本地回環,加入多播組,然后服務器向加入多播組的主機發送數據,主機接收數據,並響應服務器。
int main(int argc,char **argv)
{
int sock_fd,client_fd;
int ret;
struct sockaddr_in localaddr;
struct sockaddr_in recvaddr;
socklen_t socklen;
char recv_buf[20];
char send_buf[20];
int ttl = 10;//如果轉發的次數等於10,則不再轉發
int loop=0;
sock_fd = socket(AF_INET, SOCK_DGRAM , 0);
if(sock_fd == -1)
{
perror(" socket !");
}
memset(&localaddr,0,sizeof(localaddr));
localaddr.sin_family = AF_INET;
localaddr.sin_port = htons(6666);
localaddr.sin_addr.s_addr = htonl(INADDR_ANY);
ret = bind(sock_fd, (struct sockaddr *)&localaddr,sizeof(localaddr));
if(ret == -1)
{
perror("bind !");
}
socklen = sizeof(struct sockaddr);
//設置多播的TTL值
if(setsockopt(sock_fd, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl))<0){
perror("IP_MULTICAST_TTL");
return -1;
}
//設置數據是否發送到本地回環接口
if(setsockopt(sock_fd, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop))<0){
perror("IP_MULTICAST_LOOP");
return -1;
}
//加入多播組
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr=inet_addr(MCAST);//多播組的IP
mreq.imr_interface.s_addr=htonl(INADDR_ANY);//本機的默認接口IP,本機的隨機IP
if(setsockopt(sock_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0){
perror("IP_ADD_MEMBERSHIP");
return -1;
}
while(1)
{
ret = recvfrom(sock_fd, recv_buf, sizeof(recv_buf), 0,(struct sockaddr *)&recvaddr, &socklen);
if(ret < 0 )
{
perror(" recv! ");
}
printf(" recv client addr : %s \n", (char *)inet_ntoa(recvaddr.sin_addr));
printf(" recv client port : %d \n",ntohs(recvaddr.sin_port));
printf(" recv msg :%s \n", recv_buf);
if(strstr(recv_buf,IP_FOUND))
{
//響應客戶端請求
strncpy(send_buf, IP_FOUND_ACK, strlen(IP_FOUND_ACK) + 1);
ret = sendto(sock_fd, send_buf, strlen(IP_FOUND_ACK) + 1, 0, (struct sockaddr*)&recvaddr, socklen);//將數據發送給客戶端
if(ret < 0 )
{
perror(" sendto! ");
}
printf(" send ack msg to client !\n");
}
}
// 離開多播組
ret = setsockopt(sock_fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq));
if(ret < 0){
perror("IP_DROP_MEMBERSHIP");
return -1;
}
close(sock_fd);
return 0;
}
廣播編程
廣播的實現比較簡單,只需要設置socket允許廣播(想廣播的終端設置即可),然后在發送數據前指定為廣播地址即可。我們直接看例程吧。
boardcast_client.c
/*
# Copyright By Schips, All Rights Reserved
# https://gitee.com/schips/
#
# File Name: boardcast_client.c
# Created : Mon 23 Mar 2020 04:29:48 PM CST
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
int main()
{
char send_buf[100] = "GET Msg";
char recv_buf[100];
// 1 創建一個套接字,用於網絡通信
int sk_fd = socket(PF_INET, SOCK_DGRAM, 0);
if (sk_fd == -1)
{
perror("socket");
return -1;
}
// 2 綁定服務的IP與端口
struct sockaddr_in ser_addr;
ser_addr.sin_family = PF_INET;
ser_addr.sin_port = htons (6666) ;
ser_addr.sin_addr.s_addr = inet_addr("192.168.1.88");
int ret = bind(sk_fd, (struct sockaddr *)&ser_addr,sizeof(ser_addr));
if (ret == -1)
{
perror("bind");
return -1;
}
// 3 等待 服務器廣播
struct sockaddr_in src_addr;
socklen_t size = sizeof(ser_addr);
ret = recvfrom(sk_fd, recv_buf, sizeof(recv_buf), 0,(struct sockaddr *)&src_addr, &size);
if (ret == -1)
{
perror("reveform");
return -1;
}
printf("recv :%s\n",recv_buf);
// 4 關閉套接字
close(sk_fd);
return 0;
}
boardcast_server.c
/*
# Copyright By Schips, All Rights Reserved
# https://gitee.com/schips/
#
# File Name: boardcast_server.c
# Created : Mon 23 Mar 2020 04:29:48 PM CST
*/
// UDP 服務端
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
char send_buf[100] = "hello client 6666";
char recv_buf[100];
// 1 創建一個套接字,用於網絡通信
int sk_fd = socket(PF_INET, SOCK_DGRAM, 0);
if (sk_fd == -1)
{
perror("socket");
return -1;
}
//設置廣播使能
int on = 1;
setsockopt(sk_fd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
// 2 綁定服務的IP與端口
struct sockaddr_in ser_addr;
ser_addr.sin_family = PF_INET;
ser_addr.sin_port = htons (6666) ;
ser_addr.sin_addr.s_addr = inet_addr("192.168.1.88");
int ret = bind(sk_fd, (struct sockaddr *)&ser_addr,sizeof(ser_addr));
if (ret == -1)
{
perror("bind");
return -1;
}
// 3 廣播數據
struct sockaddr_in client_addr;
client_addr.sin_family = PF_INET;
client_addr.sin_port = htons (6666) ;
client_addr.sin_addr.s_addr = inet_addr("192.168.1.255");
socklen_t size = sizeof(struct sockaddr_in);
while(1)
{
ret = sendto(sk_fd, send_buf, sizeof(send_buf), 0,(struct sockaddr *)&client_addr, size);
if (ret == -1)
{
perror("sendto");
return -1;
}
}
// 4 關閉套接字
close(sk_fd);
return 0;
}