[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\n", 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\n", 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\n", 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\n", 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\n", 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: %s\n", 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: %s\n", 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\n"); 
 7                     clfd = accept (sockfd, NULL, NULL); 
 8                     if (clfd < 0) { 
 9                         printf ("accept error: %d, %s\n", 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\n", 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\n"); 
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\n", ret, clfd, buf); 
18                    if (ret <= 0) {
19                        FD_CLR(clfd, &cltds); 
20                        printf ("remove %d from client set\n", 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\n"); 
 5                 if (sockatmark (clfd))
 6                 {
 7                     printf ("has oob!\n"); 
 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\n", 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\n", clfd); 
23                     }
24                 }
25                 else 
26                     printf ("no oob!\n"); 
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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM