UNP Chapter 20 - 高级UDP套接口编程
20.1. 概述
本章是各种影响应用程序使用UDP套接口话题的一个集合。首先是确定UDP数据报的目的地址以及是从哪个接口接收数据报的,因为一个绑定UDP端口和通配地址的套接口能在任何接口上接收单播、广播和多播数据报。
TCP是一个字节流协议,它使用一个滑动窗口,它没有像记录边界或者允许发送者用数据淹没接收者等事情需要考虑。然而对于UDP,每个输入操作对应一个UDP数据报,所以当接收的数据报比应用进程的输入缓冲区大时,就产生了如何处理的问题。
UDP是不可靠的,但对一些应用程序来说使用UDP而不用TCP是有意义的
如果实现不支持IP_RECVDSTADDR套接口选项,那么一个确定UDP数据报目的IP地址的方法是捆绑所有的接口地址并使用select
多数UDP服务器程序是迭代执行的,但是有一些应用程序需要客户和服务器间交换多个UDP数据报,于是需要某种形式的并发
20.2. 接收标志、目的IP地址和接口索引
作为recvmsg的一个例子,我们将要写一个名为recvfrom_flags的函数,它与recvfrom类似,但他还返回:
1. 返回的msg_flags值
2. 收到的数据报的目的地址(通过设置IP_RECVDSTADDR套接口选项)
3. 接收数据报接口的索引(通过设置IP_RECIF套机口选项)
为了返回最后两项,我们定义如下结构:
struct in_pktinfo
{
struct in_addr ipi_addr; /* destination IPv4 address */
int ipi_ifindex; /* received interface index */
};
//recvfrom_flags
//为了使用宏CMSG_NXTHDR, 需要包含头文件<sys/param.h>
//包含<net/if_dl.h>头文件,它定义了sockaddr_dl结构,接收接口的索引要从它返回
#include "unp.h"
#include <sys/param.h> /* ALIGN macro for CMSG_NXTHDR() macro */
#ifdef HAVE_SOCKADDR_DL_STRUCT
#include <net/if_dl.h>
#endif
/* 函数参数与recvfrom类似,不过第四个参数现在是一个指向整数标志的指针,第七个参数是新的:它是一个指向in_pktinfo结构的指针,这个结构将填入所有接收数据报的目的地址和接收它的接口索引 */
ssize_t recvfrom_flags(int fd, void * ptr, size_t nbytes, int * flagsp, SA * sa, socklen_t * salenptr, struct in_pktinfo * pktp)
{
struct msghdr msg;
struct lovec iov[1];
ssize_t n;
#ifdef HAVE_MSGHDR_MSG_CONTROL
struct cmsghdr * cmptr;
uion
{
struct cmsghdr cm;
char control[CMSG_SPACE(sizeof(struct in_addr)) + CMSG_SPACE(sizeof(struct sockaddr_dl))];
} 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;
msg.msg_namelen = * salenptr;
iov[0].iov_base = ptr;
iov[0].iov_len = nbytes;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
/* 填写完一个msghdr结构后调用recvmsg */
if( (n = recvmsg(fd, &msg, * flagsp) < 0)
return(n);
* salenptr = msg.msg_namelen; /* pass back results */
if(pktp)
bzero(pktp, sizeof(struct in_pktinfo)); /* 0.0.0.0, i/f = 0 */
#ifndef HAVE_MSGHDR_MSG_CONTROL
* flagsp = 0; /* pass back results */
return(0); /* 如果实现不支持msg_control成员,我们只设置返回标志0就返回,剩下的函数部分处理msg_control信息 */
#else
* flagsp = msg.msg_flags; /* pass back results */
/* 返回msg_flags的值,接着返回调用者,如果没有控制信息、控制信息被截断、调用者不想要返回一个in_ptkinfo结构 */
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)) /* CMSG_FIRSTHDR 和 CMSG_NXTHDR宏处理任意数目的辅助数据对象 */
{
#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("unkonwn ancillary data, len = %d, level = %d, type = %d", cmptr->cmsg_len, cmptr->cmsg_level, cmptr->cmsg_type);
}
return(n);
#endif /* HAVE_MSGHDR_MSG_CONTROL */
}
IP_RECVIF返回的辅助数据对象
为了测试我们的函数,我们修改dg_echo函数去调用recvfrom_flags而不是recvfrom。下面是新版本的dg_echo函数
#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 in_pktinfo pktinfo;
#indef 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;
/* 数据报由调用recvfrom_flags读入 */
n = Recvfrom_flags(sockfd, mesg, MAXLINE, &flags, pcliaddr, &len, &pktinfo);
printf("%d byte datagram from %s", n, Sock_ntop(pcliaddr, len));
if( memcmp(&pktinfo.ipi_addr, &in_zero, sizeof(in_zero)) != 0)
printf(", to %s", inet_notp(AF_INET, &pktinfo.ipi_addr, str, sizeof(str)));
if(pktinfo.ipi_ifindex > 0)
printf(", recv i/f = %s", if_indextoname(pktinfo.ipi_ifindex, ifname));
#ifdef MSG_TRUNC
if(flags & MSG_TRUNC)
printf(" (datagram truncated)");
#endif
#ifdef MSG_CTRUNC
if(flags & MSG_CTRUNC)
printf(" (control info truncated)");
#endif
#indef MSG_BCAST
if(flags & MSG_BCAST)
printf(" (broadcast)");
#endif
#ifdef MSG_MCAST
if(flags & MSG_MCAST)
printf(" (multicast)");
#endif
print("\n");
Sendto(sockfd, mesg, n, 0, pcliaddr, len);
}
}
20.3. 数据报截断
前一节的例子中表明,当一个UDP数据报长度大于应用进程缓冲区时,recvmsg在msghdr结构中的msg_flags成员上设置MSG_TRUNC标志。但并非所有实现都以这种方式处理超过预期长度的UDP数据报。这里有三种可能情形:
1. 丢掉超出的字节并给应用进程返回MSG_TRUNC标志。这要求应用进程调用recvmsg来接收这个标志。
2. 丢到超出的字节但不通知应用进程。
3. 保留超出的字节并在随后这个套接口上的读操作中返回这些数据。
20.4. 何时使用UDP而不是TCP
20.5. 给UDP应用程序增加可靠性
就像在前一节提到的,如果我们想要在请求-应答式应用程序中使用UDP,那么我们必须对我们的客户增加两个特性:
1. 超时和重传以处理丢失的数据报
2. 序列号,这样客户可以验证一个应答是对应相应的请求的
这两个特性是多数使用简单的请求-应答范例的现有UDP应用程序的一部分:例如DNS解析器,SNMP代理,TFTP和RPC。我们不是试图用UDP于海量数据传输----我们的意图是为了剖析发送请求并等待应答的应用程序。
加入序列号比较简单。客户给每个请求附加一个序列号,并且服务器必须在应答中给客户返回这个号,这样可以让客户验证给定的应答是对应所发请求的应答。
老式的处理超时和重传的方法是发送一个请求后等待N秒。如果没有收到应答,则重传并再等待另外N秒。这种情况发生一定次数后放弃。这是一种线性重传定时器。这种方法的问题是数据报在一个互联网上往返的时间会从LAN上的远远不到一秒变到WAN上的许多秒。影响往返时间(RTT)的因素是距离、网速、拥塞。我们必须采用一个将实际测得的RTT以及RTT随时间的变化考虑在内的超时和重传算法。
我们想要计算用于我们发送的每个分组的重传超时(RTO)。为了计算它我们先测量RTT:分组的实际往返时间。每次我们测得一个RTT,我们就更新两个统计性估计因子:srtt是平滑了的RTT估计因子,rttvar是平滑了的平均偏差估计因子。
delta = measuredRTT - srtt
srtt <- srtt + g X delta
rttvar <- rttvar + h(|delta| - rttvar)
RTO = srtt + 4 X rttvar
delta是测得的RTT和当前平滑了的RTT估计因子(srtt)之间的差
g是施加在RTT估计因子上的增益因子,值为1/8
h是施加在平均偏差估计因子上的增益因子,值为1/4
“重传二义性问题”,当重传定时器超时时,三种可能的情形:
1. 请求丢失了
2. 应答丢失了
3. RTO太小
“长胖管道”:除了给每个请求加上一个服务器必须返回的序列号,我们还加上一个也要服务器返回的时间戳。每次我们发送一个请求,保存当前时间在时间戳中。当收到一个应答时,我们计算RTT为当前时间减去在应答中由服务器返回的时间戳。由于每个请求载有一个将由服务器返回的时间戳,因此我们可以计算我们收到的每个应答的RTT。这就再也没有二义性了。而且,由于服务器所做的只是返回客户的时间戳,因此客户可以用任何想用的时间戳形式,并且不需要客户和服务器有同步的时钟。
例子
dg_cli函数
#include "unp.h"
ssize_t Dg_send_recv(int , const void *, size_t, void *, size_t, const SA *, socklen_t);
void dg_cli(FILE * fp, int sockfd, const SA * pservaddr, socklen_t servlen)
{
ssize_t n;
char sendline[MAXLINE], recvline[MAXLINE+1];
while(Fgets(sendline, MAXLINE, fp) != NULL)
{
n = Dg_send_recv(sockfd, sendline, strlen(sendline), recvline, MAXLINE, pservaddr, servlen); /* 用dg_send_recv替换掉sendto和recvfrom */
recvline[n] = 0; /* null terminate */
Fputs(recvline, stdout);
}
}
RTT函数的框架以及它们何时被调用
static sigjmp_buf jmpbuf;
{
...
from request
signal(SIGALRM, sig_alrm); /* establish signal handler */
rtt_newpack(); /* initialize rexmt counter to 0 */
sendagain:
sendto();
alarm(rtt_start()); /* set alarm for RTO seconds */
if(sigsetjmp(jmpbuf, 1) != 0)
{
if(rtt_timeout()) /* double RTO, retransmitted enough? */
give up
goto sendagain; /* retransmit */
}
do
{
recvfrom();
} while( wrong sequence #)
alarm(0); /* trun off alarm */
rtt_stop(); /* calculate RTT and update estmiators */
process reply
...
}
void sig_alrm(int signo)
{
siglongjmp(jmpbuf, 1);
}
函数dg_send_recv
#include "unprtt.h"
#include <setjmp.h>
#define RTT_DEBUG
static struct rtt_info rttinfo;
static int rttinit = 0;
/* 我们想把给每个分组附加序列号和时间戳的事实对调用者隐藏起来 */
/* 最简单的方法是使用writev,写我们的头部(hdr结构)跟着是调用者的数据,将它作为单独的一个UDP数据报 */
/* 由于我们用的是UDP且必须指定一个目的地址,因此我们必须使用sendmsg和recvmsg的iovec功能而不是用sendto和recvfrom */
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函数 */
rtt_init(&rttinfo); /* first time we are called */
rttinit = 1;
rtt_d_flag = 1;
}
/* 填写用于输入输出的两个msghdr结构。我们给输出分组增加了发送序列号,但是直到发送这个分组时我们才设置发送时间戳(由于它可能被重传,而每次重传都需要当前时间戳) */
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[0].iov_base = outbuff;
iovsend[0].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); /* 建立一个处理SIGALRM的信号处理程序,rtt_newpack 设置重传计数为0 */
rtt_newpack(&rttinfo); /* initialize for this packet */
sendagain;
sendhdr.ts = rtt_ts(&rttinfo); /* 当前的时间戳由rtt_ts获得并被存入hdr结构中 */
Sendmsg(fd, &msgsend, 0); /* 单个UDP数据报由sendmsg送出 */
alarm(rtt_start(&rttinfo)); /* calc timeout value & start timer */ /* rtt_start返回这次超时值的秒数,我们以此调用alarm以调度SIGALRM */
if(sigsetjmp(jmpbuf, 1) != 0) /* 用sigsetjmp给信号处理程序建立了一个跳转缓冲区。通过调用recvmsg我们等待下一个数据报 */
{
if(rtt_timeout(&rttinfo) < 0) /* 当超时发生时,rtt_timeout计算下一个RTO(指数回退) */
{
err_msg("dg_send_recv: no response from server, giving up");
rttinit = 0; /* reinit in case we are called again */
errno = ETIMEOUT;
return(-1);
}
goto sendagain;
}
do
{ /* 通过调用recvmsg来等待一个数据报的到来。当它返回时,数据报长度必须至少是我们的hdr结构的长度,并且序列号与发送序列号相同。如果有一个比较失败,则再一次调用recvmsg*/
n = Recvmsg(fd, &msgrecv, 0);
} while( n < sizeof(struct hdr) || recvhdr.seq != sendhdr.seq);
/* 当期待的应答收到时,当前alarm被关闭并且rtt_stop更新RTT估计因子。rtt_ts返回当前的时间戳,从它这儿减去所收到数据报中的时间戳得到RTT */
alarm(0); /* stop SIGALRM timer */
/* calculate & 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被调用,导致dg_send_recv的sigsetjmp返回1 */
siglongmup(jmpbuf, 1);
}
由dg_send_recv函数调用的各种RTT函数
#ifndef __unp_rtt_h
#define __unp_rrt_h
#include "unp.h"
struct rtt_info { /* 这个结构含有用于给客户和服务器间的分组定时所需的变量 */
float rtt_rtt; /* most recent measured RTT, seconds */
float rtt_srtt; /* smoothed RTT esimator, 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 protptypes */
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 nozero for addl info */
#endif /* __unp_rtt_h */
RTT_RTOCALC(ptr)宏 和 RTT函数
#include "unprtt.h"
int rtt_d_flag = 0; /* debug flag; can be set nonzero by caller */
/*
* Calculate the RTO value based on current estimators:
* smoothed RTT plus four times the deviation.
*/
#define RTT_RTOCALC(ptr) ((ptr)->rtt_srtt + (4.0 * (ptr)->rtt_rttvar))
/* RTT_RTOCALC宏计算RTO为RTT估计因子加上4倍的平均偏差估计因子 */
static float rtt_minmax(float rto)
{ /* rtt_minmax保证RTO定义在上下界之中 */
if(rto < RTT_RMTMIN)
rto = RTT_RXTMIN;
else if (rto = RTT_RXTMAX)
rto = RTT_RXTMAX;
return(rto);
}
void rtt_init(struct rtt_info * ptr)
{ /* rtt_init由dg_send_recv在任何分组第一次发送时调用 */
struct itmeval tv;
Gettimeofday(&tv, NULL); /* gettimeofday返回当前的时间和日期,存放于timeval结构中 */
ptr->rtt_base = tv.tv_sec; /* # sec since 1/1/1970 at start */
ptr->rtt_rtt = 0;
ptr->rtt_srtt = 0;
ptr->rtt_rttvar = 0.75;
ptr->rtt_rto = rtt_minmax(RTT_RTOCALC(ptr));
/* first RTO at (srtt + (4 * rttvar) ) = 3 seconds */
}
uint32_t rtt_ts(struct rtt_info * ptr)
{/* rtt_ts返回当前的时间戳,让调用者把它作为一个无符号32为整数存入要发送的数据报中 */
uint32_t ts;
struct timeval tv;
Gettimeofday(&tv, NULL);
/* 我们从gettimeofday中获得当前时间和日期,接着减去调用rtt_init时的秒数(其值被保存在rtt_base中) */
ts = ((tv.tv_sec - ptr->rtt_base) * 1000) + (tv.tv_usec/1000);
return(ts);
}
void rtt_newpack(struct rtt_info * ptr)
{/* 设置重传计数为0。当新的分组第一次发送时应调用这个函数 */
ptr->rtt_nrexmt = 0;
}
int rtt_start(struct rtt_info * ptr)
{ /* 返回当前RTO秒数,这个值可以做alarm参数 */
return( (int) (ptr->rtt_rto + 0.5)); /* round float to int */
/* return value can be used as: alarm(rtt_start(&foo)) */
}
void rtt_stop(struct rtt_info * ptr, uint32_t ms)
{ /* 在一个应答收到后,调用它去更新RTT估计因子并计算新的RTO */
/* 第二个参数是测得的RTT,它是由调用者通过从当前时间戳(rtt_ts)中减去收到的应答中的时间戳获得的 */
double delta;
ptr->rtt_rtt = ms / 1000.0; /* measured RTT in seconds */
/*
* Update our estimators of RTT and mean deviation of RTT.
* See Jacobson's SIGCOMM '88 paper, Appendix A, for the details.
* We use floating point here for simplicity.
*/
delta = ptr->rtt_rtt - ptr->rtt_srtt;
ptr->rtt_srtt += delta / 8; /* g = 1/8 */
if(delta < 0.0)
delta = -delta; /* |delta| */
ptr->rtt_rttvar += (delta - ptr->rtt_rttvar) / 4; /* h = 1/4 */
ptr->rtt_rto = rtt_minmax(RTT_RTOCALC(ptr));
}
int rtt_timeout(struct rtt_info * ptr)
{/* 当重传定时器超时时,这个函数被调用 */
ptr->rtt_rto * = 2; /* next RTO */ /* 当前的RTO加倍:这就是指数回退 */
if(++ ptr->rtt_nrexmt > RTT_MAXNREXMT) /* 如果达到重传上限,就给调用者返回-1 */
return(-1); /* time to give up for this packet */
return(0);
}
20.6. 捆绑接口地址
#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;
/* get_ifi_info获取所有接口的所有IPv4地址,包括别名。接着遍历每次返回的ifi_info结构 */
for(ifihead = ifi = Get_ifi_info(AF_INET, 1); ifi != NULL; ifi = ifi->ifi_next)
{
/* bind unicast address */
sockfd = Socket(AF_INET, SOCK_DGRAM, 0); /* 创建一个UDP套接口并在其上捆绑单播地址 */
Setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); /* 设置选项*/
sa = (sturct sockaddr_in * ) ifi->ifi_addr;
sa->sin_family = AF_INET;
sa->sin_port = htons(SERV_PORT); /* 给所有IP地址捆绑同一个端口(SERV_PORT)*/
Bind(sockfd, (SA *) sa, sizeof( *sa));
printf("bound %s \n", Sock_ntop( (SA *) sa, sizeof(* sa)));
if((pid = Fork()) == 0) /* child */
{ /* fork一个子进程并由这个子进程调用mydg_echo函数,这个函数等待任意的数据报到达这个套接口并且将它回送给发送者 */
mydg_echo(sockfd, (SA *)&cliaddr, sizeof(cliaddr), (SA*)sa);
exit(0); /* never executed */
}
if(ifi->ifi_flags & IFF_BROADCAST) /* 如果接口支持广播,则创建一个UDP套接口,并将其广播地址捆绑在其上 */
{
/* try 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)
{ /* 允许bind的EADDRINUSE错误,因为如果在同一子网上的接口有多个地址(别名),那么每个不同的单播地址将会有相同的广播地址,我们只希望第一次bind成功 */
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 */
{ /* fork一个子进程并由它调用mydg_echo函数 */
mydg_echo(sockfd, (SA*)&cliaddr, sizeof(cliaddr), (SA*)sa);
exit(0); /* never executed */
}
}
}
/* bind 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); /* main 函数终止,服务器作为派生的子进程继续执行 */
}
/* 第四个参数是绑定这个套接口上的IP地址。这个套接口应该只接收目的地址为那个IP地址的数据报。如果IP地址是通配的,这个套接口应只能接收与绑定同一端口的其他套接口匹配的数据报 */
void mydg_echo(int sockfd, SA * pcliaddr, socklen_t clilen, SA * myaddr)
{
int n;
char mesg[MAXLINE];
socklen_t len;
for( ; ; )
{ /* 数据报用recvfrom读入,用sendto发回给客户。这个函数还输出客户绑定的套接口上的IP地址 */
len = clilen;
n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);
printf("child %d, datagram from %s", getpid(), Sock_ntop(pcliaddr, len));
Sendto(sockfd, mesg, n, 0, pcliaddr, len);
}
}
20.7. 并发UDP服务器
多数的UDP服务器程序是迭代执行的:服务器等待一个客户请求,读入请求,处理请求,送回应答,接着等待下一个客户请求。但是,当处理客户请求要很长时间时,就要有一定形式的并发性。
当使用TCP时,能够简化服务器并发性的根源在于每个客户连接都是唯一的,也就是说TCP套接口对于每个连接多是唯一的。但是UDP中我们必须处理两个不同类型的服务器。
1. 第一种是简单的UDP服务器,它读入一个客户请求,发送应答,接着与这个客户就无关了。在这种情形里,读客户请求的服务器可以fork一个子进程去处理请求。“请求”(也就是数据报的内容和保存在客户协议地址中的套接口地址结构)通过从fork得来的内存映像传递给子进程。子进程接着直接给客户发送它的应答。
2. 第二种是与客户交换多个数据报的UDP服务器。问题是客户只知道服务器的端口是服务器众所周知的端口。客户发送请求的第一个数据报到这个端口,但是服务器又怎么能区分这是那个客户的后继数据报还是新的请求呢?这种问题的典型解决方法是让服务器给每个客户创建一个新的套机口,bind一个临时端口到那个套机口,并且对所有的回答都用这个套机口。这要求客户看一下服务器第一个应答中的端口号,并向那个端口发送请求的后继数据报。
如果是一个独立的TFTP服务器(也就是说不是由inetd启动),假设子进程给新套接口捆绑的临时端口是2134.
如果用inetd,这就要多一个步骤
20.8. IPv6分组信息
IPv6允许应用程序对外出的数据报指定最多四条信息:
1. 源IPv6地址
2. 外出接口索引
3. 外出跳限
4. 下一跳地址
这些信息是作为辅助数据使用sendmsg发送的。对于收到的分组可以返回三条类似的信息,他们是作为辅助数据由recvmsg返回的:
1. 目的IPv6地址
2. 到达接口索引
3. 到达跳限
in6_pktinfo结构含有发送数据报的源IPv6地址和外出接口索引,或者含有收到的数据报的目的IPv6地址和到达接口索引:
struct in6_pktinfo
{
struct in6_addr ipi6_addr; /* src / dst IPv6 address */
int ipi6_ifindex; /* send / recv interface index */
};
发送该信息不要什么特别的要求:只是给sendmsg指定作为辅助数据的控制信息。但是,只有应用程序打开了IPv6_PKTINFO套接口选项,这个信息才会有recvmsg作为辅助数据返回。
20.9. 小结