您的位置:首页 > 理论基础 > 计算机网络

libjingle源码解析(5)-【PseudoTcp】建立UDP之上的TCP(3):对成块数据流的处理

2012-06-17 19:23 941 查看
PseudoTcp对成块数据流的处理

上一篇谈论了TCP和PTCP对交互数据流的处理方法。这一篇谈论另一个数据流--成块数据流。成块数据流主要采用滑动窗口协议和慢启动算法来控制成块数据的流量。

滑动窗口

滑动窗口允许发送方在停止并等待确认前可以连续发送多个分组。因此发送方不必每发一个就停下来等待,这样可以加速数据的传输。这个Nagle算法冲突么?不会,因为成块数据流的分组都是满载传输的,根据Nagle算法,当等待发送数据的大小和窗口大小都大于MSS时,会立即发送。

如果发送方一直传输数据会出现经常丢包的现象,特别是快的发送方发给慢的接收方。当接收方还没有处理数据,发送方就接连发来了数据会填满接收方的缓冲区,从而后续的数据将被丢弃,为了减少网络上丢包的次数,用一种机制来限制发送方传输数据。

因此出现了滑动窗口,如下图:



滑动窗口分为4个部分:

上图1~3为发送并确认的数据段

上图4~6为已经发送,但是没有被确认的数据段

上图7~9为可用的窗口,即滑动窗口,发送方还可以发送的数据段空间

上图10以上为不能够发送。



当接收方确认数据后,滑动窗口两边不断的向右移动。

窗口合拢:当发送方发送数据并等待确认时,滑动窗口的左边向右移动。

窗口张开:当接收方收到数据并确认且释放缓冲区数据时,右边向右移动。

窗口收缩:当接收方的缓冲区大小变小时,右边向左移动,但不建议使用这种方式。

滑动窗口时通过窗口大小来更新。当接收方收到数据后,重新计算接收缓冲区的大小,并通告发送方。如果通告窗口大小为0,则发送方不能再发送数据,等到窗口大小为非0,这样可以有效的避免因接收方缓冲区满导致的分组的丢失。

那么PTCP是怎么实现的呢?

PTCP通过m_rbuf_len来标示接收缓冲区大小。如果缓冲区大小小于65536时,m_rwnd_scale为0,m_rcv_wnd标示窗口大小,而大于65535时,通过如下算法来调整m_rbuf_len和m_rwnd_scale。调整后根据缓冲区中可用空间来更新窗口大小m_rcv_wnd 。为什么选择65535为界限呢?因为在PTCP的头部中window字段的长度为16个bit,只能支持窗口打小范围0~65535(包含65535)。

void
PseudoTcp::resizeReceiveBuffer(uint32 new_size) {
  uint8 scale_factor = 0;
  //处理大于65536字节的缓冲区,更新scale_factor
  while (new_size > 0xFFFF) {
    ++scale_factor;
    new_size >>= 1;
  }
  new_size <<= scale_factor;//当缓冲区大小大于65535时,大小会被调整
  bool result = m_rbuf.SetCapacity(new_size);//更新缓冲区
  m_rbuf_len = new_size;//更新缓冲区大小
  m_rwnd_scale = scale_factor;//更新窗口扩大因子
  m_ssthresh = new_size;
  size_t available_space = 0;
  m_rbuf.GetWriteRemaining(&available_space);
  m_rcv_wnd = available_space;//更新可用窗口大小
}


当PTCP三次握手时,通过PTCP选项TCP_OPT_WND_SCALE来通告对方m_rwnd_scale的大小。

void
PseudoTcp::queueConnectMessage() {
  talk_base::ByteBuffer buf(talk_base::ByteBuffer::ORDER_NETWORK);
  buf.WriteUInt8(CTL_CONNECT);
  if (m_support_wnd_scale) {//判断窗口扩大选项是否开启
    buf.WriteUInt8(TCP_OPT_WND_SCALE);//增加窗口扩大选项
    buf.WriteUInt8(1);
    buf.WriteUInt8(m_rwnd_scale);//窗口扩大扩大因子
  }
  m_snd_wnd = buf.Length();
  queue(buf.Data(), buf.Length(), true);
}


