UDP(用户数据报协议)是一种无连接的协议,相比于TCP(传输控制协议),它不需要建立连接、维护连接状态以及进行错误检查和重传,因此通常用于对实时性要求较高、但对数据完整性要求不高的场景,如视频流、在线游戏等。
本文将通过 UDP 展示如何发送和接收数据,适用于简单的应用场景。对于更复杂的需求,可能需要进一步扩展和优化代码。之前已实现 TCP 的接发数据。
UDP 通讯流程
UDP通讯也使用socket,但是接收和发送的函数与 TCP 不一样。由于 UDP 不存在握手这一步骤,所以在绑定地址之后,服务端不需要 listen
,客户端也不需要 connect
,服务端同样不需要 accept
。只要服务端绑定以后,就可以相互发消息了,由于没有握手过程,两端都不能确定对方是否收到消息,这也是 UDP 协议不如 TCP 协议可靠的地方。
编程基本步骤
服务端流程:
- 创建套接字,选择套接字类型(如
SOCK_DGRAM
表示数据报套接字) - 绑定地址和端口
- 接收数据
- 处理数据(可选)
- 发送响应(可选):如果需要,使用
sendto()
发送响应数据 - 关闭套接字
客户端流程:
- 创建套接字
- 指定服务器地址和端口
- 发送数据,使用
sendto()
函数发送数据 - 接收响应(可选):可以使用
recvfrom()
函数接收数据 - 关闭套接字
函数介绍
UDP 协议接收和发送数据不再使用 send
和 recv
方法,这两个方法一般用于 TCP 通信,UDP 通信使用 sendto
和 recvfrom
方法。
需要包含的头文件:
#include <sys/types.h>
#include <sys/socket.h>
1. sendto
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const
struct sockaddr *dest_addr, socklen_t addrlen);
/**
* @brief 向指定地址发送缓冲区中的数据(一般用于UDP模式)
*
* @param sockfd 套接字文件描述符
* @param buf 缓冲区指针
* @param len 缓冲区大小
* @param flags 通信标签,详细减send方法说明
* @param dest_addr 目标地址。如果用于连接模式,该参数会被忽略
* @param addrlen 目标地址长度
* @return ssize_t 发送的消息大小。发送失败返回-1
*/
2. recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct
sockaddr *src_addr, socklen_t *addrlen);
/**
* @brief 将接收到的消息放入缓冲区 buf 中。
*
* @param sockfd 套接字文件描述符
* @param buf 缓冲区指针
* @param len 缓冲区大小
* @param flags 通信标签,详见recv方法说明
* @param src_addr 可以填NULL,如果 src_addr 不是 NULL,并且底层协议提供了消
息的源地址,则该源地址将被放置在 src_addr 指向的缓冲区中。
* @param addrlen 如果src_addr不为NULL,它应初始化为与 src_addr 关联的缓冲
区的大小。返回时,addrlen 被更新为包含实际源地址的大小。如果提供的缓冲区太小,
则返回的地址将被截断;在这种情况下,addrlen 将返回一个大于调用时提供的值。
* @return ssize_t 实际收到消息的大小。如果接收失败,返回-1
*/
示例程序
udp_server.c:
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#define BUFSIZE (1024 * sizeof(char))
#define PORT_S 6666
#define handle_error(cmd, result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
}
int main(int argc, char const *argv[])
{
// 使用UDP协议完成客户端和服务端的通讯
// 收到 EOF 作为关闭的信号
int sockfd, temp_result, client_fd;
struct sockaddr_in server_addr, client_addr;
char *buf = malloc(BUFSIZE);
// 清空
memset(&server_addr, 0, sizeof(server_addr));
memset(&client_addr, 0, sizeof(client_addr));
// 声明IPV4通信协议
server_addr.sin_family = AF_INET;
// 我们需要绑定0.0.0.0地址,转换成网络字节序后完成设置
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 端口随便用一个,但是不要用特权端口
server_addr.sin_port = htons(PORT_S);
/******** 网络编程 UDP 编程流程 ********/
// 1. socket 创建
sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 数据报
handle_error("socket", sockfd);
// 2. 服务端绑定地址
socklen_t server_len = sizeof(server_addr);
socklen_t client_len = sizeof(client_addr);
temp_result = bind(sockfd, (struct sockaddr *)&server_addr, server_len);
handle_error("bind", temp_result);
// 3. 直接就可以收发数据
do
{
// 接收数据存到缓冲
memset(buf, 0, BUFSIZE); // 清空
// 接收数据
temp_result = recvfrom(sockfd, buf, BUFSIZE, 0, (struct sockaddr *)&client_addr, &client_len);
handle_error("recvfrom", temp_result);
if (strncmp(buf, "EOF", 3) != 0)
{
printf("接收到来自客户端(%s:%d)信息:%s\n",inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buf);
strcpy(buf, "OK\n");
} else {
printf("收到结束信息,准备关闭\n");
}
// 回复数据
temp_result = sendto(sockfd, buf, 4, 0, (struct sockaddr *)&client_addr, client_len);
handle_error("sendto", temp_result);
} while (strncmp(buf, "EOF", 3) != 0);
free(buf);
return 0;
}
udp_client.c
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#define BUFSIZE (1024 * sizeof(char))
#define PORT_S 6666
#define OUTTEXT ("请输入要发送的信息:")
#define OUTSIZE sizeof(OUTTEXT)
#define handle_error(cmd, result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
}
int main(int argc, char const *argv[])
{
// 使用UDP协议完成客户端和服务端的通讯
// 收到 EOF 作为关闭的信号
int sockfd, temp_result, client_fd;
struct sockaddr_in server_addr, client_addr;
char *buf = malloc(BUFSIZE);
// 清空
memset(&server_addr, 0, sizeof(server_addr));
memset(&client_addr, 0, sizeof(client_addr));
// 声明IPV4通信协议
server_addr.sin_family = AF_INET;
// 我们需要绑定0.0.0.0地址,转换成网络字节序后完成设置
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 端口随便用一个,但是不要用特权端口
server_addr.sin_port = htons(PORT_S);
/******** 网络编程 UDP 编程流程 ********/
// 1. socket 创建
sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 数据报
handle_error("socket", sockfd);
// 2. 客户端不需要绑定地址
socklen_t server_len = sizeof(server_addr);
socklen_t client_len = sizeof(client_addr);
// 3. 直接就可以收发数据
do
{
write(STDOUT_FILENO, OUTTEXT, OUTSIZE);
// 从控制台读取数据
int buf_len = read(STDIN_FILENO, buf, BUFSIZE - 1); // 字符串最后为 '\0'
temp_result = sendto(sockfd, buf, buf_len, 0, (struct sockaddr *)&server_addr, server_len);
handle_error("sendto", temp_result);
// 接收数据之前需要清空缓存
memset(buf, 0, BUFSIZE);
//recvfrom(sockfd, buf, BUFSIZE, 0, (struct sockaddr *)&server_addr, server_len);
temp_result = recvfrom(sockfd, buf, BUFSIZE, 0, NULL, NULL); // 地址指针 ==> 之前存在数据,可以为空
handle_error("recvfrom", temp_result);
// 处理接收到的数据
if (strncmp(buf, "EOF", 3) != 0)
{
printf("收到服务端(%s:%d)返回数据:%s\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port), buf);
}
} while (strncmp(buf, "EOF", 3) != 0);
free(buf);
return 0;
}
运行结果
注意事项
1. 无连接性
UDP 是一种无连接的协议,这意味着在通信开始之前不需要建立连接。因此,UDP 通信通常被称为“不可靠的”或“尽最大努力的”传输,因为它不提供确认机制、重传机制或流量控制等功能。
2. 数据报大小限制
UDP 数据报有一个最大长度限制(通常为 65535 字节),这取决于网络环境和配置。如果发送的数据超过这个限制,它将被拆分成多个数据报进行传输。然而,接收端并不保证这些拆分的数据报会按顺序到达或全部到达。
3. 缓冲区管理
UDP 没有严格的发送缓冲区管理。当调用 sendto()
函数时,内核会直接将数据发送到网络层进行传输。然而接收端有一个接收缓冲区来存储到达的数据报。如果接收缓冲区已满,则新的数据报将被丢弃。
4. 应用场景
由于 UDP 的实时性和高效性特点,它通常用于那些对实时性要求较高但对数据完整性要求不高的应用场景,如视频流、在线游戏、实时通信等。
UDP 通信流程是一个相对简单且高效的过程,它适用于那些对实时性要求较高但对数据完整性要求不高的应用场景。然而,由于UDP的无连接性和不可靠性特点,在使用时需要特别注意数据报的大小限制和缓冲区管理等问题。
狠狠的支持
哇 感谢bro的笔记 学习到了不少!
😁
学习链接:https://www.bilibili.com/video/BV1DJ4m1M77z?p=118