概述
UDP 是一个无连接、不可靠的数据报协议,任何可靠传输都需由应用程序提供,例如:超时重传、序列号应答机制,但是它在某些场合使用效率高,方便。它支持广播和多播。有关《基本 UDP 套接字编程》参照该文,这里只是在那个基础上,记录一些在 UDP 编程中容易出现的问题。
辅助数据
辅助数据(也称为控制信息)可通过调用 recvmsg 和 sendmsg 函数使用,这里两个函数的定义可参考文章《高级 I/O》,使用 msghdr 结构体中的 msg_control 和 msg_controllen 成员发送和接收辅助数据。
以下是辅助数据的各种用途:
辅助数据由一个或多个辅助数据对象构成,每个对象以一个结构体 cmsghdr 开头。结构体定义如下:
/* 结构 cmsghdr*/ struct cmsghdr { socklen_t cmsg_len; /* data byte count, including header */ int cmsg_level; /* originating protocol */ int cmsg_type; /* protocol-specific type */ /* followed by unsigned char cmsg_data[]; */ };
而 msg_control 指向第一个辅助数据对象,辅助数据的总长度则有 msg_controllen 指定。每个对象开头都是一个描述该对象的 cmsghdr 结构。在 cmsg_type 成员和实际数据之间可以有填充字节,从数据结尾处到下一个辅助数据对象之前也可以有填充字节。如下图所示:
以下是处理辅助数据的宏定义:
#include <sys/socket.h> /* 宏定义 */ struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh); struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg); size_t CMSG_ALIGN(size_t length); size_t CMSG_SPACE(size_t length); size_t CMSG_LEN(size_t length); unsigned char *CMSG_DATA(struct cmsghdr *cmsg); /* 结构 cmsghdr*/ struct cmsghdr { socklen_t cmsg_len; /* data byte count, including header */ int cmsg_level; /* originating protocol */ int cmsg_type; /* protocol-specific type */ /* followed by unsigned char cmsg_data[]; */ }; /* 这些宏定义用来创建和访问不是套接字负载部分的control控制信息(也称为辅助数据); * 这些控制信息可能包含所接收数据报的接口和各种很少使用的描述头等信息; */ CMSG_FIRSTHDR(); /* 返回指向辅助数据缓冲区的第一个cmsghdr结构的指针,若无辅助数据则返回NULL; */ CMSG_NXTHDR();/* 返回指向下一个cmsghdr结构的指针,若不再有辅助对象则返回NULL;*/ CMSG_ALIGN();/* given a length, returns it including the required align‐ ment. This is a constant expression. */ CMSG_SPACE();/* 返回给定数据量的一个辅助数据对象的大小 */ CMSG_DATA();/* 返回指向与cmsghdr结构关联的数据的第一个字节的指针; */ CMSG_LEN();/* 返回给定数据量的存放到cmsg_len中的值;*/
CMSG_LEN 和 CMSG_SPACE 的区别在于,前者不计辅助数据对象中数据部分之后可能的填充字节,因而返回的是用于存放在 cmsg_len 成员中的值,后者是计上尾处可能的填充字节,因而返回的是为辅助数据对象动态分配空间的大小。上面这些宏定义的使用方式如下:
struct msghdr msg; struct cmsghdr *cmptr; for(cmptr = CMSG_FIRSTHDR(&msg); cmptr != NULL; cmptr = CMSG_NXTHDR(&msg, cmptr)) { if(cmptr->cmsg_level == ... && cmptr->cmsg_type == ...) { u_char *ptr; ptr = CMSG_DATA(cmptr); /* process data pointer to by ptr */ } }
接收标志、目的 IP 地址和接口索引
下面利用辅助数据实现一个功能类似于 recvfrom 函数的编程,该函数返回三个值:返回 msg_flags 接收标志、接收数据报的目的 IP 地址、接收数据报的接口索引。其中头文件 un.h 只是定义了返回结构 目的 IP 地址和 接口索引。
#ifndef UN_H #define UN_H #include <netinet/in.h> /* 自定义结构体,成员包含目的地址、接口索引 */ struct unp_in_pktinfo{ struct in_addr ipi_addr; /* destination IPv4 address */ int ipi_ifindex;/* received interface index */ }; #endif
/* 函数功能:类似于recvfrom函数; * 返回值: * 1、返回msg_flags值; * 2、返回所接收数据报的目的地址(由IP_RECVDSTADDR套接字选项获取); * 3、返回所接收数据报接口的索引(由IP_RECVIF套接字选项获取); */ #include <sys/socket.h> #include <unistd.h> #include <netinet/in.h> #include <string.h> #include "un.h" ssize_t recvfrom_flag(int fd, void *ptr, size_t nbytes, int *flags, struct sockaddr * sa, socklen_t *salenptr, struct unp_in_pktinfo *pktp) { struct msghdr msg;/* 需要调用recvmsg函数,所以必须定义该结构 */ struct iovec iov[1];/* 非连续缓冲区,在这里只定义一个缓冲区 */ ssize_t n; #ifdef HAVE_MSGHDR_MSG_CONTROL /* 若支持msg_control成员则初始化以下值辅助数据 */ struct cmsghdr *cmptr; union{ struct cmsghdr cm; char control[CMSG_SPACE(sizeof(struct in_addr)) + CMSG_SPACE(sizeof(struct unp_in_pktinfo))]; } control_un; msg.msg_control = control_un.control; msg.msg_controllen = sizeof(control_un.control); msg.msg_flags = 0; #else /* 若不支持msg_control控制信息,则直接初始化为0 */ bzero(&msg, sizeof(msg)); #endif /* 赋值初始化msghdr结构 */ msg.msg_name = sa; msg.msg_namelen = *salenptr; iov[0].iov_base = ptr; iov[0].iov_len = nbytes; msg.msg_iov = iov; msg.msg_iovlen = 1; if( (n = recvmsg(fd, &msg, *flags)) < 0) return(n);/* 出错返回 */ /* 若recvmsg调用成功返回,则执行以下程序 */ *salenptr = msg.msg_namelen;/* 值-结果参数必须返回 */ if(pktp) /* 初始化unp_in_pktinfo结构,置地址为0.0.0.0,置接口索引为0 */ bzero(pktp, sizeof(struct unp_in_pktinfo));/* 0.0.0.0, i/f = 0 */ #ifndef HAVE_MSGHDR_MSG_CONTROL /* 若不支持msg_control控制信息,则把待返回标志置为0,并返回 */ *flags = 0;/* 值-结果参数返回 */ return(n); /* 以下程序都是处理支持msg_control控制信息的部分 */ #else /* 返回标志信息 */ *flags = msg.msg_flags;/* 值-结果参数返回 */ if(msg.msg_controllen < sizeof(struct cmsghdr) || (msg.msg_flags & MSG_CTRUNC) || pktp == NULL) return(n); /* 处理辅助数据 */ for(cmptr = CMSG_FIRSTHDR(&msg); cmptr != NULL; cmptr = CMSG_NXTHDR(&msg, cmptr)) { #ifdef IP_RECVDSTADDR /* 处理IP_RECVDSTADDR,返回接收数据报的目的地址 */ /* 其中IPPROTO_IP表示IPv4域 */ if(cmptr->cmsg_level == IPPROTO_IP && cmptr->cmsg_type == IP_RECVDSTADDR) { memcpy(&pktp->ipi_addr, CMSG_DATA(cmptr), sizeof(struct in_addr)); continue; } #endif #ifdef IP_RECVIF /* 处理IP_RECVIF,返回接收数据报的接口索引 */ if(cmptr->cmsg_level == IPPROTO_IP && cmptr->cmsg_type == IP_RECVIF) { struct sockaddr_dl *sdl;/* 数据链路地址结构中包含有接口索引成员 */ sdl = (struct sockaddr_dl *)CMSG_DATA(cmptr); pktp->ipi_ifindex = sdl->sdl_index; continue; } #endif err_quit("unknown ancillary data"); } return(n); #endif }
下面我们利用该函数来实现前面所记录《基本 UDP 套接字编程》的程序,其中服务器的处理程序变为以下实现:
#include <sys/socket.h> #include <string.h> #include <stdio.h> #include <netinet/in.h> #include "un.h" #include <net/if.h> #include <arpa/inet.h> /* 限定数据报的大小为 20 字节 */ #undef MAXLINE #define MAXLINE 20 /* to see datagram truncation */ extern ssize_t recvfrom_flag(int , void *, size_t, int *, struct sockaddr*, socklen_t *, struct unp_in_pktinfo *); extern char * Sock_ntop(const struct sockaddr *, socklen_t ); void dg_echo(int sockfd, struct sockaddr *pcliaddr, socklen_t clilen) { int flags; const int on = 1; socklen_t len; ssize_t n; char mesg[MAXLINE], str[INET6_ADDRSTRLEN], ifname[IFNAMSIZ]; struct in_addr in_zero; struct unp_in_pktinfo pktinfo; /* 若支持IP_RECVDSATDDR和IP_RECVIF套接字选项,则设置它们 */ #ifdef IP_RECVDSTADDR if (setsockopt(sockfd, IPPROTO_IP, IP_RECVDSTADDR, &on, sizeof(on)) < 0) err_ret("setsockopt of IP_RECVDSTADDR"); #endif #ifdef IP_RECVIF if (setsockopt(sockfd, IPPROTO_IP, IP_RECVIF, &on, sizeof(on)) < 0) err_ret("setsockopt of IP_RECVIF"); #endif bzero(&in_zero, sizeof(struct in_addr)); /* all 0 IPv4 address */ for ( ; ; ) { len = clilen; flags = 0; /* 读取来自套接字的数据报 */ n = recvfrom_flag(sockfd, mesg, MAXLINE, &flags, pcliaddr, &len, &pktinfo); /* 把所读取的字节数显示,最大字节数不能超过20字节,若超过,则发生截断情况; * 调用sock_ntop把源IP地址和端口号转换为表达格式并输出 */ printf("%d-byte datagram from %s", n, Sock_ntop(pcliaddr, len)); /* 若返回的IP地址不为0,则调用inet_ntop转化目的IP地址格式并输出 */ if (memcmp(&pktinfo.ipi_addr, &in_zero, sizeof(in_zero)) != 0) printf(", to %s", inet_ntop(AF_INET, &pktinfo.ipi_addr, str, sizeof(str))); printf(", index = %d", pktinfo.ipi_ifindex); /* 若返回的接口索引不为0,则调用if_indextoname获取接口名字并显示 */ if (pktinfo.ipi_ifindex > 0) printf(", recv i/f = %s", if_indextoname(pktinfo.ipi_ifindex, ifname)); printf(", flags = %d",flags); /* 以下是测试4个标志 */ #ifdef MSG_TRUNC if (flags & MSG_TRUNC) printf(" (datagram truncated)"); #endif #ifdef MSG_CTRUNC if (flags & MSG_CTRUNC) printf(" (control info truncated)"); #endif #ifdef MSG_BCAST if (flags & MSG_BCAST) printf(" (broadcast)"); #endif #ifdef MSG_MCAST if (flags & MSG_MCAST) printf(" (multicast)"); #endif printf("\n"); /* 回射文本字符串给客户端 */ sendto(sockfd, mesg, n, 0, pcliaddr, len); } }
使用 UDP 协议
UDP 协议的特性:支持广播和多播;不需要建立连接和拆除;
TCP 协议的特性:确认应答,超时重传,重复分组检测,排序乱序的分组;窗口式流量控制;慢启动和拥塞避免;
所以在以下情况必须使用 UDP 协议:广播或多播;简单的请求-应答应用程序;注意:对于海量数据传输应避免使用 UDP 协议。
由于 UDP 是不可靠传输协议,所以应用程序必须提供:超时重传、序列号确认应答机制;
并发 UDP 服务器
对于 TCP 并发服务器只需 fork 创建一个新子进程即可,然而对于 UDP 必须应对两种不同类型的服务器:
- UDP 简单服务器:即读入一个客户请求并发送一个确认应答后,与这个客户就不再相关了。这种情况下,读入客户请求的服务器可以 fork 创建一个新的子进程并让子进程去处理请求。
- UDP 复杂服务器:即 UDP 服务器与客户交换多个数据报。问题是客户知道服务器端口号只有服务器的一个众所周知端口。一个客户发送其请求的第一个数据报到达这个端口,但是服务器如何区分这是来自客户的同一个请求的后续数据报还是来自其他客户的请求数据报,该问题解决办法是让服务器为每个客户创建一个新的套接字,在其上bind 绑定一个临时端口,然后使用该套接字发送对客户的所有应答。
参考资料:
《Unix 网络编程》