清华大学计算中心 蒋东兴
多址广播( multicast ,也译作多点传送或组播)是一种一对多的传输方式,传输发起者通过一次传输就将信息传送到一组接收者,与单点传送( unicast )和广播( broadcast )相对应。
多址广播使用最广泛的是 IP multicast ,它标准 IP 网络层协议的扩展,由 Steve Deering 定义的 Host Extensions for IP Multicasting ( RFC 1112 )奠定基础。 IP Multicasting 的定义为:到一个“主机组”的 IP 数据报的传送,主机组是由零个或多个用同一 IP 目的地址标识的主机集合。 Multicast 数据报被传递到其目的主机组的所有成员,并且同常规单点传送的 IP 数据报一样可靠。主机组的成员是动态的,也就是说,主机可以在任何时间加入或离开主机组。主机组中成员在位置上和数量上都没有限制,一个主机可以同时是一个以上主机组的成员。
在 Windows Sockets 1.1 中没有定义多址广播,因此,绝大多数 Windows Sockets 1.1 实现不支持多址广播。 Windows Sockets 2 为支持 IP multicast 而定义了一组新的与协议无关的多址广播应用程序接口,归纳起来可以用表 1 表示。
表 1 WinSock 2 的多址广播 API
WSAEnumProtocols() | 检测多址广播支持 |
WSASocket() | 指定多址广播类型 |
WSAJoinLeaf() | 加入一个多址广播组并指定角色(发送者和/或接收者) |
WSAIoctl() SIO_MULTICAST_SCOPE | 设置 IP 生存时间 |
WSAIoctl() SIO_MULTIPOINT_LOOPBACK | 禁止内部回送( loopback ) |
函数 WSAEnumProtocols() 返回当前系统中安装的协议的详细描述,这些信息存放在一个协议信息结构( WSAPROTOCOL_INFO )的数组中。在其中的域 dwServiceFlags1 中的一些标识指示此服务由 IP/UDP 协议提供,并且位标识 XP1_SUPPORT_MULTIPOINT 指示该服务支持 IP multicast 。
为了适用不同的多址广播模式, WinSock 2 定义了数据平面( data plane )和控制平面( control plane )两个概念,每一个平面都可以是“有根( rooted )”的或“无根( non-rooted )”的。关于这些概念的详细解释,参见 WinSock 2 规范附录 B 。 IP multicast 是一种无根的数据平面和控制平面。在使用 WSASocket() 函数请求一个多址广播套接字时需要指定这些角色。这通过在 WSASocket() 函数的参数 dwFlags 中使用四个标志位来实现:
· WSA_FLAG_MULTIPOINT_C_ROOT ,用来创建一个作为 c_root 节点的套接字,并且只有在相应的 WSAPROTOCOL_INFO 入口中指示了使用 rooted 控制平面时才允许。
· WSA_FLAG_MULTIPOINT_C_LEAF ,用来创建一个作为 c_leaf 节点的套接字,并且只有在相应的 WSAPROTOCOL_INFO 入口中指示了 XP1_SUPPORT_MULTIPOINT 时才允许。
· WSA_FLAG_MULTIPOINT_D_ROOT ,用来创建一个作为 d_root 节点的套接字,并且只有在相应的 WSAPROTOCOL_INFO 入口中指示了使用 rooted 数据平面时才允许。
· 用来创建一个作为 d_leaf 节点的套接字,并且只有在相应的 WSAPROTOCOL_INFO 入口中指示了 XP1_SUPPORT_MULTIPOINT 时才允许。
注意,在 IP multicast 中,只有标志 WSA_FLAG_MULTIPOINT_C_LEAF 和 WSA_FLAG_MULTIPOINT_D_LEAF 能用来作为 WSASocket() 函数的 dwFlags 参数。
WSAIoctl() 函数的 SIO_MULTIPOINT_LOOP 命令码 用来设置是否允许内部回送。当 d_leaf 套接字用于 non-rooted 数据平面时,它通常是希望能够控制发送出去的通信流量是否能够在同一个套接字上也被接收。 WSAIoctl() 函数的 SIO_MULTIPOINT_LOOP 命令码用来允许或禁止多址广播的通信流量的内部回送。
WSAIoctl() 函数的 SIO_MULTICAST_SCOPE 命令码用来设置多址广播范围。当使用多址广播时,常常需要指定多址广播传播的范围。范围由包括的路由网段来定义。范围为 0 指示多址广播不传播信息到“网线”上,但是可以在本地主机的多个套接字间传播。范围为 1 (默认值)指示将传播信息到“网线”上,但是不跨越路由器。更高的范围值决定了可以跨越的路由器数量。注意这相当于 IP multicast 的生存时间( time-to-live , TTL )。
WSAJoinLeaf() 函数用来加入一个叶子节点到多址广播会话,其函数原型为:
SOCKET WSAAPI WSAJoinLeaf ( SOCKET s , const struct sockaddr FAR * name , int namelen , LPWSABUF lpCallerData , LPWSABUF lpCalleeData , LPQOS lpSQOS , LPQOS lpGQOS , DWORD dwFlags );
WSAJoinLeaf() 具有与 WSAConnect() 相同的参数和语法,除了它还返回一个套接字描述符(这和函数 WSAAccept() 一样)及多了一个 dwFlags 参数外。参数 dwFlags 用来指示套接字是用来作为发送者还是接收者还是两者兼具。在此函数中,只有多址广播套接字可以用来作为输入参数 s 。如果此多址广播套接字处于非阻塞模式,返回的套接字描述符只有在接收到相应的 FD_CONNECT 指示后才能使用。参数 name 指示套接字将加入的多址广播的地址。
另外,在上面的 API 中没有提到如何离开一个多址广播组。唯一与协议无关的 API 是关闭套接字,即使用标准的 closesocket() 函数,它可以用来离开多址广播组。
Windows 95 不支持 IP multicast ,如果要在 Windows 95 上开发 IP multicast 程序,需要下载 WinSock 2 的软件开发包 WS295SDK ,这可以通过 Internet 免费下载。相关站点地址为: http://www.microsoft.com/win32dev/netwrk/winsock2/ws295sdk.html 。 Windows 98 支持 IP multicast ,如果使用 Windows 98 ,则不必运行其中的 ws2setup.exe 。
下面给出一个简单的例子说明 WinSock 2 多址广播程序的设计。
#include <winsock2.h>
#include <stdio.h>
#define BUFSIZE 1024
#define MAXADDRSTR 16
#define LOOPCOUNT 100
/* 检查系统中是否安装了合适版本的 WinSock DLL 。 */
int CheckWinsockVersion(VOID) {
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2); // 异步 I/O 和多址广播只有在 WinSock 2.0 以上版本才支持
err = WSAStartup(wVersionRequested, &wsaData);
if (err==0) {
if ((LOBYTE(wsaData.wVersion)==2) && (HIBYTE(wsaData.wVersion)==2))
// 确认 WinSock DLL 支持 2.0
return 0; /* WinSock DLL 可接受,成功返回 */
WSACleanup();
err = WSAVERNOTSUPPORTED; /* 不支持,失败返回 */
}
/* Tell the user that we couldn't find a usable WinSock DLL.*/
printf("WinSock DLL does not support requested API version./n");
return err;
}
int main() {
int nRet, i;
int nIP_TTL = 1; // 在本子网中传播。如果要跨路由器,则路由器必须支持 IGMP 协议
BOOL bFlag;
DWORD dFlag, cbRet;
int iLen = MAXADDRSTR;
char strDestMulti[MAXADDRSTR] = "224.1.1.1"; // 多址广播地址范围从 224 到 239 。
SOCKADDR_IN stSrcAddr, stDestAddr;
SOCKET hSock, hNewSock;
u_short nDestPort = 6666;
WSABUF stWSABuf;
char achInBuf [BUFSIZE];
char achOutBuf[] = "Message number: ";
nRet = CheckWinsockVersion(); // 检查 WinSock 版本号
if (nRet) {
printf ("WSAStartup failed: %d/r/n", nRet);
exit (1);
}
// 将字符串地址转换为套接字地址
nRet = WSAStringToAddress ( strDestMulti, /* address string */
AF_INET, /* address family */
NULL, /* protocol info structure */
(LPSOCKADDR)&stDestAddr, /* socket address string */
&iLen); /* length of socket structure */
if (nRet) {
printf ("WSAStringToAddress(%s) failed, Err: %d/n", strDestMulti, WSAGetLastError());
exit(1);
}
// 创建一个多址广播套接字
hSock = WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP,
(LPWSAPROTOCOL_INFO)NULL, 0, WSA_FLAG_OVERLAPPED
| WSA_FLAG_MULTIPOINT_C_LEAF | WSA_FLAG_MULTIPOINT_D_LEAF);
if (hSock == INVALID_SOCKET) {
printf ("WSASocket() failed, Err: %d/n", WSAGetLastError());
exit (1);
}
bFlag = TRUE; // 设置套接字为可重用端口地址
nRet = setsockopt(hSock, /* socket */
SOL_SOCKET, /* socket level */
SO_REUSEADDR, /* socket option */
(char *)&bFlag, /* option value */
sizeof (bFlag)); /* size of value */
if (nRet == SOCKET_ERROR)
printf("setsockopt() SO_REUSEADDR failed, Err: %d/n", WSAGetLastError());
// 将套接字绑扎到用户指定端口及默认的接口
stSrcAddr.sin_family = PF_INET;
stSrcAddr.sin_port = htons (nDestPort);
stSrcAddr.sin_addr.s_addr = INADDR_ANY;
nRet = bind (hSock, (struct sockaddr FAR *)&stSrcAddr, sizeof(struct sockaddr));
if (nRet == SOCKET_ERROR)
printf ("bind failed, Err: %d/n", WSAGetLastError());
// 设置多址广播数据报传播范围( TTL )
nRet = WSAIoctl (hSock, /* socket */
SIO_MULTICAST_SCOPE, /* IP Time-To-Live */
&nIP_TTL, /* input */
sizeof (nIP_TTL), /* size */
NULL, /* output */
0, /* size */
&cbRet, /* bytes returned */
NULL, /* overlapped */
NULL); /* completion routine */
if (nRet)
printf ("WSAIoctl(SIO_MULTICAST_SCOPE) failed, Err: %d/n", WSAGetLastError());
// 允许内部回送( LOOPBACK )。 Windows 95 不支持改选项
bFlag = TRUE;
nRet = WSAIoctl (hSock, /* socket */
SIO_MULTIPOINT_LOOPBACK, /* LoopBack on or off */
&bFlag, /* input */
sizeof (bFlag), /* size */
NULL, /* output */
0, /* size */
&cbRet, /* bytes returned */
NULL, /* overlapped */
NULL); /* completion routine */
if (nRet)
printf("WSAIoctl(SIO_MULTIPOINT_LOOPBACK) failed, Err: %d/n", WSAGetLastError());
stDestAddr.sin_family = PF_INET;
nRet = WSAHtons( hSock, /* socket */
nDestPort, /* host order value */
&(stDestAddr.sin_port)); /* network order value */
if (nRet == SOCKET_ERROR)
printf("WSAHtons() failed, Err: %d/n", WSAGetLastError());
// 加入到指定多址广播组,指定为既作发送者又作接收者
// 在 IP multicast 中,返回的套接字描述符和输入的套接字描述符相同。
hNewSock = WSAJoinLeaf (hSock, /* socket */
(PSOCKADDR)&stDestAddr, /* multicast address */
sizeof (stDestAddr), /* length of addr struct */
NULL, /* caller data buffer */
NULL, /* callee data buffer */
NULL, /* socket QOS setting */
NULL, /* socket group QOS */
JL_BOTH); /* do both: send *and* receive */
if (hNewSock == INVALID_SOCKET)
printf ("WSAJoinLeaf() failed, Err: %d/n", WSAGetLastError());
// 在循环中发送/接收数据。测试时可以改为无限循环
for (i=0;i<LOOPCOUNT;i++) {
static iCounter = 1;
stWSABuf.buf = achOutBuf;
stWSABuf.len = lstrlen(achOutBuf);
cbRet = 0;
itoa(iCounter++, &achOutBuf[16], 10);
nRet = WSASendTo (hSock, /* socket */
&stWSABuf, /* output buffer structure */
1, /* buffer count */
&cbRet, /* number of bytes sent */
0, /* flags */
(struct sockaddr*)&stDestAddr, /* destination address */
sizeof(struct sockaddr), /* size of addr structure */
NULL, /* overlapped structure */
NULL); /* overlapped callback function */
if (nRet == SOCKET_ERROR)
printf("WSASendTo() failed, Err: %d/n", WSAGetLastError());
stWSABuf.buf = achInBuf;
stWSABuf.len = BUFSIZE;
cbRet = 0;
iLen = sizeof (stSrcAddr);
dFlag = 0;
nRet = WSARecvFrom (hSock, /* socket */
&stWSABuf, /* input buffer structure */
1, /* buffer count */
&cbRet, /* number of bytes recv'd */
&dFlag, /* flags */
(struct sockaddr *)&stSrcAddr /* source address */
&iLen, /* size of addr structure */
NULL, /* overlapped structure */
NULL); /* overlapped callback function */
if (nRet == SOCKET_ERROR)
printf("WSARecvFrom() failed, Err:%d/n", WSAGetLastError());
else {
u_short nPort = 0;
char achAddr[MAXADDRSTR+3] = {0}; // 如果长度太小则返回 Windows 错误 122
iLen = MAXADDRSTR+3;
nRet = WSAAddressToString( // 将地址转换为字符串
(struct sockaddr *)&stSrcAddr, /* source address */
sizeof(stSrcAddr), /* size of addr struct */
NULL, /* protocol info */
achAddr, /* address string */
&iLen); /* addr string buf len */
if (nRet == SOCKET_ERROR)
printf("WSAAddressToString() failed, Err: %d/n", WSAGetLastError());
nRet = WSANtohs(hSock, /* socket */
stSrcAddr.sin_port, /* host order value */
&nPort); /* network order value */
if (nRet == SOCKET_ERROR)
printf("WSANtohs() failed, Err: %d/n", WSAGetLastError());
printf ("WSARecvFrom() received %d bytes from %s, port %d :%s/r/n",
cbRet, achAddr[0] ? achAddr : "??", nPort, achInBuf);
}
} /* end for(;;) */
closesocket(hNewSock);
closesocket(hSock);
// 卸载 WinSock DLL
WSACleanup();
return (0);
} /* end main() */