PTCP接收窗口扩大因子对应的控制包之后,通过parseOptions方法来解析此包如下:

void
PseudoTcp::parseOptions(const char* data, uint32 len) {
  std::set<uint8> options_specified;
  talk_base::ByteBuffer buf(data, len);
  while (buf.Length()) {
    uint8 kind = TCP_OPT_EOL;
    buf.ReadUInt8(&kind);
    if (kind == TCP_OPT_EOL) {//判断是否到了缓冲区末
      break;
    } else if (kind == TCP_OPT_NOOP) {//空选项
      continue;
    }
    UNUSED(len);
    uint8 opt_len = 0;
    buf.ReadUInt8(&opt_len);
    if (opt_len <= buf.Length()) {
      applyOption(kind, buf.Data(), opt_len);//更新选项对应的值
      buf.Consume(opt_len);
    } else {
      return;
    }
    options_specified.insert(kind);
  }
  if (options_specified.find(TCP_OPT_WND_SCALE) == options_specified.end()) {
    if (m_rwnd_scale > 0) {
      resizeReceiveBuffer(DEFAULT_RCV_BUF_SIZE);//如果对端不支持窗口扩大因子,且本端的缓冲区大小超过了65535,则改为60K,因为必须两端都支持窗口扩大因子才能使用m_swnd_scale。
      m_swnd_scale = 0;
    }
  }
}


接收方调整窗口大小,如下:

窗口合拢:当接收方收到数据时,会从窗口大小里减去把接收缓冲区消耗的数据大小。

bool PseudoTcp::process(Segment& seg) {
    ......
      uint32 nOffset = seg.seq - m_rcv_nxt;
      talk_base::StreamResult result = m_rbuf.WriteOffset(seg.data, seg.len,
                                                          nOffset, NULL);
      ASSERT(result == talk_base::SR_SUCCESS);
      UNUSED(result);
      if (seg.seq == m_rcv_nxt) {//如果当前收到的分组恰好是下一个需要的分组
        m_rbuf.ConsumeWriteBuffer(seg.len);//消耗接收缓冲区
        m_rcv_nxt += seg.len;//更新下一个需要的分组
        m_rcv_wnd -= seg.len;//更新窗口大小,减去刚才消耗的缓冲区
        bNewData = true;
        RList::iterator it = m_rlist.begin();
        while ((it != m_rlist.end()) && (it->seq <= m_rcv_nxt)) {
          if (it->seq + it->len > m_rcv_nxt) {
            sflags = sfImmediateAck; // (Fast Recovery)
            uint32 nAdjust = (it->seq + it->len) - m_rcv_nxt;
            m_rbuf.ConsumeWriteBuffer(nAdjust);
            m_rcv_nxt += nAdjust;//之前收到的分组包含了下一个需要的seq number,调整m_rcv_nxt 
            m_rcv_wnd -= nAdjust;//m_rcv_nxt增加了,且接收缓冲区被填充了,窗口大小也随之更新。
          }
          it = m_rlist.erase(it);
        }
      } else {//拿到的分组不是所需要的,但是有效的分组
        RSegment rseg;
        rseg.seq = seg.seq;
        rseg.len = seg.len;
        RList::iterator it = m_rlist.begin();
        while ((it != m_rlist.end()) && (it->seq < rseg.seq)) {
          ++it;
        }
        m_rlist.insert(it, rseg);//更新接收分组列表,当收到下一个所需要的分组时,重组恢复所用。
      }
......
}


窗口张开:当应用层调用Recv来获取PTCP接收的数据时,PTCP会把此部分数据清除,腾空缓冲区并扩大窗口大小。

int PseudoTcp::Recv(char* buffer, size_t len) {
 ......
  talk_base::StreamResult result = m_rbuf.Read(buffer, len, &read, NULL);
......
  size_t available_space = 0;
  m_rbuf.GetWriteRemaining(&available_space);//获取接收缓冲区可用空间
  if (uint32(available_space) - m_rcv_wnd >=
      talk_base::_min<uint32>(m_rbuf_len / 2, m_mss)) {
    bool bWasClosed = (m_rcv_wnd == 0); // !?! Not sure about this was closed business
    m_rcv_wnd = available_space;//更新窗口大小,此为窗口张开过程
    if (bWasClosed) {
      attemptSend(sfImmediateAck);//如果窗口大小从0变为有可用空间时,立即通告对方可以继续发送数据。
    }
  }
  return read;
}


