tcp 头部选项的机制

15 篇文章 0 订阅
14 篇文章 0 订阅
TCP Option Numbers

The Transmission Control Protocol (TCP) has provision for optional header fields identified by an option kind field.  Options 0 and 1 are exactly one octet which is their kind field.  All other options have their one octet kind field, followed by a one octet length field, followed by length-2 octets of option data.

Kind   Meaning                               Reference
----   -------------------------------   ---------
  0      End of Option List                 [RFC793]
  1      No-Operation                       [RFC793]
  2      Maximum Segment Size        [RFC793]
  3      WSOPT - Window Scale        [RFC1323]
  4      SACK Permitted                    [RFC2018]
  5      SACK                                     [RFC2018]
  6      Echo (obsoleted by option 8)      [RFC1072]
  7      Echo Reply (obsoleted by option 8)[RFC1072]
  8      TSOPT - Time Stamp Option         [RFC1323]
  9      Partial Order Connection Permitted[RFC1693]
10     Partial Order Service Profile     [RFC1693]
11      CC                                      [RFC1644]
12      CC.NEW                            [RFC1644]
13      CC.ECHO                           [RFC1644]
14      TCP Alternate Checksum Request    [RFC1146]
15       TCP Alternate Checksum Data       [RFC1146]
16       Skeeter                           [Knowles]
17       Bubba                             [Knowles]
18       Trailer Checksum Option    [Subbu & Monroe]
19       MD5 Signature Option              [RFC2385]
20       SCPS Capabilities                   [Scott]
21       Selective Negative Acknowledgements [Scott]
22       Record Boundaries                   [Scott]
23       Corruption experienced              [Scott]
24       SNAP                         [Sukonnik]
25       Unassigned (released 12/18/00)
26       TCP Compression Filter           [Bellovin]

TCP头部的最后一个选项字段(options)是可变长的可选信息。这部分最多包含40字节,因为TCP头部最长是60字节(其中还包含前面讨论的20字节的固定部分)。典型的TCP头部选项结构如图3-4所示。

选项的第一个字段kind说明选项的类型。有的TCP选项没有后面两个字段,仅包含1字节的kind字段。第二个字段length(如果有的话)指定该选项的总长度,该长度包括kind字段和length字段占据的2字节。第三个字段info(如果有的话)是选项的具体信息。常见的TCP选项有7种,如图3-5所示。

kind=0是选项表结束选项。

kind=1是空操作(nop)选项,没有特殊含义,一般用于将TCP选项的总长度填充为4字节的整数倍。

kind=2是最大报文段长度选项。TCP连接初始化时,通信双方使用该选项来协商最大报文段长度(Max Segment Size,MSS)。TCP模块通常将MSS设置为(MTU-40)字节(减掉的这40字节包括20字节的TCP头部和20字节的IP头部)。这样携带TCP报文段的IP数据报的长度就不会超过MTU(假设TCP头部和IP头部都不包含选项字段,并且这也是一般情况),从而避免本机发生IP分片。对以太网而言,MSS值是1460(1500-40)字节。

kind=3是窗口扩大因子选项。TCP连接初始化时,通信双方使用该选项来协商接收通告窗口的扩大因子。在TCP的头部中,接收通告窗口大小是用16位表示的,故最大为65?535字节,但实际上TCP模块允许的接收通告窗口大小远不止这个数(为了提高TCP通信的吞吐量)。窗口扩大因子解决了这个问题。假设TCP头部中的接收通告窗口大小是N,窗口扩大因子(移位数)是M,那么TCP报文段的实际接收通告窗口大小是N乘2M,或者说N左移M位。注意,M的取值范围是0~14。我们可以通过修改/proc/sys/net/ipv4/tcp_window_scaling内核变量来启用或关闭窗口扩大因子选项。

