赞
踩
UDP(User Datagram Protocol)称为用户数据报协议,是一种无连接的传输协议。
UDP的主要应用在即使丢失部分数据,也不影响整体效果的场景。例实时传输视频或音频时,即使丢失部分数据,也不会影响整体效果,只是会有轻微的画面抖动或杂音。
UDP服务器/客户端不像TCP那样,交换数据前需进行connect和accept进行连接。UDP中只有创建套接字和数据交互的过程。
在TCP服务器/客户端程序中,套接字是一一对应的关系。服务器若要向10个客户端提供服务,除了监听套接字外,还需要10个对应客户端的服务器套接字。
而在UDP中,不管是服务器还是客户端均只有一个套接字。在服务器端,可以通过服务器端的这个套接字向多个不同的客户端提供服务。同理,在客户端,也可以通过客户端的这个套接字向不同服务器请求服务。
创建好TCP套接字后,需要事先通过bind函数绑定IP和端口,并维持和对方的连接。
而UDP没有绑定IP和端口的步骤,因此不会保持连接。那么传输数据时就需要在IO函数中指定要目的地地址。
#include <winsock2.h> int WSAAPI sendto(SOCKET s, const char *buf, int len, int flags, const sockaddr *to, int tolen); //s: 标识 (可能连接的) 套接字的描述符 //buf: 指向包含要传输数据的缓冲区的指针 //len: buf 参数指向的数据长度(以字节为单位) //flags: 一组标志,用于指定调用的进行方式 //to: 指向包含目标套接字地址的 sockaddr 结构的可选指针 //tolen: 参数指向的地址的大小(以字节为单位) //返回值:如果未发生错误, sendto 将返回发送的字节总数,这可能小于 len 指示的数字。 否则,将返回SOCKET_ERROR值 int WSAAPI recvfrom( SOCKET s, char *buf, int len, int flags, sockaddr* from, int* fromlen); //s: 标识绑定套接字的描述符 //buf: 传入数据的缓冲区 //len: buf 参数指向的缓冲区的长度(以字节为单位) //flags: 一组选项,用于修改函数调用的行为,超出为关联套接字指定的选项 //from: 指向 sockaddr 结构中缓冲区的可选指针,将在返回时保存源地址, 注意这是一个输出参数 //fromlen: 指向 参数 指向的缓冲区大小(以字节为单位)的可选指针
下面将给出基于UDP的服务器/客户端代码示例。
// UDP_Server.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <stdio.h> #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib") #define BUF_SIZE 1024 int _tmain(int argc, _TCHAR* argv[]) { if (argc != 2) { return -1; } WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup Error!\n"); return -1; } //服务器端UDP套接字,第三个参数也可传0 SOCKET srvSock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); if (INVALID_SOCKET == srvSock) { printf("socket Error!\n"); WSACleanup(); return -1; } //服务器端地址信息 SOCKADDR_IN srvAddr; memset(&srvAddr, 0, sizeof(srvAddr)); srvAddr.sin_family = PF_INET; srvAddr.sin_addr.s_addr = htonl(ADDR_ANY); srvAddr.sin_port = htons(_ttoi(argv[1])); int srvAddrLen = sizeof(srvAddr); //因为该示例先使用recvfrom从客户端接收数据,因此这里需先调用bind函数绑定服务器端UDP套接字的地址信息。否则接收到的数据不知道要给哪个应用程序 //可尝试屏蔽这部分,查看终端是否会打印客户端发来的数据 if (SOCKET_ERROR == bind(srvSock, (sockaddr*)&srvAddr, sizeof(srvAddr))) { printf("bind Error!\n"); closesocket(srvSock); WSACleanup(); return -1; } //这里事先创建并初始化客户端的地址信息变量。 //之后recvfrom时会添加该变量值 SOCKADDR_IN cltAddr; memset(&cltAddr, 0, sizeof(cltAddr)); int nCltAddrLen = sizeof(cltAddr); char Msg[BUF_SIZE]; int recvLen = 0; while (true) { printf("Wait Msg From Client...\n"); //无连接的UDP套接字,sendto和recvfrom必须一一对应,也没有数据边界 //无连接的UDP套接字不会保持连接状态,因此每次传输数据都需要添加目标地址信息。 //第五、六个参数用来填充保存客户端的地址信息 //这里使用一个服务器端UDP套接字srvSock,从多个客户端接收数据 recvLen = recvfrom(srvSock, Msg, BUF_SIZE - 1, 0, (sockaddr*)&cltAddr, &nCltAddrLen); if (recvLen == -1) { //UDP属于无连接协议,因此没有断开连接的说法,这里不会进来 printf("Client Disconnected."); break; } Msg[recvLen] = 0; printf("Receive Msg from Client: %s\n", Msg); //第五、六个参数为上面保存的客户端的地址信息 //这里使用一个服务器端UDP套接字srvSock,回复多个客户端,通过接收数据时填充的地址信息标识多个客户端 sendto(srvSock, Msg, recvLen, 0, (sockaddr*)&cltAddr, nCltAddrLen); } closesocket(srvSock); WSACleanup(); return 0; }
// UDP_Client.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <stdio.h> #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib") #define BUF_SIZE 1024 int _tmain(int argc, _TCHAR* argv[]) { if (argc != 3) { return -1; } WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup Error!\n"); return -1; } //客户端UDP套接字 SOCKET cltSock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); if (INVALID_SOCKET == cltSock) { printf("socket Error!\n"); WSACleanup(); return -1; } //服务器端地址信息 SOCKADDR_IN srvAddr; memset(&srvAddr, 0, sizeof(srvAddr)); srvAddr.sin_family = PF_INET; srvAddr.sin_addr.s_addr = inet_addr(argv[1]); srvAddr.sin_port = htons(_ttoi(argv[2])); int nSrvAddrLen = sizeof(srvAddr); char Msg[BUF_SIZE]; int recvLen = 0; while (true) { fputs("Input Msg(q to quit): ", stdout); fgets(Msg, sizeof(Msg), stdin); if (!strcmp(Msg, "q\n") || !strcmp(Msg, "Q\n")) { printf("Disconnected..."); break; } //这里向地址信息为srvAddr传输数据 //这里客户端UDP套接字cltSock没有事先绑定IP和端口,因此每次调用sendto时都会自动分配IP和端口 sendto(cltSock, Msg, strlen(Msg), 0, (sockaddr*)&srvAddr, sizeof(srvAddr)); recvLen = recvfrom(cltSock, Msg, BUF_SIZE - 1, 0, (sockaddr*)&srvAddr, &nSrvAddrLen); Msg[recvLen] = 0; printf("Msg From Server: %s\n", Msg); } closesocket(srvSock); WSACleanup(); return 0; }
还记得此前的TCP客户端代码,通过connect函数连接服务器,并自动完成客户端套接字的IP和端口分配。
而在UDP程序中,调用sendto函数传输数据前应该完成对套接字的地址分配工作。
在UDP中能否通过bind函数为套接字绑定IP和端口呢?答案是可以的,因为bind不区分UDP和TCP,因此在UDP程序中也可以使用。
另外,调用sendto函数时发送尚未分配地址信息,则在首次调用sendto函数时给相应套接字自动分配IP和端口,而且此时分配的IP和端口会一直保留到程序结束为止。
综上,在UDP中,调用sendto函数时会自动分配地址信息。
UDP是具有数据边界的协议,这意味着输入函数的调用次数必须严格对应输出函数的调用次数,这样才能接收到完整的数据。
这里在客户端调用三次sendto函数,即发送三次数据到服务器,在服务器端只调用一次recvfrom函数试图接收所有数据,这是行不通的。
服务器端代码如下:
//UDP_Server.cpp #include "stdafx.h" #include <stdio.h> #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib") #define BUF_SIZE 1024 int _tmain(int argc, _TCHAR* argv[]) { if (argc != 2) { return -1; } WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup Error!\n"); return -1; } SOCKET srvSock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); if (INVALID_SOCKET == srvSock) { printf("socket Error!\n"); WSACleanup(); return -1; } SOCKADDR_IN srvAddr; memset(&srvAddr, 0, sizeof(srvAddr)); srvAddr.sin_family = PF_INET; srvAddr.sin_addr.s_addr = htonl(ADDR_ANY); srvAddr.sin_port = htons(_ttoi(argv[1])); int srvAddrLen = sizeof(srvAddr); if (SOCKET_ERROR == bind(srvSock, (sockaddr*)&srvAddr, sizeof(srvAddr))) { printf("bind Error!\n"); closesocket(srvSock); WSACleanup(); return -1; } char Msg[BUF_SIZE]; int recvLen = 0; SOCKADDR_IN cltAddr; memset(&cltAddr, 0, sizeof(cltAddr)); int nCltAddrLen = sizeof(cltAddr); recvLen = recvfrom(srvSock, Msg, BUF_SIZE - 1, 0, (sockaddr*)&cltAddr, &nCltAddrLen); Msg[recvLen] = 0; printf("recvfrom client msg: %s\n", Msg); closesocket(srvSock); WSACleanup(); getchar(); return 0; }
客户端代码如下:
//UDP_Client.cpp #include "stdafx.h" #include <stdio.h> #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib") #define BUF_SIZE 1024 int _tmain(int argc, _TCHAR* argv[]) { if (argc != 3) { return -1; } WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup Error!\n"); return -1; } SOCKET cltSock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); if (INVALID_SOCKET == cltSock) { printf("socket Error!\n"); WSACleanup(); return -1; } SOCKADDR_IN srvAddr; memset(&srvAddr, 0, sizeof(srvAddr)); srvAddr.sin_family = PF_INET; srvAddr.sin_addr.s_addr = inet_addr(argv[1]); srvAddr.sin_port = htons(_ttoi(argv[2])); int nSrvAddrLen = sizeof(srvAddr); char Msg[BUF_SIZE] = "0123456789"; int recvLen = 0; //客户端发送三次数据,服务器端调用一次recvfrom试图接收三次数据。是行不通的 int nSendLend = 0; //因为没有事先为cltSock分配地址信息,因此这里每次调用都会自动分配IP和端口 nSendLend += sendto(cltSock, Msg, strlen(Msg), 0, (sockaddr*)&srvAddr, nSrvAddrLen); nSendLend += sendto(cltSock, Msg, strlen(Msg), 0, (sockaddr*)&srvAddr, nSrvAddrLen); nSendLend += sendto(cltSock, Msg, strlen(Msg), 0, (sockaddr*)&srvAddr, nSrvAddrLen); closesocket(cltSock); WSACleanup(); getchar(); return 0; }

