UDP(User Datagram Protocol,用户数据报协议)是一种面向无连接的传输层协议,用于在计算机网络上发送数据。它与 TCP(Transmission Control Protocol,传输控制协议)相比具有轻量、高效的特点,但牺牲了可靠性和顺序传输的保证。udp的通信过程默认也是阻塞的。
- 无连接:UDP 是无连接的,不会维护连接状态,每次发送数据都是独立的。
- 不可靠性:UDP 不保证数据的可靠传输,数据报文可能丢失、重复或顺序错乱。
- 数据包大小:UDP 数据报文大小受限制,超过网络的最大传输单元(MTU)会被分片,可能导致性能下降。
- 顺序保证:UDP 不保证数据报文的顺序交付,接收方收到数据的顺序可能与发送方不同。
- 适用场景:适合实时数据传输,如实时音视频流、游戏数据更新等,对实时性要求高而对数据完整性要求较低的场景。
1.UDP通信流程
-
创建套接字:
- 首先,服务器端和客户端分别创建 UDP 套接字。套接字是应用程序与网络之间的接口,用于发送和接收数据。
-
绑定地址和端口(可选):
- 在服务器端,可以选择将套接字绑定到特定的 IP 地址和端口上,以便客户端能够发送数据到指定的地址。客户端通常不需要绑定,而是直接向服务器发送数据。
-
发送数据报文:
- 客户端通过
sendto
函数将数据报文发送到服务器端的指定地址和端口。UDP 不需要建立连接,因此每个数据报文都是独立发送的,不会进行握手或状态管理。
- 客户端通过
-
接收数据报文:
- 服务器端使用
recvfrom
函数接收从客户端发送过来的数据报文。这个函数能够获取发送方的地址信息,以便服务器端可以知道从哪个客户端接收到数据。
- 服务器端使用
-
处理数据:
- 服务器端和客户端在接收到数据后,可以根据协议和应用程序的需求对数据进行处理,例如解析、验证、响应等操作。
-
关闭套接字:
- 当通信结束或者程序不再需要使用套接字时,服务器端和客户端分别使用
close
函数关闭套接字,释放资源。
- 当通信结束或者程序不再需要使用套接字时,服务器端和客户端分别使用
2.相关操作函数
2.1 socket
函数
cpp
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:创建一个套接字。
参数:
domain:指定协议族,常用的有 AF_INET(IPv4 地址)和 AF_INET6(IPv6 地址)。
type:指定套接字类型,常用的有 SOCK_DGRAM(数据报套接字,UDP)和 SOCK_STREAM(流式套接字,TCP)。
protocol:通常设置为 0,表示使用默认协议。
返回值:返回一个非负的文件描述符(套接字描述符),表示套接字创建成功;返回 -1 表示创建失败。
2.2 bind
函数
cpp
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:将一个本地地址绑定到套接字。
参数:
sockfd:套接字描述符,即 socket 函数返回的文件描述符。
addr:指向 sockaddr 结构体的指针,包含要绑定的 IP 地址和端口信息。
addrlen:addr 结构体的长度。
返回值:成功返回 0,失败返回 -1。
注意事项:
服务器端通常需要绑定一个固定的 IP 地址和端口,以便客户端能够发送数据到指定的地址。
客户端一般不需要绑定,而是直接发送数据到服务器端。
2.3 sendto
函数
cpp
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
功能:向指定的目标地址发送数据报文。
参数:
sockfd:套接字描述符。
buf:指向要发送数据的缓冲区。
len:要发送数据的长度。
flags:通常设置为 0,表示默认行为。
dest_addr:指向 sockaddr 结构体的指针,包含目标地址(IP 地址和端口)信息。IP和端口都存储在这里边, 是大端存储的
addrlen:dest_addr 结构体的长度。
返回值:成功返回发送的字节数,失败返回 -1。
注意事项:
UDP 是无连接的,每次发送数据时都要指定目标地址。
数据报文的大小应小于网络的最大传输单元(MTU),以免发生分片,影响性能。
2.4 recvfrom
函数
cpp
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
功能:从指定的源地址接收数据报文。
参数:
sockfd:套接字描述符。
buf:指向存放接收数据的缓冲区。
len:缓冲区的大小。
flags:通常设置为 0,表示默认行为。
src_addr:指向 sockaddr 结构体的指针,用于存放发送方的地址信息。IP和端口都存储在这里边, 是大端存储的
addrlen:src_addr 结构体的长度,在调用时需传入 src_addr 的实际长度。
返回值:成功返回接收到的字节数,失败返回 -1。
注意事项:
recvfrom 函数会阻塞直到接收到数据或发生错误。
可以通过 src_addr 参数获取发送方的 IP 地址和端口信息,用于处理接收到的数据。
2.5 close
函数
cpp
#include <unistd.h>
int close(int sockfd);
功能:关闭套接字。
参数:
sockfd:要关闭的套接字描述符。
返回值:成功返回 0,失败返回 -1。
注意事项:
在不再需要使用套接字时应及时关闭,释放系统资源。
关闭套接字后,相关的文件描述符将不再可用,不能再进行数据发送和接收操作。
3.示例代码
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8888
#define MAXLINE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr, client_addr;
char buffer[MAXLINE];
socklen_t client_len;
ssize_t n;
// 创建UDP套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&server_addr, 0, sizeof(server_addr));
memset(&client_addr, 0, sizeof(client_addr));
// 设置服务器地址结构
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(PORT);
// 绑定服务器地址到套接字
if (bind(sockfd, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
while (1) {
client_len = sizeof(client_addr);
// 接收来自客户端的消息
n = recvfrom(sockfd, (char *)buffer, MAXLINE, MSG_WAITALL, (struct sockaddr *)&client_addr, &client_len);
buffer[n] = '\0';
// 打印客户端信息
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
printf("Received message from %s:%d - %s\n", client_ip, ntohs(client_addr.sin_port), buffer);
// 发送响应消息给客户端
strcpy(buffer, "Hello from server");
sendto(sockfd, (const char *)buffer, strlen(buffer), MSG_CONFIRM, (const struct sockaddr *)&client_addr, client_len);
printf("Server : Hello message sent\n");
}
close(sockfd);
return 0;
}
作为数据接收端,服务器端通过bind()函数绑定了固定的端口,然后基于这个固定的端口通过recvfrom()函数接收客户端发送的数据,同时通过这个函数也得到了数据发送端的地址信息(recvfrom的第三个参数),这样就可以通过得到的地址信息通过sendto()函数给客户端回复数据了。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8888
#define MAXLINE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[MAXLINE];
socklen_t server_len;
ssize_t n;
// 创建UDP套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&server_addr, 0, sizeof(server_addr));
// 设置服务器地址结构
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
while (1) {
printf("Enter message to send : ");
fgets(buffer, MAXLINE, stdin);
// 发送消息给服务器
sendto(sockfd, (const char *)buffer, strlen(buffer), MSG_CONFIRM, (const struct sockaddr *)&server_addr, sizeof(server_addr));
printf("Message sent to server.\n");
// 接收服务器的响应消息
n = recvfrom(sockfd, (char *)buffer, MAXLINE, MSG_WAITALL, (struct sockaddr *)&server_addr, &server_len);
buffer[n] = '\0';
printf("Server : %s\n", buffer);
}
close(sockfd);
return 0;
}
作为数据发送端,客户端不需要绑定固定端口,客户端使用的端口是随机绑定的(也可以调用bind()函数手动进行绑定)。客户端在接收服务器端回复的数据的时候需要调用recvfrom()函数,因为客户端在发送数据之前就已经知道服务器绑定的固定的IP和端口信息了,所以接收服务器数据的时候就可以不保存服务器端的地址信息,直接将函数的最后两个参数指定为NULL即可。