和MSS选项一样,窗口扩大因子选项只能出现在同步报文段中,否则将被忽略。但同步报文段本身不执行窗口扩大操作,即同步报文段头部的接收通告窗口大小就是该TCP报文段的实际接收通告窗口大小。当连接建立好之后,每个数据传输方向的窗口扩大因子就固定不变了。关于窗口扩大因子选项的细节,可参考标准文档RFC 1323。

kind=4是选择性确认(Selective Acknowledgment,SACK)选项。TCP通信时,如果某个TCP报文段丢失,则TCP模块会重传最后被确认的TCP报文段后续的所有报文段,这样原先已经正确传输的TCP报文段也可能重复发送,从而降低了TCP性能。SACK技术正是为改善这种情况而产生的,它使TCP模块只重新发送丢失的TCP报文段,不用发送所有未被确认的TCP报文段。选择性确认选项用在连接初始化时,表示是否支持SACK技术。我们可以通过修改/proc/sys/net/ipv4/tcp_sack内核变量来启用或关闭选择性确认选项。

kind=5是SACK实际工作的选项。该选项的参数告诉发送方本端已经收到并缓存的不连续的数据块,从而让发送端可以据此检查并重发丢失的数据块。每个块边沿(edge of block)参数包含一个4字节的序号。其中块左边沿表示不连续块的第一个数据的序号,而块右边沿则表示不连续块的最后一个数据的序号的下一个序号。这样一对参数(块左边沿和块右边沿)之间的数据是没有收到的。因为一个块信息占用8字节,所以TCP头部选项中实际上最多可以包含4个这样的不连续数据块(考虑选项类型和长度占用的2字节)。

kind=8是时间戳选项。该选项提供了较为准确的计算通信双方之间的回路时间(Round Trip Time,RTT)的方法,从而为TCP流量控制提供重要信息。我们可以通过修改/proc/sys/net/ipv4/tcp_timestamps内核变量来启用或关闭时间戳选项。





2.6.2 TCP选项

每一个SYN可以含有多个TCP选项。下面是常用的TCP选项。

MSS选项。发送SYN的TCP一端使用本选项通告对端它的最大分节大小(maximum segment size)即MSS,也就是它在本连接的每个TCP分节中愿意接受的最大数据量。发送端TCP使用接收端的MSS值作为所发送分节的最大大小。我们将在7.9节看到如何使用TCP_MAXSEG套接字选项提取和设置这个TCP选项。

窗口规模选项。TCP连接任何一端能够通告对端的最大窗口大小是65535,因为在TCP首部中相应的字段占16位。然而当今因特网上业已普及的高速网络连接(45 Mbit/s或更快,如RFC 1323[Jacobson, Braden, and Borman 1992]所述)或长延迟路径(卫星链路)要求有更大的窗口以获得尽可能大的吞吐量。这个新选项指定TCP首部中的通告窗口必须扩大(即左移)的位数(0~14),因此所提供的最大窗口接近1 GB(65535×214)。在一个TCP连接上使用窗口规模的前提是它的两个端系统必须都支持这个选项。我们将在7.5节看到如何使用SO_RCVBUF套接字选项影响这个TCP选项。

为提供与不支持这个选项的较早实现间的互操作性,需应用如下规则。TCP可以作为主动打开的部分内容随它的SYN发送该选项,但是只在对端也随它的SYN发送该选项的前提下,它才能扩大自己窗口的规模。类似地,服务器的TCP只有接收到随客户的SYN到达的该选项时,才能发送该选项。本逻辑假定实现忽略它们不理解的选项,如此忽略是必需的要求,也已普遍满足,但无法保证所有实现都满足此要求。

时间戳选项。这个选项对于高速网络连接是必要的,它可以防止由失而复现的分组 可能造成的数据损坏。它是一个较新的选项,也以类似于窗口规模选项的方式协商处理。作为网络编程人员,我们无需考虑这个选项。

