[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是過時的,請不要使用它