超时与重传
TCP是面向连接的可靠的运输层。当数据丢失时,TCP需要重传包。TCP通过设置定时器解决这种问题。
对每个连接,TCP有4个不同的定时器:
1)重传定时器:用于当希望收到另一端的确认,而没有收到时。
2)坚持定时器:使窗口大小信息保持不断流动。
3)保活定时器:可检测空闲连接另一端何时崩溃或重启。
4)2MSL定时器:测量TIME_WAIT状态的时间。
PTCP本身是没有提供定时器的,而通过方法GetNextClock让调用者获取下一个定时器触发的时机,当定时器触发下一个超时时,需要调用方法NotifyClock。
超时时间设置
TCP设置获得确认ACK包的超时时间设置序列可能为1.5S,3S,6S,12S,24S,48S,64S,当超时持续时间多于9分钟时,TCP会被复位(RST),即“指数退避”。
那么这个超时值是怎么计算呢?
如果能很好的估计RTT话,如果确认包在一个RTT之内没有收到回报,那么可以认为丢包发生。
TCP最初的RTT估算方法为
R = aR+(1-a)M
其中平滑因子a取为90%,M表示这次测量的RTT,即这个包发送到获取ACK的时间间隔。
这个算法通过平滑因子来避免R的值受新的M的浮动过大的影响。然而这恰恰在RTT浮动比较大的连接中无法及时的反应连接情况。并且网络处于饱和状态时,频繁重传会导致火上烧油。Jacobson对此设计了新的算法:
Err = M - A
A = A+g*Err
D = D + h(|Err| -D)
RTO = A + 4D
增量g为0.125(1/8),Err为上一个得到的值和新的RTT的差。A为上一个测到的增量后的数据,h为0.25。
当RTT变化大时,Err也会变大,导致D也会变大,导致RTO快速上升。某一次连接的估值和真正的RTT关系估下:
PTCP实现如下:
PTCP设置最大超时时间为60S。当收到ACK时,计算RTT是通过PTCP头部的TimeStamp差值计算,所以Karn算法在此不管用。RTO的算法和上面所述一致:
1)Err = rtt - m_rx_srtt
2)D=D+0.25*(abs(Err-D))
3)m_rx_srtt = m_rx_srtt + err/8
4)RTO = m_rx_srtt+D
下面的代码实现,有一定的不同,但仔细分析和上面算法是一致的。
- bool PseudoTcp::process(Segment& seg) {
- ......
-
- if ((seg.ack > m_snd_una) && (seg.ack <= m_snd_nxt)) {
-
- if (seg.tsecr) {
- long rtt = talk_base::TimeDiff(now, seg.tsecr);
- if (rtt >= 0) {
- if (m_rx_srtt == 0) {
- m_rx_srtt = rtt;
- m_rx_rttvar = rtt / 2;
- } else {
- m_rx_rttvar = (3 * m_rx_rttvar + abs(long(rtt - m_rx_srtt))) / 4;
- m_rx_srtt = (7 * m_rx_srtt + rtt) / 8;
- }
- m_rx_rto = bound(MIN_RTO, m_rx_srtt +
- talk_base::_max<uint32>(1, 4 * m_rx_rttvar), MAX_RTO);
- } else {
- ASSERT(false);
- }
- }
- ......
- }
当重传后,仍然超时时,PTCP也采用指数退避算法。
拥塞避免算法
拥塞避免算法通常和慢启动算法一起使用,主要是限制发送方的流量。慢启动的目的是,不要过快的发送数据导致中间的路由器填满缓冲,而拥塞避免算法是当发现到网络被拥塞时限制发送方处理丢失分组的一种方法。
拥塞避免算法和慢启动算法同时在一个连接上维护两个变量cwnd和ssthresh。
1)对一个给定连接cwnd初始化为1。
2)当拥塞发生时(超时或者受到重复的第三个ack)时ssthreth取当前窗口的一半,如果超时引起的拥塞,则cwnd取为1。
3)当新的数据包受到确认时,如果cwnd<ssthreth则进行慢启动算法,否则cwnd在每个确认增加1/cwnd。
快速重传与快速恢复算法
为什么上面判断拥塞时,获得三个以上重复的ACK时,认为产生拥塞了呢?
因为,当接收方收到失序的报文段时,立即发送需要收到的下一个报文段,然而发送方发送两个以上报文时,因报文的路由不一样,会产生短暂的失序,为了避免因此而产生的重传,把拥塞判断设置为3个以上。
当收到三个以上重复报文段时,发送方认为此包被丢失,于是立即重传丢失报文段,不会等到超时定时器溢出。这就是快速重传算法。
当发送方重传后,会持续发送后面没有发送的数据,而不启动慢启动,等待ACK,是因为发送方收到了连续的3个以上ACK说明,接收方收到了3个以上数据报文,并缓存起来了。这就是快速恢复算法,实现如下:
1)当收到3个重复ACK时ssthreth设置为当前窗口的一半,并cwnd设置为ssthresh+3。
2)每次收到另一个重复的ACK时,cwnd增加一个报文段并重传。
3)当下一个ACK到达时cwdn设置为ssthreth,即采用拥塞避免,速率减半。
对于重传PTCP有一点不同,就是上述第一步,当收到重复3个ACK时,ssthresh设置为还未确认的字节数的一半。
- bool PseudoTcp::process(Segment& seg) {
- ......
- if ((seg.ack > m_snd_una) && (seg.ack <= m_snd_nxt)) {
- ......
- if (m_dup_acks >= 3) {
- if (m_snd_una >= m_recover) {
- uint32 nInFlight = m_snd_nxt - m_snd_una;
- m_cwnd = talk_base::_min(m_ssthresh, nInFlight + m_mss);
- m_dup_acks = 0;
- } else {
- if (!transmit(m_slist.begin(), now)) {
- closedown(ECONNABORTED);
- return false;
- }
- m_cwnd += m_mss - talk_base::_min(nAcked, m_cwnd);
- }
- } else {
- m_dup_acks = 0;
-
- if (m_cwnd < m_ssthresh) {
- m_cwnd += m_mss;
- } else {
- m_cwnd += talk_base::_max<uint32>(1, m_mss * m_mss / m_cwnd);
- }
- }
- }
- else if (seg.ack == m_snd_una) {
-
- m_snd_wnd = static_cast<uint32>(seg.wnd) << m_swnd_scale;
-
- if (seg.len > 0) {
-
- } else if (m_snd_una != m_snd_nxt) {
- m_dup_acks += 1;
- if (m_dup_acks == 3) {
- if (!transmit(m_slist.begin(), now)) {
- closedown(ECONNABORTED);
- return false;
- }
- m_recover = m_snd_nxt;
- uint32 nInFlight = m_snd_nxt - m_snd_una;
- m_ssthresh = talk_base::_max(nInFlight / 2, 2 * m_mss);
- m_cwnd = m_ssthresh + 3 * m_mss;
- } else if (m_dup_acks > 3) {
- m_cwnd += m_mss;
- }
- } else {
- m_dup_acks = 0;
- }
- }
- ......
- }
重新分组
当TCP超时重传时,可以允许以更大的且不大于MSS的报文发送,即合并后续的数据一起发送,PTCP也是如此处理的。