TCP的大多数实现都支持这些常用选项。后两个选项有时称为"RFC 1323选项",因为它们是在RFC 1323[Jacobson, Braden, and Borman 1992]中说明的。既然高带宽或长延迟的网络被称为"长胖管道"(long fat pipe),这两个选项也称为"长胖管道选项"。TCPv1的第24章对这些选项有详细的叙述。

本帖最后由 Godbach 于 2011-02-11 13:52 编辑

最近在看TCP的选项部分,无意间比较了 Linux 和 XP 的 SYN 包选项字段,发现两者的实现有一些区别。
上图先:
XP 的 SYN 包选项如下所示:

xp_synopt2.JPG 

Linux 的SYN 包选项如下图所示:

linux_synopt2.JPG 

对比以上两个图可以看出:
(1)当前测试的两个操作系统,Linux 和XP 均支持四个 TCP 选项:MSS,SACK,Timestamp 和 Wscale。
(2)Linux 仅用了 20 个字节存储这 4 个选项,而 XP 用了 24 个字节。

原因:
在选项构建的时候,每一个选项都是要考虑 32 bit 对齐的。因此,如果单独考虑某一个选项时,尤其是对于长度为非 32bit 整数倍的选项时,要处理对齐的问题,就要考虑 pad 若干个 NOP 字节:
(1)对于 SACK 选项,该选项本身就是 2 个字节: 04 02。因此,要考虑 pad 两个 NOP 字节。所以就变成了: 01 01 04 02,这正如 XP 所实现的。(至于是在选项前面还是后面 pad,本人并没有深入研究。只是当前抓包来看,Linux 和 XP 都是在前面 pad)。
(2)对于 Timestamp 选项,该选项时 10 个字节。因此,考虑 32bit 对齐的话,需要再 pad 两个字节,所以就变成了: 01 01 XX XX ... XX XX,这也正如 XP 实现的。

但是,如果系统同时支持 SACK 和 Timestamp 选项的话呢,正好长度是 2 + 10 = 12,满足了 32bit 的边界。如果确定系统同时支持这两个选项,那么可以考虑将 SACK 选项的两个字节放前面,后面跟上 10 个字节的 Timestamp 选项,这样就需要不需要 pad 了。这正如 Linux 的实现。这样做就节省了 4 个字节的 NOP。