服务器端也必须调用相应次数的recvfrom才能接收到客户端发来的完整数据:
//调整UDP_Server.cpp中接收数据部分处理
//上文同上,故省略
for (int i = 0; i < 3; i++)
{
recvLen = 0;
recvLen = recvfrom(srvSock, Msg, BUF_SIZE - 1, 0, (sockaddr*)&cltAddr, &nCltAddrLen);
Msg[recvLen] = 0;
printf("recvfrom client msg: %s\n", Msg);
}
//下文同上,故省略

TCP套接字中需注册待传输数据的目标IP和端口号,而UDP中无需事先注册。因此,通过sendto函数传输数据的过程大致可分为以下3个阶段:
①向UDP套接字注册目标IP和端口号
②传输数据
③删除UDP套接字中注册的模板地址信息
每次调用sendto函数时重复上述过程。每次都变更目标地址,因此可以重复利用同一UDP套接字向不同目标传输数据。
这种事先注册目标地址信息,在sendto时才注册的套接字称为未连接套接字。反之事先注册了目标地址的套接字称为连接套接字。显然UDP套接字默认属于未连接套接字。
但UDP套接字在只需向一个目标地址传输数据时就显得不太合理。
例:IP为169.21.32.110的主机9190号端口共准备了3个数据,因此需要调用三次sendto函数进行传输。
此时需要重复上述三阶段,上述三个阶段中,第①、③个阶段占整个通信过程近1/3的时间,缩短这部分时间将大大提高整体效率。
因此,要与同一主机进行长时间通信时,将UDP套接字编程连接套接字会提供效率。
如何将UDP套接字变成连接套接字?
对UDP套接字调用connect函数即可。
修改上面服务器/客户端代码如下:
//UDP_Server.cpp #include "stdafx.h" #include <stdio.h> #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib") #define BUF_SIZE 1024 int _tmain(int argc, _TCHAR* argv[]) { if (argc != 2) { return -1; } WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup Error!\n"); return -1; } SOCKET srvSock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); if (INVALID_SOCKET == srvSock) { printf("socket Error!\n"); WSACleanup(); return -1; } SOCKADDR_IN srvAddr; memset(&srvAddr, 0, sizeof(srvAddr)); srvAddr.sin_family = PF_INET; srvAddr.sin_addr.s_addr = htonl(ADDR_ANY); srvAddr.sin_port = htons(_ttoi(argv[1])); int srvAddrLen = sizeof(srvAddr); if (SOCKET_ERROR == bind(srvSock, (sockaddr*)&srvAddr, sizeof(srvAddr))) { printf("bind Error!\n"); closesocket(srvSock); WSACleanup(); return -1; } char Msg[BUF_SIZE]; int recvLen = 0; SOCKADDR_IN cltAddr; memset(&cltAddr, 0, sizeof(cltAddr)); int nCltAddrLen = sizeof(cltAddr); while (true) { printf("Wait Msg From Client...\n"); //无连接的UDP套接字,存在数据边界,sendto和recvfrom必须一一对应 //无连接的UDP套接字不会保持连接状态,因此每次传输数据都需要添加目标地址信息 recvLen = recvfrom(srvSock, Msg, BUF_SIZE - 1, 0, (sockaddr*)&srvAddr, &srvAddrLen); if (recvLen == -1) { //UDP属于无连接协议,因此没有断开连接的说法,这里不会进来 printf("Client Disconnected."); break; } Msg[recvLen] = 0; printf("Receive Msg from Client: %s\n", Msg); sendto(srvSock, Msg, recvLen, 0, (sockaddr*)&srvAddr, srvAddrLen); } closesocket(srvSock); WSACleanup(); getchar(); return 0; }
//UDP_Client.cpp #include "stdafx.h" #include <stdio.h> #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib") #define BUF_SIZE 1024 int _tmain(int argc, _TCHAR* argv[]) { if (argc != 3) { return -1; } WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup Error!\n"); return -1; } SOCKET cltSock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); if (INVALID_SOCKET == cltSock) { printf("socket Error!\n"); WSACleanup(); return -1; } SOCKADDR_IN srvAddr; memset(&srvAddr, 0, sizeof(srvAddr)); srvAddr.sin_family = PF_INET; srvAddr.sin_addr.s_addr = inet_addr(argv[1]); srvAddr.sin_port = htons(_ttoi(argv[2])); int nSrvAddrLen = sizeof(srvAddr); //将UDP套接字转换成连接套接字,在此函数内分配cltSock的地址信息 connect(cltSock, (sockaddr*)&srvAddr, sizeof(srvAddr)); char Msg[BUF_SIZE] = "0123456789"; int recvLen = 0; //客户端发送三次数据,服务器端调用一次recvfrom试图接收三次数据。是行不通的 //int nSendLend = 0; //nSendLend += sendto(cltSock, Msg, strlen(Msg), 0, (sockaddr*)&srvAddr, nSrvAddrLen); //nSendLend += sendto(cltSock, Msg, strlen(Msg), 0, (sockaddr*)&srvAddr, nSrvAddrLen); //nSendLend += sendto(cltSock, Msg, strlen(Msg), 0, (sockaddr*)&srvAddr, nSrvAddrLen); while (true) { fputs("Input Msg(q to quit): ", stdout); fgets(Msg, sizeof(Msg), stdin); if (!strcmp(Msg, "q\n") || !strcmp(Msg, "Q\n")) { printf("Disconnected..."); break; } //上面已事先分配IP和端口,因此后续的sendto调用都不再有分配和删除地址信息处理,从而提交整体效率。 //sendto(cltSock, Msg, strlen(Msg), 0, (sockaddr*)&srvAddr, sizeof(srvAddr)); //recvLen = recvfrom(cltSock, Msg, BUF_SIZE - 1, 0, (sockaddr*)&srvAddr, &nSrvAddrLen); //Msg[recvLen] = 0; //已连接的UDP套接字可以直接使用send 和 recv函数 send(cltSock, Msg, strlen(Msg), 0); recvLen = recv(cltSock, Msg, BUF_SIZE - 1, 0); Msg[recvLen] = 0; printf("Msg From Server: %s\n", Msg); } closesocket(cltSock); WSACleanup(); getchar(); return 0; }

综上,可总结UDP服务器/客户端的开发步骤如下:
服务器端:
①创建服务器端UDP套接字
②通过bind绑定服务器端UDP套接字的地址信息
③事先准备SOCKADDR_IN变量保存往来的客户端的地址信息
④使用recvfrom、sendto函数交互数据
⑤关闭服务器端套接字
客户端:
①创建客户端UDP套接字
②初始化服务器端地址信息
③【可选】调用connect函数事先分配客户端UDP套接字的地址信息
④未事先连接的情况下,可使用sendto、recvfrom函数与服务器交互。事先通过connect函数连接的情况下,还可使用send、recv与服务器交互
⑤关闭客户端套接字
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。