原文在这里:http://blog.youkuaiyun.com/raoxuanxuan/article/details/20012931
最近项目的需要,了解了一下UDP多播与广播。
1. UDP多播
UDP多播是这样子的:老师要给班上学生通知消息,他要么一个个地给每个同学去说(这就是单播),要么他写在黑板上,每个同学进教室都可以看得见。后者就是多播,即说有一台主机(老师)往一个地址上(黑板上)发数据,可以有一群客户端(班上学生)同时去那个地址上拿数据(看黑板),就不用发送主机一个一个地往这些客户端所在的地址上去发了,那么自然会节约很多主机资源和网络资源。并不是随意一个地址就可以进行多播的,多播的地址是固定了的,只有224.0.0.0到239.255.255.255这一个范围,其中这些范围的地址又做了进一步划分,具体可以去Google一下。OK, 大概了解了多播这个东西,我们来看看代码:
- #include <winsock.h>
- #include <iostream>
- #include <string>
- using namespace std;
- #pragma comment(lib,"WSOCK32.lib")
- #define MULTICAST_IP "239.239.1.1"
- #define MULTICAST_PORT 10086
- int _tmain(int argc, _TCHAR* argv[])
- {
- //启动winsock环境
- WSADATA wsd;
- if(WSAStartup(MAKEWORD(2,2), &wsd) != 0)
- {
- cerr<<"Could not open Windows Connection.\n"<<endl;
- return 1;
- }
- //创建一个UDP Socket
- SOCKET clientSocket;
- clientSocket = socket(AF_INET, SOCK_DGRAM, 0);
- if(clientSocket == INVALID_SOCKET)
- {
- cerr<<"Could not create socket.\n"<<endl;
- WSACleanup();
- return 1;
- }
- //创建地址并绑定到该Socket上
- sockaddr_in clientAddress;
- memset(&clientAddress, 0, sizeof(sockaddr_in));
- clientAddress.sin_family = AF_INET;
- clientAddress.sin_addr.s_addr = INADDR_ANY;
- clientAddress.sin_port = htons(MULTICAST_PORT);
- if (bind(clientSocket, (struct sockaddr *) &clientAddress, sizeof(clientAddress)) < 0)
- {
- cerr<<"Could not bind socket to a sockaddr_in"<<endl;
- return 1;
- }
- //加入多播组
- struct ip_mreq mreq;
- mreq.imr_multiaddr.s_addr = inet_addr(MULTICAST_IP);
- mreq.imr_interface.s_addr = INADDR_ANY;
- if (setsockopt(clientSocket, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char *)&mreq, sizeof(mreq)) < 0)
- {
- int err = GetLastError();
- cerr<<"Could not add into MultiCast Group.\n"<<endl;
- return 1;
- }
- //接收数据
- char buff[20] = {0};
- while (1)
- {
- //ssize_t recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
- int addrlen = sizeof(sockaddr);
- int nBytes = 0;
- if((nBytes = recvfrom(clientSocket, buff, 20, 0,NULL,NULL)) < 0)
- {
- cerr<<"receive from multicast group failed"<<endl;
- return 1;
- }
- buff[nBytes] ='\0';
- cout<<"Client Receive : "<<buff<<endl;
- }
- return 0;
- }
- 让客户端加入到多播组中,即用函数setsockopt(fd,IPPROTO_IP,IP_ADD_MEMBERSHIP,(char *)&mreq,sizeof(mreq),即可加入到ip_mrep结构体指明的多播地址中。
- 让客户端把socket绑定到服务器发送所用的端口的地址中。
- #define MULTICAST_IP "239.239.1.1"
- #define MULTICAST_PORT 10086
- #include <WinSock2.h>
- #include <iostream>
- #include <string>
- using namespace std;
- #pragma comment(lib,"ws2_32.lib")
- int _tmain(int argc, _TCHAR* argv[])
- {
- WSADATA wsd;
- if(WSAStartup(MAKEWORD(2,2), &wsd) != 0)
- {
- cerr<<"Could not open Windows Connection.\n"<<endl;
- return 1;
- }
- SOCKET serverSocket;
- serverSocket = socket(AF_INET, SOCK_DGRAM, 0);
- if(serverSocket == INVALID_SOCKET)
- {
- cerr<<"Could not create socket.\n"<<endl;
- WSACleanup();
- return 1;
- }
- sockaddr_in serverAddress;
- memset(&serverAddress, 0, sizeof(sockaddr_in));
- serverAddress.sin_family = AF_INET;
- serverAddress.sin_addr.s_addr = inet_addr(MULTICAST_IP);
- serverAddress.sin_port = htons(MULTICAST_PORT);
- std::string message = "Hello World!";
- while(1)
- {
- int error = sendto(serverSocket, message.c_str(), message.length(), 0, (sockaddr*)&serverAddress, sizeof(sockaddr));
- if(error < 0)
- {
- cerr<<"Send Message Failed..."<<endl;
- return 1;
- }
- cout<<"Server Send : " << message << endl;
- Sleep(1000);
- }
- return 0;
- }
注意到,在一般情况下,只有服务器的socket需要做bind操作,因为这样客户端的socket才能够知道要访问的服务器的IP和端口号。而客户端的socket是不需要bind操作的,协议栈会隐式地自动生成一个可用端口号再加上客户端本机的IP地址bind到其socket上。但是多播这里不同,服务器socket没有bind,客户端socket必须要bind到一个地址上,这个地址的端口号是服务器多播的发送端口号(IP随意,一般就是INADDR_ANY,表示由协议栈来完成bind)。我们可以这样理解这个行为:其实服务器并不是发送数据到x.x.x.255这个地址,这个地址是虚拟的,这个地址就相当于发送到x.x.x.1~x.x.x.254这一堆地址上,加上端口号,服务器发送到这一堆地址的指定端口号上,所以如果客户端socket不绑定到这个端口号上就接收不到了。而服务器自己的IP和端口则无所谓(如果客户端不需要发送数据到服务器)。
2. UDP广播
接下来是UDP广播,UDP广播的话分两种,一种是广播地址是255.255.255.255,一种是广播到x.x.x.255。前面一种是受限的广播,路由器不会转发,局域网里所有主机都收得到;后一种是子网的广播,路由器会转发到该子网的所有主机上,不过必须要保证子网掩码的正确(因为它是子网的广播嘛)。例子就是,如果某个客户端A的IP是192.168.1.10,看起来服务器广播到192.168.1.255时,客户端A可以收到,但是如果A的子网掩码不是255.255.255.0,则A就收不到,因为它不是192.168.1. 这个子网的。
- #include <WinSock2.h>
- #include <iostream>
- #include <string>
- using namespace std;
- #pragma comment(lib,"ws2_32.lib")
- int _tmain(int argc, _TCHAR* argv[])
- {
- WSADATA wsd;
- if(WSAStartup(MAKEWORD(2,2), &wsd) != 0)
- {
- cerr<<"Could not open Windows Connection.\n"<<endl;
- return 1;
- }
- SOCKET serverSocket;
- serverSocket = socket(AF_INET, SOCK_DGRAM, 0);
- if(serverSocket == INVALID_SOCKET)
- {
- cerr<<"Could not create socket.\n"<<endl;
- WSACleanup();
- return 1;
- }
- bool broadCast;
- int optLen = sizeof(broadCast);
- if(SOCKET_ERROR == setsockopt(serverSocket, SOL_SOCKET, SO_BROADCAST, (char*)&broadCast, optLen))
- {
- cerr<<"Could not Set BroadCast socket.\n"<<endl;
- closesocket(serverSocket);
- WSACleanup();
- return -1;
- }
- sockaddr_in localAddress;
- memset(&localAddress, 0,sizeof(localAddress));
- localAddress.sin_family = AF_INET;
- localAddress.sin_port = htons(10086);
- localAddress.sin_addr.s_addr = inet_addr("172.31.8.255");
- //localAddress.sin_addr.s_addr = INADDR_BROADCAST;
- string message = "hello world!";
- int server_length = (int)sizeof(sockaddr_in);
- int r;
- while (1)
- {
- r = sendto(serverSocket, message.c_str(), message.length(),0,(sockaddr*)&localAddress,sizeof(localAddress));
- if(r < 0)
- break;
- else
- {
- cout<<"Server Send : "<<message<<endl;
- Sleep(1000);
- }
- }
- return 0;
- }
以上是一个UDP广播的服务器代码,其关键之处在于:
- 要打开socket的广播属性,即setsockopt(serverSocket, SOL_SOCKET, SO_BROADCAST, (char*)&broadCast, optLen)
- 发送数据到设置了广播IP的地址中。在代码中可以看见,我的本机IP是172.31.8.41,子网掩码是255.255.255.0,所以我是172.31.8这个子网的,那么服务器发送到172.31.8.255这个广播IP上,就相当于发给我们实验室172.31.8.这个网段的所有小伙伴;另外,注释起来的这句话localAddress.sin_addr.s_addr = INADDR_BROADCAST; 即发送到受限的广播地址255.255.255.255,INADDR_BROADCAST是WinSock定义的宏,就代表这个地址。
- #include <WinSock2.h>
- #include <iostream>
- #include <string>
- using namespace std;
- #pragma comment(lib,"ws2_32.lib")
- int _tmain(int argc, _TCHAR* argv[])
- {
- WSADATA wsd;
- if(WSAStartup(MAKEWORD(2,2), &wsd) != 0)
- {
- cerr<<"Could not open Windows Connection.\n"<<endl;
- return 1;
- }
- SOCKET clientSocket;
- clientSocket = socket(AF_INET, SOCK_DGRAM, 0);
- if(clientSocket == INVALID_SOCKET)
- {
- cerr<<"Could not create socket.\n"<<endl;
- WSACleanup();
- return 1;
- }
- sockaddr_in clientAddress;
- memset(&clientAddress, 0,sizeof(clientAddress));
- clientAddress.sin_family = AF_INET;
- clientAddress.sin_port = htons(10086);
- clientAddress.sin_addr.s_addr = INADDR_ANY;
- if(-1 == bind(clientSocket, (sockaddr*)&clientAddress,sizeof(sockaddr)))
- {
- cerr<<"Could not bind socket.\n"<<endl;
- closesocket(clientSocket);
- WSACleanup();
- return -1;
- }
- char buff[20] = {0};
- int serverLength = sizeof(clientAddress);
- int r;
- while (1)
- {
- r = recvfrom(clientSocket, buff, 20, 0, NULL,NULL);
- if(r < 0)
- return 1;
- else
- {
- cout<<"Client Receive : "<<buff<<endl;
- }
- }
- return 0;
- }
总结:
- UDP多播和UDP广播都必须把客户端socket绑定到一个地址上,这个地址指明了服务端发送的端口号。
- UDP多播的客户端需要加入多播组才能收到数据;服务端则只需要向指定的多播IP和端口发送数据即可。
- UDP广播的服务端需要打开广播属性,然后才能向指定的广播IP和端口发送数据;客户端则只需要从那个端口接收即可。
补充一点:
在我的测试中,如果两台机器用网线直接相连,那么UDP子网广播和受限广播都是正常的。但是在实验室环境下,在一台主机上发,另一台主机上收,子网广播是OK的,但是受限广播就不可以。这可能是路由器不转发受限广播的缘故吧。