zoukankan      html  css  js  c++  java
  • [apue] 如何处理 tcp 紧急数据(OOB)?

    在上大学的时候,我们可能就听说了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 %d
    ", 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 %d
    ", 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 %d
    ", 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, %s
    ", 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: %s
    ", 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!
    "); 
     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: %s
    ", ret, buf); 
    11         }
    12         else 
    13             printf ("no oob!
    "); 

    这里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: %s
    ", 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紧急的意义,没有给予它优先的处理权。

    3.使用 select/epoll 多路事件分离

    这种方法是利用select或epoll,将OOB数据作为exception事件与普通数据的read事件相分离,这里以select为例:

    a) 建立 select 事件处理循环

    1     for (;;) { 
    2         // must set it in every loop.
    3         memcpy (&rdds, &cltds, sizeof (cltds)); 
    4         memcpy (&exds, &cltds, sizeof (cltds)); 
    5         FD_SET(sockfd, &rdds); 
    6         ret = select (FD_SIZE+1, &rdds, NULL, &exds, NULL); 
    7         ……
    8     }

    b) 建立连接时,将连接fd加入待监听fd_set

     1             if (FD_ISSET(clfd, &rdds))
     2             {
     3                if (clfd == sockfd)
     4                {
     5                    // the acceptor
     6                     printf ("poll accept in
    "); 
     7                     clfd = accept (sockfd, NULL, NULL); 
     8                     if (clfd < 0) { 
     9                         printf ("accept error: %d, %s
    ", errno, strerror (errno)); 
    10                         exit (1); 
    11                     }
    12 
    13                     print_sockopt (clfd, "new accepted client"); 
    14                     // remember it
    15                     FD_SET(clfd, &cltds); 
    16                     printf ("add %d to client set
    ", clfd); 
    17                } 
    18                else 
    19                {
    20                     ……
    21                }
    22             }            

    c) 连接上有数据到达时,如果是read事件,使用recv接收数据

     1             if (FD_ISSET(clfd, &rdds))
     2             {
     3                if (clfd == sockfd)
     4                {
     5                    ……
     6                } 
     7                else 
     8                {
     9                    // the normal client
    10                    printf ("poll read in
    "); 
    11                    ret = recv (clfd, buf, sizeof(buf), 0); 
    12                    if (ret > 0)
    13                        buf[ret] = 0; 
    14                    else 
    15                        sprintf (buf, "errno %d", errno); 
    16 
    17                    printf ("recv %d from %d: %s
    ", ret, clfd, buf); 
    18                    if (ret <= 0) {
    19                        FD_CLR(clfd, &cltds); 
    20                        printf ("remove %d from client set
    ", clfd); 
    21                    }
    22                }
    23             }

    d) 如果是exception事件,使用recv(..,MSG_OOB)接收带外数据

     1             if (FD_ISSET(clfd, &exds))
     2             {
     3                 // the oob from normal client
     4                 printf ("poll exception in
    "); 
     5                 if (sockatmark (clfd))
     6                 {
     7                     printf ("has oob!
    "); 
     8                     ret = recv (clfd, buf, 1, MSG_OOB); 
     9                     if (ret > 0)
    10                         buf[ret] = 0; 
    11                     else 
    12                        sprintf (buf, "errno %d", errno); 
    13 
    14                     printf ("recv %d from %d on urgent: %s
    ", ret, clfd, buf); 
    15                     if (ret > 0) {
    16                         // let clfd cleared in sig_cld
    17                        do_uptime (clfd); 
    18                     }
    19                     else 
    20                     {
    21                         FD_CLR(clfd, &cltds); 
    22                         printf ("remove %d from client set
    ", clfd); 
    23                     }
    24                 }
    25                 else 
    26                     printf ("no oob!
    "); 
    27             }

    此时,仍可使用sockatmark来判断是否为OOB数据,另外,如果在连接建立时设定了OOB_INLINE标志位,则此处应使用不带MSG_OOB的recv接收数据,

    因为OOB数据已经被当作惯常数据来处理了,此处与方法2是一致的。

    下面是方法3的输出:

    setup handler for SIGCHLD ok
    hostname length: 64
    get hostname: localhost.localdomain
    got event 1
    poll accept in
    add 4 to client set
    got event 2
    poll read in
    recv 2 from 4: ab
    poll exception in
    has oob!
    recv 1 from 4 on urgent: c
    start worker process 4511
    goto serve next client..
    got event 1
    poll read in
    recv 2 from 4: de
    got event 1
    poll accept in
    add 5 to client set
    got event 2
    poll read in
    recv 2 from 5: ab
    poll exception in
    has oob!
    recv 1 from 5 on urgent: c
    start worker process 4513
    goto serve next client..
    got event 1
    poll read in
    recv 2 from 5: de
    got event 1
    poll accept in
    add 6 to client set
    got event 2
    poll read in
    recv 2 from 6: ab
    poll exception in
    has oob!
    recv 1 from 6 on urgent: c
    start worker process 4516
    goto serve next client..
    got event 1
    poll read in
    recv 2 from 6: de
    SIGCHLD received
    wait child 4511 return 0
    find clfd 4 for that pid
    remove 4 from client set
    interrupted by signal, some child process done ?
    SIGCHLD received
    wait child 4513 return 0
    find clfd 5 for that pid
    remove 5 from client set
    interrupted by signal, some child process done ?
    SIGCHLD received
    wait child 4516 return 0
    find clfd 6 for that pid
    remove 6 from client set
    interrupted by signal, some child process done ?
    ^C
    

    需要注意的是,在某些场景下,OOB会被识别为惯常数据,此时exception事件在处理时将得不到OOB数据,不过这有一定的随机性,不是每次都能复现。

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

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

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

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

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

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

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

    测试程序1

    测试程序2

    测试程序3

  • 相关阅读:
    什么是 Spring Boot?
    Spring Cloud Gateway?
    什么是 Netflix Feign?它的优点是什么?
    SpringBoot和SpringCloud的区别?
    什么是Hystrix?
    什么是Spring Cloud Bus?
    什么是Ribbon?
    eureka自我保护机制是什么?
    spring cloud 和dubbo区别?
    如何在 Spring Boot 中禁用 Actuator 端点安全性?
  • 原文地址:https://www.cnblogs.com/goodcitizen/p/11793274.html
Copyright © 2011-2022 走看看