TCP首部中的Window字段,表示当前套接字的接收窗口,即目前可以接收的数据大小,对端不会发送超过接收窗口大小的数据。如果在三次握手时,两端都支持Windows Scale选项,则实际的接收窗口还要乘以Windows Scale的值。
这个主题将分为两部分:本文是第一部分,是TCP的初始接收窗口大小是如何决定的。第二部分,分析TCP的动态接收窗口。
TCP主动发起连接,即发送三次握手中的第一个SYN报文。这时,TCP窗口的大小自然取决于本地的参数。函数tcp_connect_init负责初始化TCP连接,包括window的大小。
1static void tcp_connect_init(struct sock *sk)
2{
3 const struct dst_entry *dst = __sk_dst_get(sk);
4 struct tcp_sock *tp = tcp_sk(sk);
5 __u8 rcv_wscale;
6 /* We'll fix this up when we get a response from the other end.
7 * See tcp_input.c:tcp_rcv_state_process case TCP_SYN_SENT.
8 */
9 /* 根据系统配置参数,计算该套接字将要发送的TCP首部大小。 */
10 tp->tcp_header_len = sizeof(struct tcphdr) +
11 (sysctl_tcp_timestamps ? TCPOLEN_TSTAMP_ALIGNED : 0);
12#ifdef CONFIG_TCP_MD5SIG
13 if (tp->af_specific->md5_lookup(sk, sk) != NULL)
14 tp->tcp_header_len += TCPOLEN_MD5SIG_ALIGNED;
15#endif
16 /* 如果用户配置MSS大小,将其保存下来,作为MSS的最大值 */
17 if (tp->rx_opt.user_mss)
18 tp->rx_opt.mss_clamp = tp->rx_opt.user_mss;
19 /* max_window表示对端window的最大值 */
20 tp->max_window = 0;
21 /* 初始化MTU探测 */
22 tcp_mtup_init(sk);
23 /* 利用出口的MTU同步MSS */
24 tcp_sync_mss(sk, dst_mtu(dst));
25 /* 如果没有设置windows的最大值,则尝试使用下一跳的最大窗口值 */
26 if (!tp->window_clamp)
27 tp->window_clamp = dst_metric(dst, RTAX_WINDOW);
28 /* 出口的MSS值即握手时交换的MSS值 */
29 tp->advmss = dst_metric_advmss(dst);
30 /*
31 如果应用层配置MSS,并且小于交换的MSS值,则将选择用户配置的MSS。
32 对于TCP协议来说,重要的是要保证MSS是有效的。当用户配置的MSS值大于出口的MSS时,为了保证TCP数据报文的正常通信,则选择较小的值。
33 */
34 if (tp->rx_opt.user_mss && tp->rx_opt.user_mss < tp->advmss)
35 tp->advmss = tp->rx_opt.user_mss;
36 /* 初始化接收MSS,其实是对对端MSS的猜测判断 */
37 tcp_initialize_rcv_mss(sk);
38 /* limit the window selection if the user enforce a smaller rx buffer */
39 /* 如果用户设置接收缓存的大小,并且窗口的最大值大于了接收缓存或者没有设置窗口的上限,则设置窗口的最大值为接收缓存的大小。*/
40 if (sk->sk_userlocks & SOCK_RCVBUF_LOCK &&
41 (tp->window_clamp > tcp_full_space(sk) || tp->window_clamp == 0))
42 tp->window_clamp = tcp_full_space(sk);
43 /* 选择初始的接收窗口 */
44 tcp_select_initial_window(tcp_full_space(sk),
45 tp->advmss - (tp->rx_opt.ts_recent_stamp ? tp->tcp_header_len - sizeof(struct tcphdr) : 0),
46 &tp->rcv_wnd,
47 &tp->window_clamp,
48 sysctl_tcp_window_scaling,
49 &rcv_wscale,
50 dst_metric(dst, RTAX_INITRWND));
51 …… ……
52}
接下来进入tcp_select_initial_window函数
1void tcp_select_initial_window(int __space, __u32 mss,
2 __u32 *rcv_wnd, __u32 *window_clamp,
3 int wscale_ok, __u8 *rcv_wscale,
4 __u32 init_rcv_wnd)
5{
6 unsigned int space = (__space < 0 ? 0 : __space);
7 /* If no clamp set the clamp to the max possible scaled window */
8 /*
9 如果没有设置窗口最大值,则将其设置为协议规定的最大值。Windows Scale的最大限制为14,则TCP协议支持的最大窗口值为(65535 << 14)字节。
10 对于SYN包来说,*windows_clamp等于套接字的缓存大小或者用户配置的窗口上限。
11 */
12 if (*window_clamp == 0)
13 (*window_clamp) = (65535 << 14);
14 /* space最大不能超过窗口上限大小*window_clamp */
15 space = min(*window_clamp, space);
16 /* 保证space是mss的整数倍 */
17 if (space > mss)
18 space = (space / mss) * mss;
19 /* NOTE: offering an initial window larger than 32767
20 * will break some buggy TCP stacks. If the admin tells us
21 * it is likely we could be speaking with such a buggy stack
22 * we will truncate our initial window offering to 32K-1
23 * unless the remote has sent us a window scaling option,
24 * which we interpret as a sign the remote TCP is not
25 * misinterpreting the window field as a signed quantity.
26 */
27 /* 如果需要与将windows大小视为有符号数的对端通信,则接收窗口的大小最大不能超过MAX_TCP_WINDOW(32767)。不然对端可能将其视为负数。 */
28 if (sysctl_tcp_workaround_signed_windows)
29 (*rcv_wnd) = min(space, MAX_TCP_WINDOW);
30 else
31 (*rcv_wnd) = space;
32 (*rcv_wscale) = 0;
33 if (wscale_ok) {
34 /* 系统支持windows scale,下面来确定windows scale可能支持的最大值。先确定可能最大的接收缓存,然后通过移位得到最大支持的scale值。 */
35 space = max_t(u32, space, sysctl_tcp_rmem[2]);
36 space = max_t(u32, space, sysctl_rmem_max);
37 space = min_t(u32, space, *window_clamp);
38 /* 这时得到了缓存的可能的最大值 */
39 while (space > 65535 && (*rcv_wscale) < 14) {
40 space >>= 1;
41 (*rcv_wscale)++;
42 }
43 }
44 if (mss > (1 << *rcv_wscale)) {
45 /* 初始化拥塞窗口 */
46 int init_cwnd = TCP_DEFAULT_INIT_RCVWND;
47 /* 当MSS大于1460字节时,要限制拥塞窗口不能太大。最小是2个MSS,最大值按照MSS为1460来运算。 */
48 if (mss > 1460)
49 init_cwnd =
50 max_t(u32, (1460 * TCP_DEFAULT_INIT_RCVWND) / mss, 2);
51 /*
52 计算初始接收窗口。
53 对于SYN包来说,init_rcv_wnd即dst_metric(dst, RTAX_INITRWND)。一般来说,在未配置dst的接收窗口时,取当前*rcv_wnd和拥塞窗口的较小值。
54 */
55 if (init_rcv_wnd)
56 *rcv_wnd = min(*rcv_wnd, init_rcv_wnd * mss);
57 else
58 *rcv_wnd = min(*rcv_wnd, init_cwnd * mss);
59 }
60 /* 最大的窗口值不能超过可表示值 */
61 /* Set the clamp no higher than max representable value */
62 (*window_clamp) = min(65535U << (*rcv_wscale), *window_clamp);
63}
至此,主动连接已经完成了初始窗口的选择。
下面看看TCP被动连接时,如何选择窗口大小。函数tcp_make_synack是用于生成三次握手中的syn+ack报文,下面请看它的部分代码。
1 if (req->rcv_wnd == 0) {
2 __u8 rcv_wscale;
3 /* 设置窗口的上限 */
4 req->window_clamp = tp->window_clamp ? : dst_metric(dst, RTAX_WINDOW);
5 /* 根据接收缓存的大小设置窗口的上限 */
6 if (sk->sk_userlocks & SOCK_RCVBUF_LOCK &&
7 (req->window_clamp > tcp_full_space(sk) || req->window_clamp == 0))
8 req->window_clamp = tcp_full_space(sk);
9 /* 通过tcp_select_initial_window选择初始窗口 */
10 tcp_select_initial_window(tcp_full_space(sk),
11 mss - (ireq->tstamp_ok ? TCPOLEN_TSTAMP_ALIGNED : 0),
12 &req->rcv_wnd,
13 &req->window_clamp,
14 ireq->wscale_ok,
15 &rcv_wscale,
16 dst_metric(dst, RTAX_INITRWND));
17 ireq->rcv_wscale = rcv_wscale;
18 }
函数tcp_select_initial_window在前文中已经分析过了,这里不再重复。
通过上面两种情况,我们已经可以总结出TCP初始窗口值大小取决于哪几个因素:
而一般来说,大家都很少设置window上限,所以TCP初始接收窗口的大小就决定于套接字的缓存和拥塞窗口(这个由系统决定)。
居安思危,手不释卷。