1. 紧急标志
当你再次看到图 1 时,相信你已经无比的亲切,再观察下面彩色的 6 个标志位,有 5 个你已经熟知了,还剩下最后一个 URG,对,就是图 1 中那个鲜红的,醒目的那个位置。
图1 TCP 首部
除此之外,还有一个字段—— 16 位紧急指针,它正是配合 URG 标志位一起使用的,言外之意就是这个字段只有在 URG 被置位时才有意义。因为只有一个紧急指针,这也意味着它只能标识一个字节的数据。这个指针指向了紧急数据最后一个字节的下一个字节。
图2 紧急数据与紧急指针
我们知道 TCP 在传输数据时是有顺序的,它有字节号,URG 配合紧急指针,就可以找到紧急数据的字节号。紧急数据的字节号公式如下:
比如图 2 中的例子,如果 seq = 10, urgpoint = 5, 那么字节序号 urgSeq = 10 + 5 -1 = 14.
知道了字节号后,就可以计算紧急数据字位于所有传输数据中的第几个字节了,如果从第 0 个字节开始算起,那么紧急数据就是第 urgSeq - ISN - 1 个字节(还记得 ISN 吗,它表示初始序列号),减 1 表示不包括第一个 SYN 段,因为一个 SYN 段会消耗一个字节号。
2. 紧急标志的作用
紧急标志可以用来通知对端:我放了一个紧急数据在数据流中,你看着办吧!这个特性往往可以来达到通知的目的。
一旦 TCP 知道了你要发送紧急数据,那么在接下来的数据发送中,TCP 会将所有的 TCP 报文段中的 URG 标志置位,哪怕该报文段中不包含紧急数据,这个行为会持续到紧急数据被发送出去为止。
3. 一个示例
该数据是运行 unp/protocol/tools/tcpserver/urg_server.c
和 unp/protocol/tools/winclient/urg_client.cpp
时抓取的。
urg_client 程序每次发送 1024 字节的数据,一共发送 8 次。在第 4 次的时候,发送了 1 字节的紧急数据(字符 'X'
)和 1024 字节普通数据。类似下面这段代码:
for (i = 1; i <= 8; ++i) { if (i == 4) write(sockfd, 'X', 1, URG); write(sockfd, buf, 1024); }
服务器 urg_server 接收到客户端的连接请求后,先等待 3 秒,再接收数据,每次接收 1024 字节,类似下面这样:
while(1) { sleep(3); read(sockfd, buf, 1024); //... }
最后,抓取到的数据如图 3.
图3 抓取的包含 URG 的 TCP 报文
可以看到,当客户端在第一次发送了 1024 字节后(数据包 4),5 号数据包就发送了一个 URG。实际上,write 函数首先将数据写入自己的缓冲区,如果客户端发送缓冲区足够大,几乎一瞬间就会把 8193 个字节写入到缓冲区。
第一次 write 1024 字节到缓冲区后,TCP 直接将这 1024 字节发出去了。在收到对方的 ACK 前,TCP 都不会再发送数据(Nagle 算法),客户端又连续 write 了 7 次,写入发送缓冲区。因为在第 4 次的时候,客户端发送了一个字节紧急数据,因此,接下来 TCP 在每次发送报文时都会打开 URG 标志。直到紧急数据被发送出去。
我们看看最后一个红框框,也就是 22 号数据包:
图4 紧急数据
'X'
紧急指针 urgent pointer 的值为 1, 也就是当前 TCP 段中第一个字节。这一个字节的紧急数据,随普通数据一起发送了出去。
4. 一些坑
如果发送方多次发送紧急数据,最后一个数据的紧急指针会将前面的覆盖。比方说你发送了一个字节的紧急数据 'X'
,在 'X'
尚未被 TCP 发送前,你又发送了一个紧急数据 'Y'
,那么在后面的 TCP 报文中,紧急指针都是指向了 'Y'
的。
很多系统的实现,包括 Linux 将紧急数据称之为带外数据(out-of-band data, OOB),意为在连接之外传送的数据,实际上这是不对的(《TCP/IP 详解》一书称此不正确的)。即使是紧急数据,仍然会随着普通数据流一起发送,并不会单独为紧急数据开辟一条新的连接通道单独发送。这从图 3 中我们也可以看到,紧急数据并没有被优先发送出去。
5. 总结
- 知道紧急标志位的作用
- 紧急指针的含义
- 紧急数据与带外数据