UDP协议特点与网络通信
1.UDP协议的特点
处于不同网络的两台主机上的应用进程如果想要进行通信,则需要通过物理层、数据链路层、网络层这三层进行数据包转发,再通过传输层把数据包发送给主机中的指定进程,所以网络模型中的传输层至关重要。
传输层中最为常见的两个协议分别是传输控制协议TCP(Transmission Control Protocol)和用户数据报协议UDP(User Datagram Protocol),想要掌握这两种协议,则需要阅读协议的标准文件,UDP标准内容如下:
通过标准可以知道,UDP协议提供面向事务的简单不可靠信息传送服务,因为无法保证数据包的交付以及当数据包出现丢包之后不具备对丢失的数据包重传的功能。如果用户打算有序可靠的传输数据流给某个应用程序则推荐使用TCP协议。
对于UDP协议而言,还存在其他特性,比如UDP协议提供的是面向无连接的服务,也就是说双方在通信之前不需要建立连接,接收方收到数据之后也无需进行回复确认送达,所以UDP协议无法知道报文发送之后是否安全完整到达,体现出“不可靠”的特性。
注意:UDP协议也不保证数据包可以有序到达,以及不提供数据包进行分组和组装的服务。
2.UDP协议的报首
当然,通过标准可以知道,UDP协议的数据包体积较小,因为UDP协议的数据包报首较小。
2.1源端口号
源端口号指的是发送数据的主机进程对应的端口号,可以看到是16bit,也就是2个字节,该成员是可选的,如果不指定端口,则会填充为0。
2.2目标端口
目标端口指的是接收数据的主机的进程对应的端口号,可以看到是16bit,也就是2个字节,这里注意:并不是每台主机的进程的端口号都相同,也就是端口号是本地有效的,换个说法,两台主机中相同的端口号不一定指的是相同进程!
2.3包总长度
包长度指的是UDP报首长度 + 数据内容长度之和,而包长度占16bit,也就是包长度的最大值是65535字节,可以看到UDP数据报报首的长度是8字节,而UDP协议接收到的网络层转发过来的数据包中还存在IP协议的报首,而IP协议的报首固定为20字节(不使用可选选项的情况下),所以UDP携带的数据内容的最大值是65507字节。
2.4校验和
校验和是用于检测UDP用户数据包传输过程中是否出错,如果出错,则直接丢弃,如果源主机不想生成校验和,可以填充把校验和的16bit设置为0。
3.UDP的接口说明
在Linux系统中提供了UDP协议的描述和UDP协议相关的接口说明,建议通过man手册的第7章了解UDP协议的使用规则:
可以看到,UDP协议提供的是一种无连接、不可靠的服务,并且可以通过RFC 768标准了解UDP协议。
另外,通过手册发现想要指定UDP协议进行通信,需要先调用socket函数创建UDP套接字,如果只打算向对方主机发送数据包,则可以调用sendto()函数或者sendmsg()函数,注意要把对方主机的有效IP地址作为参数。
如果打算接收对方主机发送的数据包,则需要提前调用bind()函数把UDP套接字和本地主机地址进行绑定,然后调用recvfrom()函数来接收数据包。
3.1socket()函数
Linux系统的思想是“一切皆文件”,所以Linux系统把文件分为7类、其中有一种文件是为了实现在不同主机的进程间进行通信而设计出来的,也被称为套接字文件(标识符是s,指的是socket的缩写)。
如果双方主机打算通信,则双发都需要在本地创建套接字文件,Linux系统中提供了一个名称叫做socket()的函数接口,使用规则如下:
3.1.1函数参数
第一个参数:domain指的是要使用的协议族,因为协议族决定了通信地址类型,常用的是AF_INET协议族,这样就采用IPV4地址进行通信。
第二个参数:type指的是套接字类型,一般常用的类型有SOCK_STREAM 和 SOCK_DGRAM。
SOCK_STREAM类型 :提供有序、可靠、双向、基于连接的字节流,也就是基于TCP协议。
SOCK_DGRAM类型 :提供不可靠、无连接、固定长度数据的数据报,是基于UDP协议。
第三个参数:protocol指的是和套接字类型相关的协议,一般设置为0即可,这样系统会自动根据套接字类型选择合适协议。
3.1.2返回结果
socket函数创建套接字成功,则会返回对应的文件描述符,如果套接字创建失败,则返回-1。
如果想要使用IPV4协议族,并通过UDP协议进行通信,可以参考man手册的案例
3.2bind()函数
双方主机通信时一般采用双向通信,也就是主机在发送数据的同时也可能等待接收对方的数据,如果打算接收对方主机的数据,则需要把创建的套接字和本地地址以及端口进行绑定,Linux系统提供了一个名称叫做bind()的函数接口,该接口的使用规则如下:
3.2.1函数参数
第一个参数:sockfd指的是创建成功的套接字返回的文件描述符,就是socket()函数返回值。
第二个参数:addr指的是要绑定的本地地址的结构体指针,但是实际addr的结构体类型是和协议族有关的,比如采用AF_INET协议族,则需要阅读man手册的第7章了解地址结构。
可以看到,采用IPV4协议需要指定IP地址和端口号,对应的结构体类型是struct sockaddr_in,该结构体有3个成员:
成员sin_family :指的是协议族,需要设置为AF_INET
成员sin_port :指的是端口号,必须转换为网络字节序,需调用htons()函数实现转换
成员sin_addr :指的是IP地址,必须转换为网络字节序,需调用inet_addr()实现转换
3.2.1返回结果
bind()函数调用成功则返回0,bind()函数调用失败则返回-1,建议绑定地址后进行错误处理。
思考:请问什么是网络字节序,为什么一定要把端口号和IP地址都转换为网络字节序才行?
回答:对于不同网络中的主机而言可能采用的平台都各不相同,而不同平台的主机在存储数据的方式也不同,一般分为两种方案:大端存储(Big_Endian)or 小端存储(Little_Endian)。
大端存储:数据的高字节数据存储在内存的低地址,如ARM平台就采用大端方式存储数据
小端存储:数据的低字节数据存储在内存的低地址,比如X86平台就采用小端方式存储数据
由于不同平台存储数据的方式不同,所以把数据封包发送出去,对方主机接收到数据包进行解包之后得到的原始数据的值可能含义完全不同,导致数据异常,所以为了统一标准,就设计出网络字节序,网络字节序统一采用大端方式传输数据。
当然,Linux系统提供很多函数实现把主机字节序转换为网络字节序也提供了函数接口实现把网络字节序转换为主机字节序,比如htons()、htonl()、ntohs()、ntohl()、inet_addr()等等。
3.3sendto()函数
如果打算向对方主机发送数据,则双方主机都需要创建UDP套接字,发送端可以调用sendto()函数发送数据,函数使用规则如下:
3.3.1函数参数
第一个参数:sockfd指的是创建的套接字对应的文件描述符,其实是socket()函数的返回值。
第二个参数:buf指的是要发送的消息对应的缓冲区的地址,const void * 表示地址类型任意。
第三个参数:len指的是要发送的消息的大小,以字节为单位,不要超过UDP的数据包大小!
第四个参数:flags指的是发送消息的标志,一般设置为0,和系统调用write()函数作用类似。
第五个参数:dest_addr指的是目标主机的IP地址,因为UDP是无连接的,需要指定接收端。
第六个参数:addrlen指的是目标主机的IP地址的大小,一般通过sizeof()进行计算即可得到。
3.3.2返回结果
sendto()函数发送消息成功,则返回发送的消息的字节个数,sendto()函数调用失败则返回-1。
思考:如果待发送的数据包的大小超过UDP规定的数据包大小,请问会出现什么现象?????
回答:UDP是不可靠的传输协议,为了减少UDP包丢失的风险,我们最好能控制UDP包在IP层的传输过程中不要被切割,在下层数据链路层MTU是1500字节的情况下要想IP层不分包,那么UDP数据包的最大大小应该是1500字节- IP头(20字节) - UDP头(8字节) = 1472字节。
这也就是说IP数据报大于1500字节,大于MTU,这个时候发送方IP层就需要分片。把数据报分成若干片,使每一片都小于MTU,而接收方IP层则需要进行数据报的重组。但是由于UDP的特性,当某一片数据传送中丢失时,接收方无法重组数据报,将导致丢弃整个UDP数据报,所以通常建议UDP的数据包不要超过MTU的大小。
3.4recvfrom()函数
接收方主机想要得到发送过来的数据包,则需要调用recvfrom()函数实现,函数的使用规则如下:
3.4.1函数参数
第一个参数:sockfd指的是创建的套接字对应的文件描述符,其实是socket()函数的返回值。
第二个参数:buf指的是接收的消息要存储的缓冲区的地址,void * 表示地址类型可以任意。
第三个参数:len指的是要接收的消息的大小,以字节为单位,大小可以结合实际情况设置。
第四个参数:flags指的是接收消息的标志,一般设置为0,和系统调用read()函数作用类似。
第五个参数:src_addr指的是源主机的IP地址,如果对发送方不感兴趣,可以设置为NULL。
第六个参数:addrlen指的是源主机的IP地址的大小,注意是一个指针变量,需要传地址!!!
3.4.2返回结果
recvfrom()函数接收消息成功返回接收的消息的字节个数,recvfrom()函数调用失败则返回-1。
设计两个程序A和B,程序A作为客户端,程序B作为服务器,要求双方采用UDP协议实现一对一通信。两个程序应该分别使用多线程实现数据收发。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define SERVER_PORT 8888
#define SERVER_IP "127.0.0.1" // 服务器IP地址,可修改为实际地址
#define BUFFER_SIZE 1024int sockfd;
struct sockaddr_in server_addr;
socklen_t server_len = sizeof(server_addr);
int is_running = 1;// 接收线程函数:持续接收服务器消息
void *receive_thread(void *arg) {char buffer[BUFFER_SIZE];while (is_running) {memset(buffer, 0, BUFFER_SIZE);// 接收服务器消息ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,(struct sockaddr*)&server_addr, &server_len);if (recv_len < 0) {if (is_running) perror("接收失败");continue;}// 显示服务器消息printf("服务器: %s\n", buffer);// 检查退出命令if (strcmp(buffer, "exit") == 0) {printf("服务器已退出\n");is_running = 0;break;}}pthread_exit(NULL);
}// 发送线程函数:持续读取输入并发送给服务器
void *send_thread(void *arg) {char buffer[BUFFER_SIZE];while (is_running) {// 读取客户端输入if (fgets(buffer, BUFFER_SIZE, stdin) == NULL) {break;}// 去除换行符buffer[strcspn(buffer, "\n")] = '\0';// 发送数据到服务器if (sendto(sockfd, buffer, strlen(buffer), 0,(struct sockaddr*)&server_addr, server_len) < 0) {perror("发送失败");continue;}// 检查退出命令if (strcmp(buffer, "exit") == 0) {printf("客户端退出\n");is_running = 0;break;}}pthread_exit(NULL);
}int main() {pthread_t recv_tid, send_tid;// 创建UDP套接字if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {perror("套接字创建失败");exit(EXIT_FAILURE);}// 初始化服务器地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT);// 转换服务器IP地址if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {perror("无效的服务器IP");close(sockfd);exit(EXIT_FAILURE);}printf("UDP客户端启动,连接到 %s:%d\n", SERVER_IP, SERVER_PORT);printf("输入\"exit\"退出\n");// 发送初始消息,告知服务器已连接const char *init_msg = "客户端已连接";sendto(sockfd, init_msg, strlen(init_msg), 0,(struct sockaddr*)&server_addr, server_len);// 创建接收和发送线程if (pthread_create(&recv_tid, NULL, receive_thread, NULL) != 0) {perror("创建接收线程失败");close(sockfd);exit(EXIT_FAILURE);}if (pthread_create(&send_tid, NULL, send_thread, NULL) != 0) {perror("创建发送线程失败");pthread_cancel(recv_tid);close(sockfd);exit(EXIT_FAILURE);}// 等待线程结束pthread_join(recv_tid, NULL);pthread_join(send_tid, NULL);// 清理资源close(sockfd);return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define PORT 8888
#define BUFFER_SIZE 1024// 全局变量存储客户端地址信息,供发送线程使用
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int sockfd;
int is_running = 1;// 接收线程函数:持续接收客户端消息
void *receive_thread(void *arg) {char buffer[BUFFER_SIZE];while (is_running) {memset(buffer, 0, BUFFER_SIZE);// 接收客户端消息ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,(struct sockaddr*)&client_addr, &client_len);if (recv_len < 0) {if (is_running) perror("接收失败");continue;}// 显示客户端消息printf("客户端 %s:%d: %s\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port),buffer);// 检查退出命令if (strcmp(buffer, "exit") == 0) {printf("客户端请求退出\n");is_running = 0;break;}}pthread_exit(NULL);
}// 发送线程函数:持续读取输入并发送给客户端
void *send_thread(void *arg) {char buffer[BUFFER_SIZE];while (is_running) {// 读取服务器输入if (fgets(buffer, BUFFER_SIZE, stdin) == NULL) {break;}// 去除换行符buffer[strcspn(buffer, "\n")] = '\0';// 发送数据到客户端if (sendto(sockfd, buffer, strlen(buffer), 0,(struct sockaddr*)&client_addr, client_len) < 0) {perror("发送失败");continue;}// 检查退出命令if (strcmp(buffer, "exit") == 0) {printf("服务器退出\n");is_running = 0;break;}}pthread_exit(NULL);
}int main() {pthread_t recv_tid, send_tid;struct sockaddr_in server_addr;// 创建UDP套接字if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {perror("套接字创建失败");exit(EXIT_FAILURE);}// 初始化服务器地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);// 绑定端口if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {perror("绑定失败");close(sockfd);exit(EXIT_FAILURE);}printf("UDP服务器启动,端口 %d\n", PORT);printf("等待客户端连接...\n");printf("输入\"exit\"退出\n");// 先接收一次客户端消息以获取客户端地址char init_buffer[BUFFER_SIZE];recvfrom(sockfd, init_buffer, BUFFER_SIZE, 0,(struct sockaddr*)&client_addr, &client_len);printf("客户端 %s:%d 已连接\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));printf("客户端: %s\n", init_buffer);// 创建接收和发送线程if (pthread_create(&recv_tid, NULL, receive_thread, NULL) != 0) {perror("创建接收线程失败");close(sockfd);exit(EXIT_FAILURE);}if (pthread_create(&send_tid, NULL, send_thread, NULL) != 0) {perror("创建发送线程失败");pthread_cancel(recv_tid);close(sockfd);exit(EXIT_FAILURE);}// 等待线程结束pthread_join(recv_tid, NULL);pthread_join(send_tid, NULL);// 清理资源close(sockfd);return 0;
}
4.UDP的广播功能
由于UDP协议是面向无连接的,所以客户端和服务器之间不需要建立连接,所以利用UDP协议除了可以实现单播通信(一对一),还可以实现广播通信(一对多)和组播通信(一对多)。
广播通信指的是可以给处于局域网中的所有主机发送消息,只不过需要使用特定的广播地址,局域网大多数情况使用的是C类地址,对于某个C类网络而言,本地地址占8bit,如果本地地址8bit全部为1,则表示所有主机,举例:192.168.64.255就是192.168.64.xxx的广播地址。
思考:如果知道了某个局域网的广播地址,是否某台主机直接向该地址发送数据包就可以让处于该局域网的所有主机都收到该数据包?
回答:不可以,因为IP地址只能标识局域网中的主机,而每台主机都可能运行了多个网络进程,所以主机收到的数据包到底交给哪个网络进程是由系统中网络进程的端口决定的。
所以处于局域网的每台主机必须提供一个相同的端口号,这样才可以把接收的数据包保留,如果某个主机没有提供这个端口号,则收到的数据包会被丢弃。
由于广播功能属于Linux系统套接字文件的属性选项之一,所以想要启动广播功能,则需要设置套接字的属性选项,Linux系统中提供了两个函数接口来获取和设置套接字的属性选项,分别是getsockopt()和setsockopt(),使用规则如下所示:
4.1函数参数
第一个参数:sockfd指的是创建的套接字对应的文件描述符,其实是socket()函数的返回值。
第二个参数:level指的是选项对应的协议级别,一般建议把该参数设置为SOL_SOCKET即可。
SOL_SOCKET
:通用套接字选项(适用于所有协议)。
IPPROTO_TCP
:TCP 协议专属选项。
IPPROTO_IP
:IPv4 协议专属选项。
IPPROTO_IPV6
:IPv6 协议专属选项。
第三个参数:optname指的是选项的名称,比如广播选项的名称是SO_BROADCAST,如下:
注意:关于套接字选项的描述可以通过man手册的第7章来了解,输入 man 7 socket 即可。
第四个参数:optval指的是要设置的选项值,比如要启用广播选项,则设置optval为非0值。
setsockopt
:指向要设置的选项值的缓冲区。
getsockopt
:指向存储获取结果的缓冲区。
第五个参数:optlen指的是选项值的长度,一般可以通过sizeof计算对应选项值的长度大小。
setsockopt
:optval
缓冲区的大小(字节数)。
getsockopt
:输入时为缓冲区大小,输出时为实际获取的数据大小。
4.2返回结果
setsockopt()函数和getsockopt()函数调用成功则返回0,如果调用失败,则返回-1和错误码。
设计程序,创建UDP套接字,首先检查UDP套接字的广播选项是否启动,如果没有启动,则启动UDP套接字的广播选项。
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int main() {int sockfd;int broadcast_enabled;socklen_t optlen = sizeof(broadcast_enabled);// 创建UDP套接字if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {perror("创建套接字失败");exit(EXIT_FAILURE);}printf("UDP套接字创建成功,描述符: %d\n", sockfd);// 检查当前广播选项状态if (getsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast_enabled, &optlen) < 0) {perror("获取广播选项状态失败");close(sockfd);exit(EXIT_FAILURE);}// 输出当前广播选项状态if (broadcast_enabled) {printf("当前已启用广播选项\n");} else {printf("当前未启用广播选项,正在启用...\n");// 启用广播选项broadcast_enabled = 1; // 非0值表示启用if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast_enabled, sizeof(broadcast_enabled)) < 0) {perror("启用广播选项失败");close(sockfd);exit(EXIT_FAILURE);}printf("广播选项已成功启用\n");}// 再次验证广播选项状态if (getsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast_enabled, &optlen) < 0) {perror("再次验证广播选项失败");close(sockfd);exit(EXIT_FAILURE);}if (broadcast_enabled) {printf("最终验证: 广播选项已正常启用\n");} else {printf("最终验证: 广播选项启用失败\n");}// 关闭套接字close(sockfd);return 0;
}
5.UDP的组播功能
思考:通过刚才的学习,可以知道UDP套接字支持广播,也就是向处于局域网中的所有主机发送数据包,但是有时可能只打算向局域网中的部分主机发送数据包,但是又不想一对一进行通信,请问有什么方案可以实现?
回答:可以利用UDP的组播功能实现(组播也被称为多播),其实组播功能还是依赖于UDP的广播属性,只不过不再使用局域网的广播地址,而是采用组播地址实现,只有加入了该组的主机才可以收到数据包,这样可以降低负载。
IP协议中规定了的IP地址的分类,其中包含A类地址、B类地址和C类地址,但是这三类地址并没有耗尽所有的IP地址,所以剩余的IP地址又分为D类地址和E类地址。
只不过这两类地址不用于标识主机,因为这两类地址没有分配本地地址,也就是这两类地址的32bit全部分配给网络编号。
用户只需要从D类地址中选择一个IP地址作为组播地址,并把主机IP地址加入该组中即可,这样发送方只需要把数据包发给组播地址,处于该组的主机都可以接收到数据包。
思考:发送方只需要通过sendto()函数就可以向组播地址发送数据包,但是请问应该如何把主机加入到组内呢?
回答:组播也属于套接字的属性选项,所以还是调用setsockopt()函数进行设置即可,只不过函数参数需要修改,具体可参考man手册的第7章关于ip协议的描述:man 7 ip 如下:
可以看到,如果想要把主机IP加入到多播组中,需要使用一个名称叫做struct ip_mreqn的结构体,该结构体有3个成员,分别是多播组的IP地址、主机的IP地址、多播组接口索引。
注意:一般接收端才需要加入多播组,所以接收端设置即可,发送端只需要知道多播组的IP地址和端口号即可,另外,接收端的端口号需要和发送端一致,否则无法收到数据包!!!!!
设计两个程序,程序A作为服务器,程序B作为客户端,程序B加入到一个多播组中并等待服务器发送数据包,如果收到数据包则把消息内容输出到终端,程序A每隔一段时间向多播组发送一条消息即可。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>// 多播组地址和端口(需与服务器一致)
#define MULTICAST_GROUP "239.0.0.1"
#define MULTICAST_PORT 8888
#define BUFFER_SIZE 1024int main() {int sockfd;struct sockaddr_in local_addr, multicast_addr;char buffer[BUFFER_SIZE];struct ip_mreq mreq; // 多播组加入请求结构// 1. 创建UDP套接字if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {perror("套接字创建失败");exit(EXIT_FAILURE);}// 2. 绑定本地地址和端口(必须绑定才能接收多播消息)memset(&local_addr, 0, sizeof(local_addr));local_addr.sin_family = AF_INET;local_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有本地接口local_addr.sin_port = htons(MULTICAST_PORT); // 绑定到多播端口if (bind(sockfd, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0) {perror("绑定失败");close(sockfd);exit(EXIT_FAILURE);}// 3. 配置多播组加入信息// 设置多播组地址if (inet_pton(AF_INET, MULTICAST_GROUP, &mreq.imr_multiaddr.s_addr) <= 0) {perror("无效的多播地址");close(sockfd);exit(EXIT_FAILURE);}// 设置要加入多播组的本地网络接口(INADDR_ANY表示任意接口)mreq.imr_interface.s_addr = htonl(INADDR_ANY);// 4. 加入多播组if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP,&mreq, sizeof(mreq)) < 0) {perror("加入多播组失败");close(sockfd);exit(EXIT_FAILURE);}printf("客户端已加入多播组 %s:%d,等待消息...\n",MULTICAST_GROUP, MULTICAST_PORT);printf("按Ctrl+C退出...\n");// 5. 循环接收多播消息while (1) {socklen_t addr_len = sizeof(multicast_addr);memset(buffer, 0, BUFFER_SIZE);// 接收消息ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,(struct sockaddr*)&multicast_addr, &addr_len);if (recv_len < 0) {perror("接收消息失败");break;}// 显示收到的消息printf("收到多播消息: %s\n", buffer);}// 6. 退出多播组(程序正常退出时执行)setsockopt(sockfd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq));close(sockfd);return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>// 多播组地址(D类地址:224.0.0.0-239.255.255.255)
#define MULTICAST_GROUP "239.0.0.1"
#define MULTICAST_PORT 8888
// 发送间隔(秒)
#define SEND_INTERVAL 3int main() {int sockfd;struct sockaddr_in multicast_addr;int count = 0; // 消息计数器char message[256];// 1. 创建UDP套接字if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {perror("套接字创建失败");exit(EXIT_FAILURE);}// 2. 配置多播地址结构memset(&multicast_addr, 0, sizeof(multicast_addr));multicast_addr.sin_family = AF_INET;multicast_addr.sin_port = htons(MULTICAST_PORT);// 转换多播组地址为网络字节序if (inet_pton(AF_INET, MULTICAST_GROUP, &multicast_addr.sin_addr) <= 0) {perror("无效的多播地址");close(sockfd);exit(EXIT_FAILURE);}printf("多播服务器启动,向 %s:%d 发送消息(每%d秒一次)\n",MULTICAST_GROUP, MULTICAST_PORT, SEND_INTERVAL);printf("按Ctrl+C停止...\n");// 3. 循环发送消息到多播组while (1) {// 构建消息snprintf(message, sizeof(message), "多播消息 %d: 这是来自服务器的广播", ++count);// 发送消息if (sendto(sockfd, message, strlen(message), 0,(struct sockaddr*)&multicast_addr, sizeof(multicast_addr)) < 0) {perror("发送消息失败");break;}printf("已发送: %s\n", message);sleep(SEND_INTERVAL); // 等待指定时间}// 清理资源close(sockfd);return 0;
}
6.UDP协议的应用
6.1音视频流
UDP报文没有可靠性保证、顺序保证和流量控制字段等,可靠性较差。但是正因为UDP协议的控制选项较少,在数据传输过程中延迟小、数据传输效率高,适合对可靠性要求不高的应用程序。当强调传输性能而不是传输的完整性时UDP是最好的选择,比如音视频和多媒体应用。
在网络质量令人十分不满意的环境下,UDP协议数据包丢失会比较严重。但是由于UDP的特性:它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。比如聊天用的QQ就是使用的UDP协议。
6.2域名解析
此外,UDP协议也常用于域名解析服务。比如当计算机向DNS服务器查询域名对应的IP地址时通常使用UDP协议进行通信。由于DNS查询通常是简短的请求和响应,所以UDP协议适合这种快速而简单的通信。
比如主机打算响应某个网站的网络请求,但是只知道网站域名是无法通信的,需要对域名进行地址解析,得到网站的公有IP地址,所以Linux系统提供了名称叫做gethostbyname()的函数接口实现该功能,使用规则如下:
可以看到,该函数的返回值是一个指向hostent结构体类型的指针,该结构体中有一个名称叫做h_assr_list的成员,该成员是一个记录了主机域名对应IP地址的数组(一个主机域名可能会注册多个IP地址),数组中记录的IP地址是以网络字节序存储的。
为了直观的输出主机域名的IP地址(采用点分十进制最直观),所以需要把网络字节序的IP地址转换为字符串形式的点分十进制IP,Linux系统提供了一个名称叫做inet_ntoa()的函数实现,使用规则如下: