关于UDP的二三事及C++编程实现
最近需要做一个通过UDP组播收发消息的程序用于无人机的遥测数据接收和转发,收集了一些关于UDP的资料并记录下来。
一 简单介绍UDP
1 什么是UDP?
UDP,即用户数据报协议(UDP,User Datagram Protocol),用户无需建立连接就可以发送封装的IP数据包。UDP协议与TCP协议一样用于处理数据包,在OSI模型中,两者都位于传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。UDP用来支持那些需要在计算机之间传输数据的网络应用。包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都需要使用UDP协议。UDP协议从问世至今已经被使用了很多年,虽然其最初的光彩已经被一些类似协议所掩盖,但即使在今天UDP仍然不失为一项非常实用和可行的网络传输层协议。许多应用只支持UDP,如:多媒体数据流,不产生任何额外的数据,即使知道有破坏的包也不进行重发。当强调传输性能而不是传输的完整性时,如:音频和多媒体应用,UDP是最好的选择。在数据传输时间很短,以至于此前的连接过程成为整个流量主体的情况下,UDP也是一个好的选择。
2 UDP的使用场景
举例说明UDP的应用场景,例如游戏直播,网络通话通话等应用。当对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快,这时就可以使用UDP。
3 UDP的优缺点
3.1 优点
-
分组首部开销小,相比于TCP的20个字节的首部开销,UDP的首部仅有8个字节 -
无需建立连接且无连接状态,无需建立连接意味着UDP不会引入建立连接的时延,无连接状态意味着UDP可以支持更多活跃的用户,UDP协议的实时性较高,适用于对高速传输和实时性要求较高的应用场景,例如QQ聊天、微信聊天场景及直播平台直播。 UDP的优点可以总结为:简单、传输速度快、传输效率高。
3.2 缺点
不可靠,UDP可能丢包且无法保证数据的顺序正确。
4 UDP的报文格式
?
UDP报文段结构
?
UDP的首部有4个字段,每个字段有=由两个字节构成。其中长度字段主要是记录UDP报文段中的字节数(首部+数据段),检验和字段的作用是接收方收到UDP报文后检查该报文段中是否出现了差错
5 单播和组播
单播和组播是当前网络中的通讯模式:
-
单播:网络节点之间的通信就好像是人们之间的对话一样。单播是主机之间“一对一”的通讯模式,网络中的交换机和路由器对数据只进行转发不进行复制。如果10个客户机需要相同的数据,则服务器需要逐一传送,重复10次相同的工作。但由于其能够针对每个客户的及时响应,所以现在的网页浏览全部都是采用IP单播协议。网络中的路由器和交换机根据其目标地址选择传输路径,将IP单播数据传送到其指定的目的地。 -
组播:在网络技术的应用并非非常多,网上视频会议、网上视频点播特别适合采用多播方式。组播是主机之间“一对一组”的通讯模式,也就是加入了同一个组的主机可以接受到此组内的所有数据,网络中的交换机和路由器只向有需求者复制并转发其所需数据。主机可以向路由器请求加入或退出某个组,网络中的路由器和交换机有选择的复制并传输数据,即只将组内数据传输给那些加入组的主机。这样既能一次将数据传输给多个有需要(加入组)的主机,又能保证不影响其他不需要(未加入组)的主机的其他通讯。
二 VS中C++编程实现UDP单播和组播
1 关键函数讲解
1)初始化WinSock库
//初始化WinSock库;
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsadata;
2)创建套接字,设置相关参数,本地IP地址和端口号
SOCKET sClient = socket(AF_INET, SOCK_DGRAM, 0);
sockaddr_in sclient;
sclient.sin_family = AF_INET;
sclient.sin_port = htons(8888);
sclient.sin_addr.S_un.S_addr = inet_addr("192.168.0.105");
int nlen = sizeof(sclient);
int socket(int af, int type, int protocol); 是socket函数的声明,在VS中其函数原型如下,af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。
socket(
? ?_In_ int af,
? ?_In_ int type,
? ?_In_ int protocol
? );
3)利用sendto() 函数进行数据发送
sendto(sClient,
(char*)sendData,
strlen(sendData),
0,
? ? ? (sockaddr*)&sclient,
? ? ? nlen);
sendto() 函数的函数原型如下:
sendto(
? ?_In_ SOCKET s,
? ?_In_reads_bytes_(len) const char FAR * buf,
? ?_In_ int len,
? ?_In_ int flags,
? ?_In_reads_bytes_(tolen) const struct sockaddr FAR * to,
? ?_In_ int tolen
? );
其参数含义为: s:一个标识套接口的描述字。 buf:包含待发送数据的缓冲区。 len:buf缓冲区中数据的长度。 flags:调用方式标志位。 to:(可选)指针,指向目的套接口的地址。 tolen:to所指地址的长度。
4)关闭socket连接
closesocket(sClient);
5)bind函数绑定IP地址和端口号
bind函数的定义如下:
int
bind(
? ?_In_ SOCKET s,
? ?_In_reads_bytes_(namelen) const struct sockaddr FAR * name,
? ?_In_ int namelen
? );
函数中每个参数的含义为:
?
s:标识一未捆绑套接口的描述字。 ?
name:赋予套接口的地址。 ?
namelen:name的长度,一般可以传sizeof(name) ?
????????其作用是将一本地地址与一套接口捆绑。本函数适用于未连接的数据报或流类套接口,在connect()或listen()调用前使用。当用socket()创建套接口后,它便存在于一个名字空间(地址族)中,但并未赋名。bind()函数通过给一个未命名套接口分配一个本地名字来为套接口建立本地捆绑(主机地址/端口号)。
6)recvfrom函数接收数据
?
?
int
WSAAPI
recvfrom(
? ?_In_ SOCKET s,
? ?_Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf,
? ?_In_ int len,
? ?_In_ int flags,
? ?_Out_writes_bytes_to_opt_(*fromlen, *fromlen) struct sockaddr FAR * from,
? ?_Inout_opt_ int FAR * fromlen
? );
2 实现
2.1 UDP客户端示例全部代码
#include <iostream>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <string>
#pragma comment(lib,"ws2_32.lib")
int main()
{
//初始化WinSock库;
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsadata;
if (WSAStartup(sockVersion, &wsadata) != 0)
{
printf("WinSock2 initialize failed!");
return 0;
}
/*const char* strptr = "192.168.0.105";
void* addptr=nullptr;*/
/*创建套接字,设置相关参数,本地ip地址和端口号*/
SOCKET sClient = socket(AF_INET, SOCK_DGRAM, 0);
sockaddr_in sclient;
sclient.sin_family = AF_INET;
sclient.sin_port = htons(8888);
sclient.sin_addr.S_un.S_addr = inet_addr("192.168.0.105");
int nlen = sizeof(sclient);
/*输入想要发送的数据*/
cout << "please input data you want transport:" << endl;
string str;
getline(cin, str);
const int nLen = sizeof(str);
/*将string 类型的数据转换成char型数据并进行发送*/
char sendData[nLen];
strcpy_s(sendData, str.c_str());
/*利用sendto()函数进行数据发送,注意参数列表代表的含义。*/
sendto(sClient, (char*)sendData, strlen(sendData), 0, (sockaddr*)&sclient, nlen);
closesocket(sClient);
WSACleanup();
system("pause");
return 0;
}
UDP发送调用socket套接字实现很简单,无非就是调用sendto 函数发送信息,调用recvfrom 函数接收数据,其关键点就是接收方需要绑定组播地址和端口,相当于加入组播,才能接收到数据。另外需要明确两个概念,组播地址和本机地址,例如234.3.3.3 和192.168.1.100 。
首先看发送方:
?1)构造socket
?
? udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
?
? //如果需要绑定,记得绑定本地地址
?
?2)指定发送的组播地址和端口
?
struct sockaddr_in mcast_addr;
memset(&mcast_addr, 0, sizeof(mcast_addr));
mcast_addr.sin_family = AF_INET;
mcast_addr.sin_addr.s_addr = inet_addr(mcast_ip.c_str());
mcast_addr.sin_port = htons(mcast_port);
?
?3)发送组播数据
?
?//it为组播数据内容描述
?
? int n = sendto(udp_socket,
? ? ? ? ? ? ? ? ?it.c_str(),
? ? ? ? ? ? ? ? ?it.length(),
? ? ? ? ? ? ? ? ?0,
? ? ? ? ? ? ? ? (struct sockaddr*)&mcast_addr,
? ? ? ? ? ? ? ? ?sizeof(mcast_addr));
?
?4)关闭socket
?
? close(udp_socket);
2.1 UDP服务端示例全部代码
//server
#include <iostream>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <string>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
int main_server() {
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsadata;
if (WSAStartup(sockVersion, &wsadata) != 0)
{
cout << "WinSock startup failed!";
return 0;
}
/*创建一个监听套接字,用来作为监听客户端的发送信息状态*/
SOCKET slisten = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(8888);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
/*将该套接字绑定到一个sokcaddr_in的结构体。*/
if (bind(slisten, (SOCKADDR*)&sin, sizeof(sin)) == SOCKET_ERROR)
{
cout << "bind socket error!" << endl;
system("pause");
return 0;
}
//指向存有源地址的相关信息的缓冲区
sockaddr_in remoteddr;
int nlen = sizeof(remoteddr);
/*进入循环监听状态,使客户端一直处于循环监听客户端,并且接受由客户端发送是数据。*/
while (true)
{
char recdata[255];
int ret = recvfrom(slisten, recdata, 255, 0, (SOCKADDR*)&remoteddr, &nlen);
if (ret > 0)
{
recdata[ret] = 0x00;
printf("接受一个链接:%s\t\r", inet_ntoa(remoteddr.sin_addr));
printf(recdata);
}
}
/*关闭套接字和WSA库*/
closesocket(slisten);
WSACleanup();
system("pause");
return 0;
}
接收方的实现步骤
1)构建socket
?
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
?
2)绑定本机地址
? ?struct sockaddr_in local_addr;
? ?memset(&local_addr, 0, sizeof(local_addr));
? ?local_addr.sin_family = AF_INET;
? ?local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
? local_addr.sin_port = htons(mcast_port);
? ?//建立本地捆绑(主机地址/端口号)
? ?int err = -1;
? ?err = bind(udp_socket,(struct sockaddr*)&local_addr, sizeof(local_addr));
3)加入组播,才能接收到组播信息
? ?struct ip_mreq mreq;
? ?mreq.imr_multiaddr.s_addr = inet_addr(mcast_ip.c_str());
? ?if(local_ip.empty()){
? ? ? ?mreq.imr_interface.s_addr = htonl(INADDR_ANY); ? ?//任意接口接收组播信息
? }else{
? ? ? ?mreq.imr_interface.s_addr = inet_addr(local_ip.c_str()); ?//指定接口接收组播信息
? }
?
? err = setsockopt(udp_socket, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
?
4)接收数据
?
? sockaddr_in srv_Addr;//用于存储发送方信息
? socklen_t addr_len = sizeof(srv_Addr);
? char *buff = new char[buf_size];
?
? memset(buff, 0, buf_size);
? n = recvfrom(udp_socket, buff, buf_size, 0,(struct sockaddr*)&srv_Addr,&addr_len);
?
//5)使用结束后退出组播,关闭socket
?
? ?err = setsockopt(udp_socket, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq));
? ?close(udp_socket);
接收端需要将接受的字符数组转换成十六进制,即如下所示
// 把字节码转为十六进制码,一个字节两个十六进制,内部为字符串分配空间
char* ByteToHex(const ?char* vByte, const int vLen)
{
if (!vByte)
return NULL;
char* tmp = new char[vLen * 2]; // 一个字节两个十六进制码,最后要多一个'/0'
int tmp2;
for (int i = 0; i < vLen; i++)
{
tmp2 = (int)(vByte[i]) / 16;
tmp[i * 2] = (char)(tmp2 + ((tmp2 > 9) ? 'A' - 10 : '0'));
tmp2 = (int)(vByte[i]) % 16;
tmp[i * 2 + 1] = (char)(tmp2 + ((tmp2 > 9) ? 'A' - 10 : '0'));
}
tmp[vLen * 2 + 1] = { 0 };
return tmp;
}
|