多播

多播方式的数据传输是基于UDP完成的,因此,与UDP服务端/客户端的实现非常接近。区别在于,UDP数据传输以单一目标进行,而多播数据同时传递到加入(注册)特定组的大量主机。换言之,采用多播方式时,可以同时向多个主机传递数据

多播的数据传输方式及流量方面的优点:

  • 多播服务端针对特定多播组,只发送一次数据
  • 即使只发送一次数据,但该组内的所有客户端都会接收数据
  • 多播组数可在IP地址范围内任意增加
  • 加入特定组即可接收发往该多播组的数据

多播组是D类IP(224.0.0.0~239.255.255.255),“加入多播组”可以理解为通过程序完成如:“在D类IP地址中中,我希望接收发往目标239.234.218.234的多播数据”。多播是基于UDP完成的,也就是说,多播数据包的格式与UDP数据包相同。只是与一般的UDP数据包不同,向网络传递一个多播数据包时,路由器将复制该数据包并传递到多个主机。像这种,多播需要借助路由器完成。如图1-1所示

图1-1   多播路由

图1-1表示传输至AAA组的多播数据包借助路由器传递到加入AAA组的所有主机的过程,可能有人认为这种方式不利于网络流量,但像这样向大量客户端发送数据时,也会对服务端和网络流量产生负面影响,所以可以借助多播技术解决该问题。只看图1-1,各位会认为不利于网络流量,因为路由器频繁复制同一数据包。但请从另一方面考虑,这样做至少不会向同一区域发送多个相同数据包。

若通过TCP或UDP向1000个主机发送文件,则共需要传递1000次。即便将10台主机合为一个网络,使99%的传输路径相同的情况下也是如此。但此时若使用多播方式传输文件,则只需发送一次,这时由1000台主机构成的网络中的路由器负责复制文件并传递到主机。就因为这种特性,多播主要用于“多媒体数据的实时传输”

另外,虽然理论上可以完成多播通信,但不少路由器并不支持多播,或即便支持也因网络拥堵问题故意阻断多播。因此,为了在不支持多播的路由器完成多播通信,也会使用隧道技术(非多播程序员需考虑的问题)。我们只讨论支持多播服务的环境下的编程方法

路由(Routing)和TTL(Time to Live,生存时间),以及加入组的方法

接下来讨论多播相关编程,为了传递多播数据包,必须设置TTL,TTL是Time to Live的简写,是决定“数据包传递距离”的主要因素。TTL用整数表示,并且每经过一个路由器就减1。当TTL变为0时,该数据包无法再被传递,只能销毁。因此,TTL的值设置过大将影响网络流量。当然,设置过小也会无法传递到目标主机

图1-2   TTL和多播路由

接下来给出TTL设置方法,程序中TTL设置是通过套接字可选项(TCP/IP网络编程之套接字的多种可选项)完成的。与设置TTL相关的协议层为IPPROTO_TCP ,选项名为IP_MULTICAST_TTL。因此,可以用如下代码把TTL设置为64 :

int send_sock;
int time_live=64;
……
send_sock=socket(PF_INET, SOCK_DGRAM, 0);
setsockopt(send_sock, IPPROTO_IP, IP_MULTICAST_TTL, (void*) &time_live, sizeof(time_live));
……

  

另外,加入多播组也通过设置套接字选项完成,加入多播组相关的协议层为IPPROTO_IP,选项名为IP_ADD_MEMBERSHIP。可通过如下代码加入多播组:

int recv_sock;
struct ip_mreq join_adr;
……
recv_sock=socket(PF_INET, SOCK_DGRAM, 0);
……
join_adr.imr_multiaddr.s_addr="多播组地址信息";
join_adr.imr_interface.s_addr="加入多播组的主机地址信息";
setsockopt(recv_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void*) &join_adr, sizeof(join_adr));
……

  

