转载自:http://blog.csdn.net/leehark/article/details/7671462
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;
-
- while (new_size > 0xFFFF) {
- ++scale_factor;
- new_size >>= 1;
- }
- new_size <<= scale_factor;
- 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);
- 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;
- uint32 nAdjust = (it->seq + it->len) - m_rcv_nxt;
- m_rbuf.ConsumeWriteBuffer(nAdjust);
- m_rcv_nxt += nAdjust;
- m_rcv_wnd -= nAdjust;
- }
- 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);
- m_rcv_wnd = available_space;
- if (bWasClosed) {
- attemptSend(sfImmediateAck);
- }
- }
- 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) {
- ......
-
- if ((seg.ack > m_snd_una) && (seg.ack <= m_snd_nxt)) {
- if (m_dup_acks >= 3) {
- ......
- }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);
- }
- }
- }
- ......
- }
当发送方发送数据时,取窗口大小为通告窗口(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)) {
- 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);
- ......
- }