这里仅考虑单向可靠数据传输
。而不是双向可靠数据传输
。
首先考虑最简单的版本,底层信道完全可靠。
发送端应用层只需要调用rdt_send
函数。网络层提供了一个函数udt_send
来给运输层调用。现在假设udt_send
是可靠的。
function rdt_send($data) {
//组装报文
$packet = make_pkt($data);
//调用网络层传输
udt_send($packet);
}
接收端网络层只需要调用rdt_rev
函数。应用层提供了一个deliver_data
函数来接受运输层的数据。
function rdt_rev($packet) {
//解析报文
$data = extract($packet);
//把数据给应用层
deliver_data($data);
}
再来画一下对应的有限状态机(FSM).
发送端只有一个状态,等待调用
。
接收端也只有一个状态,等待调用
。
现在底层信道有可能造成比特的错误。
回想一下打电话的时候,如果我们说的话对方没听清,会怎么样。会再说一遍
也就是重传
。
那么什么情况下会重传
。当接收方说我没听清
的时候。
所以在rdt2.0
里面我们让接收方接受完信息后回传一个标志,告诉我们正确
还是错误
。
如果正确,那么我们继续等待调用。
如果错误,那么我们重传
。
基于这样重传机制的可靠数据传输协议称为自动重传请求协议
(Automatic Repeat reQuest)ARQ,需要下面三个功能
看一下发送端的简单实现
function rdt_send($data) {
//生成校验和
$checkSum = generateCheckSum($data);
//组装报文
$packet = make_pkt($data, $checkSum);
//调用网络层传输
udt_send($packet);
//等待接收方回传ack或者nak
$isAck = rdt_rev();
//判断ack
if ($isAck == 1) {
//收到了Ack分组,可以结束了
return true;
} else if ($isAck == 0) {
//收到了Nak分组,需要重传
return rdt_send($data);
}
}
function rdt_rev($packet) {
//差错检测
if (check($packet)) {
//解析报文
$data = extract($packet);
//没有错,把数据交付给应用层并回传ack
//把数据给应用层
deliver_data($data);
//回传ACK
$ack = make_pkt(1);
udt_send($ack);
} else {
//有错,回传一个nak,不交付数据
$nak = make_pkt(0);
udt_send($nak);
}
}
现在发送端有两个状态
接收端还是一个状态
rdt2.0也被称为停等协议
。因为发送端处于等待ack
状态是不能被上层调用的。
从上面的代码可以看出来,接收端发送ack
使用的是udt_send
函数,这个函数是不可靠的。那么如果我们的ack
或者nak
损坏了怎么办。
这时候可以像处理损坏分组一样。我们校验ack是否受损,如果受损,那么我们重传分组。
可是重传分组就会造成接收方不知道这个分组我有没有收到过。所以我们需要增加分组序号
。
对于停等协议来说,0和1就够用了。因为停等协议只有两个状态,发完会等待ack。
//序号
$num = 0;
function rdt_send($data) {
//生成校验和
$checkSum = generateCheckSum($data);
//组装报文
$packet = make_pkt($data, $checkSum, $num);
//调用网络层传输
udt_send($packet);
//等待接收方回传ack或者nak
$isAck = rdt_rev();
//差错检测
if (check($isAck)) {
//没出问题,那么把改变序号
$num = !$num;
//判断ack
if ($isAck == 1) {
//收到了Ack分组,可以结束了
return true;
} else if ($isAck == 0) {
//收到了Nak分组,需要重传
return rdt_send($data);
}
} else {
//ack出问题了,那么这个时候重传
return rdt_send($data);
}
}
//序号
$num = 0;
function rdt_rev($packet) {
//差错检测
if (check($packet)) {
//判断报文序号
if ($packet['num'] == $num) {
//解析报文
$data = extract($packet);
//序号对的
//没有错,把数据交付给应用层并回传ack
//把数据给应用层
deliver_data($data);
//回传ACK
$ack = make_pkt(1, $num);
udt_send($ack);
} else {
//序号错了,说明这不是我们要的,我们回传一个ack,告诉发送端这个分组我们收到了。
//回传ACK
$ack = make_pkt(1, $num);
udt_send($ack);
}
} else {
//有错,回传一个nak,不交付数据
$nak = make_pkt(0, $num);
udt_send($nak);
}
}
发送端有4个状态
接收端有2个状态
从上面的代码可以看出来,发送端在接收到nak
的时候和丢失ack
或者nak
的时候都是重传。
所以我们只需要判断ack
就可以了。那么同样接收方只需要回传ack
就可以了。
这样一来,代码更见简单了。
//序号
$num = 0;
function rdt_send($data) {
//生成校验和
$checkSum = generateCheckSum($data);
//组装报文
$packet = make_pkt($data, $checkSum, $num);
//调用网络层传输
udt_send($packet);
//等待接收方回传ack
$isAck = rdt_rev();
//差错检测
if (check($isAck) && $isAck['num'] == $num) {
//没出问题,那么把改变序号
$num = !$num;
//收到了Ack分组,可以结束了
return true;
} else {
//ack出问题了,那么这个时候重传
return rdt_send($data);
}
}
//序号
$num = 0;
function rdt_rev($packet) {
//差错检测通过了并且报文序号正确
if (check($packet) && $packet['num'] == $num) {
//解析报文
$data = extract($packet);
//序号对的
//没有错,把数据交付给应用层并回传ack
//把数据给应用层
deliver_data($data);
//回传ACK
$ack = make_pkt(1, $num);
udt_send($ack);
} else {
//没通过差错检测或者序号错误,我们回传一个上一个ack,告诉发送端上一个分组我们收到了,当前分组没收到。
//回传ACK
$ack = make_pkt(1, !$num);
udt_send($ack);
}
}
发送端有4个状态
接收端有2个状态
现在底层信道除了会出错,还会丢包了。
如果遇到丢包怎么办呢,也就是接收方接收不到数据了。这个时候也就回传不了ack
。
那么可以在发送端加上超时机制。如果长时间没收到ack
。那么就重传分组。
//序号
$num = 0;
function rdt_send($data) {
//生成校验和
$checkSum = generateCheckSum($data);
//组装报文
$packet = make_pkt($data, $checkSum, $num);
//调用网络层传输
udt_send($packet);
//启动一个定时器
start_timer();
//等待接收方回传ack 并且没有超时
if ($isAck = rdt_rev() && !timeout()) {
//差错检测
if (check($isAck) && $isAck['num'] == $num) {
//没出问题,那么把改变序号
$num = !$num;
//收到了Ack分组,可以结束了
return true;
} else {
//ack出问题了,那么这个时候重传
return rdt_send($data);
}
} else {
//没接收到ack或者超时 重发
return rdt_send($data);
}
}
无变化
停等协议的缺点是性能受限。因为每次要等待上一个ack回来才能发送下一个报文。
而采用流水线
就是不等待ack直接发送下一个报文。
这样会有下面的影响
滑动窗口协议
选择重传协议