上述代码只给出了与setsockopt函数相关的部分,详细内容会在后面给出。此处之讲解ip_mreq结构体,该结构体定义如下:

struct ip_mreq
{
struct in_addr imr_multiaddr; //多播组的IP地址
struct in_addr imr_interface; //加入的客服端主机IP地址
}

  

之前我们介绍过in_addr结构体,因此这里只介绍ip_mreq的结构体成员。第一个成员imr_multiaddr中写入加入的组IP地址,第二个成员imr_interface是加入该组的套接字所属主机的IP地址,也可用INADDR_ANY

实现多播Sender和Receiver

多播中用“发送者”(以下称为Sender)和“接受者”(以下称为Receiver)替代服务端和客户端,顾名思义,此处的Sender是多播数据的发送主体,Receiver是需要多播组加入过程的数据接收主体。下面讨论即将给出的示例,该示例的运行场景如下:

  • Sender:向AAA组广播文件中保存的新闻信息
  • Receiver:接收传递到AAA组的新闻信息

接下来只给出Sender代码,Sender比Receiver简单,因为Receiver需要经过加入组的过程,而Sender只需创建UDP套接字,并向多播地址发送数据

news_sender.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h> #define TTL 64
#define BUF_SIZE 30
void error_handling(char *message); int main(int argc, char *argv[])
{
int send_sock;
struct sockaddr_in mul_adr;
int time_live = TTL;
FILE *fp;
char buf[BUF_SIZE];
if (argc != 3) {
printf("Usage : %s <GroupIP> <PORT>\n", argv[0]);
exit(1);
} send_sock = socket(PF_INET, SOCK_DGRAM, 0);
memset(&mul_adr, 0, sizeof(mul_adr));
mul_adr.sin_family = AF_INET;
mul_adr.sin_addr.s_addr = inet_addr(argv[1]); // Multicast IP
mul_adr.sin_port = htons(atoi(argv[2])); // Multicast Port setsockopt(send_sock, IPPROTO_IP,
IP_MULTICAST_TTL, (void *)&time_live, sizeof(time_live));
if ((fp = fopen("news.txt", "r")) == NULL)
error_handling("fopen() error"); while (!feof(fp)) /* Broadcasting */
{
fgets(buf, BUF_SIZE, fp);
sendto(send_sock, buf, strlen(buf),
0, (struct sockaddr *)&mul_adr, sizeof(mul_adr));
sleep(2);
}
fclose(fp);
close(send_sock);
return 0;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

  

  • 第24行:多播数据通信是通过UDP完成的,因此创建UDP套接字
  • 第26~28行:设置传输数据的目标地址信息,重要的是,必须将IP地址设置为多播地址
  • 第30行:指定套接字TTL信息,这是Sender中的必要过程
  • 第35~41行:实际传输数据的区域,基于UDP套接字传输数据,因此需要利用sendto函数。另外,第40行的sleep函数调用主要是为了给传输数据提供一定的时间间隔而添加的,没有其他特殊意义

从上述代码中可以看到,Sender与普通的UDP套接字程序相比差别不大。但多播Receiver则有些不同,为了接收传向任意多播地址的数据,需要经过加入多播组的过程。除此之外,Receiver同样与UDP套接字程序差不多,接下来给出上述示例结合使用的Receiver程序

