数据报套接字。它提供了一种无连接、不可靠的双向数据传输服务。数据包以独立的形式被发送,并且保留了记录边界,不提供可靠性保证。数据在传输过程中可能会丢失或重复,并且不能保证在接收端按发送顺序接收数据。在TCP/IP协议簇中,使用UDP协议来实现数据报套接字。在出现差错的可能性较小或允许部分传输出错的应用场合,可以使用数据报套接字进行数据传输,这样通信的效率较高。其服务灵活简单,在现实生活中得到了广泛的应用。
在了解UDP协议之前,我们需要先来了解一下TCP协议存在哪些缺陷呢?
相当于UDP协议来说,TCP协议增加了可靠性,流量控制、拥塞控制等机制,能够保证数据传递的可靠性,那么是不是在所有情况下使用TCP协议都是最合适的呢?
由此分析来看,尽管TCP提供了可靠的数据传输服务,简化了上层应用程序的设计复杂性,但同时也有一些性能和资源方面的损失,TCP协议未必是所有网络应用程序在选择传输协议时最佳的的选择。
UDP协议是一个无连接的传输层协议,提供面向事务的简单、不可靠的信息传送服务。
UDP协议的传输特点表现在以下方面:
多对多通信:UDP在通信实体的数据量上具有更大的灵活性,多个发送方可以向一个接收方发送报文,一个发送方也可以向多个接收方发送数据,更重要的是,UDP能让应用使用底层网络的广播或者组播设施交付报文。
不可靠服务:UDP提供的服务是不可靠交付的,即报文可以丢失、重复或失序,它没有重传设施,如果发生故障,也不会通知发送方。
缺乏流量控制:UDP不提供流量控制,当数据包到达的速度比接收系统或应用的处理速度快时,只是将其丢弃而不会发出警告。
报文模式:UDP提供了面向报文的传输方式,在需要传输数据的时候,发送方准确指明要求发送数据的字节数,UDP将这些数据放置在一个外发送报文中,在接受方,UDP一次交付一个传入报文。因此当有数据交付时,接收到的数据拥有和发送方应用程序所指定的一样的报文边界。
UDP数据报文封装在IP数据包的数据部分,UDP数据在IP数据包中的封装如下图所示:
数据报套接字基于不可靠的报文传输服务,这种服务的特点是无连接、不可靠。无连接的特点决定了数据报套接字的传输非常灵活,具有资源消耗小、处理速度快的优点。而不可靠的特点意味着在网络质量不佳的环境下,发生数据包丢失的现象会比较严重,因此上层应用程序在设计开发时需要考虑网络应用程序运行的环境以及数据在传输过程中的丢失、乱序、重复对应用程序带来的负面影响。总体来看,数据报套接字适合于在以下场合使用:
1)音频、视频的实时传输应用。数据报套接字适合用于音频、视频这类对实时性要求比较高的数据传输应用。传输内容通常被切分为独立的数据报,其类型多为编码后的媒体信息。在这种应用场景下,通常要求实时音视频传输,与TCP协议相比,UDP 协议减少了确认、同步等操作,节省了很大的网络开销。UDP协议能够提供高效率的传输服务,实现数据的实时性传输,因此在网络音视频的传输应用中,应用UDP协议的实时性并增加控制功能是较为合理的解决方案,如RTP和RTCP在音视频传输中是两个广泛使用的协议组合,通常RTP基于UDP传输音视频数据,RTCP基于TCP传输提供服务质量的监视与反馈、媒体间同步等功能。
2)广播或多播的传输应用。流式套接字只能用于1对1的数据传输,如果应用程序需要广播或多播传送数据,那么必须使用UDP协议,这类应用包括多媒体系统的的多播或广播业务、局域网聊天室或者以广播形式实现的局域网扫描器等。
3)简单高效需求大于可靠需求的传输应用。尽管UDP不可靠,但其高效的传输特点使其在一些特殊的传输应用中受到欢迎,比如聊天软件常常用到UDP协议传送文件,日志服务器通常设计位基于UDP协议来接受日志。这些应用不希望在每次传递小数据时消耗昂贵的TCP连接建立与维护代价,而且即使偶尔丢失一两个数据包,也不会对接受结果产生大影响,在这种场景下,UDP协议的简单高效特性就会非常的合适。
使用数据报套接字传送数据类似于生活中的邮件发送,与流式套接字的通信过程有所不同,数据报套接字不需要建立连接,而是直接根据目的地址构造数据包进行传送。
在通信过程中,服务器进程作为服务提供方,被动接受客户的请求,使用UDP协议与客户交互,其基本通信过程如下:
1) Windows Sockets DLL初始化,协商版本号:
2)创建食接字,指定使用UDP (无连接的传输服务)进行通信:
3)指定本地地址和通信端口;
4)等待客户的数据请求;
5)进行数据传输;
6)关闭套接字;
7)结束对Windows Sockets DLL的使用,释放资源。
在通信过程中,客户进程作为服务请求方,主动向服务器发送服务器请求,使用UDP协议与服务器交互,其基本通信过程如下:
1) Windows Sockets DLL初始化,协商版本号;
2)创建套接字,指定使用UDP (无连接的传输服务)进行通信;
3)指定服务器地址和通信端口;
4)向服务器发送数据请求;
5)进行数据传输;
6)关闭套接字;
7)结束对Windows Sockets DLL的使用,释放资源。
我们知道,UDP是一个无连接协议,也就是说,它仅仅传输独立的有目的地址的数据报。“连接”的概念似乎与数据报套接字无关,而实际上,在有些情况下,“连接”在数据报套接字中的使用可以帮助网络应用程序在可靠性和效率方面有一一定程度的优化。
1.两种数据报套接字的使用模式
在数据报套接字的使用过程中,可以有两种数据发送和接收的方式。
非连接模式是数据报套接字默认使用的数据发送和接收方式,这种模式的优点是数据发送的灵活性较好。
对于TCP来说,调用connect0将导致双方进人TCP的三次握手初始化连接阶段,客户会发送SYN段给服务器,接收服务器返回的确认和同步请求,在连接建立好后,双方交换了一些初始的状态信息,包括双方的IP地址和端口号。因此,对于流式套接字的connect()函数操作而言,connect() 函数完成的功能是: 1) 在调用方为套接字关联远程主机的地址和端口号; 2)与远端主机建立连接。该函数的成功暗示着服务器是正在提供服务的且双方的路径是可达的。从使用次数上来看,connect() 函数只能在流式套接字上调用次。
对于UDP来说,由于双方没有共享状态要交换,所以调用connect()函数完全是本地操作,不会产生任何网络数据。因此,对于数据报套接字的connect()操作而言,conect)函数完成的功能是:在调用方为套接字关联远程主机的地址和端口号。由于没有网络通信行为发生,该函数的成功并不意味着对等方- -定会对后续的数据请求产生回应,可能服务器是关闭的,也可能网络根本就没用连通。也可能网络根本就没有连通。一个 数据报套接字可以多次调用cnnect()函数,目的可能是: 1)指定新的IP地址和端口号; 2)断开套接字。对于第一个目的,通过再次调用connect(), 可以使得数据报套接字更新所关联的远端端点地址;对于第二个目的,为了断开一个已连接的数据报套接字,在再次调用connect()函数时,把套接字地址结构的地址族成品设置为AF_UNSPEC,此时,后续的send()/WSASend()、recvO/WSARecv()函数都将返回错误。
客户端:
#define WIN32_LEAN_AND_MEAN
#include
#include
#include
#include
#include
#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"
int __cdecl main(int argc, char **argv)
{
WSADATA wsaData;
SOCKET ConnectLessSocket = INVALID_SOCKET;
struct addrinfo *result = NULL, *ptr = NULL, hints;
char *sendbuf = "this is a test";
char recvbuf[DEFAULT_BUFLEN];
int iResult;
int recvbuflen = DEFAULT_BUFLEN;
if (argc != 2) {
printf("usage: %s server-name\n", argv[0]);
return 1;
}
//初始化套接字
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
//解析服务器地址和端口号
iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("getaddrinfo failed with error: %d\n", iResult);
WSACleanup();
return 1;
}
//创建数据报套接字
ConnectLessSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
if (ConnectLessSocket == INVALID_SOCKET) {
printf("scoket failed with error: %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
//发送缓冲区中的测试数据
iResult = sendto(ConnectLessSocket, sendbuf, (int)strlen(sendbuf), 0, result->ai_addr, (int)result->ai_addrlen);
if (iResult == SOCKET_ERROR) {
printf("send failed with error: %d\n", WSAGetLastError());
closesocket(ConnectLessSocket);
WSACleanup();
return 1;
}
freeaddrinfo(result);
printf("Bytes Sent: %ld\n", iResult);
//接收数据
iResult = recvfrom(ConnectLessSocket, recvbuf, recvbuflen, 0, NULL, NULL);
if (iResult > 0)
printf("Bytes received: %d\n", iResult);
else if (iResult == 0)
printf("Connection closed\n");
else
printf("recv failed with error: %d\n", WSAGetLastError());
//关闭套接字
closesocket(ConnectLessSocket);
//释放资源
WSACleanup();
system("pause");
return 0;
}
服务端:
#define _CRT_SECURE_NO_WARNINGS 1
#define WIN32_LEAN_AND_MEAN
#include
#include
#include
#include
#include
#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"
int __cdecl main(int argc, char **argv)
{
WSADATA wsaData;
int iResult;
SOCKET ServerSocket = INVALID_SOCKET;
struct addrinfo *result = NULL;
struct addrinfo hints;
sockaddr_in clientaddr;
int clientlen = sizeof(clientaddr);
int iSendResult;
char recvbuf[DEFAULT_BUFLEN];
int recvbuflen = DEFAULT_BUFLEN;
//初始化WinSock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
ZeroMemory(&hints, sizeof(hints));
//声明IPV4地址族,流式套接字,UDP协议
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
hints.ai_flags = AI_PASSIVE;
//解析服务器地址和端口号
iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("getaddrinfo failed with error: %d\n", iResult);
WSACleanup();
return 1;
}
//为无连接的服务器创建套接字
ServerSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
if (ServerSocket == INVALID_SOCKET) {
printf("socket failed with error: %ld\n", WSAGetLastError());
freeaddrinfo(result);
WSACleanup();
return 1;
}
//为监听套接字绑定本地地址和端口号
iResult = bind(ServerSocket, result->ai_addr, (int)result->ai_addrlen);
if (iResult == SOCKET_ERROR) {
printf("bind failed with error: %d\n", WSAGetLastError());
freeaddrinfo(result);
closesocket(ServerSocket);
WSACleanup();
return 1;
}
freeaddrinfo(result);
printf("UDP server starting\n");
ZeroMemory(&clientaddr, sizeof(clientaddr));
//recvfrom函数直接在参数中指定接收数据的源地址
iResult = recvfrom(ServerSocket, recvbuf, recvbuflen, 0, (SOCKADDR*)&clientaddr, &clientlen);
if (iResult > 0) {
//情况1:成功接收到数据
printf("Bytes received: %d\n", iResult);
//将缓冲区的内容回送给客户端
//sendto函数也是同理,在参数中指定数据要发送到的目的地址
iSendResult = sendto(ServerSocket, recvbuf, iResult, 0, (SOCKADDR*)&clientaddr, clientlen);
if (iSendResult == SOCKET_ERROR){
printf("send failed with error: %d\n", WSAGetLastError());
closesocket(ServerSocket);
WSACleanup();
return 1;
}
printf("Bytes sent: %d\n", iSendResult);
}
else if (iResult == 0)
//情况2:关闭连接
printf("Connection closing...\n");
else {
//情况3:接收发生错误
printf("recv failed with error: %d\n", WSAGetLastError());
closesocket(ServerSocket);
WSACleanup();
return 1;
}
//关闭套接字
closesocket(ServerSocket);
//释放资源
WSACleanup();
system("pause");
return 0;
}