一、什么是 UDP 协议?

UDP 是 User Datagram Protocol 的简称,中文名是用户数据报协议,是 OSI 参考模型中的传输层协议,它是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。

UDP 的正式规范是IETF RFC768。UDP 在 IP 报文的协议号是 17。

ISO 七层模型:

二、UDP 报头

UDP 报头的结构如图:

UDP 报头由 4 个部分组成,其中两个是可选的(粉红背景标出部分):

  • 各 16bit 的来源端口目的端口用来标记发送和接受的应用进程。因为 UDP 不需要应答,所以来源端口是可选的,如果来源端口不用,那么置为零。

  • 在目的端口后面是长度固定的以字节为单位的报文长度域,用来指定 UDP 数据报包括数据部分的长度,长度最小值为 8byte。

  • 首部剩下地 16bit 是用来对首部和数据部分一起做校验和(Checksum)的,这部分是可选的,但在实际应用中一般都使用这一功能。

  • UDP 和 TCP 的校验和都覆盖到了他们的首部和数据,而之前介绍的 IP 首部的校验和只覆盖了 IP 首部。

三、TCP 和 UDP 区别

特征点 TCP UDP
是否连接 面向连接 面向非连接
传输可靠性 可靠 会丢包,不可靠
应用场景 传输数据量大 传输量小
速度

TCP(传输控制协议)提供的是面向连接、可靠的字节流服务。当客户端和服务器彼此交换数据前,必须先在双方之间建立一个 TCP 连接,之后才能传输数据。TCP 提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。
UDP(用户数据报协议)是一个简单的面向数据报的运输层协议。UDP 不提供可靠性,它只是把应用程序传给 IP 层的数据报发送出去,但是并不能保证它们能到达目的地。由于 UDP 在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。

由于 UDP 缺乏拥塞控制(congestion control),需要基于网络的机制来减少因失控和高速 UDP 流量负荷而导致的拥塞崩溃效应。换句话说,因为 UDP 发送者不能够检测拥塞,所以像使用包队列和丢弃技术的路由器这样的网络基本设备往往就成为降低 UDP 过大通信量的有效工具。数据报拥塞控制协议(DCCP)设计成通过在诸如流媒体类型的高速率 UDP 流中,增加主机拥塞控制,来减小这个潜在的问题。

四、应用场景

由于缺乏可靠性且属于非连接导向协议,UDP 的应用一般必须允许一定量的丢包、出错和复制粘贴。但有些应用,比如 TFTP,需要可靠性保证,则必须在应用层增加根本的可靠机制。但是绝大多数 UDP 应用都不需要可靠机制,甚至可能因为引入可靠机制而降低性能。流媒体、即时多媒体游戏和 IP 电话(VoIP)就是典型的 UDP 应用。如果某个应用需要很高的可靠性,那么可以用传输控制协议(即 TCP 协议)来代替 UDP。

使用 UDP 协议的应用有:域名系统(DNS)、简单网络管理协议(SNMP)、动态主机配置协议(DHCP)、路由信息协议(RIP)等等。

因为 UDP 不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用 UDP 较多,因为它们即使偶尔丢失几个数据包,也不会对接收结果产生太大影响。

五、单播、多播、广播、组播

假设 A(all 简写)代表所有的机器,M(multiple 简写)代表 A 中的多个机器,G(group 简写)代表一组机器,1 代表一台机器,那么:

1
2
3
4
5
6
7
1 -> 1 就是单播;
1 -> M 就是多播;
1 -> A 就是广播;
1 -> G 就是组播;

当M=A时,多播就是广播;
当M=G时,多播就是组播;

多播包括组播和广播,组播、广播都是多播的一种表现形式。

5.1 单播

单播是主机之间“一对一”的通讯模式。发送方需要指定一个接收方的 IP 和端口,只有这个接收方会收到数据报。不会对子网内的其他机器产生影响。
在单播模式下,服务器针对每个客户机都要发送数据流,服务器流量=客户机数量×客户机流量,在客户机数量大、每个客户机流量大的应用(如流媒体)中,服务器将不堪重负。

5.1.1 单播发送端

因为 UDP 不是面向连接的,且不可靠的,所以发送端在调用sendto之后,就算sendto返回成功,也不代表接收端一定收到了数据,可能接收端压根都没启动,也是有可能的。不能根据sendto的返回值来确保接收端一定收到了数据。如果需要数据传输的可靠性得到保证,可以使用 TCP 或者通过业务逻辑来保证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <WinSock2.h>
#pragma comment(lib, "Ws2_32.lib")

void SendLogic() {
WSADATA wsaData;
WORD wVersionRequested = MAKEWORD(1, 0);
WSAStartup(wVersionRequested, &wsaData);

SOCKET socket = ::WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, nullptr, 0, 0);
if (socket == INVALID_SOCKET) {
printf("WSASocket failed, error=%d\n", WSAGetLastError());
return;
}

sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(6000); // 接收端端口
addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 接收端IP

