1. UDP的特点
UDP(User Datagram Protocol,用户数据报协议)是互联网协议套件中的一种传输层协议,与广泛使用的TCP(Transmission Control Protocol,传输控制协议)相比,它是一种无连接、不可靠的协议。UDP 被用于对传输速度要求较高、但对可靠性要求较低的场景。
2. UDP与TCP的区别
UDP(用户数据报协议)和TCP(传输控制协议)都是传输层协议,负责在网络中传输数据,但它们的设计目标和实现方式有很大的区别。
以下是UDP和TCP的主要区别:
3. UDP的包头格式
下面是各个字段的解释:
3.1. UDP伪首部(Pseudo Header):
伪首部是用于计算校验和的虚拟头部信息,它不包含在实际传输的数据中,但用于保证数据完整性。伪首部包含如下字段:
- 32位源IP地址:表示数据包的源IP地址,即发送方的IP地址。
- 32位目的IP地址:表示数据包的目的IP地址,即接收方的IP地址。
- 0:固定填充的8位0字段,不使用。
- 8位协议(17):标识传输协议类型,对于UDP协议来说,这个字段的值是
17
。 - 16位UDP长度:表示整个UDP数据报的长度,包括UDP首部和数据部分。
3.2. UDP首部:
UDP首部是真正的数据报头部,它包含了与UDP通信相关的基本信息。UDP首部固定为8字节,包含以下字段:
- 16位源端口号:发送方的端口号,标识发送数据的应用程序。如果不需要,值可以为
0
。 - 16位目的端口号:接收方的端口号,表示接收数据的应用程序。
- 16位UDP长度:表示UDP报文的总长度(包括UDP首部和数据部分)。由于UDP首部固定为8字节,因此长度至少为8。
- 16位UDP校验和:用于确保UDP数据报在传输过程中没有被损坏。它由UDP伪首部、UDP首部和数据部分计算而得。如果发送方不计算校验和,则该字段可以为
0
。
3.3. 数据部分:
UDP数据报的实际数据内容。数据部分的长度可以根据具体的应用需求而变化,但必须与首部中的UDP长度
字段保持一致。
3.4. 填充字节(0):
数据包需要按一定的字节对齐规则(如32位对齐)填充到合适的长度,以确保数据包的完整性和便于传输
3.5. 重点:
- UDP伪首部并不实际存在于UDP报文中,它仅在计算校验和时使用,用于提供更多的上下文(如IP地址)来验证数据的完整性。
- UDP首部非常简单,仅有8字节,保证了UDP的轻量和高效。
- UDP数据:携带的实际传输数据,长度可以根据应用而变化。
这个图展示了UDP协议的简单性和高效性,因为UDP协议不需要复杂的连接管理或传输控制机制。
4. UDP Socket编程流程
4.1. UDP客户端流程:
- socket():创建一个UDP套接字(Socket)。这是启动UDP通信的第一步,客户端通过调用
socket()
函数生成一个用于通信的套接字。 - sendto():向服务器发送数据。客户端使用
sendto()
函数来将数据报发送到指定的服务器IP地址和端口。这是一个无连接的操作,不需要事先建立连接。 - 等待响应:客户端调用
recvfrom()
函数,进入阻塞状态,等待从服务器返回的数据。recvfrom()
会接收来自服务器的数据报,函数会在接收到数据后解除阻塞。 - recvfrom():接收到服务器返回的数据后,继续处理该数据。
- close():通信完成后,关闭客户端套接字,释放系统资源。
4.1. UDP客户端流程:
- socket():创建一个UDP套接字(Socket)。这是启动UDP通信的第一步,客户端通过调用
socket()
函数生成一个用于通信的套接字。 - sendto():向服务器发送数据。客户端使用
sendto()
函数来将数据报发送到指定的服务器IP地址和端口。这是一个无连接的操作,不需要事先建立连接。 - 等待响应:客户端调用
recvfrom()
函数,进入阻塞状态,等待从服务器返回的数据。recvfrom()
会接收来自服务器的数据报,函数会在接收到数据后解除阻塞。 - recvfrom():接收到服务器返回的数据后,继续处理该数据。
- close():通信完成后,关闭客户端套接字,释放系统资源。
4.2. UDP服务器流程:
- socket():与客户端一样,服务器首先创建一个UDP套接字,通过调用
socket()
函数。 - bind():将套接字与指定的IP地址和端口绑定。服务器必须绑定到一个特定的端口上,这样才能接收来自客户端的数据。
bind()
是服务器端特有的操作,客户端通常不需要显式调用bind()
。 - recvfrom():服务器使用
recvfrom()
接收客户端发送的数据报,并进入阻塞状态,直到接收到数据为止。 - 处理请求:收到数据后,服务器可以处理这个请求。例如,解析数据、执行相关操作。
- sendto():处理完成后,服务器通过
sendto()
向客户端发送响应数据。 - 继续等待:服务器可以继续调用
recvfrom()
来接收下一个数据请求。
4.3. 图中的其他元素:
- 阻塞直到收到数据:无论是客户端还是服务器,调用
recvfrom()
后,程序会进入阻塞状态,等待对方发送数据。这是UDP通信中的常见模式。 - 数据请求和数据响应:图中显示了客户端向服务器发送请求数据,服务器处理后返回响应数据的流程。
4.4. UDP通信的特点:
- 无连接:UDP协议是无连接的,客户端不需要先与服务器建立连接,直接发送数据。服务器收到数据后可以立即处理。
- 阻塞模式:图中显示的
recvfrom()
操作是阻塞的,直到有数据到来才会继续执行。 - 简单轻量:由于UDP不需要维护连接状态,它比TCP更加简单和轻量,适用于对实时性要求高但对数据可靠性要求较低的场景。
5. UDP代码实现
下面是一个使用C++实现UDP通信的简单示例,包括UDP服务器和客户端。
通过Socket编程,服务器接收客户端发送的消息,并作出回应。
5.1. UDP服务器代码:
#include <iostream>
#include <cstring> // for memset
#include <sys/socket.h> // for socket functions
#include <arpa/inet.h> // for sockaddr_in and inet_ntoa
#include <unistd.h> // for close#define PORT 8081
#define BUFFER_SIZE 1024int main() {int sockfd;char buffer[BUFFER_SIZE];struct sockaddr_in server_addr, client_addr;socklen_t addr_len = sizeof(client_addr);// 创建UDP Socketsockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0) {perror("Socket creation failed");exit(EXIT_FAILURE);}// 配置服务器地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET; // IPv4server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定到本地所有IP地址server_addr.sin_port = htons(PORT); // 指定端口号// 绑定Socket到地址if (bind(sockfd, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("Bind failed");close(sockfd);exit(EXIT_FAILURE);}std::cout << "UDP server is listening on port " << PORT << std::endl;while (true) {// 接收消息int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&client_addr, &addr_len);buffer[n] = '\0'; // 将接收到的数据转换为字符串格式std::cout << "Client: " << buffer << std::endl;// 响应消息const char *response = "Message received";sendto(sockfd, response, strlen(response), 0, (const struct sockaddr *)&client_addr, addr_len);}close(sockfd);return 0;
}
5.2. UDP客户端代码:
#include <iostream>
#include <string>
#include <cstring> // 使用strerror
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h> // 使用close()#define PORT 8081 // 定义端口号
#define BUFFER_SIZE 1024 // 定义缓冲区大小
#define IP "110.41.83.50" // 定义服务器IP地址int main() {int sockfd;char buffer[BUFFER_SIZE];struct sockaddr_in server_addr;// 创建UDP Socketsockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0) {std::cerr << "Socket creation failed: " << strerror(errno) << std::endl;return 1;}// 配置服务器地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(PORT);// 使用inet_pton将IP地址从字符串转换为网络字节顺序if (inet_pton(AF_INET, IP, &server_addr.sin_addr) <= 0) {std::cerr << "Invalid address/ Address not supported" << std::endl;close(sockfd);return 1;}while (true) {// 发送消息到服务器std::string message;std::cout << "Enter message: ";std::getline(std::cin, message);// 发送消息int send_result = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));if (send_result < 0) {std::cerr << "sendto failed: " << strerror(errno) << std::endl;break;}// 接收服务器的响应socklen_t addr_len = sizeof(server_addr); // 地址长度参数int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, (struct sockaddr*)&server_addr, &addr_len);if (n < 0) {std::cerr << "recvfrom failed: " << strerror(errno) << std::endl;break;}buffer[n] = '\0'; // 添加字符串结束符std::cout << "Server: " << buffer << std::endl; // 输出服务器响应}// 关闭Socketclose(sockfd);return 0;
}
5.3. Windows客户端版本:
-
头文件和库的引入:
- 在Windows中需要引入
winsock2.h
和ws2tcpip.h
,并且需要链接Ws2_32.lib
库。
- 在Windows中需要引入
-
Winsock初始化和清理:
- 在Windows中,使用网络功能之前需要调用
WSAStartup()
进行初始化,使用完毕后需要调用WSACleanup()
释放资源。
- 在Windows中,使用网络功能之前需要调用
-
Windows和Linux之间的一些函数差异:
close()
在Windows上对应的是closesocket()
。perror()
函数在Windows上不常用,通常使用std::cerr
输出错误。
5.3.1. Windows版本的UDP客户端代码:
#include <iostream>
#include <string>
#include <winsock2.h>
#include <ws2tcpip.h>#pragma comment(lib, "Ws2_32.lib") // 链接Ws2_32.lib库#define PORT 8081 // 定义端口号
#define BUFFER_SIZE 1024 // 定义缓冲区大小
#define IP "110.41.83.50" // 定义服务器IP地址,注意需要加上双引号int main() {WSADATA wsaData;SOCKET sockfd;char buffer[BUFFER_SIZE];struct sockaddr_in server_addr;// 初始化Winsockif (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {std::cerr << "WSAStartup failed." << std::endl;return 1;}// 创建UDP Socketsockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == INVALID_SOCKET) {std::cerr << "Socket creation failed: " << WSAGetLastError() << std::endl;WSACleanup();return 1;}// 配置服务器地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(PORT);// 使用inet_pton将IP地址从字符串转换为网络字节顺序if (inet_pton(AF_INET, IP, &server_addr.sin_addr) <= 0) {std::cerr << "Invalid address/ Address not supported" << std::endl;closesocket(sockfd);WSACleanup();return 1;}while (true) {// 发送消息到服务器std::string message;std::cout << "Enter message: ";std::getline(std::cin, message);// 发送消息int send_result = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));if (send_result == SOCKET_ERROR) {std::cerr << "sendto failed: " << WSAGetLastError() << std::endl;break;}// 接收服务器的响应int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, nullptr, nullptr);if (n == SOCKET_ERROR) {std::cerr << "recvfrom failed: " << WSAGetLastError() << std::endl;break;}buffer[n] = '\0'; // 添加字符串结束符std::cout << "Server: " << buffer << std::endl; // 输出服务器响应}// 关闭Socketclosesocket(sockfd);// 清理WinsockWSACleanup();return 0;
}
5.3.2. 主要修改点说明:
WSAStartup()
和WSACleanup()
:这些函数分别在程序开始时初始化Winsock库,结束时清理它。socket()
:创建Socket时,错误返回值为INVALID_SOCKET
,而不是Linux中的-1
。closesocket()
:在Windows中,用closesocket()
代替Linux中的close()
函数来关闭套接字。WSAGetLastError()
:用于获取最近的套接字操作错误码。
5.4. 函数细节说明
5.4.1. 关于recvfrom函数
recvfrom
函数是套接字编程中用于从套接字接收数据的一个函数,特别用于UDP协议下的数据接收。它允许程序从一个未连接的套接字(如UDP套接字)接收数据报。下面是对recvfrom
函数及其参数int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, nullptr, nullptr);
的详细解释:
函数原型
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
参数解释
- sockfd: 套接字描述符,指定了接收数据的套接字。这个套接字通常是通过
socket
函数创建的,并且绑定到了一个特定的端口(对于UDP来说)。 - buf: 指向数据缓冲区的指针,这个缓冲区用于存储接收到的数据。接收到的数据会被复制到这个缓冲区中。
- len: 指定了
buf
缓冲区的长度,即可以接收的最大数据量(以字节为单位)。 - flags: 标志位,用于修改
recvfrom
的行为。常用的标志包括MSG_PEEK
(查看数据但不从队列中移除)、MSG_WAITALL
(请求阻塞操作直到接收到完整的请求数据,但这对于UDP来说通常不适用,因为UDP是无连接的、数据报驱动的协议)等。在这个例子中,flags
被设置为0,表示使用默认行为。 - src_addr: 指向
sockaddr
结构体的指针,用于存储发送方的地址信息。如果不需要这个信息,可以传递nullptr
。在这个例子中,src_addr
被设置为nullptr
,表示不关心发送方的地址。 - addrlen: 指向
socklen_t
变量的指针,该变量在调用前应该被初始化为src_addr
所指向的地址结构的大小。在函数调用后,这个变量会被更新为实际存储在src_addr
中的地址结构的大小。如果src_addr
是nullptr
,则addrlen
也应该是nullptr
。在这个例子中,addrlen
被设置为nullptr
。
返回值
recvfrom
函数返回成功接收到的字节数。如果返回0,表示连接已正常关闭(但这对UDP来说并不常见,因为UDP是无连接的)。如果返回-1,表示发生了错误,错误类型可以通过errno
来检查。
示例解释
在给出的代码示例中:
int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, nullptr, nullptr);
sockfd
是一个有效的UDP套接字描述符。buffer
是一个足够大的缓冲区,用于存储接收到的数据。BUFFER_SIZE
是buffer
的大小,即可以接收的最大数据量。flags
设置为0,表示使用默认行为。src_addr
和addrlen
都设置为nullptr
,表示不关心发送方的地址信息。
n
会被赋值为实际接收到的字节数,或者-1表示出错。
5.4.2. 关于sendto函数
sendto
函数是套接字编程中用于发送数据的一个函数,特别适用于UDP协议下的数据发送。它允许程序向指定的地址发送数据报。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
参数解释
- sockfd: 套接字描述符,指定了发送数据的套接字。这个套接字通常是通过
socket
函数创建的,对于UDP来说,它可能还没有通过connect
函数与特定的远程地址关联。 - buf: 指向数据缓冲区的指针,这个缓冲区包含了要发送的数据。
- len: 指定了
buf
缓冲区的长度,即要发送的数据量(以字节为单位)。在这个例子中,通过strlen(response)
来获取response
字符串的长度。 - flags: 标志位,用于修改
sendto
的行为。常用的标志包括MSG_CONFIRM
(请求确认消息已发送)、MSG_DONTROUTE
(绕过路由表直接发送)等。在这个例子中,flags
被设置为0,表示使用默认行为。 - dest_addr: 指向
sockaddr
结构体的指针,用于指定接收方的地址信息。这个结构体包含了目标主机的IP地址和端口号。 - addrlen: 指定了
dest_addr
所指向的地址结构的大小。这个值通常是通过sizeof
操作符获取的。
返回值
sendto
函数返回成功发送的字节数。如果返回-1,表示发生了错误,错误类型可以通过errno
来检查
示例解释
在给出的代码示例中:
const char *response = "Message received";
sendto(sockfd, response, strlen(response), 0, (const struct sockaddr *)&client_addr, addr_len);
response
是一个指向常量字符串的指针,包含了要发送的消息。sockfd
是一个有效的UDP套接字描述符。strlen(response)
计算了response
字符串的长度,即要发送的数据量。flags
设置为0,表示使用默认行为。(const struct sockaddr *)&client_addr
是一个指向sockaddr
结构体的指针,该结构体包含了接收方的地址信息。这里假设client_addr
已经被正确初始化,并且包含了目标主机的IP地址和端口号。addr_len
是client_addr
结构体的大小,通常是通过sizeof(client_addr)
获取的。
当sendto
函数被调用时,它会尝试将response
字符串发送到由client_addr
指定的地址。如果发送成功,它会返回发送的字节数;如果失败,它会返回-1并设置errno
来指示错误类型。
5.4.3. <ws2tcpip.h>
<ws2tcpip.h>
是 Windows 的附加头文件,扩展了 Winsock2 的功能,提供了一些与现代网络协议相关的功能和 API。主要包括支持 IPv6 和通用的地址解析功能。
主要功能:
- 支持 IPv6:
-
提供了与 IPv6 相关的常量、数据结构和函数,例如:
-
sockaddr_in6
:用于表示 IPv6 地址的结构体 -
inet_pton()
和inet_ntop()
:分别用于将字符串转换为二进制 IP 地址和将二进制 IP 地址转换为字符串(支持 IPv4 和 IPv6)
- 地址解析和管理:
-
提供了现代的地址解析和主机名解析功能:
-
getaddrinfo()
:根据主机名、服务名获取地址信息,支持 IPv4 和 IPv6 的通用 API -
freeaddrinfo()
:释放通过getaddrinfo()
分配的内存 -
getnameinfo()
:将地址转换为主机名或服务名
- 端口和地址转换函数:
-
提供了 IP 地址和端口号的转换函数,例如:
-
inet_pton()
:将点分十进制的 IPv4 或 IPv6 地址转换为网络字节序的二进制形式 -
inet_ntop()
:将二进制的 IP 地址转换为字符串形式
5.4.4. WSADATA 结构体
WSADATA
是 Windows Sockets API(Winsock)中的一个结构体,包含有关 Windows Sockets 的实现版本和系统的配置信息。在调用 WSAStartup()
函数时,应用程序必须传递该结构体的指针,以便 Winsock 初始化并返回相关信息。
WSADATA
结构体定义在 <winsock2.h>
头文件中,具体如下:
typedef struct WSAData {WORD wVersion; // Winsock实现的版本号WORD wHighVersion; // 支持的最高版本号char szDescription[WSADESCRIPTION_LEN + 1]; // 描述Winsock的文本字符串char szSystemStatus[WSASYSSTATUS_LEN + 1]; // 当前的状态或配置unsigned short iMaxSockets; // 系统允许的最大套接字数unsigned short iMaxUdpDg; // 支持的最大UDP数据报大小char* lpVendorInfo; // 供应商特定的信息
} WSADATA, *LPWSADATA;
5.4.5. 关于memset
memset
是一个 C/C++ 标准库函数,用于将一块内存区域的内容设置为指定的值。它通常用于初始化数组或结构体,以确保在使用这些数据之前内存中的内容是已知的。memset
函数定义在 <cstring>
(C++)或 <string.h>
(C)头文件中。
void* memset(void* ptr, int value, size_t num);
参数说明
ptr
:指向要设置的内存块的指针。value
:要设置的值。这个值会被转换为unsigned char
类型,并且将其填充到内存块中。num
:要设置的字节数。