news_receiver.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h> #define BUF_SIZE 30
void error_handling(char *message); int main(int argc, char *argv[])
{
int recv_sock;
int str_len;
char buf[BUF_SIZE];
struct sockaddr_in adr;
struct ip_mreq join_adr; if (argc != 3) {
printf("Usage : %s <GroupIP> <PORT>\n", argv[0]);
exit(1);
} recv_sock = socket(PF_INET, SOCK_DGRAM, 0);
memset(&adr, 0, sizeof(adr));
adr.sin_family = AF_INET;
adr.sin_addr.s_addr = htonl(INADDR_ANY);
adr.sin_port = htons(atoi(argv[2])); if (bind(recv_sock, (struct sockaddr *)&adr, sizeof(adr)) == -1)
error_handling("bind() error"); join_adr.imr_multiaddr.s_addr = inet_addr(argv[1]);
join_adr.imr_interface.s_addr = htonl(INADDR_ANY); setsockopt(recv_sock, IPPROTO_IP,
IP_ADD_MEMBERSHIP, (void *)&join_adr, sizeof(join_adr)); while (1)
{
str_len = recvfrom(recv_sock, buf, BUF_SIZE - 1, 0, NULL, 0);
if (str_len < 0)
break;
buf[str_len] = 0;
fputs(buf, stdout);
}
close(recv_sock);
return 0;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

  

  • 第33、34行:初始化结构体ip_mreg变量,第26行初始化多播组地址,第27行初始化待加入组的主机IP地址
  • 第36行:利用套接字选项IP_ADD_MEMBERSHIP加入多播组,至此完成了接收第33行指定的多播组数据的所有准备
  • 第41行:通过调用recvfrom函数接收多播数据,如果不需要知道传输数据的主机地址信息,可以向recvfrom函数的第五个和第六个参数分别传递NULL和0

创建一个news.txt

# cat news.txt
Hello world!

    

编译news_receiver.c并运行

# gcc news_receiver.c -o news_receiver
# ./news_receiver 224.1.1.2 8500
Hello world!
Hello world!

  

编译news_sender.c 并运行

# gcc news_sender.c -o news_sender
# ./news_sender 224.1.1.2 8500

  

Sender和Receiver之间的端口应保持一致,虽然未讲,但理所应当。运行顺序并不重要,因为不像TCP套接字在连接状态下收发数据。只是因为多播属于广告的范畴,如果延迟运行Receiver,则无法接收之前传输的多播数据

广播

本节介绍的广告在“一次性向多个主机发送数据”这一点上与多播类似,但传输数据的范围有区别。多播即使在跨越不同网络的情况下,只要加入多播组就能接收数据。相反,广播只能向同一网络中的主机传输数据。广播是向同一网络中的所有主机传输数据的方法,广播是基于UDP完成的,这一点与多播相同。根据传输数据时使用的IP地址的形式,广播分为以下两种:

  • 直接广播
  • 本地广播

二者在代码实现上的差别主要在IP地址,直接广播的IP地址中除了网络之外,其余主机地址全部设置为1。例如,希望向网络地址192.12.34中的所有主机传输数据时,可以向192.12.34.255传输。换言之,可以采用直接广播的方式向特定区域内所有主机传输数据

反之,本地广播中使用的IP地址限定为255.255.255.255。例如,192.32.24网络中的主机向255.255.255.255传输数据时,数据将传递到192.32.24网络中的所有主机

那么,应当如何实现Sender和Receiver呢?实际上,如果不仔细观察广播示例中通信时使用的IP地址,则很难与UDP示例进行区分。也就是说,数据通信中使用的IP地址是与UDP示例的唯一区别。默认生成的套接字会阻止广播,因此,只需要通过如下代码更改默认设置

int send_sock;
int bcast = 1;
……
send_sock = socket(PF_INET, SOCK_DGRAM, 0);
……
setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (void*) &bcast, sizeof(bcast));
……

  

调用setsockopt函数,将SO_BROADCAST选项设置为bcast变量中的值1。这意味着可以进行数据广播,当然,上述套接字选项只需在Sender中更改,Receiver的实现不需要该过程

实现广播数据的Sender和Receiver

下面是基于广播的Sender和Receiver,为了与多播示例进行对比,将之前的news_sender.c和news_receiver.c改为广播的示例

