TCP 带外数据传输 OOB 数据完全解析

18 篇文章 0 订阅

带外数据说明

TCP 的带外数据可传输一字节内容,实际上带外数据和其他数据是一起发送,一起接收。区别在于:

对于发送端:

发送带外数据,会将当前发送缓冲区待发送的 TCP 报文 header 设置 flag 的 URG 标志和紧急指针 Urgent pointer 的值,仅仅如此而已。带外数据的位置为该次发送带外数据调用的最后一个字节。

对于接收端:

接收端,则是读取接口的行为的差异。默认情况下,带外数据需要专用的 socket API 才能读取,recv、recvmfg、recvfrom。当然,发送也需要send、sendto、sendmsg才行。

带外数据的通知方式为发送 SIGURG 信号,首先需要设置文件描述符所属的进程,并注册 SIGURG 信号的处理函数。

OOB 数据完全解析

接收端一直等待,发送端先后 3 次发送,每次发送 10 字节。第 20 字节为带外数据。

图 1 带外数据初探

左边为接收端,右边为发送端。

ss 命令可以在接收端查查看 30 字节的内容,不过使用 ioctlFIONREAD 选项查看只有 19 字节。

发送端进行第 3 和 第 4 次发送,第3次发送的最后一个字节是带外数据。

图 2 接收端带外数据只能有一个

此时接收端使用 ss 查看是有 50 字节,使用 ioctlFIONREAD 选项查看的是 39 字节,所以 FIONREAD 准确的含义是表示本次最多能够读取的字节数,并不是缓冲区中的所有未读取的长度。

 

接收端进行 2 次读取,第一次只能读取 39 字节,第二次读取了剩余的 50 字节。

图 3 接收端数据读取

第一次只能读取 39 字节,暗含了 ioctlFIONREAD 的返回值。第二次读取了 10 字节。

图 4 整个过程在发送端抓包

抓包中的 sequrg 就能定位带外数据的位置:urg 相对于同个包的 seq 就是带外数据的位置。

带外数据只能有一份

无论是发送端还是接收端,都只能有一个字节的带外数.

如果一个新的带外数据达到,已经在内核接收缓冲区中的带外数据成为普通数据。

对于发送端,如果已经有带外数据在内核缓冲区中未被发送,新的带外数据写入内核,那么之前的带外数据成为普通数据。

图 5 发送端内核缓冲区,已有的带外数据会被取代成为普通数据

发送端连续调用两次发送包含带外数据的数据包,从抓包结果来看,第 9 个包已经调整了 Urg 的值,说明此时发送缓冲区里,只有最新写入的带外数据才作为带外数据标识,之前的成为普通数据。

图 6 图 5 中的操作对应的抓包

2 个有意思的地方

  • 只要当前未有效传输的数据(即进程A发送的数据没有被进程B接收)中包含带外数据(无论是带外数据在发送端的缓冲区,还是在接收端的缓冲区),只要接收端已经被通知了有带外数据,那么使用 MSG_OOB 标志去获取,如果带外数据还在发送端的缓冲区,直接非阻塞的返回 EAGAIN Resource temporarily unavailable 错误。MSG_OOB 标志读取带外数据总是非阻塞的。 这也是合理的,因为在包含带外数据的数据包达到之前,接收端可能已经进入紧急状态。但是用户态其实没法知道带外数据是否真的到达,所以如果此时读取代码数据是阻塞的,一旦接收缓冲区满,无法接收新的数据,带外数据包永远不会被接收,这将导致接收端一直阻塞下去。

  • 如果当前没有带外数据或者已经被获取,使用 MSG_OOB 标志去获取,返回 EINVAL Invalid argument 错误。

对于接收端,如果带外数据在内核缓冲区中未被读取,又接收了新的带外数据,那么之前的带外数据成为普通数据。

某一个特定位置的带外数据在发送端写入到内核缓冲区,之后发送的数据包在这个带外数据传输前,都将标志这个带外数据。但是,接收端对于一个特定位置的带外数据,只会在收到第一个带有该带外数据标记的包时产生一次信号。

带外数据对于 recv 行为的影响

不读取带外数据

相当于带外数据把缓冲区分隔成3部分。第一次 recv 操作能只能把带外数据之前的所有数据读完,达到带外数据的位置。再次使用 recv 能够所有剩余的数据。

假设接收端缓冲区现在有 8 个字节 1111b222,其中 b 表示带外数据。那么第一次 recv(100) 能读取1111 这 4 个字节;接收缓冲区中剩余 4 个字节;在此使用 recv(100) 将读取剩下的 222 这 3 个字节。

 

设置 MSG_OOB 标志读取带外数据

带外数据需要传递 MSG_OOB 标志才能读取,否则将跳过带外数据。图中左边接收端记录了整个过程。

图 7 带外数据的 recv 方式之 MSG_OOB 标志

一旦带外数据在接收缓冲区当中,无论其在接收缓冲区中的位置,此时就能使用带 MSG_OOB 标志的 recv 进行读取。几个有意思的地方在于:

  • 带外数据只能读一次,第二次报 Invalid argument 错误。

  • 缓冲区中的第一个数据是带外数据,使用 FIONREAD 得到的结果是 0,但是 recv 读取带外数据位置后的 3 个字节。

设置 SO_OOBINLINE 套接字属性读取带外数据

设置了 FIONREAD 套接字属性,带外数据将和带外数据之后的数据一起被不带任何标志的 recv 正常读取。图 6 左边的记录了整个过程。

图 8 带外数据的 recv 方式之 SO_OOBINLINE 套接字属性

  • 设置了 SO_OOBINLINE 套接字属性, FIONREAD 返回的是包含带外数据的整个缓冲区的长度,但是读取依旧在带外数据处中断,需要 2 次才能读完。所以之前 FIONREAD 准确的含义是表示本次最多能够读取的字节数 的说法也不完全正确。

总结

带外数据最多有 2 份,此时的情况是接收端缓冲区和发送端缓冲区各有一份并且发送端新的带外数据通知没有达到接收端。一旦发送端新的带外数据通知到达接收端,即使发送端的带外数据还未传输,那么接收端在收到通知时,将更新带外数据的信息:将发送端还未发送的带外数据才视为新的带外数据,之前本地的变成普通数据。

问答

如果接收端缓冲区有 5 个字节的内容,最后一个时带外数据。使用不带任何标志的 recv 去读取,立马回返回 4 个字节。此时再次用不带任何标志的 recv 去读取,回发生什么?

因为不带任何标志的 recv 会去读取带外数据的位置之后的数据,所以如果没有新的数据到来,recv 将一直阻塞。

代码

发送端

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
client.connect(("192.168.1.236", 7777))


client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

接收端

import socket, time, signal, fcntl, os, array, termios

global_int_count = 0

def handler(sig, frame):
    global global_int_count
    global_int_count += 1
    print(global_int_count, sig, frame)

#c : socket.socket
def get_noread(c):
    buf = array.array("i", [0xff])
    fcntl.ioctl(c, termios.FIONREAD, buf)
    print(buf)

serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
serv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serv.bind(("0.0.0.0", 7777))
serv.listen(1024)
client, addr = serv.accept()


client.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 640)

#client.setsockopt(SOL_SOCKET, SO_OOBINLINE, 1)

print(client.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF))

fcntl.fcntl(client, fcntl.F_SETOWN, os.getpid())  #一定需要设置,才能收到信号
fcntl.fcntl(client, fcntl.F_SETSIG, signal.SIGURG)  ### 没法通过 SETSIG 设置其他信号

#设置信号处理函数
signal.signal(signal.SIGURG, handler)

 

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值