滑动窗口所谓的“滑动”,并不是说窗口在动,而是因为数据在不断进入和离开窗口,也就是说真正“动”的是数据,下面一幅图就表示了这点:
滑动窗口在TCP首部中的位置如下图所示:
RFC793对它的解释是"发送方希望接收到的以ACK标志开头的数据字节数"。滑动窗口是跟ACK一起的,因此ACK标志必须置为1,同时指定窗口大小。可以看到,滑动窗口大小通过16个bit来描述,所以变化范围0-65535(这个范围其实是可以缩放的)。
Window: 16 bits
The number of data octets beginning with the one indicated in the
acknowledgment field which the sender of this segment is willing to
accept.
三、滑动窗口的工作原理
首先,TCP不是每个报文段都会返回TCP的,可能对多个报文返回一个AC****K。
我们再举一个栗子,制造某机器需要A,B,C三种零件,并且组装顺序A→B→C,某天不凑巧,B,C零件先到,这个时候往往把A的位置预留出来,等待A到达之后,再进行组装;如果A丢失了,那么B,C也丧失作用,被丢弃了。
在TCP中也有这样一个“预留的地方”,我们称之为“空洞(hole)”。假设我们依次发送3个报文(A,B,C)。如果B,C报文先到,那么先把A的位置预留出来,只有A报文到达了,接收端才返回一个ACK(不是3个)进行确认。
在介绍滑动窗口原理之前,我们先讲解一个重要概念——MSS (Max Segment Size,最大段大小),数据被TCP分割成合适发送的数据块,称为段(Segment)。注意:这里说的段(Segment)不包括协议首部,只包含数据!
与MSS最为相关的一个参数就是网络设备接口的MTU(Max Transfer Unit)。
我们完整讲一下滑动窗口的原理:
- 有一组数据通过TCP传输,TCP先 将其分成若干段,假设有四个段seg1,seg2,seg3,seg4,依次发送出去,此时假设接收端接收到了 seg1 seg2 seg4;
- 此时接收端的行为是回复一个 ACK 包说明已经接收到,并将 seg4 进行缓存(保证顺序,产生一个保存 seg3 的 hole);
- 发送端收到 ACK 之后,就会将对应的数据包变为已确认状态,这个时候窗口向右移动;
- 假设接收端通告的 Window Size 仍然不变,此时窗口右移,产生一些新的空位,这些是接收端允许发送的范畴;
- 对于丢失的 seg3,如果超过一定时间,TCP 就会重新传送(重传机制),重传成功会 seg3 seg4 一块被确认,不成功,seg4 也将被丢弃。
前面我们讲到,这个滑动窗口是可以动态调整的,下面讲一下滑动窗口动态调整的原理。
四、滑动窗口的动态调整原理
这一部分可能需要阅读Linux源码。有兴趣的读者可以来肝哦。
内核版本: linux3.2.12
文件目录:linux-3.2.12\include\linux\tcp.h
struct tcp_sock { ... /* 最早接收但未确认的段的序号,即当前接收窗口的左端*/ u32 rcv_wup; /* rcv_nxt on last window update sent */ u16 advmss; /* Advertised MSS. 本端能接收的MSS上限,建立连接时用来通告对端*/ u32 rcv_ssthresh; /* Current window clamp. 当前接收窗口大小的阈值*/ u32 rcv_wnd; /* Current receiver window,当前的接收窗口大小*/ u32 window_clamp; /* 接收窗口的最大值,这个值也会动态调整*/ ... struct tcp_options_received rx_opt; /* 接收选项 */ u32 mss_cache; /* Cached effective mss, not including SACKS */}
struct tcp_options_received { ... snd_wscale : 4, /* Window scaling received from sender, 对端接收窗口扩大因子 */ rcv_wscale : 4; /* Window scaling to send to receiver, 本端接收窗口扩大因子 */ u16 user_mss; /* mss requested by user in ioctl */ u16 mss_clamp; /* Maximal mss, negotiated at connection setup,对端的最大mss */}
struct tcp_options_received {/* PAWS/RTTM data */ long ts_recent_stamp;/* Time we stored ts_recent (for aging) */ u32 ts_recent; /* Time stamp to echo next */ u32 rcv_tsval; /* Time stamp value */ u32 rcv_tsecr; /* Time stamp echo reply */ u16 saw_tstamp : 1, /* Saw TIMESTAMP on last packet */ tstamp_ok : 1, /* TIMESTAMP seen on SYN packet */ dsack : 1, /* D-SACK is scheduled */ wscale_ok : 1, /* Wscale seen on SYN packet */ sack_ok : 4, /* SACK seen on SYN packet */ snd_wscale : 4, /* Window scaling received from sender */ rcv_wscale : 4; /* Window scaling to send to receiver */ u8 cookie_plus:6, /* bytes in authenticator/cookie option */ cookie_out_never:1, cookie_in_always:1; u8 num_sacks; /* Number of SACK blocks */ u16 user_mss; /* mss requested by user in ioctl */ u16 mss_clamp; /* Maximal mss, negotiated at connection setup */};
tcp_sock表示的是TCP结构体。我们只关注里面最重要的几个成员:
(1)tp->advmss
这里的adv是advertised告知的意思。本端在建立连接时使用的MSS,是本端能接收的MSS上限。这是从路由缓存中获得的(dst->metrics[RTAX_ADVMSS - 1]),一般是1460。
(2)tp->rx_opt.mss_clamp
对端的能接收的MSS上限,其值为tcp_sock->rx_opt.user_mss和 对端在建立连接时通告的MSS的较小值。
(3)tp->mss_cache
本端当前有效的发送MSS,不包括SACKS。显然不能超过对端接收的上限,即tp->mss_cache <= tp->mss_clamp。
(4)tcp_sock->rx_opt.user_mss
用户通过TCP_MAXSEG选项设置的MSS上限,用于决定本端和对端的接收MSS上限。
文件目录:linux-3.2.12\include\net\sock.h
struct sock { ... struct sk_buff_head sk_receive_queue; /* 表示接收队列sk_receive_queue中所有段的数据总长度*/#define sk_rmem_alloc sk_backlog.rmem_alloc int sk_rcvbuf; /* 接收缓冲区长度的上限*/ int sk_sndbuf; /* 发送缓冲区长度的上限*/ struct sk_buff_head sk_write_queue; ...}
主要对接收和发送缓冲区进行定义。
接收缓存sk->sk_rcvbuf分为两部分:
(1) network buffer,一般占3/4,这部分是协议能够使用的。
(2)application buffer,一般占1/4。
我们在计算连接可用接收缓存的时候,并不会使用整个的sk_rcvbuf,防止应用程序读取数据的速度比网络数据包到达的速度慢时,接收缓存被耗尽的情况。
下面是根据RFC793和RFC1122定义的窗口更新参数。
我们先从初始情况开始进行分析。
/* Determine a window scaling and initial window to offer. * Based on the assumption that the given amount of space will be offered. * Store the results in the tp structure. * NOTE: for smooth operation initial space offering should be a multiple of mss * if possible. We assume here that mss >= 1. This MUST be enforced by all calllers. */void tcp_select_initial_window (int __space, __u32 mss, __u32 *rcv_wnd, __u32 *window_clamp, int wscale_ok, __u8 *rcv_wscale, __u32 init_rcv_wnd){ unsigned int space = (__space < 0 ? 0 : __space); /* 接收缓存不能为负*/ /* If no clamp set the clamp to the max possible scaled window。 * 如果接收窗口上限的初始值为0,则把它设成最大。 */ if (*window_clamp == 0) (*window_clamp) = (65535 << 14); /*这是接收窗口的最大上限*/ /* 接收窗口不能超过它的上限 */ space = min(*window_clamp, space); /* Quantize space offering to a multiple of mss if possible. * 接收窗口大小最好是mss的整数倍。 */ if (space > mss) space = (space / mss) * mss; /* 让space为mss的整数倍*/ /* NOTE: offering an initial window larger than 32767 will break some * buggy TCP stacks. If the admin tells us it is likely we could be speaking * with such a buggy stack we will truncate our initial window offering to * 32K - 1 unless the remote has sent us a window scaling option, which * we interpret as a sign the remote TCP is not misinterpreting the window * field as a signed quantity. */ /* 当协议使用有符号的接收窗口时,则接收窗口大小不能超过32767*/ if (sysctl_tcp_workaround_signed_windows) (*rcv_wnd) = min(space, MAX_TCP_WINDOW); esle (*rcv_wnd) = space; (*rcv_wscale) = 0; /* 计算接收窗口扩大因子rcv_wscale,需要多大才能表示本连接的最大接收窗口大小?*/ if (wscale_ok) { /* Set window scaling on max possible window * See RFC1323 for an explanation of the limit to 14 * tcp_rmem[2]为接收缓冲区长度上限的最大值,用于调整sk_rcvbuf。 * rmem_max为系统接收窗口的最大大小。 */ space = max_t(u32, sysctl_tcp_rmem[2], sysctl_rmem_max); space = min_t(u32, space, *window_clamp); /*受限于具体连接*/ while (space > 65535 && (*rcv_wscale) < 14) { space >>= 1; (*rcv_wscale)++; } } /* Set initial window to a value enough for senders starting with initial * congestion window of TCP_DEFAULT_INIT_RCVWND. Place a limit on the * initial window when mss is larger than 1460. * * 接收窗口的初始值在这里确定,一般是10个数据段大小左右。 */ if (mss > (1 << *rcv_wscale)) { int init_cwnd = TCP_DEFAULT_INIT_RCVWND; /* 10 */ if (mss > 1460) init_cwnd = max_t(u32, 1460 * TCP_DEFAULT_INIT_RCVWND) / mss, 2); /* when initializing use the value from init_rcv_wnd rather than the * default from above. * 决定初始接收窗口时,先考虑路由缓存中的,如果没有,再考虑系统默认的。 */ if (init_rcv_wnd) /* 如果路由缓存中初始接收窗口大小不为0*/ *rcv_wnd = min(*rcv_wnd, init_rcv_wnd * mss); else *rcv_wnd = min(*rcv_wnd, init_cwnd *mss); } /* Set the clamp no higher than max representable value */ (*window_clamp) = min(65535 << (*rcv_wscale), *window_clamp);}
初始的接收窗口的取值(mss的整数倍):
(1)先考虑路由缓存中的RTAX_INITRWND
(2)在考虑系统默认的TCP_DEFAULT_INIT_RCVWND(10)
(3)最后考虑min(3/4 * sk_rcvbuf, window_clamp),如果这个值很低.
接下来我们可以看到,接收窗口的大小主要取决于剩余的接收缓存,以及接收窗口当前阈值。
决定接收窗口大小的函数tcp_select_window()在tcp_transmit_skb()中调用,也就是说每次我们要发送数据包时,都要使用tcp_select_window()来决定通告的接收窗口大小。
static int tcp_transmit_skb (struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask){ const struct inet_connection_sock *icsk = inet_csk(sk); struct inet_sock *inet; struct tcp_sock *tp; struct tcp_skb_cb *tcb; struct tcphdr *th; ... /* Build TCP header and checksum it,以下是TCP头的赋值*/ th = tcp_hdr(skb); /* skb->transport_header */ th->source = inet->inet_sport; th->dest = inet->inet_dport; th->seq = htonl(tcb->seq); th->ack_seq = htonl(tp->rcv_nxt); /* 这个语句可以看出C语言的强大*/ *(((__be16 *) th) + 6) = htons(((tcp_header_size >> 2) << 12) | tcb->tcp_flags); if (unlikely(tcb->tcp_flags & TCPHDR_SYN)) { /* RFC1323: The window in SYN & SYN/ACK segments in never scaled. * 从这里我们可以看到,在三次握手阶段,接收窗口并没有按扩大因子缩放。 */ th->window = htons(min(tp->rcv_wnd, 65535U)); } else { th->window = htons(tcp_select_window(sk)); /* 更新接收窗口的大小*/ } th->check = 0; th->urg_ptr = 0; ...}
这里有几个函数,大家可能没见过,所以稍微解释一下:
网络字节顺序NBO(Network Byte Order)
按从高到低的顺序存储,在网络上使用同一的网络字节顺序,可避免兼容性问题;
主机字节顺序HBO(Host Byte Order)
不同的机器HBO不相同,与CPU的设计有关,数据的顺序是由CPU决定的,而与操作系统无关;
如Intel x86结构下,short型数0x1234表示为34 12,int型数0x12345678表示为78 56 34 12;
如IBM power PC结构下,short型数0x1234表示为 12 34,int型数0x12345678表示为 12 34 56 78.
由于这个原因,不同体系结构的机器之间不能直接通信,所以要转换成一种约定的顺序,也就是网络字节顺序,其实就是如同power pc那样的顺序。
ntohs =net to host short int 16位
htons=host to net short int 16位
ntohl =net to host long int 32位
htonl=host to net long int 32位
接下来看一下tcp_select_window(),这个是核心函数。
static u16 tcp_select_window(struct sock *sk){ struct tcp_sock *tp = tcp_sk(sk); u32 cur_win = tcp_receive_window(tp); /* 当前接收窗口的剩余大小*/ u32 new_win = __tcp_select_window(sk); /*根据剩余的接收缓存,计算新的接收窗口的大小 */ /* Never shrink the offered window,不允许缩小已分配的接收窗口*/ if (new_win < cur_win) { /* Danger Will Robinson! * Don't update rcv_wup/rcv_wnd here or else * we will not be able to advertise a zero window in time. --DaveM * Relax Will Robinson. */ new_win = ALIGN(cur_win, 1 << tp->rx_opt.rcv_wscale); } /* 更新接收窗口大小。个人觉得这句代码应该后移,因为此时接收窗口的大小还未最终确定!*/ tp->rcv_wnd = new_win; tp->rcv_wup = tp->rcv_nxt; /* 更新接收窗口的左边界,把未确认的数据累积确认*/ /* 确保接收窗口大小不超过规定的最大值。 * Make sure we do not exceed the maximum possible scaled window. */ if (! tp->rx_opt.rcv_wscale && sysctl_tcp_workaround_signed_windows) /* 不能超过32767,因为一些奇葩协议采用有符号的接收窗口大小*/ new_win = min(new_win, MAX_TCP_WINDOW); else new_win = min(new_win, (65535U << tp->rx_opt.rcv_wscale)); /* RFC1323 scaling applied. 按比例因子缩小接收窗口,这样最多能表示30位*/ new_win >>= tp->rx_opt.rcv_wscale; /* If we advertise zero window, disable fast path. */ if (new_win == 0) tp->pred_flags = 0; return new_win; /* 返回最终的接收窗口大小*/}
每次发送一个TCP数据段,都要构建TCP首部,这时会调用tcp_select_window选择接收窗口大小。窗口大小选择的基本算法:
- 计算当前接收窗口的剩余大小
cur_win。 - 计算新的接收窗口大小
new_win,这个值为剩余接收缓存的3/4,且不能超过rcv_ssthresh。 - 取
cur_win和new_win中值较大者作为接收窗口大小。
计算当前接收窗口的剩余大小cur_win。
/* * Compute the actual receive window we are currently advertising. * rcv_nxt can be after the window if our peer push more data than * the offered window. */static inline u32 tcp_receive_window (const struct tcp_sock *tp){ s32 win = tp->rcv_wup + tp->rcv_wnd - tp->rcv_nxt; if (win < 0) win = 0; return (u32) win;}
__tcp_select_window计算新的接收窗口大小new_win,这个是关键函数,我们将看到rcv_ssthresh所起的作用。
/* * calculate the new window to be advertised. */u32 __tcp_select_window(struct sock *sk){ struct inet_connection_sock *icsk = inet_csk(sk); struct tcp_sock *tp = tcp_sk(sk); /* MSS for the peer's data. Previous versions used mss_clamp here. * I don't know if the value based on our guesses of peer's MSS is better * for the performance. It's more correct but may be worse for the performance * because of rcv_mss fluctuations. —— SAW 1998/11/1 */ int mss = icsk->icsk_ack.rcv_mss;/*这个是估计目前对端有效的发送mss,而不是最大的*/ int free_space = tcp_space(sk); /* 剩余接收缓存的3/4 */ int full_space = min_t(int, tp->window_clamp, tcp_full_space(sk)); /* 总的接收缓存 */ int window; if (mss > full_space) mss = full_space; /* 减小mss,因为接收缓存太小了*/ /* receive buffer is half full,接收缓存使用一半以上时要小心了 */ if (free_space < (full_space >> 1)) { icsk->icsk_ack.quick = 0; /* 可以快速发送ACK段的数量置零*/ if (tcp_memory_pressure)/*有内存压力时,把接收窗口限制在5840字节以下*/ tp->rcv_ssthresh = min(tp->rcv_ssthresh, 4U * tp->advmss); if (free_space < mss) /* 剩余接收缓存不足以接收mss的数据*/ return 0; } if (free_space > tp->rcv_ssthresh) /* 看!不能超过当前接收窗口阈值,这可以达接收窗口平滑增长的效果*/ free_space = tp->rcv_ssthresh; /* Don't do rounding if we are using window scaling, since the scaled window will * not line up with the MSS boundary anyway. */ window = tp->rcv_wnd; if (tp->rx_opt.rcv_wscale) { /* 接收窗口扩大因子不为零*/ window = free_space; /* Advertise enough space so that it won't get scaled away. * Import case: prevent zero window announcement if 1 << rcv_wscale > mss. * 防止四舍五入造通告的接收窗口偏小。 */ if (((window >> tp->rx_opt.rcv_wscale) << tp->rx_opt.rcv_wscale) != window) window =(((window >> tp->rx_opt.rcv_wscale) + 1) << tp->rx_opt.rcv_wscale); } else { /* Get the largest window that is a nice multiple of mss. * Window clamp already applied above. * If our current window offering is within 1 mss of the free space we just keep it. * This prevents the divide and multiply from happening most of the time. * We also don't do any window rounding when the free space is too small. */ /* 截取free_space中整数个mss,如果rcv_wnd和free_space的差距在一个mss以上*/ if (window <= free_space - mss || window > free_space) window = (free_space / mss) * mss; /* 如果free space过小,则直接取free space值*/ else if (mss = full_space && free_space > window + (full_space >> 1)) window = free_space; /* 当free_space -mss < window < free_space时,直接使用rcv_wnd,不做修改*/ } return window;}
/* 剩余接收缓存的3/4。 * Note: caller must be prepared to deal with negative returns. */static inline int tcp_space (const struct sock *sk){ return tcp_win_from_space(sk->sk_rcvbuf - atomic_read(&sk->sk_rmem_alloc));} static inline int tcp_win_from_space(int space){ return sysctl_tcp_adv_win_scale <= 0 ? (space >> (-sysctl_tcp_adv_win_scale)) : space - (space >> sysctl_tcp_adv_win_scale);} /* 最大的接收缓存的3/4 */static inline int tcp_full_space(const struct sock *sk){ return tcp_win_from_space(sk->sk_rcvbuf);}
总体来说,新的接收窗口大小值为:剩余接收缓存的3/4,但不能超过接收缓存的阈值。
小结
接收窗口的调整算法主要涉及:
(1)window_clamp和sk_rcvbuf的调整。
(2)rcv_ssthresh接收窗口当前阈值的动态调整,一般增长2*advmss。
(3)rcv_wnd接收窗口的动态调整,一般为min(3/4 free space in sk_rcvbuf, rcv_ssthresh)。
如果剩余的接收缓存够大,rcv_wnd受限于rcv_ssthresh。这个时候每收到一个大的数据包,rcv_wnd就增大2920字节(由于缩放原因这个值可能波动)。这就像慢启动一样,接收窗口指数增长。
接收窗口当然不能无限制增长,当它增长到一定大小时,就会受到一系列因素的限制,比如window_clamp和sk_rcvbuf,或者剩余接收缓存区大小。
当应用程序读取接收缓冲区数据不够快时,或者发生了丢包时,接收窗口会变小,这主要受限于剩余的接收缓存的大小。
五、实验环节
这里要给大家推荐一个超好用的网络协议分析工具,wireshark。网上可以免费下载。
我选择的是无线网卡WLAN
这里使用wireshark抓取baidu.com的TCP报文。可以看到,No.36是本机对服务器之前发送数据(No.31)的一个ACK确认(ACK的Flag标记成1),同时声明窗口大小(window size)为1040。
紧接着是No.37对No.30发送一个ACK确认,受系统进程资源的影响,这时窗口的大小动态调整为948。
可以看到滑动窗口确实是会自动调整的。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新