一、什么是UDP协议
UPD协议(User Datagram Protocol,用户数据报协议)是Internet协议族中的一个无连接协议,属于传输层,它不保证数据传输的可靠性或完整性,只是把应用程序发给网络层的数据封装成数据包进行传输。
二、特性
1、无连接性:UDP协议不需要在发送数据之前建立连接,也不需要在传输过程中维持连接状态。
2、数据报式:UDP协议把应用程序发送的数据封装成报文(数据报)进行传输,每个报文都是独立的,独立传输,不关心前后次序的。
3、快速性:UDP协议没有传输确认和重传机制,数据传输快速,适用于实时应用。
4、轻量级:UDP协议的头部信息较小,占用网络带宽较少。
5、不可靠性:UDP协议不保证数据传输的可靠性和完整性,数据包发送后无法得到确认,也无法保证数据包能否到达目标地点。
6、丢包率高:UDP协议传输的数据包容易被网络拥塞、路由错误或网络故障等因素导致丢失。(需要特别注意,丢包是否需要重传)
三、使用场景
1、视频及音频应用:UDP协议适合用于实时视频和音频传输应用,如视频会议、网络电视、实时语音对话等。
2、游戏应用:在线游戏对实时性的要求非常高,因此很多游戏都采用UDP协议来传输游戏数据,如游戏画面、玩家动作、游戏状态等。
3、广播:UDP协议支持广播和多播,适用于局域网内广播数据等应用场景。
4、简单请求-响应应用:UDP协议适用于一些对数据传输可靠性要求不高,但是需要快速取得响应的应用,如DNS、NTP等。
5、IoT应用:在物联网应用场景下,由于传输数据量小,且对实时性要求较高,UDP协议被广泛应用于传输数据。
总之,UDP协议适用于对数据传输可靠性要求不高,但对实时性要求较高的场景,能够提供快速的数据传输和较低的网络延迟。
四、C/S架构UDP通信流程
1、客户端
(1)、建立套接字。使用socket()函数
(2)、设置端口复用。使用setsockopt()函数,这一步可选(推荐)
(3)、绑定自己的IP地址和端口号。使用bind()函数,这一步可以省略
(4)、和UDP服务器进行收发数据。使用sendto()发送,recvfrom()接收
(5)、关闭套接字。使用close()
2、服务端
(1)、建立套接字。使用socket()函数
(2)、设置端口复用。使用setsockopt()函数,这一步可选(推荐)
(3)、绑定自己的IP地址和端口号。使用bind()函数,这一步不可以省略!
(4)、和客户端进行收发数据。使用sendto()发送,recvfrom()接收
(5)、关闭套接字。使用close()
五、相关函数API接口
1、建立套接字
// 建立套接字 int socket(int domain, int type, int protocol); // 接口说明 返回值:成功返回一个套接字文件描述符,失败返回-1 参数domain:用来指定使用何种地址类型,有很多,具体看别的资源 (1)PF_INET 或者 AF_INET 使用IPV4网络协议 (2)其他很多的,看别的资源 参数type:通信状态类型选择,有很多,具体看别的资源 (1)SOCK_STREAM 提供双向连续且可信赖的数据流,即TCP (2)SOCK_DGRAM 使用不连续不可信赖的数据包连接,即UDP 参数protocol:用来指定socket所使用的传输协议编号,通常不用管,一般设为0
2、设置端口状态
// 设置端口的状态 int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); // 接口说明 返回值:成功返回0,失败返回-1 参数sockfd:待设置的套接字 参数level: 待设置的网络层,一般设成为SOL_SOCKET以存取socket层 参数optname:待设置的选项,有很多种,具体看别的资源,这里讲常用的 (1)、SO_REUSEADDR 允许在bind()过程中本地地址可复用,即端口复用 (2)、SO_BROADCAST 使用广播的方式发送,通常用于UDP广播 (3)、SO_SNDBUF 设置发送的暂存区大小 (4)、SO_RCVBUF 设置接收的暂存区大小 参数optval:待设置的值 参数optlen:参数optval的大小,即sizeof(optval)
3、绑定自己的IP地址和端口号
// 绑定自己的IP地址和端口号 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // 接口说明 返回值: 参数sockfd:待绑定的套接字 参数addrlen:参数addr的大小,即sizeof(addr) 参数addr:IP地址和端口的结构体,通用的结构体,根据sockfd的类型有不同的定义 当sockfd的domain参数指定为IPV4时,结构体定义为 struct sockaddr_in { unsigned short int sin_family; // 需与sockfd的domain参数一致 uint16_t sin_port; // 端口号 struct in_addr sin_addr; // IP地址 unsigned char sin_zero[8]; // 保留的,未使用 }; struct in_addr { uin32_t s_addr; } // 注意:网络通信时,采用大端字节序,所以端口号和IP地址需要调用专门的函数转换成网络字节序
4、字节序转换接口
// 第一组接口 // 主机转网络IP地址,输入主机IP地址 uint32_t htonl(uint32_t hostlong); // 主机转网络端口,输入主机端口号 uint16_t htons(uint16_t hostshort); // 常用 // 网络转主机IP,输入网络IP地址 uint32_t ntohl(uint32_t netlong); // 网络转主机端口,输入网络端口 uint16_t ntohs(uint16_t netshort); // 第二组接口,只能用于IPV4转换,IP地址 // 主机转网络 int inet_aton(const char *cp, struct in_addr *inp); // 主机转网络 in_addr_t inet_addr(const char *cp); // 常用 // 网络转主机 int_addr_t inet_network(const char *cp); // 网络转主机 char *inet_ntoa(struct in_addr in); // 常用
5、发送数据
// UDP协议发送数据 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // 接口说明 返回值:成功返回成功发送的字节数,失败返回-1 参数sockfd:发送者的套接字 参数buf:发送的数据缓冲区 参数len:发送的长度 参数flags:一般设置为0,还有其他数值,具体查询别的资源 参数dest_addr:接收者的网络地址 参数addrlen:接收者的网络地址大小,即sizeof(dest_addr)
6、接收数据
// UDP协议接收数据 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); // 接口说明: 返回值:成功返回成功接收的字节数,失败返回-1 参数sockfd:接收者的套接字 参数buf:接收数据缓的冲区 参数len:接收的最大长度 参数flags:一般设置为0,还有其他数值,具体查询别的资源 参数src_addr:发送者的网络地址,可以设置为NULL 参数addrlen: 发送者的网络地址大小,即sizeof(src_addr)
7、关闭套接字
// 关闭套接字 int close(int fd); // 接口说明 返回值:成功返回0,失败返回-1 参数fd:套接字文件描述符
六、案例
使用UDP协议完成C/S架构的客户端和服务端通信演示
客户端UdpClient.c
// UDP客户端的案例 #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #define CLIENT_IP "192.168.64.128" // 记得改为自己IP #define CLIENT_PORT 10000 // 不能超过65535,也不要低于1000,防止端口误用 int main(int argc, char *argv[]) { // 1、建立套接字,使用IPV4网络地址,UDP协议 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if(sockfd == -1) { perror("socket fail"); return -1; } // 2、设置端口复用(推荐) int optval = 1; // 这里设置为端口复用,所以随便写一个值 int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); if(ret == -1) { perror("setsockopt fail"); close(sockfd); return -1; } // 3、绑定自己的IP地址和端口号(可以省略) struct sockaddr_in client_addr = {0}; socklen_t addr_len = sizeof(struct sockaddr); client_addr.sin_family = AF_INET; // 指定协议为IPV4地址协议 client_addr.sin_port = htons(CLIENT_PORT); // 端口号 client_addr.sin_addr.s_addr = inet_addr(CLIENT_IP); // IP地址 ret = bind(sockfd, (struct sockaddr*)&client_addr, addr_len); if(ret == -1) { perror("bind fail"); close(sockfd); return -1; } // 4、收发数据 uint16_t port = 0; // 端口号 char ip[20] = {0}; // IP地址 struct sockaddr_in server_addr = {0}; char msg[128] = {0}; // 数据缓冲区 pid_t pid = fork(); // 父进程发送数据 if(pid > 0) { printf("please input receiver IP and port\n"); scanf("%s %hd", ip, &port); printf("IP = %s, port = %hd\n", ip, port); server_addr.sin_family = AF_INET; // 指定用IPV4地址 server_addr.sin_port = htons(port); // 接收者的端口号 server_addr.sin_addr.s_addr = inet_addr(ip); // 接收者的IP地址 while(getchar() != '\n'); // 清空多余的换行符 while(1) { printf("please input data:\n"); fgets(msg, sizeof(msg)/sizeof(msg[0]), stdin); // 发送数据,注意要填写接收者的地址 ret = sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr*)&server_addr, addr_len); if(ret > 0) { printf("success: send %d bytes\n", ret); } } } // 子进程接收数据 else if(pid == 0) { while(1) { // 接收数据,注意使用发送者的地址来接收 ret = recvfrom(sockfd, msg, sizeof(msg)/sizeof(msg[0]), 0, (struct sockaddr*)&server_addr, &addr_len); if(ret > 0) { memset(ip, 0, sizeof(ip)); // 先清空IP strcpy(ip, inet_ntoa(server_addr.sin_addr)); // 网络IP转主机IP port = ntohs(server_addr.sin_port); // 网络端口号转主机端口号 printf("[%s:%d] send data: %s\n", ip, port, msg); memset(msg, 0, sizeof(msg)); // 清空数据区 } } } else { perror("fork fail"); close(sockfd); return -1; } // 5、关闭套接字 close(sockfd); return 0; }
服务端UdpServer.c
// UDP服务端的案例 #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #define SERVER_IP "192.168.64.128" // 记得改为自己IP #define SERVER_PORT 20000 // 不能超过65535,也不要低于1000,防止端口误用 int main(int argc, char *argv[]) { // 1、建立套接字,使用IPV4网络地址,UDP协议 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if(sockfd == -1) { perror("socket fail"); return -1; } // 2、设置端口复用(推荐) int optval = 1; // 这里设置为端口复用,所以随便写一个值 int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); if(ret == -1) { perror("setsockopt fail"); close(sockfd); return -1; } // 3、绑定自己的IP地址和端口号(不能省略) struct sockaddr_in server_addr = {0}; socklen_t addr_len = sizeof(struct sockaddr); server_addr.sin_family = AF_INET; // 指定协议为IPV4地址协议 server_addr.sin_port = htons(SERVER_PORT); // 端口号 server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // IP地址 ret = bind(sockfd, (struct sockaddr*)&server_addr, addr_len); if(ret == -1) { perror("bind fail"); close(sockfd); return -1; } // 4、收发数据 uint16_t port = 0; // 端口号 char ip[20] = {0}; // IP地址 struct sockaddr_in client_addr = {0}; char msg[128] = {0}; // 数据缓冲区 pid_t pid = fork(); // 父进程发送数据 if(pid > 0) { printf("please input receiver IP and port\n"); scanf("%s %hd", ip, &port); printf("IP = %s, port = %hd\n", ip, port); client_addr.sin_family = AF_INET; // 指定用IPV4地址 client_addr.sin_port = htons(port); // 接收者的端口号 client_addr.sin_addr.s_addr = inet_addr(ip); // 接收者的IP地址 while(getchar() != '\n'); // 清空多余的换行符 while(1) { printf("please input data:\n"); fgets(msg, sizeof(msg)/sizeof(msg[0]), stdin); // 发送数据,注意要填写接收者的地址 ret = sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr*)&client_addr, addr_len); if(ret > 0) { printf("sccuess: send %d bytes\n", ret); } } } // 子进程接收数据 else if(pid == 0) { while(1) { // 接收数据,注意使用发送者的地址来接收 ret = recvfrom(sockfd, msg, sizeof(msg)/sizeof(msg[0]), 0, (struct sockaddr*)&client_addr, &addr_len); if(ret > 0) { memset(ip, 0, sizeof(ip)); // 先清空IP strcpy(ip, inet_ntoa(client_addr.sin_addr)); // 网络IP转主机IP port = ntohs(client_addr.sin_port); // 网络端口号转主机端口号 printf("[%s:%d] send data: %s\n", ip, port, msg); memset(msg, 0, sizeof(msg)); // 清空数据区 } } } else { perror("fork fail"); close(sockfd); return -1; } // 5、关闭套接字 close(sockfd); return 0; }
通信演示
注:上面只是对C/S架构UDP通信流程进行演示,并没有做什么丢包重传的检测,以上演示还介绍了一些测试命令用来建立一个UDP服务端
七、总结
UDP协议是属于传输层的,适用于一些对数据传输的实时性要求高、但对数据传输的可靠性要求较低的场景,如视频会议等流媒体应用。注意要考虑UDP协议丢包的问题,是否需要重传。UDP协议下的客户端通信流程和服务器通信流程大致一致,可以结合案例加深对UDP协议的理解。