基于Winsock的UDP组播通信入门

您所在的位置:网站首页 udp组播原理 基于Winsock的UDP组播通信入门

基于Winsock的UDP组播通信入门

2023-09-06 04:56| 来源: 网络整理| 查看: 265

        在实际工程中,网络通信是是多个设备通信的一种常用手法。这些设备在本地组成一个局域网。为了实现高效的信息共享,这些设备还会形成组播组。当一个设备向组播地址和固定端口发送数据时,组播组内的所有设备均能收到数据,不仅简化了通信过程,还提高了效率。

        本文将介绍Windows平台下基于Winsock的UDP组播通信技术。

一、基本概念[1] 1.UDP

        UDP即用户数据报协议(UDP,User Datagram Protocol),无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。

        UDP的主要特点包括:

(1)无连接的,即通信时不需要创建连接(发送数据结束时也没有连接可以释放)所以减小了开销和发送数据前的时延;

(2)采用最大努力交付,不保证可靠交付,因此主机不需要维护复杂的连接状态;

(3)无阻塞控制,即使网络中存在阻塞,也不会影响发送端的发送频率

(4)支持一对一、一对多、多对一、多对多的交互通信

        UDP报文结构如图1所示,报文头包括源端口、目的端口、长度和校验值等4个字段。

源端口:这个字段占据 UDP 报文头的前 16 位,通常包含发送数据报的应用程序所使用的 UDP 端口。接收端的应用程序利用这个字段的值作为发送响应的目的地址。这个字段是可选的,所以发送端的应用程序不一定会把自己的端口号写入该字段中。如果不写入端口号,则把这个字段设置为 0。这样,接收端的应用程序就不能发送响应了。

目的端口:接收端计算机上 UDP 软件使用的端口,占据 16 位。

长度:该字段占据 16 位,表示 UDP 数据报长度,包含 UDP 报文头和 UDP 数据长度。因为 UDP 报文头长度是 8 个字节,所以这个值最小为 8。

校验值:该字段占据 16 位,可以检验数据在传输过程中是否被损坏。

图1 UDP报文结构

2.组播通信

        UDP通信包括单播、多播、广播三种模式。多播又称为“组播”,是本文介绍的主题。

图2 UDP通信模式

        单播即网络中的两个主机之间点对点地通信[4]。广播为一个主机对整个局域网上所有主机上的数据通信。组播介于二者之间,是一台或多台主机对一组特定的主机进行通信,而不是所有局域网上的主机。

        组播通信必须依赖于 IP 多播地址,在 IPv4 中它是一个 D 类 IP 地址,范围从 224.0.0.0 到 239.255.255.255.

        使用同一个 IP 多播地址接收多播数据包的所有主机构成了一个主机组,也称为多播组。一个多播组的成员是随时变动的,一台主机可以随时加入或离开多播组,多播组成员的数目和所在的地理位置也不受限制,一台主机也可以属于几个多播组。

        多播地址就类似于微信群号,多播组相当于微信群,一个个的主机就相当于群里面的成员。每个主机都有自己的IP地址,相同多播组的主机还有对应的多播组地址。

