如果想要让请求应答式程序使用UDP, 那么必须在客户程序中增加以下两个特性:
1. 超时和重传
2. 供客户验证一个应答是否匹配相应的请求
首先, 为了防止数据包的丢失:
增加序列号比较简单. 客户为每个请求冠以一个序列号, 服务器必须在给客户的响应中echo这个序列号, 这样客户端就能验证某个 给定的应答是否匹配早先发出的请求
其次, 为了解决数据包超时:
处理超时和重传则相对繁琐. 这里通过TCP给出的经验, 单纯的对响应等待一段固定的时间是不可靠的, 需要考虑到广域网相对局域网的区别. 影响往返时间的因素包括距离, 网速和拥塞等
这方面在TCP上的研究颇多.
想要计算用于发送每个分组的 重传超时(RTO), 为此先测量每个实际往返时间RTT. 每测得一个RTT, 我们就更新2个统计估算因子: strr是平滑化RTT估算因子, rttvar是平滑化平均偏差估算因子. 有了这两个因子, 那么RTO就是srtt加上4倍的rttvar. (这将在接下来的代码中使用到).于是:
delta = 测得的RTT - srtt
srtt <- srtt + g * delta
rttvar <- rttvar + h(|delta| - rttvar)
RTO = srtt + 4 * rttvar
(其中delta是测得RTT和当前平滑化RTT估算因子(srtt)之差, g是施加在RTT估算因子上的增益, 值为1/8, h是施加在平均偏差估算因子上的增益, 值为1/4)
如果定时器超时也依旧没有收到应答, 下一个RTO必须进行指数回退, 比如4秒超时, 下一次就设置8秒, 再没收到, 下一次就是16秒
最后, 使用超时重传还可能导致的问题是, 数据包的二义性. 比如:
1. 客户端请求(可能)丢失, 重传, 之后接收到一个数据包, 不知它是第一个请求的还是重传请求的
2. 服务器的应答丢失
3. 使用的RTO太小, 在重传后接收到的数据包不知是初始请求还是重传请求的
这里使用的办法, 来自TCP用于应对"长胖管道"(有较高宽带或较长RTT, 抑或两者都有的网络)的扩展. 本办法除了为每个请求增加一个服务器必须回射的序列号外, 还为每个请求增加一个服务器同样必须回射的时间戳, 每次发送一个请求, 我们把当前时间保存在该时间戳中. 收到服务器的应答时, 我们从当前时间减去由服务器在其应答中回射的时间戳就 算出RTT.
通过取出得到的应答中的时间戳(即为客户端发送请求时添加的时间戳), 得到的RTT将不再具有二义性
此外, 既然服务器只是简单的回射客户端提供的时间戳, 那么客户端也就能够使用任何期望的单位, 且客户端与服务器不需要具备同步的时钟
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
补充: UDP的并发讨论
对于TCP服务器而言, 实现并发处理只需要简单的让多个进程(线程)处理某个连接就可以, 每个客户连接都是唯一的(由于标识每个客户连接的是唯一的TCP套接字对)
对于UDP, 我们可能需要应对两种不同类型的服务器
1. 超时和重传
2. 供客户验证一个应答是否匹配相应的请求
首先, 为了防止数据包的丢失:
增加序列号比较简单. 客户为每个请求冠以一个序列号, 服务器必须在给客户的响应中echo这个序列号, 这样客户端就能验证某个 给定的应答是否匹配早先发出的请求
其次, 为了解决数据包超时:
处理超时和重传则相对繁琐. 这里通过TCP给出的经验, 单纯的对响应等待一段固定的时间是不可靠的, 需要考虑到广域网相对局域网的区别. 影响往返时间的因素包括距离, 网速和拥塞等
这方面在TCP上的研究颇多.
想要计算用于发送每个分组的 重传超时(RTO), 为此先测量每个实际往返时间RTT. 每测得一个RTT, 我们就更新2个统计估算因子: strr是平滑化RTT估算因子, rttvar是平滑化平均偏差估算因子. 有了这两个因子, 那么RTO就是srtt加上4倍的rttvar. (这将在接下来的代码中使用到).于是:
delta = 测得的RTT - srtt
srtt <- srtt + g * delta
rttvar <- rttvar + h(|delta| - rttvar)
RTO = srtt + 4 * rttvar
(其中delta是测得RTT和当前平滑化RTT估算因子(srtt)之差, g是施加在RTT估算因子上的增益, 值为1/8, h是施加在平均偏差估算因子上的增益, 值为1/4)
如果定时器超时也依旧没有收到应答, 下一个RTO必须进行指数回退, 比如4秒超时, 下一次就设置8秒, 再没收到, 下一次就是16秒
最后, 使用超时重传还可能导致的问题是, 数据包的二义性. 比如:
1. 客户端请求(可能)丢失, 重传, 之后接收到一个数据包, 不知它是第一个请求的还是重传请求的
2. 服务器的应答丢失
3. 使用的RTO太小, 在重传后接收到的数据包不知是初始请求还是重传请求的
这里使用的办法, 来自TCP用于应对"长胖管道"(有较高宽带或较长RTT, 抑或两者都有的网络)的扩展. 本办法除了为每个请求增加一个服务器必须回射的序列号外, 还为每个请求增加一个服务器同样必须回射的时间戳, 每次发送一个请求, 我们把当前时间保存在该时间戳中. 收到服务器的应答时, 我们从当前时间减去由服务器在其应答中回射的时间戳就 算出RTT.
通过取出得到的应答中的时间戳(即为客户端发送请求时添加的时间戳), 得到的RTT将不再具有二义性
此外, 既然服务器只是简单的回射客户端提供的时间戳, 那么客户端也就能够使用任何期望的单位, 且客户端与服务器不需要具备同步的时钟
//
//关于UDP的可靠性传输, 下面为封装的超时重传头文件
//
#include "unp.h"
#ifndef __unp_rrt_h
#define __unp_rrt_h
struct rtt_info{
float rtt_rtt; //most recent measured RTT, in seconds
float rtt_srtt; //smoothed RTT estimator, in seconds
float rtt_rttvar; //smoothed mean deviation, in seconds
float rtt_rto; //current RTO to use, in seconds
int rtt_nrexmt; //times retransmitted, 0, 1, 2 ...
uint32_t rtt_base; //from 1970/1/1
};
#define RTT_RXTMIN 2 //min retr.. timeout value, in seconds, 最小超时时间
#define RTT_RXTMAX 60 //max ..
#define RTT_MAXNREXMT 3 //max retr times
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; //debug falg
#endif
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
//头文件的具体实现
#include "unprtt.h"
int rtt_d_flag = 0; //debug flag, can be set by caller
//RTO的计算方式
#define RTT_RTOCALC(ptr) ((ptr)->rtt_srtt + (4.0 * (ptr)->rtt_rttvar))
//确保RTO不太大, 不太小
static float
rtt_minmax(float rto){
return (rto > RTT_RXTMAX) ? RTT_RXTMAX : (rto < RTT_RXTMIN ? RTT_RXTMIN : rto);
}
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;
//first set RTO to 3 secs
ptr->rtt_rto = rtt_minmax(RTT_RTOCALC(ptr));
}
//函数返回当前时间戳. 是以毫秒为单位.
//时间戳的意义并不是说判断这个包是不是就是我想要的那个, 因为这个功能已经通过序列实现了
//时间戳是为了计算RTT而来的. 通过将发送时的时间记下来, 在收到回应后取出, 拿现在的时间去做减法, 得到RTT
//将结果放在32位无符号整数中
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;
}
//把重传计时器重置为0
//每当新发送一个分组都会调用这个函数
void
rtt_newpack(struct rtt_info *ptr){
ptr->rtt_nrexmt = 0;
}
//返回发送某数据包的初始超时时间
int
rtt_start(struct rtt_info *ptr){
return ((int)(ptr->rtt_rto + 0.5)); //四舍五入为整数
}
//传入的第二个参数即为测得的RTT值
//这里表示上一个数据包传送完毕后, 需要根据其RTT计算新的RTO给下一个数据包做准备
void
rtt_stop(struct rtt_info *ptr, uint32_t ms){
double delta;
ptr->rtt_rtt = ms / 1000.0; //turn into sec
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));
}
//超时后, 需要指数回退超时时间
int
rtt_timeout(struct rtt_info *ptr){
ptr->rtt_rto = rtt_minmax(ptr->rtt_rto * 2);
if(++ptr->rtt_nrexmt > RTT_MAXNREXMT)
return -1; //give up
return 0;
}
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
//在UDP上尽可能的保持可靠性
#include "unprtt.h"
#include "unp.h"
ssize_t dg_send_recv(int , const void *, size_t ,const struct sockaddr*, socklen_t);
void dg_cli(FILE *fp, int sockfd, const struct sockaddr* pservaddr, socklen_t servlen)
{
ssize_t n;
char sendline[MAXLINE], recvline[MAXLINE+1];
while(fgets(sendline, MAXLINE, fp) != NULL){
//下面这个函数用于发送字符串以及接收回射的字符串
//且在此函数中, 将进行UDP的可靠性工作
//每调用一次这个函数, 就是发送一个数据包
n = dg_send_recv(sockfd, sendline, strlen(sendline), recvline, MAXLINE, pservaddr, servlen);
recvline[n] = 0;
fputs(recvline, stdout);
}
}
//用于超时重传
static struct rtt_info rttinfo;
//查看是否初始化所需数据
static int rttinit = 0;
//用于recvmsg和sendmsg的参数
static struct msghdr msgsend, msgrecv;
static struct hdr{
uint32_t seq; //序列号
uint32_t ts; //时间戳
}sendhdr, recvhdr;
static void sig_alrm(int);
static sigjmp-buf jmpbuf;
ssize_t
dg_send_recv(int fd, const void *outbuf, size_t outlen, void *inbuf, size_t inlen, const struct sockaddr *destaddr, socklen_t destlen){
ssize_t n;
struct iovec iovsend[2], iovrecv[2];
if(rttinit == 0){
rtt_init(&rttinfo);
rttinit = 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 = outbuf; //第二部分才是真正的数据
iovsend[1].iov_len = outlen;
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 = inbuf;
iovrecv[1].iov_len = inlen;
//以上, 都为初始化, 选择的是recvmsg系列函数, 使用分散读和集中写的方式
//
signal(SIGALRM,sig_alrm);
rtt_newpack(&rttinfo); //为发送这个数据包而初始化此包已经发送的次数
//这里是如果发生超时则会跳转到这里来
sendagain:
//在这里赋值时间戳, 因为可能导致重传, 那么时间戳也会发生变化, 序列就不同了, 它不该变化
sendhdr.ts = rtt_ts(&rttinfo);
sendmsg(fd, &msgsend, 0);
alarm(rtt_start(&rttinfo));
if(sigsetjmp(jmpbuf, 1) != 0){
//发生超时了, 重传并重新设置超时时间. 如果重传次数过多, 返回-1
if(rtt_timeout(&rttinfo) < 0){
fprintf(stderr, "no response from the server\n");
rttinit = 0;
errno = ETIMEDOUT;
return -1;
}
goto sendagain;
}
//在没有收到与我们期望序列相匹配的数据包时, 我们选择继续等待
//为什么会收到不匹配的呢? 可能是因为可能上个包重传而遗留的.
//所以我们可以不去理会它, 而是选择继续等待我们需要的包出现
do{
n = recvmsg(fd, msgrecv, 0);
}while((n<sizeof(hdr)) || recvhdr.seq != sendhdr.seq);
alarm(0);
rtt_stop(&rttinfo, rtt_ts(&rttinfo)-recvhdr.ts);
return (n - sizeof(struct hdr));
}
void sig_alrm(int s)
{
siglongjmp(jmpbuf, 1);
}
补充: UDP的并发讨论
对于TCP服务器而言, 实现并发处理只需要简单的让多个进程(线程)处理某个连接就可以, 每个客户连接都是唯一的(由于标识每个客户连接的是唯一的TCP套接字对)
对于UDP, 我们可能需要应对两种不同类型的服务器
1. 第一种, 即为客户端与服务器进行一次请求应答后就不再相关. 对于这种, 只需要fork将父进程的地址数据传到子进程, 对该地址回应一次数据即可
2. 第二种, 客户端需要与服务器交换多个数据包. 服务器如何知道接收到的数据包是客户发来的第一个包还是同一个请求的后续数据呢? (因为接收包的方法只有使用recv等接收)这个问题的典型解决方案是, 让服务器为每个客户创建一个新的套接字, 在其上bind一个临时端口, 然后使用该套接字发送对该客户的所有应答. 这个办法要求客户查看服务器第一次应答中的源端口号, 并把本请求的后续数据发送到这个端口