char buf[100] = { "hello" };
int err = sendto(socket, buf, strlen(buf), 0, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr));
if (err == SOCKET_ERROR) {
printf("sendto failed, error=%d\n", WSAGetLastError());
return;
}

printf("[SEND] %s OK\n", buf);

WSACleanup();
}

int main()
{
SendLogic();

getchar();
return 0;
}

5.1.2 单播接收端

因为 UDP 不是面向连接的,所以接收端不用 listen,也不用 accept,只需要绑定到指定的端口和地址即可。
recvfrom是同步的,会阻塞住等待数据的到来。如果要使用异步方式,可以使用WSARecvFrom结合 ICOP 的方式来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <WinSock2.h>
#pragma comment(lib, "Ws2_32.lib")

void RecvLogic() {
WSADATA wsaData;
WORD wVersionRequested = MAKEWORD(1, 0);
WSAStartup(wVersionRequested, &wsaData);

SOCKET socket = ::WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, nullptr, 0, 0);
if (socket == INVALID_SOCKET) {
printf("WSASocket failed, error=%d\n", WSAGetLastError());
return;
}

sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(6000); // 端口
addr.sin_addr.s_addr = INADDR_ANY; // 任意IP地址

int err = bind(socket, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr));
if (err == SOCKET_ERROR) {
printf("bind failed, error=%d\n", WSAGetLastError());
return;
}

while (true) { // TODO:未考虑退出的情况
char buf[100] = { 0 };
int fromlen = sizeof(addr);
err = recvfrom(socket, buf, 100, 0, reinterpret_cast<sockaddr*>(&addr), &fromlen);
if (err == SOCKET_ERROR) {
printf("recvfrom failed, error=%d\n", WSAGetLastError());
return;
}

printf("[RECV] %s\n", buf);
}

WSACleanup();
}

int main()
{
RecvLogic();

getchar();
return 0;
}

5.2 广播

广播是主机之间“一对所有”的通讯模式。子网的一台主机作为发送发广播一条信息,该子网中的所有主机都可以接收到该信息(不管你是否需要该信息)。
在广播模式下,由于服务器不用向每个客户机单独发送数据,所以服务器流量负载极低。
无法在广域网上进行广播,而且广播消息不会被路由转发,所以只能在一个子网中进行广播。因为如果路由器转发了广播信息,那么势必会引起网络瘫痪。这也是为什么 IP 协议的设计者故意没有定义互联网范围的广播机制。

主机发送广播消息时,需要指定目的 IP 地址为255.255.255.255和接受者的端口号。

UDP 的广播和单播的不同在于发送端(接收端的实现和单播方式没有区别)的实现上:

  1. 发送端将套接字配置为发送广播消息,使用setsockopt函数。
  2. 发送地址更改为受限的广播地址255.255.255.255

需要说明的是广播地址不仅仅只有255.255.255.255一个。广播地址分为受限的广播、指向网络的广播、指向子网的广播、指向所有子网的网广播。255.255.255.255只是受限的广播地址。

5.2.1 广播发送端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <WinSock2.h>
#pragma comment(lib, "Ws2_32.lib")

void SendLogic() {
WSADATA wsaData;
WORD wVersionRequested = MAKEWORD(1, 0);
WSAStartup(wVersionRequested, &wsaData);

SOCKET socket = ::WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, nullptr, 0, 0);
if (socket == INVALID_SOCKET) {
printf("WSASocket failed, error=%d\n", WSAGetLastError());
return;
}

// 将套接字配置为发送广播消息
int broadcast = 1;
int err = setsockopt(socket, SOL_SOCKET, SO_BROADCAST, reinterpret_cast<const char*>(&broadcast), sizeof(int));
if (err == SOCKET_ERROR) {
printf("setsockopt failed, error=%d\n", WSAGetLastError());
return;
}

sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(6000);
addr.sin_addr.s_addr = INADDR_BROADCAST; // 也可以换成inet_addr("255.255.255.255")

char buf[100] = { "hello" };
err = sendto(socket, buf, strlen(buf), 0, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr));
if (err == SOCKET_ERROR) {
printf("sendto failed, error=%d\n", WSAGetLastError());
return;
}

printf("[BROADCAST] %s OK\n", buf);

WSACleanup();
}

int main()
{
SendLogic();

getchar();
return 0;
}

5.2.2 广播接收端(和单播一样)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <WinSock2.h>

#pragma comment(lib, "Ws2_32.lib")

void RecvLogic() {
WSADATA wsaData;
WORD wVersionRequested = MAKEWORD(1, 0);
WSAStartup(wVersionRequested, &wsaData);

SOCKET socket = ::WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, nullptr, 0, 0);
if (socket == INVALID_SOCKET) {
printf("WSASocket failed, error=%d\n", WSAGetLastError());
return;
}

sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(6000);
addr.sin_addr.s_addr = INADDR_ANY;

int err = bind(socket, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr));
if (err == SOCKET_ERROR) {
printf("bind failed, error=%d\n", WSAGetLastError());
return;
}