Linux 的内核源码我们可以看到,那就从代码上再次确认一下(2.6.24.4, tcp_output.c):
static void tcp_syn_build_options(__be32 *ptr, int mss, int ts, int sack,
                                  int offer_wscale, int wscale, __u32 tstamp,
                                  __u32 ts_recent, __u8 **md5_hash)
{
         /*此处省略几百字。。。  */
        *ptr++ = htonl((TCPOPT_MSS << 24) | (TCPOLEN_MSS << 16) | mss);
        if (ts) {
                if (sack)

                        *ptr++ = htonl((TCPOPT_SACK_PERM << 24) |  /*支持 SACK,先构建 SACK,再构建 Timestamp*/
                                       (TCPOLEN_SACK_PERM << 16) |
                                       (TCPOPT_TIMESTAMP << 8 ) |
                                       TCPOLEN_TIMESTAMP);
                else
                        *ptr++ = htonl((TCPOPT_NOP << 24) |   /*没有 SACK,需要 pad 两个字节的 NOP*/
                                       (TCPOPT_NOP << 16) |
                                       (TCPOPT_TIMESTAMP << 8 ) |
                                       TCPOLEN_TIMESTAMP);
                *ptr++ = htonl(tstamp);                /* TSVAL */
                *ptr++ = htonl(ts_recent);        /* TSECR */
        } else if (sack)


细微之处,可见 Linux 实现之巧妙。

另外,XP中TSval为0,而linux中就是包的实际发送时间。
XP默认下是不支持时间戳的。而linux是支持的。

  1.         if (likely(sysctl_tcp_timestamps && *md5 == NULL)) {
  2.                 opts->options |= OPTION_TS;
  3.                 opts->tsval = TCP_SKB_CB(skb)->when; 《------- tsval 值
  4.                 opts->tsecr = tp->rx_opt.ts_recent;
  5.                 remaining -= TCPOLEN_TSTAMP_ALIGNED;
  6.         }
复制代码

一个是要支持该选项,然后通过设置不同读缓存大小(tcp_rmem),就可以改变数值。

顺带再说个笑话,某电视台网站有一条到美国40M的专线,不丢包的,但是他们单个连接的速度连5M都跑不到,我想了半天觉得不可能,最后一查,原来是他们用的AS4,没开启窗口扩大因子(或者是扩大因子太小了)。


对端的wscale值跟本地的wsale是不一样的,也没有任何的关系。

任何一端的wscale值只是说明了自己的接收能力。
对方的wsacle值也只是说明对方的接收能力。

如果wsale值是0,则只说明只能接收window大小的数据。

如果对方的wsale值是8,则说明这端可以给对方发送的最大数据是Window*(2^R)-1.

另外,21楼说的不对。
在不设置接收缓存(SO_RCVBUF选项)的情况下,实际的因子值一般是由tcp_rmem, rmem_max最大的值来决定的。
  1. 215         if (wscale_ok) {
  2. 216                 /* Set window scaling on max possible window
  3. 217                  * See RFC1323 for an explanation of the limit to 14
  4. 218                  */
  5. 219                 space = max_t(u32, sysctl_tcp_rmem[2], sysctl_rmem_max);
  6. 220                 space = min_t(u32, space, *window_clamp);
  7. 221                 while (space > 65535 && (*rcv_wscale) < 14) {
  8. 222                         space >>= 1;
  9. 223                         (*rcv_wscale)++;
  10. 224                 }
  11. 225         }
复制代码

SYN 选项中的 wscale 是声明本地系统窗口缩放的比例,为 0 说明自己的窗口没有缩放。
如果对端声明的 wsacle 大于 1, 本地协议栈应该能够正确的计算出对端的 WINDOW 的值

是tcp_rmem数组里的最后一个,第一个是最小 第二个是初始默认值  第三个最大值


发现WIN7 进行了改进,采用了和 Linux相同的处理方法
选项变成 20 个字节了。
SACK  和 Timestamp 进行了合并

win7_synack.png





TCP_DEFER_ACCEPT这个选项可能大家都知道,不过我这里会从源码和数据包来详细的分析这个选项。要注意,这里我所使用的内核版本是3.0.

首先看man手册中的介绍(man 7 tcp):

TCP_DEFER_ACCEPT (since Linux 2.4)
Allow a listener to be awakened only when data arrives on the socket. Takes an integer value (seconds), this can bound the maximum number of attempts TCP will make to complete the connection. This option should not be used in code intended to be portable.

我先来简单介绍下,这个选项主要是针对server端的服务器,一般来说我们三次握手,当客户端发送syn,然后server端接收到,然后发送syn + ack,然后client接收到syn+ack之后,再次发送ack(client进入establish状态),最终server端收到最后一个ack,进入establish状态。

而当正确的设置了TCP_DEFER_ACCEPT选项之后,server端会在接收到最后一个ack之后,并不进入establish状态,而只是将这个socket标记为acked,然后丢掉这个ack。此时server端这个socket还是处于syn_recved,然后接下来就是等待client发送数据, 而由于这个socket还是处于syn_recved,因此此时就会被syn_ack定时器所控制,对syn ack进行重传,而重传次数是由我们设置TCP_DEFER_ACCEPT传进去的值以及TCP_SYNCNT选项,proc文件系统的tcp_synack_retries一起来决定的(后面分析源码会看到如何来计算这个值).而我们知道我们传递给TCP_DEFER_ACCEPT的是秒,而在内核里面会将这个东西转换为重传次数.

这里要注意,当重传次数超过限制之后,并且当最后一个ack到达时,下一次导致超时的synack定时器还没启动,那么这个defer的连接将会被加入到establish队列,然后通知上层用户。这个也是符合man里面所说的(Takes an integer value (seconds), this can bound the maximum number of attempts TCP will make to complete the connection.) 也就是最终会完成这个连接.


我们来看抓包,这里server端设置deffer accept,然后客户端connect并不发送数据,我们来看会发生什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//客户端发送syn
19:38:20.631611 IP T-diaoliang.60277 > T-diaoliang.sunproxyadmin: Flags [S], seq 2500439144, win 32792, options [mss 16396,sackOK,TS val 9008384 ecr 0,nop,wscale 4], length 0
//server回了syn+ack
19:38:20.631622 IP T-diaoliang.sunproxyadmin > T-diaoliang.60277: Flags [S.], seq 1342179593, ack 2500439145, win 32768, options [mss 16396,sackOK,TS val 9008384 ecr 9008384,nop,wscale 4], length 0
 
//client发送最后一个ack
19:38:20.631629 IP T-diaoliang.60277 > T-diaoliang.sunproxyadmin: Flags [.], ack 1, win 2050, options [nop,nop,TS val 9008384 ecr 9008384], length 0
 
//这里注意时间,可以看到过了大概1分半之后,server重新发送了syn+ack
19:39:55.035893 IP T-diaoliang.sunproxyadmin > T-diaoliang.60277: Flags [S.], seq 1342179593, ack 2500439145, win 32768, options [mss 16396,sackOK,TS val 9036706 ecr 9008384,nop,wscale 4], length 0
19:39:55.035899 IP T-diaoliang.60277 > T-diaoliang.sunproxyadmin: Flags [.], ack 1, win 2050, options [nop,nop,TS val 9036706 ecr 9036706,nop,nop,sack 1 {0:1}], length 0
 
//再过了1分钟,server close掉这条连接。
19:40:55.063435 IP T-diaoliang.sunproxyadmin > T-diaoliang.60277: Flags [F.], seq 1, ack 1, win 2048, options [nop,nop,TS val 9054714 ecr 9036706], length 0
 
19:40:55.063692 IP T-diaoliang.60277 > T-diaoliang.sunproxyadmin: Flags [F.], seq 1, ack 2, win 2050, options [nop,nop,TS val 9054714 ecr 9054714], length 0
 
19:40:55.063701 IP T-diaoliang.sunproxyadmin > T-diaoliang.60277: Flags [.], ack 2, win 2048, options [nop,nop,TS val 9054714 ecr 9054714], length 0

这里要注意,close的原因是当synack超时之后,nginx接收到了这条连接,然后读事件超时,最终导致close这条连接。

接下来就来看内核的代码。

先从设置TCP_DEFER_ACCEPT开始,设置TCP_DEFER_ACCEPT是通过setsockopt来做的,而传递给内核的值是秒,下面就是内核中对应的do_tcp_setsockopt函数,它用来设置tcp相关的option,下面我们能看到主要就是将传递进去的val转换为将要重传的次数。

1
2
3
4
5
6
7
     case TCP_DEFER_ACCEPT:
         /* Translate value in seconds to number of retransmits */
//注意参数
         icsk->icsk_accept_queue.rskq_defer_accept =
             secs_to_retrans(val, TCP_TIMEOUT_INIT / HZ,
                     TCP_RTO_MAX / HZ);
         break ;

这里可以看到通过调用secs_to_retrans来将秒转换为重传次数。接下来就来看这个函数,它有三个参数,第一个是将要转换的秒,第二个是RTO的初始值,第三个是RTO的最大值。 可以看到这里都是依据RTO来计算的,这是因为这个重传次数是syn_ack的重传次数。

这个函数实现很简单,就是一个定时器退避的计算过程(定时器退避可以看我前面的blog的介绍),每次乘2,然后来计算重传次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static u8 secs_to_retrans( int seconds, int timeout, int rto_max)
{
     u8 res = 0;
 
     if (seconds > 0) {
         int period = timeout;
//重传次数
         res = 1;
//开始遍历
         while (seconds > period && res < 255) {
             res++;
//定时器退避
             timeout <<= 1;
             if (timeout > rto_max)
                 timeout = rto_max;
//定时器的秒数
             period += timeout;
         }
     }
     return res;
}

然后来看当server端接收到最后一个ack的处理,这里只关注defer_accept的部分,这个函数是tcp_check_req,它主要用来检测SYN_RECV状态接收到包的校验。

req->retrans表示已经重传的次数。
acked标记主要是为了syn_ack定时器来使用的。

1
2
3
4
5
6
7
8
//两个条件,一个是重传次数小于defer_accept,一个是序列号,这两个都必须满足。
     if (req->retrans < inet_csk(sk)->icsk_accept_queue.rskq_defer_accept &&
         TCP_SKB_CB(skb)->end_seq == tcp_rsk(req)->rcv_isn + 1) {
//此时设置acked。
         inet_rsk(req)->acked = 1;
         NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPDEFERACCEPTDROP);
         return NULL;
     }

