os:花了一周时间实现了UDP协议下的组播程序,上面是网上给出的一般组播实现的架构。2~4是针对socketopt即socket属性的设置;TTL设置超时时间即每次发送数据之间的间隔,必须有,但不一定用soketopt实现,下文会细说。LOOP是设置回环,一开始不明白什么意思,之后看师兄做了测试,如果不开启回环则发送的机器将不会接收到自己发送的数据。(LOOP默认开启)ADD是加入多播组,这一步很关键,相当于把所加入的播组ip和本机ip绑定到一起。
一,我需要准备什么
之前做过点对点的基于MFC的UDP和TCP程序,这次直接用API编写,有一些地方不太一样,首先是不能用MFC提供的socket组件alsocket了,直接用API编程必做的链接是
#include
#include
using namespace std;
#pragma comment (lib,"wsock32.lib")
当然要想实现组播功能#include
是必不可少的。下文会对它简单介绍。
二,我想要做什么
用英语都写不出来的东西就别指望用代码写了。在写组播程序之前,需要先了解组播和之前的点对点通信的区别和相同点在哪里。 首先TCP协议是面向连接的,可以通过多次点对点通信实现组播,但这不是我们需要的。相比之下UDP协议提供了组播功能和相应的函数。
通过之后的编写,我发现称UDP组播过程中双方只有接收方的socket绑定了端口。也就是说组播的发送端(所谓服务器)使用的是未命名端口,而接受端(所谓客户端)则需要绑定端口,这和之前的经验恰恰相反。于是我又重新查看了bind()函数的定义:
bind()函数并不是总是需要调用的,只有用户进程想与一个具体的地址或端口相关联的时候才需要调用这个函数。如果用户进程没有这个需要,那么程序可以依赖内核的自动的选址机制来完成自动地址选择,而不需要调用bind()函数,同时也避免不必要的复杂度。在一般情况下,对于服务器进程问题需要调用bind()函数,对于客户进程则不需要调用bind()函数。
从这段解释我们不难得到几个观点:1,sokcet必须要绑定己方地址才能工作,但是win提供了自动绑定机制,如果不需要知道己方socket的地址和端口,则无需手动bind。2,与在create时bind的CSocket不同,socket函数只能选择地址族而不绑定。3,是否bind与客户端还是服务器无关,而与发送或接收有关,如果一个程序只需要发送,那么无需手动bind交给系统分配就好,如果程序需要接受信息,那么必须bind固定端口和地址,以保证发送方可以正确寻址。
经过上面的分析可以总结一下UDP组播程序的一般编写流程:
发送端:
创建socket(选定地址族和协议版本等),设置socketopt为允许广播,设置socketopt中的IP_MULTICAST_TTL(需包含ws2tcpip头文件,或用Sleep函数实现),设置socketopt允许回环(默认允许,可省略),设置发往的组播ip和端口号,调用sendto()函数,关闭socket。
接收端:
创建socket(选定地址族和协议版本等),绑定本地ip和端口号【注意:这一步很重要,此时接收端绑定的ip只要是本机ip就可以了,而不是发送端发往的ip,但是端口号一定要是发送端发往的端口号。】,
设置socketopt允许回环(默认允许,可省略),设置socketopt中的IP_MULTICAST_TTL(需包含ws2tcpip头文件,或用Sleep函数实现),设置socketopt,“绑定”要加入的组播地址和本机地址【此处不用再设置端口了,之前没有理解这一步的意义将本地ip写成了本地端口致错】,recvfrom接收【没错,from的地址不是组播地址也不是发送方地址而是本机地址】,退出播组,关闭socket。
三,我要怎么做
直接用winAPI和win控制台来编写这个程序,网上linux的组播代码很多,基于win32的确不多。考虑到代码段不长,于是干脆自己写一个。
按照上面的流程操作即可,需要注意的有两点。一个是setsockopt
函数中第四个参数需要强制类型转换我用的是reinterpret_cast
实现。第二点比较坑,就是是会有一个warning导致编译不!通!过!
error C4996: 'inet_addr': Use inet_pton() or InetPton() instead or define _WINSOCK_DEPRECATED_NO_WARNINGS to disable deprecated API warnings
1> c:\program files (x86)\windows kits\8.1\include\um\winsock2.h(1850) : 参见“inet_addr”的声明
就是上面这个,用vs2013调用inet_addr
函数的话就会报错,微软希望你使用inet_pton这个新函数。要想用这个新函数需要包含一个新的头文件,而且用法和inet_addr也不大一样。网上给出了一个便捷的解决方法参考:
http://jingyan.todgo.com/shuma/1567699qmh.html
这位老哥给出了关闭项目属性中sdl检查的方法,关闭之后编译通过。
四,代码
发送端
#include
#include
using namespace std;
#pragma comment (lib,"wsock32.lib")
#define MCAST_PORT 8888
#define MCAST_ADDR "224.0.0.88" // 多播地址
#define MCAST_DATA "BROADCAST TEST DATA" // 多播内容
#define MCAST_INTERVAL 5000 //多播时间间隔
using namespace std;
void main()
{
SOCKET sock;
struct sockaddr_in mcast_addr;
WORD wVersionRequested = MAKEWORD(2,0);
WSADATA wsaData;
int err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
cout << err << endl;
cout << "socket版本初始化失败" << endl;
}
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 0)
{
WSACleanup();
return;
}
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock == INVALID_SOCKET)
{
cout << "socket创建失败" << endl;
cout << WSAGetLastError() << endl;
return;
}
memset(&mcast_addr, 0, sizeof(mcast_addr));
bool opt = 1;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, reinterpret_cast(&opt), sizeof(opt));
mcast_addr.sin_family = AF_INET;
mcast_addr.sin_addr.s_addr = inet_addr(MCAST_ADDR); //inet_npton
mcast_addr.sin_port = htons(MCAST_PORT);
while (1)
{ //向局部多播地址发送多播内容
int n = sendto(sock, MCAST_DATA, sizeof(MCAST_DATA), 0, (struct sockaddr*)&mcast_addr, sizeof(mcast_addr));
if (n<0)
{
cout << "send error" << endl;
return ;
}
else
{
cout << "send message is going ...." << endl;
}
Sleep(MCAST_INTERVAL);
}
if (!closesocket(sock)) //关闭套接字
{
WSAGetLastError();
return;
}
if (!WSACleanup()) //关闭Socket库
{
WSAGetLastError();
return;
}
}
接收端
#include
#include
#pragma comment(lib,"Ws2_32.lib")
#include
using namespace std;
#define MCAST_PORT 8888
#define MCAST_ADDR "224.0.0.88"
#define LOCAL_ADDR "172.31.171.89"
#define MCAST_INTERVAL 5000
#define MCAST_SIZE 1024
void main()
{
SOCKET sock;
WORD wVersionRequested = MAKEWORD(2,0);
WSADATA wasData;
sockaddr_in local_addr;
int err = WSAStartup(wVersionRequested, &wasData);
if (err != 0)
{
cout << err << endl;
cout << "套接字版本错误" << endl;
return;
}
sock = socket(AF_INET, SOCK_DGRAM, 0);
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_port = htons(MCAST_PORT);
local_addr.sin_addr.s_addr = inet_addr(LOCAL_ADDR);
err = bind(sock, (struct sockaddr*)&local_addr, sizeof(local_addr));
if (err < 0)
{
cout << "bind error" << endl;
cout << err << endl;
return;
}
bool loop = 1;
err = setsockopt(sock,IPPROTO_IP, IP_MULTICAST_LOOP, reinterpret_cast(&loop), sizeof(loop));
if (err<0)
{
cout << "set sock error" << endl;
return ;
}
struct ip_mreq mreq;
mreq.imr_multiaddr.S_un.S_addr = inet_addr(MCAST_ADDR);
mreq.imr_interface.s_addr = inet_addr(LOCAL_ADDR);
err = setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, reinterpret_cast(&mreq), sizeof(mreq));
if (err< 0)
{
cout << err << endl;
cout << "set sock error2" << endl;
return;
}
socklen_t addr_len = 0;
int times = 0;
char buff[MCAST_SIZE];
int n = 0;
for (times = 0;times <= 5; times++)
{
addr_len = sizeof(local_addr);
memset(buff, 0, MCAST_SIZE);
n = recvfrom(sock, buff, MCAST_SIZE, 0, (struct sockaddr*)&local_addr, &addr_len);
if (n == -1)
{
cout << "cont recv" << endl;
times = 0;
return;
}
cout << times+1 <<" "<< buff << endl;
Sleep(MCAST_INTERVAL);
}
err = setsockopt(sock, IPPROTO_IP, IP_DROP_MEMBERSHIP, reinterpret_cast(&mreq), sizeof(mreq));
closesocket(sock);
return;
}