通告窗口大小给对方:

IPseudoTcpNotify::WriteResult PseudoTcp::packet(uint32 seq, uint8 flags,
                                                uint32 offset, uint32 len) {
  ASSERT(HEADER_SIZE + len <= MAX_PACKET);
  uint32 now = Now();
  uint8 buffer[MAX_PACKET];
  long_to_bytes(m_conv, buffer);
  long_to_bytes(seq, buffer + 4);
  long_to_bytes(m_rcv_nxt, buffer + 8);
  buffer[12] = 0;
  buffer[13] = flags;
  short_to_bytes(static_cast<uint16>(m_rcv_wnd >> m_rwnd_scale), buffer + 14);//这里会把窗口扩大因子也算进去
......
}


当发送方收到接收方发送的窗口大小后,可发送大小计算为窗口大小减去已经发送但未被确认的数据大小。

void PseudoTcp::attemptSend(SendFlags sflags) {
......
    uint32 nWindow = talk_base::_min(m_snd_wnd, cwnd);//接收方窗口大小
    uint32 nInFlight = m_snd_nxt - m_snd_una;//已经发送但未被确认的数据大小
uint32 nUseable = (nInFlight < nWindow) ? (nWindow - nInFlight) : 0;//发送方可发送数据大小
......
}


慢启动

当接收方和发送方之间存在多个路由器和速率较慢的链路时,一些中间的路由器必须缓存分组。一开始发送方向接收方发送多个分组,可能会把缓存填满,这会严重降低TCP的吞吐量。

TCP通过慢启动算法解决上述问题:首先设置拥塞窗口cwnd为1,当发送方每收到一个ACK拥塞窗口加1个报文段。发送方取拥塞窗口和通告窗口的最小值为发送上限。拥塞窗口是发送方使用的流量控制,而通告窗口时接收方使用的流量控制。

发送方首先发送一个报文段,当收到ACK时,cwnd变为2,可以发送2个报文段,当收到2个ACK时cwnd变为4,发送方可以发送4个报文段,依次类推,慢启动算法是指数增长的。

PTCP实现慢启动算法如下:

Cwnd初始值为2个MSS,当收到ACK时cwnd增加一个MSS。

Bool PseudoTcp::process(Segment& seg) {
......
  // Check if this is a valuable ack
  if ((seg.ack > m_snd_una) && (seg.ack <= m_snd_nxt)) {
if (m_dup_acks >= 3) {
......
}else{
      m_dup_acks = 0;
      // Slow start, congestion avoidance
      if (m_cwnd < m_ssthresh) {
        m_cwnd += m_mss;//当收到有效的ACK时,cwnd增加一个MSS。
      } else {
        m_cwnd += talk_base::_max<uint32>(1, m_mss * m_mss / m_cwnd);
      }
}
  }
......
}


当发送方发送数据时,取窗口大小为通告窗口(m_snd_wnd)和拥塞窗口(cwnd)的最小值,然后减去已经发送的未被确认的大小为当前可发送数据大小(nUseable )。

void PseudoTcp::attemptSend(SendFlags sflags) {
......
  while (true) {
    uint32 cwnd = m_cwnd;
    if ((m_dup_acks == 1) || (m_dup_acks == 2)) { // Limited Transmit
      cwnd += m_dup_acks * m_mss;
    }
    uint32 nWindow = talk_base::_min(m_snd_wnd, cwnd);//取窗口大小为通告窗口和拥塞窗口的最小值
    uint32 nInFlight = m_snd_nxt - m_snd_una;
    uint32 nUseable = (nInFlight < nWindow) ? (nWindow - nInFlight) : 0;//减去已经发送的未被确认的大小为当前可发送数据大小
    size_t snd_buffered = 0;
    m_sbuf.GetBuffered(&snd_buffered);
    uint32 nAvailable =
        talk_base::_min(static_cast<uint32>(snd_buffered) - nInFlight, m_mss);//已经缓存的数据中可发送数据大小
......
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