而当tcp_check_req返回之后,在tcp_v4_do_rcv中会丢掉这个包,让socket继续保存在半连接队列中。

然后来看syn ack定时器,这个定时器我以前有分析过(http://simohayha.iteye.com/admin/blogs/481989)
,因此我这里只是简要的再次分析下。如果需要更详细的分析,可以看我上面的链接,这个定时器会调用inet_csk_reqsk_queue_prune函数,在这个函数中做相关的处理。

这里我们就主要关注重试次数。其中icsk_syn_retries是TCP_SYNCNT这个option设置的。这个值会比sysctl_tcp_synack_retries优先.然后是rskq_defer_accept,它又比icsk_syn_retries优先.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
void inet_csk_reqsk_queue_prune( struct sock *parent,
                 const unsigned long interval,
                 const unsigned long timeout,
                 const unsigned long max_rto)
{
........................
//最大的重试次数
     int max_retries = icsk->icsk_syn_retries ? : sysctl_tcp_synack_retries;
     int thresh = max_retries;
     unsigned long now = jiffies;
     struct request_sock **reqp, *req;
     int i, budget;
 
....................................
//更新设置最大的重试次数。
     if (queue->rskq_defer_accept)
         max_retries = queue->rskq_defer_accept;
 
     budget = 2 * (lopt->nr_table_entries / (timeout / interval));
     i = lopt->clock_hand;
 
     do {
         reqp=&lopt->syn_table[i];
         while ((req = *reqp) != NULL) {
             if (time_after_eq(now, req->expires)) {
                 int expire = 0, resend = 0;
//这个函数主要是判断超时和是否重新发送syn ack,然后保存在expire和resend这个变量中。
                 syn_ack_recalc(req, thresh, max_retries,
                            queue->rskq_defer_accept,
                            &expire, &resend);
....................................................
                 if (!expire &&
                     (!resend ||
                      !req->rsk_ops->rtx_syn_ack(parent, req, NULL) ||
                      inet_rsk(req)->acked)) {
                     unsigned long timeo;
//更新重传次数。
                     if (req->retrans++ == 0)
                         lopt->qlen_young--;
                     timeo = min((timeout << req->retrans), max_rto);
                     req->expires = now + timeo;
                     reqp = &req->dl_next;
                     continue ;
                 }
//如果超时,则丢掉这个请求,并对应的关闭连接.
                 /* Drop this request */
                 inet_csk_reqsk_queue_unlink(parent, req, reqp);
                 reqsk_queue_removed(queue, req);
                 reqsk_free(req);
                 continue ;
             }
             reqp = &req->dl_next;
         }
 
         i = (i + 1) & (lopt->nr_table_entries - 1);
 
     } while (--budget > 0);
...............................................
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值