[apue] 如何处理 tcp 紧急数据(OOB)?

  • 2019 年 11 月 4 日
  • 筆記

在上大学的时候,我们可能就听说了OOB(Out Of Band 带外数据,又称紧急数据)这个概念。

当时老师给的解释就是在当前处理的数据流之外的数据,用于紧急的情况。然后就没有然后了……

毕业这么多年了,回想一下,还真是没有接触过OOB的场景,更没有实地发送、接收过OOB。

那么到底该怎样处理OOB呢?OOB在所谓的紧急情况下是否有用呢?下面一一道来。

 

首先产生OOB是非常简单的,只需要在寻常send的最后一个参数,加入MSG_OOB标志位:

ret = send (sockfd, ptr, n, MSG_OOB);  

如果考虑一个完整的测试场景,需要有惯常数据,中间夹带OOB数据,这样才能比较好的测试接收端是否能正确的区分他们,

所以客户端可以写成这样:

 1     strcpy(buf, "abcdefghijklmn");   2     char const* ptr = buf;   3     if ((ret = send (sockfd, ptr, 2, 0)) < 0)   4         err_sys ("send normal head failed");   5     else   6         printf ("send normal head %dn", ret);   7   8     ptr += 2;   9     n = 1;  10     if ((ret = send (sockfd, ptr, n, MSG_OOB)) < 0)  11         err_sys ("send oob failed");  12     else  13         printf ("send oob %dn", ret);  14  15     ptr += n;  16     if ((ret = send (sockfd, ptr, 2, 0)) < 0)  17         err_sys ("send normal tail failed");  18     else  19         printf ("send normal tail %dn", ret); 

算法比较简单,先发送2字节惯常数据,接着1字节OOB,最后2字节惯常数据结尾。

需要注意的是,目前只有TCP支持OOB,UDP没所谓顺序,更没所谓带内带外之分,所以也没有OOB;

另外TCP目前大多数实现只支持1字节OOB,大于1字节的OOB,只有最后一字节会被当为OOB处理,之前的作为普通数据。

 

然后我们来说一下接收OOB的两种方法:

1. 使用SIGURG信号专门处理OOB

这种方法是将OOB与惯常数据分开处理,具体步骤如下:

a) 进程起始时,建立SIGURG信号处理器

1     struct sigaction sa;  2     sa.sa_handler = on_urg;  3     sa.sa_flags |= SA_RESTART;  4     sigemptyset (&sa.sa_mask);  5     sigaction (SIGURG, &sa, NULL); 

b) 建立新连接时,设置连接句柄的信号处理进程(为当前进程)

1 fcntl (clfd, F_SETOWN, getpid ()); 

c) 在信号处理器中使用MSG_OOB接收带外数据

 1 int g_fd = 0;   2 void on_urg (int signo)   3 {   4     int ret = 0;   5     char buf[BUFLEN] = { 0 };   6     ret = recv (g_fd, buf, sizeof (buf), MSG_OOB);   7     if (ret > 0)   8         buf[ret] = 0;   9     else  10         strcpy (buf, "n/a");  11  12     printf ("got urgent data on signal %d, len %d, %sn", signo, ret, buf);  13  14 }

d) 惯常数据,可以在主处理流程中使用不带MSG_OOB的recv,像以前那样处理

1         ret = recv (clfd, buf, sizeof(buf), 0);  2         if (ret > 0)  3             buf[ret] = 0;  4         else  5             strcpy (buf, "n/a");  6  7         printf ("recv %d: %sn", ret, buf); 

由于惯常数据的接收,会被OOB打断,因此这里可能需要一个循环,不断接收惯常数据。

下面是方法1的接收输出:

hostname length: 64  get hostname: localhost.localdomain  setup SIGURG for oob data  setown to 31793  got urgent data on signal 23, len 1, c  recv 2: ab  has oob!  recv -1: n/a  recv 2: de  write back 70  recv 2: ab  recv 2: ab  got urgent data on signal 23, len 1, c  has oob!  recv -1: n/a  recv 2: de  write back 70  recv 2: ab  no oob!  got urgent data on signal 23, len 1, c  recv 2: de  write back 70  recv 2: ab  recv 2: ab  got urgent data on signal 23, len 1, c  has oob!  recv -1: n/a  recv 2: de  write back 70  ^C  

 可以看到信号处理器中接收到的总是OOB数据’c’,而普通recv只能读到非OOB数据’a”b”d”e’。而且普通数据的接收,会被OOB数据打断成两块,无法一次性读取。

 

2.使用SO_OOBINLINE标志位将OOB作为惯常数据处理

这种方法是将OOB数据当作惯常数据接收,在接收前通过判断哪些是普通数据哪些是OOB数据,具体步骤如下:

a) 新连接建立时,设置套接字选项SO_OOBINLINE

1 setsockopt (fd, SOL_SOCKET, SO_OOBINLINE, &oil, sizeof (oil));

b) 在接收数据前,先判断下一个字节是否为OOB,如果是,则接收1字节OOB数据(注意不使用MSG_OOB标志)

 1         if (sockatmark (clfd))   2         {   3             printf ("has oob!n");   4             ret = recv (clfd, buf, sizeof(buf), 0);   5             if (ret > 0)   6                 buf[ret] = 0;   7             else   8                 strcpy (buf, "n/a");   9  10             printf ("recv %d: %sn", ret, buf);  11         }  12         else  13             printf ("no oob!n"); 

这里sockatmark当下个字节为OOB时返回1,否则返回0。

c) 如果不是,按惯常数据接收

1         ret = recv (clfd, buf, sizeof(buf), 0);  2         if (ret > 0)  3             buf[ret] = 0;  4         else  5             strcpy (buf, "n/a");  6  7         printf ("recv %d: %sn", ret, buf); 

同理,由于惯常数据会被OOB打断,上述代码总是可以正确的分离OOB与普通数据。

下面是方法2的接收输出:

hostname length: 64  get hostname: localhost.localdomain  setown to 31883  recv 2: ab  no oob!  recv 3: cde  write back 70  recv 2: ab  has oob!  recv 1: c  recv 2: de  write back 70  recv 2: ab  has oob!  recv 1: c  recv 2: de  write back 70  recv 2: ab  no oob!  recv 3: cde  write back 70  recv 2: ab  has oob!  recv 1: c  recv 2: de  write back 70  ^C  

 可以看出,有时候OOB数据不能被正常的识别,会被当作普通数据处理掉。而且这种方式也不能体现OOB紧急的意义,没有给予它优先的处理权。

 

最后,总结一下OOB这个功能。

这么多年来没有遇到OOB的处理,可能本身就说明了大家对它的态度——就是挺鸡肋的一功能,

而且即使真的需要紧急处理了,1字节的限制也导致不能传递什么更多的信息,且本身OOB的处理又有些复杂和局限性,

例如使用信号处理器,如果有多个连接,我怎么知道是哪个连接上的OOB?

如果使用SO_OOBINLINE,OOB被当作普通数据,这里面如果有个结构体被生生插入一个OOB字节,

而且还没有正确识别出来,这里面的对齐问题可要了老命了。

 

 

所以最后的结论是:OOB是过时的,请不要使用它

 

测试程序1

测试程序2