news_sender_brd.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h> #define BUF_SIZE 30
void error_handling(char *message); int main(int argc, char *argv[])
{
int send_sock;
struct sockaddr_in broad_adr;
FILE *fp;
char buf[BUF_SIZE];
int so_brd = 1;
if (argc != 3) {
printf("Usage : %s <Boradcast IP> <PORT>\n", argv[0]);
exit(1);
} send_sock = socket(PF_INET, SOCK_DGRAM, 0);
memset(&broad_adr, 0, sizeof(broad_adr));
broad_adr.sin_family = AF_INET;
broad_adr.sin_addr.s_addr = inet_addr(argv[1]);
broad_adr.sin_port = htons(atoi(argv[2])); setsockopt(send_sock, SOL_SOCKET,
SO_BROADCAST, (void *)&so_brd, sizeof(so_brd));
if ((fp = fopen("news.txt", "r")) == NULL)
error_handling("fopen() error"); while (!feof(fp))
{
fgets(buf, BUF_SIZE, fp);
sendto(send_sock, buf, strlen(buf),
0, (struct sockaddr *)&broad_adr, sizeof(broad_adr));
sleep(2);
}
close(send_sock);
return 0;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

  

第29行更改第23行创建的UDP套接字的可选项,使其能够发送广播数据,其余部分与UDP Sender一致,下面给出广播Receiver

news_receiver_brd.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h> #define BUF_SIZE 30
void error_handling(char *message); int main(int argc, char *argv[])
{
int recv_sock;
struct sockaddr_in adr;
int str_len;
char buf[BUF_SIZE]; if (argc != 2) {
printf("Usage : %s <PORT>\n", argv[0]);
exit(1);
} recv_sock = socket(PF_INET, SOCK_DGRAM, 0); memset(&adr, 0, sizeof(adr));
adr.sin_family = AF_INET;
adr.sin_addr.s_addr = htonl(INADDR_ANY);
adr.sin_port = htons(atoi(argv[1])); if (bind(recv_sock, (struct sockaddr *)&adr, sizeof(adr)) == -1)
error_handling("bind() error"); while (1)
{
str_len = recvfrom(recv_sock, buf, BUF_SIZE - 1, 0, NULL, 0);
if (str_len < 0)
break;
buf[str_len] = 0;
fputs(buf, stdout);
} close(recv_sock);
return 0;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

  

编译news_receiver_brd.c并运行

# gcc news_receiver_brd.c -o news_receiver_brd
# ./news_receiver_brd 8500
Hello world!
Hello world!

  

编译news_sender_brd.c 并运行

# gcc news_sender_brd.c -o news_sender_brd
# ./news_sender_brd 255.255.255.255 8500

  

TCP/IP网络编程之多播与广播的更多相关文章

  1. 《TCP/IP网络编程》

    <TCP/IP网络编程> 基本信息 作者: (韩)尹圣雨 译者: 金国哲 丛书名: 图灵程序设计丛书 出版社:人民邮电出版社 ISBN:9787115358851 上架时间:2014-6- ...

  2. UNIX环境高级编程——TCP/IP网络编程 常用网络信息检索函数

    UNIX环境高级编程——TCP/IP网络编程   常用网络信息检索函数 gethostname()   getppername()   getsockname()   gethostbyname() ...

  3. TCP/IP网络编程之套接字的多种可选项

    套接字可选项进而I/O缓冲大小 我们进行套接字编程时往往只关注数据通信,而忽略了套接字具有的不同特性.但是,理解这些特性并根据实际需要进行更改也十分重要.之前我们写的程序在创建好套接字后都是未经特别操 ...

  4. 浅谈TCP/IP网络编程中socket的行为

    我认为,想要熟练掌握Linux下的TCP/IP网络编程,至少有三个层面的知识需要熟悉: 1. TCP/IP协议(如连接的建立和终止.重传和确认.滑动窗口和拥塞控制等等) 2. Socket I/O系统 ...

  5. TCP/IP网络编程系列之四(初级)

    TCP/IP网络编程系列之四-基于TCP的服务端/客户端 理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于流的 ...

  6. TCP/IP网络编程系列之三(初级)

    TCP/IP网络编程系列之三-地址族与数据序列 分配给套接字的IP地址和端口 IP是Internet Protocol (网络协议)的简写,是为首发网络数据而分配给计算机的值.端口号并非赋予计算机值, ...

  7. TCP/IP网络编程系列之二(初级)

    套接字类型与协议设置 我们先了解一下创建套接字的那个函数 int socket(int domain,int type,int protocol);成功时返回文件描述符,失败时返回-1.其中,doma ...

  8. TCP/IP网络编程之多线程服务端的实现(二)

    线程存在的问题和临界区 上一章TCP/IP网络编程之多线程服务端的实现(一)的thread4.c中,我们发现多线程对同一变量进行加减,最后的结果居然不是我们预料之内的.其实,如果多执行几次程序,会发现 ...

  9. TCP/IP网络编程之优于select的epoll(二)

    基于epoll的回声服务端 在TCP/IP网络编程之优于select的epoll(一)这一章中,我们介绍了epoll的相关函数,接下来给出基于epoll的回声服务端示例. echo_epollserv ...

随机推荐

  1. 【php】

    1 <?php $arr = [ 'a' => 'aaa', ]; $arr2 = $arr; $arr2['a'] = 'ccc'; print_r($arr); print_r($ar ...

  2. 基于python3.6的如何爬取百度地图

    先前参考了其他的代码,大多数是python2.7写的,而3.6用的类库以及规则有了很大的变动,所以自己写了一个这样的代码,供给大家参考. def get_station(i): station=[] ...

  3. 【server 安全】更改本地安全策略及禁用部分服务以进一步增强windows server的安全性

    本地安全策略 以上内容的备份 注册表路径: System\CurrentControlSet\Control\ProductOptionsSystem\CurrentControlSet\Contro ...

  4. Sql Server 表的复制

    声名:A,B ,都是表 --B表存在(两表结构一样)insert into B select * from A 若两表只是有部分(字段)相同,则 insert into B(col1,col2,col ...

  5. NOIP2018赛前停课集训记(10.24~11.08)

    前言 为了不久之后的\(NOIP2018\),我们的停课从今天(\(Oct\ 24th\))起正式开始了. 本来说要下周开始的,没想到竟提早了几天,真是一个惊喜.毕竟明天有语文考试.后天有科学考试,逃 ...

  6. apache的安全增强配置(使用mod_chroot,mod_security)

    apache的安全增强配置(使用mod_chroot,mod_security) 作者:windydays      2010/8/17 LAMP环境的一般入侵,大致经过sql注入,上传webshel ...

  7. 题解 CF20A 【BerOS file system】

    对于此题,我的心近乎崩溃 这道题,注意点没有什么,相信大佬们是可以自己写出来的 我是蒟蒻,那我是怎么写出来的啊 好了,废话少说,开始进入正题 这道题,首先我想到的是字符串的 erase 函数,一边运行 ...

  8. 完结篇OO总结

    目录 前言 一.第四单元两次架构设计 二.架构设计及OO方法理解的演进 三.测试理解与实践的演进 四.课程收获 五.改进建议 前言 持续了17周的OO终于走向了尾声,想想寒假的时候连类都不知道是什么, ...

  9. Bootstrap 警告框(Alert)插件

    警告消息大多来是用来向终端用户提示警告或确认的消息,使用警告框插件,您可以向所有的警告框消息添加取消功能. 用法 您有以下两种方式启用警告框的可取消功能. 1.通过data属性:通过数据添加可取消功能 ...

  10. LeetCode955删列造序 ||

    问题:删列造序 || 给定由 N 个小写字母字符串组成的数组 A,其中每个字符串长度相等. 选取一个删除索引序列,对于 A 中的每个字符串,删除对应每个索引处的字符. 比如,有 A = [" ...