标 题: IP多播技术及其编程
发信站: 幽幽黄桷兰 (Mon Jun 16 10:35:39 2003)
转信站: SMTH!maily.cic.tsinghua.edu.cn!news2.uestc.edu.cn!CQUPT
IP多播(也称多址广播或组播)技术,是一种允许一台或多台主机(多播源)发送单一
数据包到多台主机(一次的,同时的)的TCP/IP网络技术。多播作为一点对多点的通信
,是节省网络带宽的有效方法之一。在网络音频/视频广播的应用中,当需要将一个节点
的信号传送到多个节点时,无论是采用重复点对点通信方式,还是采用广播方式,都会
严重浪费网络带宽,只有多播才是最好的选择。多播能使一个或多个多播源只把数据包
发送给特定的多播组,而只有加入该多播组的主机才能接收到数据包。目前,IP多播技
术被广泛应用在网络音频/视频广播、AOD/VOD、网络视频会议、多媒体远程教育、“pu
sh”技术(如股票行情等)和虚拟现实游戏等方面。
一、IP多播技术简介
1.IP多播地址和多播组
IP多播通信必须依赖于IP多播地址,在IPv4中它是一个D类IP地址,范围从224.0.0.0到
239.255.255.255,并被划分为局部链接多播地址、预留多播地址和管理权限多播地址三
类。其中,局部链接多播地址范围在224.0.0.0~224.0.0.255,这是为路由协议和其它用
途保留的地址,路由器并不转发属于此范围的IP包;预留多播地址为224.0.1.0~238.25
5.255.255,可用于全球范围(如Internet)或网络协议;管理权限多播地址为239.0.0
.0~239.255.255.255,可供组织内部使用,类似于私有IP地址,不能用于Internet,可
限制多播范围。
使用同一个IP多播地址接收多播数据包的所有主机构成了一个主机组,也称为多播组。
一个多播组的成员是随时变动的,一台主机可以随时加入或离开多播组,多播组成员的
数目和所在的地理位置也不受限制,一台主机也可以属于几个多播组。此外,不属于某
一个多播组的主机也可以向该多播组发送数据包。
2.IP多播技术的硬件支持
要实现IP多播通信,要求介于多播源和接收者之间的路由器、集线器、交换机以及主机
均需支持IP多播。目前,IP多播技术已得到硬件、软件厂商的广泛支持。
(1)主机
支持IP多播通信的平台包括Windows CE 2.1、Windows 95、Windows 98、Windows NT 4
和Windows 2000等,运行这些操作系统的主机都可以进行IP多播通信。此外,新生产的
网卡也几乎都提供了对IP多播的支持。
(2)集线器和交换机
目前大多数集线器、交换机只是简单地把多播数据当成广播来发送接收,但一些中、高
档交换机提供了对IP多播的支持。例如,在3COM SuperStack 3 Swith 3300交换机上可
启用802.1p或IGMP多播过滤功能,只为已侦测到IGMP数据包的端口转发多播数据包。
(3)路由器
多播通信要求多播源节点和目的节点之间的所有路由器必须提供对Internet组管理协议
(IGMP)、多播路由协议(如PIM、DVMRP等)的支持。
当一台主机欲加入某个多播组时,会发出“主机成员报告”的IGMP消息通知多播路由器
。当多播路由器接收到发给那个多播组的数据时,便会将其转发给所有的多播主机。多
播路由器还会周期性地发出“主机成员查询”的IGMP消息,向子网查询多播主机,若发
现某个多播组已没有任何成员,则停止转发该多播组的数据。此外,当支持IGMP v2的主
机(如Windows 98/2000计算机)退出某个多播组时,还会向路由器发送一条“离开组
”的IGMP消息,以通知路由器停止转发该多播组的数据。但只有当子网上所有主机都退
出某个多播组时,路由器才会停止向该子网转发该多播组的数据。
使用多播路由协议,路由器可建立起从多播源节点到所有目的节点的多播路由表,从而
实现在子网间转发多播数据包。例如,PIM(协议独立多播)就是一种多播路由协议,它
有两种类型:稀疏模式(sparse-mode)和密集模式(dense-mode)。以Cisco 2621路由
器为例,启用IP多播转发功能的基本设置如下:
c2621(config)# ip multicast-routing 启动IP多播,使路由器成为一个多播路由器
c2621(config)# int f0/0 配置快速以太网端口0
c2621(config-if)# ip pim dense-mode(或sparse-mode)启动PIM,同时激活IGMP协议
c2621(config-if)# int f0/1 配置快速以太网端口1
c2621(config-if)# ip pim dense-mode(或sparse-mode)
二、IP多播应用的编程方法
在实际应用中,编程人员通常需要自己编制底层网络应用程序来实现网上的底层通信,
如具体实现IP多播通信的功能。编制底层网络应用程序通常要借助于网络数据通信编程
接口,而在不同的操作系统中所提供的网络编程接口是有所不同的,如在Microsoft Wi
ndows环境下的网络编程接口就是Windows套接字(Windows Socket,简称Winsock)。
Winsock提供了包括TCP/IP、IPX等多种通信协议下的编程接口。不同的Windows版本支持
不同的Winsock版本,其中Windows 95等早期版本本身只支持Winsock1.1(16位)下的编
程(可以通过安装相关的软件包使其支持Winsock2.0),而Windows98、Windows NT4.0
、Windows 2000则直接支持Winsock2.0(32位)。Winsock2.0是Winsock1.1的扩展,除
兼容Winsock1.1 API外,还定义了一套可支持IP多播的与协议无关的API。
使用Winsock 2.0实现IP多播的一般步骤如下:
1.初始化Winsock资源
在使用Winsock之前,必须调用WSAStartup()函数初始化Windows Sockets DLL。它允许
应用程序或DLL指定Windows Sockets API要求的版本。
2.创建套接字
调用WSASocket()函数可以创建一个使用UDP协议的套接字,它是加入多播组的初始化套
接字,并且以后数据的发送和接收都在该套接字上进行。针对IP多播通信,可将参数dw
Flags设置为WSA_FLAG_MULTIPOINT_C_LEAF、WSA_FLAG_MULTIPOINT_D_LEAF和WSA_FLAG_
OVERLAPPED的位和,指明IP多播通信在控制层面和数据层面都是“无根的”,只存在叶
节点,它们可以任意加入一个多播组,而且从一个叶节点发送的数据会传送到每一个叶
节点(包括它自己);创建的套接字具有重叠属性。
3.设置套接字的选项
调用setsockopt()函数为套接字设置SO_REUSEADDR选项,以允许套接字绑扎到一个已在
使用的地址上。
4.绑定套接字
调用bind()函数绑定套接字,从而将创建好的套接字与本地地址和本地端口联系起来。
对于多播通信来说,发送和接收数据通常采用同一个端口。
5.设置多播套接字的模式
WSAIoctl()函数的命令码SIO_MULTICAST_LOOP用来允许或禁止多播通信时发送出去的通
信流量是否也能够在同一个套接字上被接收(即多播返回)。值得注意的是,在Window
s 95/98/NT 4中,默认是允许多播返回,但不能设置禁止,否则会出错;只有在Window
s 2000以上版本中,才能设置允许/禁止多播返回。
WSAIoctl()函数的命令码SIO_MULTICAST_SCOPE用来设置多播传播的范围,即生存时间T
TL。每当多播路由器转发多播数据包时,数据包中的TTL值都会被减1,若数据包的TTL减
少到0,则路由器将抛弃该数据包。TTL的值是多少,多播数据便最多能经过多少个多播
路由器。例如,TTL值为0,则多播只能在本地主机的多个套接字间传播,而不能传播到
“网线”上;TTL值为1(默认值),则多播数据遇到第一个路由器,便会被它“无情”
地丢弃,不允许传出本地网络之外,即只有同一个网络内的多播组成员才能收到多播数
据。
6.加入一个多播组
调用WSAJoinLeaf()函数可加入一个多播组并指定角色(发送者/接收者)。调用时,参
数dwFlags可指定套接字作为发送者(JL_SENDER_ONLY)、接收者(JL_RECEIVER_ONLY)
或身兼两者(JL_BOTH)。调用成功后会返回一个多播套接字,调用closesocket()函数
关闭该套接字就离开了多播组,此时可以调用WSAJoinLeaf()函数再次加入多播组。注意
,对多播组数据的接收和发送不能在该套接字上完成。
7.向多播组发送数据
调用sendto()函数,可在指定的UDP套接字上向指定的多播组发送多播数据。调用时,参
数to应指向多播组的IP地址。值得注意的是,若一个应用程序只是打算给多播组发送数
据,便不必加入一个多播组。
8.等待事件
调用WSAAsyncSelect()函数,使套接字置于非阻塞模式,这时应用程序就可在该套接字
上接收以Windows消息为基础的网络事件通知。例如,若参数lEvent值为FD_READ,则应
用程序可在套接字上接收到“数据正等待被读入”的通知。
9.从多播组接收数据
调用recvfrom函数,可在指定的UDP套接字上读取输入数据。多播通信中数据的发送与接
收一般采用同一个端口,因此其发送套接字和接收套接字是一样的。
10.关闭套接字,释放Winsock资源。
在多播通信结束后,先调用closesocket()函数关闭多播套接字和UDP套接字,然后调用
WSACleanup()函数结束对Windows Sockets DLL的使用。
三、应用实例
为了说明IP多播技术的应用方法,本人在Visual C++.NET环境下设计了一个简单的基于
Windows Socket 2的IP多播应用程序,通过该例子读者可以掌握IP多播应用程序设计的
一般方法。该程序的具体设计方法如下:
1.在Visual Studio.NET中建立一个基于对话框的MFC项目CMulticastSocket。注意在“
高级功能”设置中不要选择“Windows套接字”,这是因为MFC只支持Windows Socket 1
而不支持Windows Socket 2。为了能使用Winsock 2 API编程,在程序中应包含“winso
ck2.h”头文件,并在项目中加入ws2_32.lib的静态库,该静态库应设置在项目属性的“
链接器”\“输入”\“附加依赖项”中。
2.在对话框(类名CCMulticastSocketDlg)资源中,设置它的Caption为“WinSock 2多
播应用程序”,并添加以下控件:
静态文本:Caption为“接收到的信息:”;
编辑框:ID为IDC_RECEIVE_EDIT,Read Only、Auto Vscroll、Vertical Scroll和Mult
iline属性值都为True
静态文本:Caption为“发送的信息:”
编辑框:ID为IDC_SEND_EDIT
第一个按钮:Caption为“加入多播组(&J)”,ID为IDC_JOIN_BUTTON
第二个按钮:Caption为“多播发送(&S)”,ID为IDC_SEND_BUTTON
第三个按钮:Caption为“离开多播组(&L)”,ID为IDC_LEAVE_BUTTON
第四个按钮:Caption为“退出(&Q)”,ID为IDC_QUIT_BUTTON
为两个编辑框分别添加相关联的CString类型的变量m_SendMessage和m_ReceiveMessage
;为四个按钮添加相应的消息处理函数;为对话框添加定时器消息(用于定时显示接收
到的消息)及其消息处理函数。
3.添加一个新的对话框资源,设置它的Caption为“加入多播组”,保留默认的两个按
钮控件,同时添加添加以下控件:
静态文本:Caption为“IP多播组地址:”
编辑框:ID为IDC_IPADDRESS_EDIT
静态文本:Caption为“IP多播端口:”
编辑框:ID为IDC_PORT_EDIT
静态文本:Caption为“生存时间:”
编辑框:ID为IDC_TTL_EDIT
复选框:Caption为“多播返回:”,ID为IDC_LOOPBACK_CHECK,Left Text属性值为Tr
ue。
为该对话框添加新的类CJoinGroupDlg,它的基类为CDialog,然后为该对话框中的三个
编辑框分别添加相关联的变量,即CSting m_IPAddress、UINT m_nPort、UINT m_nTTL;
为复选框添加相关联的BOOL类型的变量m_Loopback。
4.在CMulticastSocketDlg.h文件的前面添加CJoinGroupDlg的头文件:#include “Jo
inGroupDlg.h”,并在CCMulticastSocketDlg类中添加了一个CJoinGroupDlg类实例对象
m_JoinDlg。
5.为了能在对话框中接收网络事件通知,应增加一个用户自定义的消息及消息处理函数
,具体实现方法如下:在CMulticastSocketDlg.h文件的前面自定义消息:#define WM_
SOCK_MSG(WM_USER+166),并在afx_msg块中说明消息处理函数:afx_msg LRESULT OnSo
cketMsg (WPARAM wParam,LPARAM lParam);在CMulticastSocketDlg.cpp文件中的消息
映射块中,使用ON_MESSAGE (WM_SOCK_MSG,OnSocketMsg)宏指令将消息映射到消息处理
函数中,并具体实现消息处理函数:LRESULT CCMulticastSocketDlg:: OnSocketMsg(W
PARAM wParam,LPARAM lParam){…}。
该程序的主要代码可参见程序清单,相关函数的详细说明可参看Microsoft MSDN帮助系
统。为了节省篇幅,程序中省略了部分自动生成的和用于错误处理的代码。
程序清单:
// CMulticastSocketDlg.cpp : 实现文件
#include "stdafx.h"
#include "winsock2.h"
#include "CMulticastSocket.h"
#include "CMulticastSocketDlg.h"
……
DWORD cbRet;
SOCKET Sock,SockM; //UDP套接字,多播套接字
BOOL bFlag,bJoin;
SOCKADDR_IN local,Remote,From; //分别指向本地、多播组和数据来源的IP地址与端口
int Fromlen;
char ReceiveBuf[32000]; //接收缓冲区
BOOL bDataReceived;
……
BEGIN_MESSAGE_MAP(CCMulticastSocketDlg, CDialog)
……
ON_BN_CLICKED(IDC_JOIN_BUTTON, OnBnClickedJoinButton)
ON_BN_CLICKED(IDC_LEAVE_BUTTON, OnBnClickedLeaveButton)
ON_BN_CLICKED(IDC_QUIT_BUTTON, OnBnClickedQuitButton)
ON_BN_CLICKED(IDC_SEND_BUTTON, OnBnClickedSendButton)
ON_WM_TIMER()
ON_MESSAGE(WM_SOCK_MSG,OnSocketMsg)
END_MESSAGE_MAP()
BOOL CCMulticastSocketDlg::OnInitDialog()
{
CDialog::OnInitDialog();
……
SetTimer(1,100,NULL); //设置定时器
Fromlen=sizeof(From);
bDataReceived=TRUE;
bJoin=FALSE;
return TRUE; // 除非设置了控件的焦点,否则返回 TRUE
}
……
void CCMulticastSocketDlg::OnBnClickedJoinButton() //加入多播组
{
if(m_JoinDlg.DoModal()==IDOK)
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2,2);
err = WSAStartup(wVersionRequested, &wsaData); //初始化WinSock2资源
if(err!= 0){
AfxMessageBox("不能加载Windows套接字动态链接库,MB_OK");
return;
}
if (LOBYTE(wsaData.wVersion) !=2 || HIBYTE(wsaData.wVersion) !=2){
AfxMessageBox("WinSock DLL不支持2.0版本,MB_OK");
WSACleanup( );
return;
}
//创建一个套接字
Sock=WSASocket(AF_INET,SOCK_DGRAM,IPPROTO_UDP,
(LPWSAPROTOCOL_INFO)NULL,0,WSA_FLAG_OVERLAPPED
| WSA_FLAG_MULTIPOINT_C_LEAF|WSA_FLAG_MULTIPOINT_D_LEAF);
bFlag=TRUE; //设置套接字选项,使套接字为可重用端口地址
setsockopt(Sock,SOL_SOCKET,SO_REUSEADDR,(char*)&bFlag,sizeof(bFlag));
//将套接字绑定到用户指定端口及默认的接口
memset(&local,0,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons((USHORT)m_JoinDlg.m_nPort);
local.sin_addr.s_addr=htonl(INADDR_ANY);
bind(Sock,(struct sockaddr FAR *)&local,sizeof(local));
//设置多播数据报传播范围(生存时间TTL)
WSAIoctl(Sock,SIO_MULTICAST_SCOPE,&m_JoinDlg.m_nTTL,sizeof(int),
NULL,0,&cbRet,NULL,NULL);
//设置多播返回(LOOKBACK)
BOOL nLoopBack=m_JoinDlg.m_Loopback;
WSAIoctl(Sock,SIO_MULTIPOINT_LOOPBACK,&nLoopBack,sizeof(nLoopBack),
NULL,0,&cbRet,NULL,NULL);
memset(&Remote,0,sizeof(Remote));
Remote.sin_family=AF_INET;
Remote.sin_addr.s_addr=inet_addr(m_JoinDlg.m_IPAddress);
Remote.sin_port=htons(m_JoinDlg.m_nPort);
//加入到指定的多播组,并指定为既作为发送者又作为接收者(JL_BOTH)
SockM=WSAJoinLeaf(Sock,(sockaddr*)&Remote,sizeof(Remote),
NULL,NULL,NULL,NULL,JL_BOTH);
WSAAsyncSelect(Sock,m_hWnd,WM_SOCK_MSG,FD_READ); //注册网络消息及其网络事件
bJoin=TRUE;
}
}
void CCMulticastSocketDlg::OnBnClickedSendButton() //多播发送
{
if(bJoin){
UpdateData(TRUE);
const char* strMessage=LPCTSTR(m_SendMessage);
int nSize=m_SendMessage.GetLength()+1;
sendto(Sock,strMessage,nSize,0,(sockaddr*)&Remote,sizeof(Remote));
}
else
AfxMessageBox("请先加入多播组!");
m_SendMessage="";
UpdateData(FALSE);
}
void CCMulticastSocketDlg::OnBnClickedLeaveButton()
{ //离开多播组
closesocket(SockM);
closesocket(Sock);
WSACleanup();
m_SendMessage="";
m_ReceiveMessage="";
bDataReceived=TRUE;
bJoin=FALSE;
UpdateData(FALSE);
}
void CCMulticastSocketDlg::OnBnClickedQuitButton()
{ //退出
DestroyWindow();
}
void CCMulticastSocketDlg::OnTimer(UINT nIDEvent) //定时器处理函数,实现接收
信息的定时更新
{
if(bDataReceived)
{
m_ReceiveMessage+=ReceiveBuf;
m_ReceiveMessage+="\r\n";
bDataReceived=FALSE;
UpdateData(FALSE);
}
CDialog::OnTimer(nIDEvent);
}
LRESULT CCMulticastSocketDlg::OnSocketMsg(WPARAM wParam,LPARAM lParam)
{
//检索网络事件
switch(WSAGETSELECTEVENT(lParam)){
case FD_READ:
recvfrom(Sock,ReceiveBuf,32000,0,(sockaddr *)&From,&Fromlen);
bDataReceived=TRUE; //设置已接收到一条信息标志
break;
}
return TRUE;
}