UDP是不可靠的(它使用不可靠的IP协议),它只是一直发送数据,而不管数据有没有被对方成功接收。怎样能确保发送的数据报被对方成功接收?这需要发送方和接收方共同协作。
1. 接收方成功收到数据后发回一个确认,发送方收到这个确认后就知道接收方已成功收到数据。
2. 如果发送方在一定的时间内没有收到确认,则重传数据。
在我们的UDP回射客户和服务器例子中,客户发送的数据报都会被服务器回射,也就是每个数据报都对应了一个回应。为了使这个回应变为“确认”,我们为每个数据报添加一个序列号,这样客户就能通过检查回射的数据报中的序列号来确认这个数据报已被对方接收了。
第2点“一定的时间内”究竟是多长呢?我们把这个时间称为超时重传时间RTO。这个值是动态计算的,它和报文往返时间RTT相关。为了计算RTT,我们为每个发送的数据报添加一个时间戳。在收到这个数据报的回应时,用当前时间减去数据报中的时间戳,就得出了RTT。
RTO在下面两种情形下更新:
每次定时器超时时
(下一次的RTO = 本次RTO * 2)。
每次确认数据报已被对方接收时(验证收到的数据报的序列号)
使用下面的算法计算下一次发送数据报时的重传时间RTO:
delta = 测得的RTT - srtt
srtt = srtt + g * delta
rttvar = rttval + h (|delta| - rttval)
RTO = srtt + 4 * rttval
srtt 称为平滑的RTT估算因子,rttvar称为平滑的平均偏差估算因子。
初始时,srtt值为0,rttval值为0.75,初始超时时间RTO为3秒钟。
为了实现超时重传我们定义了以下结构体和宏:
struct rtt_info {
float rtt_rtt; /*往返时间*/
float rtt_srtt; /*平滑化RTT估算因子*/
float rtt_rttvar; /*平滑化平均偏差估算因子*/
float rtt_rto; /*超时重传时间RTO*/
float rtt_nrexmt; /*重传次数*/
uint32_t rtt_base; /*以秒为单位的时间基准*/
};
#define RTT_RXTMIN 2 /*最小超时重传时间*/
#define RTT_RXTMAX 60 /*最大超时重传时间*/
#define RTT_MAXNREXMT 3 /*最大重传次数*/
增加可靠性的dg_cli函数代码如下:
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);
recvline[n] = 0;
Fputs(recvline, stdout);
}
}
Dg_send_recv函数就是我们实现的可靠地发送数据的函数,它的功能是等待确认数据已被对方收到,如果超时了,则重传数据,超过一定重传次数而没有收到确认时则报错。代码如下:
static struct rtt_info rttinfo;
static int rttinit = 0;
static struct msghdr msgsend, msgrecv;
static struct hdr {
uint32_t seq; /*序列号*/
uint32_t ts; /*时间戳*/
} 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);
rttinit = 1;
rtt_d_flag = 1;
}
/*填充发送数据序列号*/
sendhdr.seq++;
msgsend.msg_name = (void *)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 = (void *)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;
Signal1(SIGALRM, sig_alrm);
/*重传次数设置为0*/
rtt_newpack(&rttinfo);
sendagain:
/*填充发送时间戳*/
sendhdr.ts = rtt_ts(&rttinfo);
/*发送数据*/
Sendmsg(fd, &msgsend, 0);
/*设置计时为RTO的定时器*/
alarm(rtt_start(&rttinfo));
if (sigsetjmp(jmpbuf, 1) != 0) {
/*定时器超时时执行*/
/*超过最大重传次数,放弃重传*/
if (rtt_timeout(&rttinfo) < 0) {
err_msg("dg_send_recv: no response from server, giving up");
rttinit = 0;
errno = ETIMEDOUT;
return -1;
}
goto sendagain; /*重传数据*/
}
do { /*等待直到确认发出数据报被对方接收*/
n = Recvmsg(fd, &msgrecv, 0);
} while (n < sizeof(struct hdr) || recvhdr.seq != sendhdr.seq);
/*接收成功,关闭定时器*/
alarm(0);
/*更新RTO*/
rtt_stop(&rttinfo, rtt_ts(&rttinfo) - recvhdr.ts);
return n - sizeof(struct hdr);
}
static void sig_alrm(int signo)
{
/*跳转到sigsetjmp函数的位置执行sigsetjmp函数*/
siglongjmp(jmpbuf, 1);
}
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;
if ((n = dg_send_recv(fd, outbuff, outbytes, inbuff, inbytes,
destaddr, destlen)) < 0)
err_sys("dg_send_recv error");
return n;
}
调用dg_send_recv第一次发送数据时我们调用rtt_init函数初始化时间基准和初始的RTO。
/*由dg_send_recv函数在首次发送任意一个分组时调用*/
void rtt_init(struct rtt_info *ptr)
{
struct timeval tv;
Gettimeofday(&tv, NULL); /*获取系统当前时间*/
ptr->rtt_base = tv.tv_sec;
ptr->rtt_rtt == 0;
ptr->rtt_srtt = 0;
ptr->rtt_rttvar = 0.75;
/*初始RTO = srtt + 4 * rttvar = 3 seconds*/
ptr->rtt_rto = rtt_minmax(RTT_RTOCALC(ptr));
}
其中rtt_minmax函数确保RTO在指定的范围。
static float rtt_minmax(float rto)
{
if (rto < RTT_RXTMIN)
rto = RTT_RXTMIN;
else if (rto > RTT_RXTMAX)
rto = RTT_RXTMAX;
return rto;
}
每次发送数据时
我们(1)调用rtt_newpack函数将重传次数清零。
/*将重传计数器设置为0,每当第一次发送一个新分组时,都得调用这个函数*/
void rtt_newpack(struct rtt_info *ptr)
{
ptr->rtt_nrexmt = 0;
}
(2)调用rtt_ts函数计算当前的时间戳,这个时间戳单位是毫秒,是个相对时间值,基准时间由rtt_init函数设置。
uint32_t rtt_ts(struct rtt_info *ptr)
{
uint32_t ts;
struct timeval tv;
Gettimeofday(&tv, NULL);
ts = ((tv.tv_sec - ptr->rtt_base) * 1000) + (tv.tv_usec / 1000);
return ts;
}
(3)调用alram设置定时器,定时器的值即是使用rtt_start函数返回的RTO。
int rtt_start(struct rtt_info *ptr)
{
return (int)(ptr->rtt_rto + 0.5);
}
每次定时器超时时
我们调用rtt_timeout函数,使用指数退避算法计算下次使用的RTO,并且如果超过最大重传次数则返回-1,dg_cli函数就会报超时错误。
int rtt_timeout(struct rtt_info *ptr)
{
/*RTO加倍,指数回退*/
ptr->rtt_rto *= 2;
/*重传次数超过最大次数时返回-1*/
if (++ptr->rtt_nrexmt > RTT_MAXNREXMT)
return -1;
return 0;
}
每次确认数据报已成功被对方接收时
,我们关闭定时器,调用rtt_stop函数计算下一次调用dg_send_recv函数发送数据报时使用的RTO,第二个参数即是RTT,由rtt_ts(&rttinfo) - recvhdr.ts计算得到。
/*第二个参数是测得的RTT,应用方程式更新rtt_srtt,rtt_rttvar,rtt_rto的值*/
void rtt_stop(struct rtt_info *ptr, uint32_t ms)
{
double delta;
ptr->rtt_rtt = ms / 1000.0;
delta = ptr->rtt_rtt - ptr->rtt_srtt;
ptr->rtt_srtt += delta / 8;
if (delta < 0.0)
delta = -delta;
ptr->rtt_rttvar += (delta - ptr->rtt_rttvar) / 4;
ptr->rtt_rto = rtt_minmax(RTT_RTOCALC(ptr));
}
RTT_RTOCALC宏就是计算RTO的,定义如下:
#define RTT_RTOCALC(ptr) ((ptr)->rtt_srtt + (4.0 * (ptr)->rtt_rttvar))