多播的含义
“多播”亦称“多点传送”(Multicasting),是一种让数据从一个成员送出,然后复制给其他多个成员的技术。
多播通信具有两个层面的重要特征:控制层面和数据层面。其中,“控制层面”(Control Plane)定义了组成员的组织方式;而“数据层面”(Data Plane)决定了在不同的成员之间,数据如何传送。这两方面的特征既可以是“有根的”(Rooted),也可以是“无根的”(Nonrooted)。
在一个“有根的”控制层面内,存在着一个特殊的多播组成员,叫作 c_root(控制根,或根节点)。而剩下的每个组成员都叫作c_leaf(控制叶,或叶节点)。大多数情况下,c_root 需负责多播组的建立,其间涉及到建立同任意数量的 c_leaf 的连接。而在某些特殊情况下,c_leaf 则可在以后的某个时间申请加入一个特定的多播组(或者说,取得那个组的成员资格)。要注意的是,对任何一个具体的组来说,都只能存在一个根节点。ATM协议便是“有根控制层面”的典型例子。
而对一个“无根的”控制层面来说,它则允许任何人加入一个组,其间不存在任何例外。在这种情况下,所有组成员均为 c_leaf 节点(叶节点)。每个成员都有权加入一个多播组。IP多播便是无根控制层面的一个典型例子。
数据层面也存在着“有根的”和“无根的”两种形式。对一个有根数据层面而言,它有一个参与者叫作 d_root(数据根,或根节点)。数据传输只能在 d_root 和多播会话的其他所有成员之间进行。显然,那些成员是 d_leaf(数据叶,或叶节点)。这种传输既可单向进行,亦可双向进行。但既然是一个有根数据层面,便暗示着出自一个 d_leaf 叶节点的数据只会被 d_root 根节点接收到;而自 d_root发出的数据却可由每个 d_leaf 收到。ATM也是“有根数据层面”的一个典型例子。
在一个无根数据层面上,所有组成员都能将数据发给组内的其他所有成员。从一个组成员发出的数据块会投递给其他所有成员,同时所有接收者都能回送数据。至于谁能接收或发送数据,则不存在任何限制。同样地, IP多播采用的是数据层面上的“无根”通信方式。
IP多播
IP多播通信需要依赖一个特殊的地址组,名为“多播地址”。我们正是用这个组地址对一个指定的组进行命名。举个例子来说,假定五个节点都想通过IP多播,实现彼此间的通信,它们便可加入同一个组地址。全部加入之后,由一个节点发出的任何数据均会一模一样地复
制一份,发给组内的每个成员,甚至包括始发数据的那个节点。
多播IP地址是一个D类IP地址,范围在224.0.0.0到239.255.255.255之间。但是,其中还有许多地址是为特殊用途而保留的。
比如,224.0.0.0根本没有使用(也不能使用),224.0.0.1代表子网内的所有系统(主机),而224.0.0.2代表子网内的所有路由器。上述最后两个特殊地址只能由IGMP协议使用。
对于多播的简介就到此结束,如果还想了解得更详细,那就只有找本书来慢慢啃了。
程序示例
下面的程序演示了如何建立IP多播,并可以接受和发送多播消息。
#pragma once
#pragma comment(lib, "ws2_32.lib")
#include "targetver.h"
#include <winsock2.h>
#include <ws2tcpip.h>
#include <process.h>
#include <stdio.h>
#define MCASTADDR "234.5.6.7" // 加入的多播组IP
#define MCASTPORT 34567 // 加入的多播组所使用的端口
void do_send(void* arg);
void do_read(void* arg);
int main(int argc, char **argv)
{
const int on = 1; // 用于指定本地同一端口是否允许被多个套接口绑定,非零表示允许
const int routenum = 10; // 用于指定多播数据包的TTL
const int loopback = 1; // 用于指定多播数据包是否回馈,非零表示允许
WSAData wsaData;
SOCKET server,
sockM;
sockaddr_in local,
remote;
int ret ;
if( WSAStartup(MAKEWORD(2,0), &wsaData) != 0 )
{
printf("WSAStartup 调用出错./n");
return 0;
}
// SOCK_DGRAM 创建一个UDP套接口
// WSA_FLAG_OVERLAPPED 套接字支持重叠 I/O 操作
// WSA_FLAG_MULTIPOINT_C_LEAF 套接字支持 c_leaf(控制叶) 的多点会议
// WSA_FLAG_MULTIPOINT_D_LEAF 套接字支持 d_leaf(数据叶) 的多点会议
if ((server = WSASocket(AF_INET, SOCK_DGRAM, 0, NULL, 0,
WSA_FLAG_MULTIPOINT_C_LEAF
| WSA_FLAG_MULTIPOINT_D_LEAF
| WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET)
{
printf("创建套接字失败,错误代码: %d/n",WSAGetLastError());
return -1;
}
// SO_REUSEADDR 设置是否允许本地同一端口被多个套接口绑定
ret = setsockopt(server, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on));
if( ret == SOCKET_ERROR )
{
closesocket(server);
WSACleanup();
printf("套接口选项设置(SO_REUSEADDR)失败,错误代码: %d/n",WSAGetLastError());
return 0;
}
// IP_MULTICAST_TTL 设置多播数据包的转发范围(TTL,Time To Live)
ret = setsockopt(server,IPPROTO_IP,IP_MULTICAST_TTL,(char*)&routenum,sizeof(routenum));
if( ret == SOCKET_ERROR )
{
closesocket(server);
WSACleanup();
printf("套接口选项设置(IP_MULTICAST_TTL)失败,错误代码: %d/n",WSAGetLastError());
return 0;
}
// IP_MULTICAST_LOOP 禁止或允许回馈多播数据包
ret = setsockopt(server,IPPROTO_IP,IP_MULTICAST_LOOP,(char*)&loopback,sizeof(loopback));
if( ret == SOCKET_ERROR )
{
closesocket(server);
WSACleanup();
printf("套接口选项设置(IP_MULTICAST_LOOP)失败,错误代码: %d/n",WSAGetLastError());
return 0;
}
// 初始化
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(MCASTPORT); // 本地端口
local.sin_addr.S_un.S_addr = INADDR_ANY; // 本地全部地址
// 绑定套接口
ret = bind(server, (sockaddr*)(&local), sizeof(local));
if( ret == SOCKET_ERROR )
{
closesocket(server);
WSACleanup();
printf("套接口绑定失败,错误代码: %d/n",WSAGetLastError());
return 0;
}
// 初始化
memset(&remote, 0, sizeof(remote));
remote.sin_family = AF_INET;
// 对有根控制方案,该地址指定了欲邀请加入的客户机
// 对无根控制方案,该地址指定了要加入的多播组
remote.sin_port = htons(MCASTPORT);
remote.sin_addr.S_un.S_addr = inet_addr(MCASTADDR);
// 加入多播组
// JL_BOTH 收发兼并
// JL_SENDER_ONLY 只发
// JL_RECEIVER_ONLY 只读
// sockM 返回的套接字描述符取决于输入套接字(server)
// 对于异步操作,除非加入操作完成,否则返回的套接字描述符是无用的
// 对于异步操作下的有根方案,若非原来的套接字(server)接收到一个对应的FD_CONNECT通知,
// 否则返回的描述符便是无效的
// sockM 主要用于根节点对叶节点的维护管理
sockM = WSAJoinLeaf(server,(SOCKADDR *)&remote,sizeof(remote),NULL,NULL,NULL,NULL,JL_BOTH);
if(sockM == INVALID_SOCKET)
{
closesocket(server);
WSACleanup();
printf("加入多播组失败,错误代码: %d/n",WSAGetLastError());
return 0;
}
//创建了两个线程,一个读用户输入并发送,一个读多播组数据
HANDLE hHandle[2];
hHandle[0] = (HANDLE)_beginthread(do_send,0,(void*)server);
hHandle[1] = (HANDLE)_beginthread(do_read,0,(void*)server);
// 主线程挂起,等待hHandle[0]线程的信号状态发生变化
// INFINITE 等待时间无限
// 如果用户输入结束,hHandle[0]线程被释放,该函数检测到信号变化,便返回结果
WaitForSingleObject(hHandle[0], INFINITE);
closesocket(server);
WSACleanup();
printf("退出多播组,程序结束. ");
return 0;
}
void do_send(void* arg)
{
const char end[] = "end",
END[] = "END";
SOCKET server = (SOCKET)arg;
char SsendData[1024] = "";
unsigned int i;
sockaddr_in remote;
memset(&remote, 0, sizeof(sockaddr_in));
remote.sin_family = AF_INET ;
remote.sin_port = htons(MCASTPORT);
remote.sin_addr.s_addr = inet_addr(MCASTADDR);
while(TRUE) //读取用户输入直到用户输入"end"
{
printf("请输入信息,按回车发送,输入/"end/"退出:/n");
for(i = 0; i < 1024;i++) // 读取用户输入
{
SsendData[i] = getchar();
// 读到回车(CR)或换行符(LF),便停止读取
if ((SsendData[i] == (char)0x0A) || (SsendData[i] == (char)0x0D))
break;
}
// 不发送空消息
if ((SsendData[0] == (char)0x0A) || (SsendData[0] == (char)0x0D))
continue;
// 判断退出信息
if ((SsendData[0] == end[0] || SsendData[0] == END[0]) &&
(SsendData[1] == end[1] || SsendData[1] == END[1]) &&
(SsendData[2] == end[2] || SsendData[2] == END[2]))
break;
// 发送用户输入的数据到多播组
// strlen(SsendData) 不包括结尾字符
sendto(server, SsendData, strlen(SsendData), 0, (sockaddr*)(&remote), sizeof(sockaddr_in));
// 清除数据,为了下一次发送
memset(&SsendData, 0, strlen(SsendData));
}
}
void do_read(void* arg)
{
SOCKET server = (SOCKET)arg;
char ReadData[1024];
int ret;
sockaddr_in client;
int clientLen;
while(TRUE) //一直读取直到主线程终止
{
// 清除IP信息缓存
clientLen = sizeof(sockaddr_in);
memset(&client, 0, clientLen);
// client 结构用来接受发包人的网络地址
ret = recvfrom(server, ReadData,sizeof(ReadData),
0, (sockaddr*)(&client), &clientLen);
if( ret == SOCKET_ERROR )
{
if( WSAGetLastError() == WSAEINTR ) //主线程终止时recvfrom返回的错误
break;
else
printf("读取数据失败,错误代码: %d/n",WSAGetLastError());
break ;
}
// 加入结尾字符
ReadData[ret] = '/0';
// inet_ntoa 将网络地址转换成点分字符串格式
printf("收到来至: [ %s ] 的消息: %s",inet_ntoa(client.sin_addr),ReadData);
}
}
补充
IP_MULTICAST_IF 这个选项用于设置自哪个IP接口发出多播数据。正常情况下(即非多播环境),只能参照
路由表来决定一个数据报自哪个接口发出。此时,依据一个特定数据报本身以及它的目的地,系统可判断出哪个接口最适合用来发出这个数据报。然而,由于多播地址可由任何人使用,所以仅仅依靠路由表的自动判断是不够的。程序员必须多少施加一些自己的影响。当然,只有打算发出多播数据的那台机器已通过网卡建立了与多个网络的连接,才需要考虑到这方面的问题。
示例:
DWORD dwInterface;
dwInterface = inet_addr("192.168.2.2");
if (setsockopt(s,IPPROTO_IP,IP_MULTICAST_IF,(char*)&dwInterface,
sizeof(DWORD)) == SOCKET_ERROR)
{
// Error
}
上例中,我们将本地接口设为 192.168.2.2 对通过套接字s发出的任何多播数据而言,都会通过分配了那个IP地址的网络接口送出。