3.Winsock

        Winsock是Windows平台下的一种标准API,用于两个或多个应用程序(或进程)之间通过网络进行数据通信。主要有Winsock 1和Winsock 2等两个版本。在本文中使用的Winsock 2包含的头文件为WinSock2.h,对应的库为ws2_32.lib.

        Winsock初始化过程:首先确保包含对应版本的头文件,然后保证链接对应的库文件(可以在代码中使用#pragma comment(lib, "WS2_32"),或在编译器项目属性中链接器->输入->附加依赖项中添加ws2_32.lib);接着通过调用WSAStartup函数来实现加载Winsock库。

        使用Winsock结束后,需要释放资源,并取消应用程序挂起的Winsock操作。使用 WASCleanup()。

二、基于Winsock的UDP组播通信 1.组播通信原理[1]

        组播通信包括发送端和接收端。发送端将数据发送到组播地址和固定端口上;接收端在本地绑定对应的固定端口,然后加入到组播的群组,最终实现数据的共享。

图3 组播通信的发送端和接收端

2.组播通信设置方法

(1)在window平台下,创建套接字前,要先初始化套接字windows socket api(初始化Winsock)。

(2)创建地址结构保存组播地址。

(3)接收端和发送端的设置有所差异。         对于接收端,要绑定“特定的”本地端口接收数据,端口号由组播通信协议事先约定,组播组中所有设备均使用该端口号接收数据;同时,在接收数据前需要设置端口加入组播,不再接收数据时需要设置端口退出组播。         对于发送端,只需要向组播地址发送数据即可。不需要设置加入组播,绑定特定端口也是非必须的(此时使用的是默认端口)。

(4)接收端设置方法: (4.1)绑定本地端口。对于接收端,要用bind()将套接字与本地端口绑定,端口号要与事先约定的组播通信一致。

(4.2)加入组播。用setsockopt()将套接字与组播地址关联,套接字选项为IP_ADD_MEMBERSHIP。

(4.3) 接收数据。接收端用recvfrom()接收数据。

(4.4) 退出组播。用setsockopt()设置退出组播,套接字选项为IP_DROP_MEMBERSHIP。

(5)发送端设置方法: 对于发送端,不需要将套接字与组播地址关联。只需要用sendto()通过套接字向特定组播地址发送数据。

(6)结束通信后,关闭套接字api.

三、例程 1.接收端

        接收端例程的思路为设置一个循环,用于接收指定组播地址("224.168.0.1")的数据,需要绑定的本地端口号为7777。收到的数据被存储到特定文件中。当数据存储发生错误时跳出循环、结束组播接收。

/* # Copyright By Schips, All Rights Reserved # 代码链接来源:https://www.cnblogs.com/huty/p/8517279.html # # File Name: group_client.c # 功能:组播客户端 */ #include #include #include #pragma comment(lib,"ws2_32.lib") #include #include int main() { //1.windows下要先初始化套接字 WSADATA wsaData; if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) { printf("初始化套接字失败!\n"); return -1; } printf("初始化套接字成功!\n"); //2.建立客户端SOCKET SOCKET client; client=socket(AF_INET,SOCK_DGRAM,0); if(client==INVALID_SOCKET) { printf("建立客户端套接字失败; %d\n",WSAGetLastError()); WSACleanup(); return -1; } printf("建立客户端套接字成功!\n"); sockaddr_in serveraddress; //服务器地址,该变量用于存储recvfrom()捕获的数据发送源的地址 //3.绑定端口(接收端需要绑定,发送端不需要绑定) client=socket(AF_INET,SOCK_DGRAM,0); // 接收组播socket sockaddr_in AddrClient; AddrClient.sin_family=AF_INET; AddrClient.sin_addr.s_addr=INADDR_ANY; //任意 AddrClient.sin_port=htons(7777);//组播监听端口号,需根据实际组播端口号设定 int ret = bind(client,(sockaddr *)&AddrClient,sizeof(sockaddr_in)); if(ret==SOCKET_ERROR) { printf("绑定广播接收端口1错误!\n"); } //4.加入组播 struct ip_mreq mreq; memset(&mreq,0,sizeof(struct ip_mreq)); mreq.imr_multiaddr.S_un.S_addr=inet_addr("224.168.0.1"); //组播源地址 mreq.imr_interface.S_un.S_addr=INADDR_ANY; //本地地址 // 将要添加到多播组的 IP,类似于 成员号 int m=setsockopt(client,IPPROTO_IP,IP_ADD_MEMBERSHIP,(char FAR *)&mreq,sizeof(mreq)); //关键函数,用于设置组播、链接组播。此时套接字client已经连上组播 if(m==SOCKET_ERROR) { perror("setsockopt"); return -1; } //创建接收数据缓冲区 char recvbuf[1000000]; //回头注意重新设定缓冲区大小 int n; DWORD dwWrite; //DWORD在windows下常用来保存地址(或者存放指针) BOOL bRet; int len=sizeof(sockaddr_in); //创建文件 HANDLE hFile=CreateFile(_T("接收数据.txt"),GENERIC_WRITE,0,0,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0); if(hFile!=INVALID_HANDLE_VALUE) { printf("创建文件成功!\n"); } while(1) { n=recvfrom(client,recvbuf,sizeof(recvbuf),0,(sockaddr*)&serveraddress,&len); //从组播socket接收数据 if(n==SOCKET_ERROR) { printf("recvfrom error:%d\n",WSAGetLastError()); printf("接收数据错误!\n"); } //将接收到的数据写到hFile中 bRet=WriteFile(hFile,recvbuf,n,&dwWrite,NULL); //如果有写数据错误,则跳出循环;否则一直接收数据 if(bRet==FALSE) { MessageBox(NULL,_T("Write Buf ERROR!"),_T("ERROR"),MB_OK); break; } } //传送成功 MessageBox(NULL,_T("Receive file OK!"),_T("OK"),MB_OK); closesocket(client); WSACleanup(); return 0; } 2.发送端

        发送端例程的思路为设置一个循环,一直向特定组播地址("224.1.1.1")和端口(65000)发送数据。

/* # 代码来源链接:https://www.cnblogs.com/huty/p/8517279.html # File Name: group_server.c */ #include #include #include #pragma comment(lib,"ws2_32.lib") #include #include const int MAX_BUF_LEN = 255; int main(int argc, char* argv[]) { WORD wVersionRequested; WSADATA wsaData; int err; // 1.启动初始化套接字 wVersionRequested = MAKEWORD( 2, 2 ); err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return -1; } if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 ) { WSACleanup( ); return -1; } // 2.创建socket SOCKET connect_socket; connect_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if(INVALID_SOCKET == connect_socket) { err = WSAGetLastError(); printf("socket error! error code is %d/n", err); return -1; } // 2.1 获取主机名称和ip地址(非必须) char hostname[128]; struct hostent*pHost; if(gethostname(hostname,128)==0) // gethostname() 返回本地主机的标准主机名 { printf("%s\n",hostname);//计算机名字 } pHost = gethostbyname(hostname); // 用hostname获取host,返回对应于给定主机名的包含主机名字和地址信息的hostent结构的指针 printf("ip %s\n",inet_ntoa(*(struct in_addr*)pHost->h_addr_list[0])); // 3.设置要发送的IP地址和端口,取名为sin SOCKADDR_IN sin; sin.sin_family = AF_INET; sin.sin_port = htons(65000); sin.sin_addr.s_addr = inet_addr("224.1.1.1");//组播地址 bool bOpt = true; // 准备发送数据将套接字设置为广播类型 // 注:与接收不同,发送时不需要加入组播,直接向组播地址发送即可 setsockopt(connect_socket, SOL_SOCKET, SO_BROADCAST, (char*)&bOpt, sizeof(bOpt)); //设置该套接字为广播类型 int nAddrLen = sizeof(SOCKADDR); char buff[MAX_BUF_LEN] = ""; int nLoop = 0; while(1) //除非发送错误,否则一直循环发数据 { nLoop++; sprintf(buff, "%8d", nLoop); // 通过connect_socket套接字向sin地址和端口发送数据,该发送方式是直接向组播地址发送。 int nSendSize = sendto(connect_socket, buff, strlen(buff), 0, (SOCKADDR*)&sin, nAddrLen); // 发送数据 if(SOCKET_ERROR == nSendSize) { err = WSAGetLastError(); printf("sendto error!, error code is %d/n", err); return -1; } printf("Send: %s\n", buff); Sleep(500); } return 0; } 四、工程经验总结

        对于“基于项目学习”的代码类工程,要平衡完成工程和学习这两个方面所投入的精力。在任何一方面投入过多或过少都会导致无法顺利完成工程任务。根据当前经验,总结出以下三轮步骤。

        (1)第一轮,结合实际工程,了解工程中最核心3~5个的基本概念。了解概念有助于开展后续工作。注意时间有限,切忌贪多和深究。

        (2)第二轮,在百度上查找教程,尝试根据教程修改代码,完成目标。如果可以完成任务,则本轮结束。否则根据发现的问题,提炼概念,百度或请人求教。

        (3)第三轮,完成任务后做总结,视情况发布成果(专利、论文、github、CSDN等)。

参考资料

[1]UDP(组播)原理及应用

[2]Winsock网络编程快速入门

[3]【网络开发】winsock组播​​​​​​​​​​​​​​

[4] 基于 UDP 的组播、广播详解



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3