1、介绍UDP相关论题
i、确定某个外来UDP数据报是目的地址入其接收接口(存在多宿主机),因为绑定某个UDP端口和通配地址的一个套接字能够在任何接口上接收单播、广播和多播数据报。
ii、TCP是一个字节流协议,又使用滑动窗口(每次发送分节都会告知对端,本端的所能容纳的字节数),所以不存在发送能力超过接收者数据报接收能力之类的事情。而对于UDP,每个输入操作对应一个UDP数据报,因而当收取的数据报大于应用进程的输入缓冲区时该如何处理(大多数是截断处理)。
iii、UDP是不可靠协议,通过序列号(用于匹配应答与请滶)和超时重传机制增强UDP传输的可靠性。
iv、如果实现不支持IP_RECVDSTADDR,那么确定外来UDP数据报目的IP地址的方法之一是依次创建新的套接字并捆绑所有的接口地址,并使用select。
v、多数UDP服务器是迭代运行的,不过有些应用系统在客户和服务器之间交换多个UDP数据报,因而需要某种形式的并发。
vi、可作为每个IPv6数据报的辅助数据指定的特定于分组的信息:源IP地址、发送接口、外出跳限和下一跳地址。可随每个IPv6数据报返回的类似信息还有:目的IP地址、接收接口和接收跳限。
2、接收标志、目的IP地址、接口索引和数据报截断
利用recvmsg,编写名为recvfrom_flags的函数,同时还返回,msg_flags值、所收取数据报的目的地址(通过IP_RECVDSTADDR套接字选项获取)、所收取数据报接收接口的索引(通过IP_RECVIF套接字选项获取)。为获取后两项,类比于IPv6套接字返回的同样两项的in6_pktinfo结构,定义如下结构:
struct unp_in_pktinfo { struct in_addr ipi_addr;//destination IPv4 address int ipi_ifindex;//received interface index };以下给使用UDP套接字的recvfrom_flags函数代码实现,
#include "unp.h" #include <sys/param.h> /* ALIGN macro for CMSG_NXTHDR() macro */ ssize_t recvfrom_flags(int fd, void *ptr, size_t nbytes, int *flagsp, SA *sa, socklen_t *salenptr, struct unp_in_pktinfo *pktp) { struct msghdr msg; struct iovec iov[1]; ssize_t n; #ifdef HAVE_MSGHDR_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 bzero(&msg, sizeof(msg)); /* make certain msg_accrightslen = 0 */ #endif msg.msg_name = sa;//真写一个msghdr并调用recvmsg 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, *flagsp)) < 0) return(n); *salenptr = msg.msg_namelen; /* pass back results */ if (pktp) bzero(pktp, sizeof(struct unp_in_pktinfo)); /* 0.0.0.0, i/f = 0 */ /* end recvfrom_flags1 */ #ifndef HAVE_MSGHDR_MSG_CONTROL *flagsp = 0; /* pass back results */ return(n); #else *flagsp = msg.msg_flags; /* pass back results */ 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地址作为控制信息返回给调用者 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 //接收接口的索引作为控制信息返回给调用者 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, len = %d, level = %d, type = %d", cmptr->cmsg_len, cmptr->cmsg_level, cmptr->cmsg_type); } return(n); #endif /* HAVE_MSGHDR_MSG_CONTROL */ }调用recvfrom_flags函数的dg_echo函数,此dg_echo函数内通过内核返回的msg_flags与MSG_TRUNC相与,判断数据报是否有截断。
#include "unpifi.h" #undef MAXLINE #define MAXLINE 20 /* to see datagram truncation */ void dg_echo(int sockfd, SA *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; #ifdef IP_RECVDSTADDR if (setsockopt(sockfd, IPPROTO_IP, IP_RECVDSTADDR, &on, sizeof(on)) < 0)//在收到UDP的目的IP地址由recvmsg函数作为辅助数据返回 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_flags(sockfd, mesg, MAXLINE, &flags, pcliaddr, &len, &pktinfo); printf("%d-byte datagram from %s", n, Sock_ntop(pcliaddr, len));//显示客户端IP if (memcmp(&pktinfo.ipi_addr, &in_zero, sizeof(in_zero)) != 0) printf(", to %s", Inet_ntop(AF_INET, &pktinfo.ipi_addr,//显示所收取数据的目的IP地址 str, sizeof(str))); if (pktinfo.ipi_ifindex > 0) printf(", recv i/f = %s", If_indextoname(pktinfo.ipi_ifindex, ifname));//调用if_indextoname获取接口名字,再显示出来 #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); } }
3、何时用UDP代替TCP
UDP的优势:
i、UDP支持广播和多播
ii、UDP没有连接建立和拆除。UDP只需要两个分组就能交换一个请求和一个应答。TCP却需要大约20个分组(假设每次请求-应答交换建立一个新的TCP连接)。
TCP较UDP所具的特性
i、正面确认,丢失分组重传,重复分组检测,给被网络打乱次序的分组排序
ii、窗口式流量控制
iii、慢启动和拥塞避免
何时该使用UDP场合:
i、对于广播或多播应用程序必须使用UDP
ii、对于简单的请求-应答应用程序可以使用UDP,不过错误检测功能必须加到应用程序内部,如确认、超时和重传
iii、对于海量数据传输(如文件传输)不应该使用UDP
4、给UDP应用增加可靠性(即添加序列号、超时重传机制)
增加如下两大特性,提高UDP应用的可靠性
超时和重传:用于处理丢失的数据报
序列号:供客户验证一个应答是否匹配相应的请求
以下是一个增加了可靠性的UDP应用实例代码,
//以下即是属于unprtt.h文件 #ifndef __unp_rtt_h #define __unp_rtt_h #include "unp.h" struct rtt_info { float rtt_rtt; /* most recent measured RTT, seconds */ float rtt_srtt; /* smoothed RTT estimator, seconds */ float rtt_rttvar; /* smoothed mean deviation, seconds */ float rtt_rto; /* current RTO to use, seconds */ int rtt_nrexmt; /* #times retransmitted: 0, 1, 2, ... */ uint32_t rtt_base; /* #sec since 1/1/1970 at start */ }; #define RTT_RXTMIN 2 /* min retransmit timeout value, seconds */ #define RTT_RXTMAX 60 /* max retransmit timeout value, seconds */ #define RTT_MAXNREXMT 3 /* max #times to retransmit */ /* function prototypes */ void rtt_debug(struct rtt_info *); void rtt_init(struct rtt_info *); void rtt_newpack(struct rtt_info *); int rtt_start(struct rtt_info *); void rtt_stop(struct rtt_info *, uint32_t); int rtt_timeout(struct rtt_info *); uint32_t rtt_ts(struct rtt_info *); extern int rtt_d_flag; /* can be set nonzero for addl info */ #endif /* __unp_rtt_h */ #include "unprtt.h" #include <setjmp.h> #define RTT_DEBUG static struct rtt_info rttinfo; static int rttinit = 0; static struct msghdr msgsend, msgrecv; /* assumed init to 0 */ static struct hdr { uint32_t seq; /* sequence # */ uint32_t ts; /* timestamp when sent */ } sendhdr, recvhdr; static void sig_alrm(int signo); static sigjmp_buf jmpbuf;//设置跳转缓冲区 ssize_t dg_send_recv(int fd, const void *outbuff, size_t outbytes, void *inbuff, size_t inbytes, const SA *destaddr, socklen_t destlen) { ssize_t n; struct iovec iovsend[2], iovrecv[2]; if (rttinit == 0) {//首次被调用时,进行初始化 rtt_init(&rttinfo); /* first time we're called */ rttinit = 1; rtt_d_flag = 1; } sendhdr.seq++;//给当前分组递增发送序列号 msgsend.msg_name = destaddr; msgsend.msg_namelen = destlen; msgsend.msg_iov = iovsend; msgsend.msg_iovlen = 2; iovsend[0].iov_base = &sendhdr; iovsend[0].iov_len = sizeof(struct hdr); iovsend[1].iov_base = outbuff; iovsend[1].iov_len = outbytes; msgrecv.msg_name = NULL; msgrecv.msg_namelen = 0; msgrecv.msg_iov = iovrecv; msgrecv.msg_iovlen = 2; iovrecv[0].iov_base = &recvhdr; iovrecv[0].iov_len = sizeof(struct hdr); iovrecv[1].iov_base = inbuff; iovrecv[1].iov_len = inbytes; Signal(SIGALRM, sig_alrm); rtt_newpack(&rttinfo); /* initialize for this packet */ sendagain: #ifdef RTT_DEBUG fprintf(stderr, "send %4d: ", sendhdr.seq); #endif sendhdr.ts = rtt_ts(&rttinfo);//获取时间戳 Sendmsg(fd, &msgsend, 0);//发 alarm(rtt_start(&rttinfo)); /* calc timeout value & start timer */ #ifdef RTT_DEBUG rtt_debug(&rttinfo); #endif if (sigsetjmp(jmpbuf, 1) != 0) { if (rtt_timeout(&rttinfo) < 0) {//用于计算下一个RTO(指数回退) err_msg("dg_send_recv: no response from server, giving up"); rttinit = 0; /* reinit in case we're called again */ errno = ETIMEDOUT; return(-1); } #ifdef RTT_DEBUG err_msg("dg_send_recv: timeout, retransmitting"); #endif goto sendagain; } do { n = Recvmsg(fd, &msgrecv, 0);//收 #ifdef RTT_DEBUG fprintf(stderr, "recv %4d\n", recvhdr.seq); #endif } while (n < sizeof(struct hdr) || recvhdr.seq != sendhdr.seq);//确认序列号, alarm(0); /* stop SIGALRM timer */ /* 4calculate & store new RTT estimator values */ rtt_stop(&rttinfo, rtt_ts(&rttinfo) - recvhdr.ts); return(n - sizeof(struct hdr)); /* return size of received datagram */ } static void sig_alrm(int signo) { siglongjmp(jmpbuf, 1); }
5、捆绑接口地址(利用接口解析函数)
通过get_ifi_info函数监视本地主机所有接口以便获悉某个数据报在何时及哪个接口上到达的UDP应用程序。此用途允许接收程序获悉该UDP数据报的目的地址(但我不明白,即使获悉了该UDP数据报的目的地址(既然能发给我,目的地址就该是我),有什么用吗?),决定一个数据报的递送套接字的正是它的目的地址,即使主机不支持IP_RECVDSTADDR套接字选项也不影响目的地址的获悉。
以下给出使用该技术的一个简UDP服务器程序例子,其捆绑所有单播地址、所有广播地址以及及通配地址。
/* include udpserv1 */ #include "unpifi.h" void mydg_echo(int, SA *, socklen_t, SA *); int main(int argc, char **argv) { int sockfd; const int on = 1; pid_t pid; struct ifi_info *ifi, *ifihead; struct sockaddr_in *sa, cliaddr, wildaddr; for (ifihead = ifi = Get_ifi_info(AF_INET, 1);//通过for循环监视并获取本地主机所有IPv4接口 ifi != NULL; ifi = ifi->ifi_next) { /*4bind unicast address */ sockfd = Socket(AF_INET, SOCK_DGRAM, 0); Setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));//开启SO_REUSEADDR选项 sa = (struct sockaddr_in *) ifi->ifi_addr; sa->sin_family = AF_INET; sa->sin_port = htons(SERV_PORT); Bind(sockfd, (SA *)sa, sizeof(*sa)); printf("bound %s\n", Sock_ntop((SA *)sa, sizeof(*sa))); if ((pid = Fork()) == 0) { /* child */ mydg_echo(sockfd, (SA *)&cliaddr, sizeof(cliaddr), (SA *)sa); exit(0); /* never executed */ } /* end udpserv1 */ /* include udpserv2 */ if (ifi->ifi_flags & IFF_BROADCAST) { /* 4try to bind broadcast address */ sockfd = Socket(AF_INET, SOCK_DGRAM, 0); Setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); sa = (struct sockaddr_in *) ifi->ifi_brdaddr; sa->sin_family = AF_INET; sa->sin_port = htons(SERV_PORT); if (bind(sockfd, (SA *)sa, sizeof(*sa)) < 0) { if (errno == EADDRINUSE) { printf("EADDRINUSE: %s\n", Sock_ntop((SA *)sa, sizeof(*sa))); Close(sockfd); continue; } else err_sys("bind error for %s", Sock_ntop((SA *)sa, sizeof(*sa))); } printf("bound %s\n", Sock_ntop((SA *)sa, sizeof(*sa))); if ((pid = Fork()) == 0) { /* child */ mydg_echo(sockfd, (SA *)&cliaddr, sizeof(cliaddr), (SA *)sa); exit(0); /* never executed */ } } } /* end udpserv2 */ /* include udpserv3 */ /* 4bind wildcard address */ sockfd = Socket(AF_INET, SOCK_DGRAM, 0); Setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); bzero(&wildaddr, sizeof(wildaddr)); wildaddr.sin_family = AF_INET; wildaddr.sin_addr.s_addr = htonl(INADDR_ANY); wildaddr.sin_port = htons(SERV_PORT); Bind(sockfd, (SA *)&wildaddr, sizeof(wildaddr)); printf("bound %s\n", Sock_ntop((SA *)&wildaddr, sizeof(wildaddr))); if ((pid = Fork()) == 0) { /* child */ mydg_echo(sockfd, (SA *)&cliaddr, sizeof(cliaddr), (SA *)sa); exit(0); /* never executed */ } exit(0); } /* end udpserv3 */ /* include mydg_echo */ void mydg_echo(int sockfd, SA *pcliaddr, socklen_t clilen, SA *myaddr) { int n; char mesg[MAXLINE]; socklen_t len; for (; ; ) { len = clilen; n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len); printf("child %d, datagram from %s", getpid(),//以下采用两个printf函数主要是因为sock_ntop()使用 Sock_ntop(pcliaddr, len)); //静态缓冲区,如在printf中,作为参数两次调用sock_ntop() printf(", to %s\n", Sock_ntop(myaddr, clilen));//将会被覆盖每一次调用时信息。 Sendto(sockfd, mesg, n, 0, pcliaddr, len); } } /* end mydg_echo */
6、并发UDP服务器
i、第一种UDP服务器,读入一个客户请求并发送一个应答后,与这个客户就不再相关了。读入客户请求的服务器可以fork一个子进程并让子进程去处理该请求。该“请求”(即请求数据报的内容以及含有客户协议 地址的套接字地址结构)通过由fork复制的内存映像传递给子进程。然后子进程把它的应答直接发送给客户。
ii、第二种UDP服务器与客户交换多个数据报。存在一个问题,客户知道的服务器端口号只有服务器的一个众所周知的端口,一个客户发送其请求的第一个数据报到这个端口,但是服务器如何区分这是来自该客户同一个请求的后续数据报还是来自其他客户请求的数据报?解决办法,让服务器为每个客户创建一个新的套接字,在其上bind一个临时端口,然后使用该套接字发送对该客户的所有应答。此办法要求客户查看服务器第一个应答中的源端口号,并把本请求的后续数据报发送到该端口(这点很好理解,如果不查看第一个应答中的信息,那么客户还是会将信息发往原来众所周知的端口)。
对第二种UDP服务器,以下分成两种实现方法(一,不是由inetd激发的,二,由inetd激发的)图。
以上知识点来均来自steven先生所著UNP卷一(version3),刚开始学习网络编程,如有不正确之处请大家多多指正。