while (true) {
char buf[100] = { 0 };
int fromlen = sizeof(addr);
err = recvfrom(socket, buf, 100, 0, reinterpret_cast<sockaddr*>(&addr), &fromlen);
if (err == SOCKET_ERROR) {
printf("recvfrom failed, error=%d\n", WSAGetLastError());
return;
}

printf("[RECV] %s\n", buf);
}

WSACleanup();
}

int main()
{
RecvLogic();

getchar();
return 0;
}

5.3 组播

组播是主机之间“一对多”的通讯模式。一台主机加入一个组播 IP 后,之后向该组播 IP 发送的数据报都会发送到该主机。
专门为组播划出了一个地址范围,在 IPv4 中为 D 类地址,范围是224.0.0.0 ~ 239.255.255.255,并将 D 类地址划分为局部链接组播地址、预留组播地址、管理权限组播地址如下:
局部链接地址:224.0.0.0~224.0.0.255,用于局域网,路由器不转发属于此范围的 IP 包。

预留组播地址:224.0.1.0~238.255.255.255,用于全球范围或网络协议。

管理权限地址:239.0.0.0~239.255.255.255,组织内部使用,用于限制组播范围。

组播就是将数据发送到一组主机。接收端如果要接收消息,则需要加入到该分组,分组是用 IP 来标识的。
网络协议(3)--IP协议可以知道,适用于分组的 IP 有224.0.0.0 ~ 239.255.255.255
同样,发送端就需要将数据发送到该分组 IP。

5.3.1 组播发送端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <WinSock2.h>
#pragma comment(lib, "Ws2_32.lib")


void SendLogic() {
WSADATA wsaData;
WORD wVersionRequested = MAKEWORD(1, 0);
WSAStartup(wVersionRequested, &wsaData);

SOCKET socket = ::WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, nullptr, 0, 0);
if (socket == INVALID_SOCKET) {
printf("WSASocket failed, error=%d\n", WSAGetLastError());
return;
}

// 将套接字配置为发送广播消息
int broadcast = 1;
int err = setsockopt(socket, SOL_SOCKET, SO_BROADCAST, reinterpret_cast<const char*>(&broadcast), sizeof(int));
if (err == SOCKET_ERROR) {
printf("setsockopt failed, error=%d\n", WSAGetLastError());
return;
}

sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(6000);
addr.sin_addr.s_addr = inet_addr("225.0.0.37"); // 向指定广播组发送消息

char buf[100] = { "hello" };
err = sendto(socket, buf, strlen(buf), 0, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr));
if (err == SOCKET_ERROR) {
printf("sendto failed, error=%d\n", WSAGetLastError());
return;
}

printf("[BROADCAST] %s OK\n", buf);

WSACleanup();
}

int main()
{
SendLogic();

getchar();
return 0;
}

5.3.2 组播接收端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <WinSock2.h>
#include <Ws2ipdef.h>


#pragma comment(lib, "Ws2_32.lib")

void RecvLogic() {
WSADATA wsaData;
WORD wVersionRequested = MAKEWORD(1, 0);
WSAStartup(wVersionRequested, &wsaData);

SOCKET socket = ::WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, nullptr, 0, 0);
if (socket == INVALID_SOCKET) {
printf("WSASocket failed, error=%d\n", WSAGetLastError());
return;
}

sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(6000);
addr.sin_addr.s_addr = htonl(INADDR_ANY);

int err = bind(socket, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr));
if (err == SOCKET_ERROR) {
printf("bind failed, error=%d\n", WSAGetLastError());
return;
}

// 将SOCKET加入广播组
//
struct ip_mreq mreq; // 引入头文件<Ws2ipdef.h>
mreq.imr_multiaddr.s_addr = inet_addr("225.0.0.37"); // 广播组地址
mreq.imr_interface.s_addr = htonl(INADDR_ANY);

// 注:使用IP_DROP_MEMBERSHIP可以离开广播组
err = setsockopt(socket, IPPROTO_IP, IP_ADD_MEMBERSHIP, reinterpret_cast<const char*>(&mreq), sizeof(ip_mreq));
if (err == SOCKET_ERROR) {
printf("setsockopt failed, error=%d\n", WSAGetLastError());
return;
}

while (true) {
char buf[100] = { 0 };
int fromlen = sizeof(addr);
err = recvfrom(socket, buf, 100, 0, reinterpret_cast<sockaddr*>(&addr), &fromlen);
if (err == SOCKET_ERROR) {
printf("recvfrom failed, error=%d\n", WSAGetLastError());
return;
}

printf("[RECV] %s\n", buf);
}

WSACleanup();
}

int main()
{
RecvLogic();

getchar();
return 0;
}

《TCP/IP 详解 卷 1:协议》在线阅读地址:http://www.52im.net/topic-tcpipvol1.html

文章图片带有“CSDN”水印的说明:
由于该文章和图片最初发表在我的CSDN 博客中,因此图片被 CSDN 自动添加了水印。