帶外數據
有些傳輸層協議具有帶外(Out Of Band,OOB)數據的概念,用於迅速通告對端本端所發生的重要事件。因此,帶外數據比普通數據(也稱爲帶內數據)有更高的優先級,它應該總是立即被髮送,而不論發送緩衝區中是否有排隊等待發送的普通數據或因流量控制而導致發送端的通告窗口大小爲 0(即停止發送數據) 。帶外數據的傳輸可以使用一條獨立的傳輸層連接,也可以映射到傳輸普通數據的連接中。
UDP 沒有實現帶外數據傳輸,TCP 也沒有真正的帶外數據。只不過 TCP 利用其首部中的 緊急指針標誌 和 緊急指針 兩個字段,給應用程序提供了一種緊急方式。TCP 的緊急方式利用傳輸普通數據的連接來傳輸緊急數據。實際應用中,帶外數據的使用很少見,它一般總是被映射到傳輸普通數據的連接中,例如 telnet,rlogin,ftp 等遠程程序會使用帶外數據。前兩個程序會將中止字符作爲緊急數據 發送到遠程端,這允許遠程端沖洗所有未處理的輸入,並且丟棄所有未發送的終端輸出。ftp 命令使用帶外數據來中斷一個文件的傳輸。注意:緊急數據並不是帶外數據,它只是帶外數據的一種體現。
首先介紹 TCP 發送帶外數據的過程。假設一個進程已經往某個 TCP 連接的發送緩衝區中寫入了 N 字節的普通數據,並等待其發送。在數據被髮送前,該進程又向這個連接寫入了 3 字節的帶外數據 “abc”。此時,待發送的 TCP 報文段的首部將被設置 URG 標誌,並且緊急指針被設置爲指向最後一個帶外數據的下一字節(進一步減去當前 TCP 報文段的序號值得到其首部中的緊急偏移值),如圖
1 所示。
由圖 1 可見,發送端一次發送的多字節的帶外數據中只有最後一字節被當作帶外數據(字母c),而其他數據(字母a和b)被當成了普通數據。如果 TCP 模塊以多個 TCP 報文段來發送圖 1 所示 TCP 發送緩衝區中的內容,則每個 TCP 報文段都將設置 URG 標誌,並且它們的緊急指針指向同一個位置(數據流中帶外數據的下一個位置),但只有一個 TCP 報文段真正攜帶帶外數據。
現在考慮 TCP 接收帶外數據的過程。TCP 接收端只有在接收到緊急指針標誌時才檢查緊急指針,然後根據緊急指針所指的位置確定帶外數據的位置,並將它讀入一個特殊的緩存中。這個緩存只有1字節,稱爲帶外緩存。如果上層應用程序沒有及時將帶外數據從帶外緩存中讀出,則後續的帶外數據(如果有的話)將覆蓋它。
前面討論的帶外數據的接收過程是 TCP 模塊接收帶外數據的默認方式。如果我們給 TCP 連接設置了 SO_OOBINLINE 選項,則帶外數據將和普通數據一樣被 TCP 模塊存放在 TCP 接收緩衝區中。此時應用程序需要像讀取普通數據一樣來讀取帶外數據。這種情況下,使用緊急指針來區分普通數據和帶外數據,緊急指針可以用來指出帶外數據的位置, socket
編程接口也提供了系統調用來識別帶外數據。
帶外標記
在 Linux 系統中,內核檢查到 TCP 緊急標誌時,將通知應用程序帶外數據需要接收。內核通知應用程序帶外數據到達有兩種常見的方式:I/O 複用產生的異常事件 和 SIGURG 信號。但是,即使應用程序得到了有帶外數據需要接收通知,還必須直到帶外數據在數據流中的具體位置,這樣才能準確接收帶外數據。這點可通過系統調用 sockatmark 函數實現。
/* 函數功能:確認套接字是否處於帶外標記;
* 返回值:若處於帶外標記則返回1,若不處於帶外標記則返回0,若出錯則返回-1;
* 函數原型:
*/
#include <sys/socket.h>
int sockatmark(int sockfd);
處理帶外數據
利用 SIGURG 信號處理帶外數據
發送數據(包含普通數據和帶外數據)程序:
#include <string.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdio.h>
#include <netdb.h>
#include <unistd.h>
extern int my_connect(const char *, const char *);
int main(int argc, char **argv)
{
int sockfd;
if(argc != 3)
{
perror("usage: %s <host> <port#>");
exit(1);
}
sockfd = my_connect(argv[1], argv[2]);
/* 發送普通數據 */
write(sockfd, "123", 3);
printf("wrote 3 bytes of normal data\n");
/* sleep函數的作用是讓套接字處於阻塞狀態,
* 使write和send的數據能夠作爲單個TCP分段發送到對端 */
sleep(1);
/* 發送帶外數據 */
send(sockfd, "4", 1, MSG_OOB);
printf("wrote 1 byte of OOB data\n");
sleep(1);
write(sockfd, "56", 2);
printf("wrote 2 bytes of normal data\n");
sleep(1);
send(sockfd, "7", 1, MSG_OOB);
printf("wrote 1 byte of OOB data\n");
sleep(1);
write(sockfd, "89", 2);
printf("wrote 2 bytes of normal data\n");
sleep(1);
exit(0);
}
接收數據(包含普通數據和帶外數據)程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <unistd.h>
#include <sys/socket.h>
#include <signal.h>
#include <fcntl.h>
typedef void Sigfunc(int);
extern int my_listen(const char *, const char *, socklen_t *);
extern void err_quit(const char *, ...);
extern Sigfunc *MySignal(int signo, Sigfunc *func);
extern int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int listenfd, connfd;
void sig_urg(int);
int
main(int argc, char **argv)
{
int n;
char buff[100];
/* 服務器套接字處於監聽狀態 */
if (argc == 2)
listenfd = my_listen(NULL, argv[1], NULL);
else if (argc == 3)
listenfd = my_listen(argv[1], argv[2], NULL);
else
err_quit("usage: tcprecv01 [ <host> ] <port#>");
/* 接受來自客戶端的連接請求 */
connfd = Accept(listenfd, NULL, NULL);
/* 捕獲SIGURG信號,並對該信號進行處理 */
MySignal(SIGURG, sig_urg);
/* 設置已連接套接字的屬主 */
fcntl(connfd, F_SETOWN, getpid());
for ( ; ; ) {
/* 從套接字讀取數據 */
if ( (n = read(connfd, buff, sizeof(buff)-1)) == 0) {
printf("received EOF\n");
exit(0);
}
buff[n] = 0; /* null terminate */
printf("read %d bytes: %s\n", n, buff);
}
}
void
sig_urg(int signo)
{
int n;
char buff[100];
printf("SIGURG = %d received\n", signo);
/* 讀入帶外數據 */
n = recv(connfd, buff, sizeof(buff)-1, MSG_OOB);
buff[n] = 0; /* null terminate */
printf("read %d OOB byte: %s\n", n, buff);
}
運行結果如下所示:
$ ./recv 127.0.0.1 9877 &
[1] 17193
$ ./send 127.0.0.1 9877
wrote 3 bytes of normal data
read 3 bytes: 123
wrote 1 byte of OOB data
SIGURG = 23 received
read 1 OOB byte: 4
wrote 2 bytes of normal data
read 2 bytes: 56
wrote 1 byte of OOB data
SIGURG = 23 received
read 1 OOB byte: 7
wrote 2 bytes of normal data
read 2 bytes: 89
received EOF
[1]+ Done ./recv 127.0.0.1 9877
利用 I/O 複用產生的異常事件處理帶外數據
發送端的程序不變,接收端的程序使用 I/O 複用產生的異常事件通知進程需要讀取帶外數據:
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <unistd.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <sys/select.h>
extern int my_listen(const char *, const char *, socklen_t *);
extern void err_quit(const char *, ...);
extern int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int listenfd, connfd;
int
main(int argc, char **argv)
{
int n;
char buff[100];
/* 爲select函數使用的變量 */
int maxfdp1;
int justreadoob = 0;/* 標誌是否讀過由異常事件通知的帶外數據 */
fd_set rset, xset;
/* 服務器套接字處於監聽狀態 */
if (argc == 2)
listenfd = my_listen(NULL, argv[1], NULL);
else if (argc == 3)
listenfd = my_listen(argv[1], argv[2], NULL);
else
err_quit("usage: tcprecv01 [ <host> ] <port#>");
/* 接受來自客戶端的連接請求 */
connfd = Accept(listenfd, NULL, NULL);
/* 初始化fd_set結構 */
FD_ZERO(&rset);
FD_ZERO(&xset);
for ( ; ; ) {
FD_SET(connfd, &rset);
if(justreadoob == 0)
FD_SET(connfd, &xset);
maxfdp1 = connfd+1;
select(maxfdp1, &rset, NULL, &xset, NULL);
/* 若產生異常事件,則讀取帶外數據 */
if(FD_ISSET(connfd, &xset))
{
n = recv(connfd, buff, sizeof(buff)-1, MSG_OOB);
buff[n] = 0;
printf("read %d OOB bytes: %s\n", n, buff);
/* 防止多次讀取帶外數據 */
justreadoob = 1;
FD_CLR(connfd, &xset);
}
/* 從套接字讀取普通數據 */
if(FD_ISSET(connfd, &rset))
{
if ( (n = read(connfd, buff, sizeof(buff)-1)) == 0) {
printf("received EOF\n");
exit(0);
}
buff[n] = 0; /* null terminate */
printf("read %d bytes: %s\n", n, buff);
justreadoob = 0;
}
}
}
輸出結果:
$ ./recv02 127.0.0.1 9877 &
[1] 17748
$ ./send 127.0.0.1 9877
wrote 3 bytes of normal data
read 3 bytes: 123
wrote 1 byte of OOB data
read 1 OOB bytes: 4
wrote 2 bytes of normal data
read 2 bytes: 56
wrote 1 byte of OOB data
read 1 OOB bytes: 7
wrote 2 bytes of normal data
read 2 bytes: 89
received EOF
[1]+ Done ./recv02 127.0.0.1 9877
利用 sockatmark 讀取帶外標記
帶外標記有以下兩個特性:
- 帶外標記總是指向普通數據最後一個字節緊後的位置。這意味着,如果設置 SO_OOBINLINE 套接字選項使帶外數據在線接收,則若下一個待讀入的字節是使用 MSG_OOB 標誌發送,sockatmark 返回真;若沒有設置在線接收帶外數據,則若下一個待讀入的字節是跟在帶外數據後發送的第一個字節,則 sockatmark 返回真。
- 讀取操作總是停在帶外標記上。也就是說,假設套接字接收緩衝區有 20 個字節,若帶外標記之前只有 5 個字節,而進程執行一個 20 個字節的 read 調用,那麼真正返回的只是帶外標記之前的 5 個字節。
發送端程序:
#include <string.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdio.h>
#include <netdb.h>
#include <unistd.h>
extern int my_connect(const char *, const char *);
int main(int argc, char **argv)
{
int sockfd;
if(argc != 3)
{
perror("usage: %s <host> <port#>");
exit(1);
}
sockfd = my_connect(argv[1], argv[2]);
/* 發送普通數據 */
write(sockfd, "123", 3);
printf("wrote 3 bytes of normal data\n");
/* 發送帶外數據 */
send(sockfd, "4", 1, MSG_OOB);
printf("wrote 1 byte of OOB data\n");
write(sockfd, "56", 2);
printf("wrote 2 bytes of normal data\n");
send(sockfd, "7", 1, MSG_OOB);
printf("wrote 1 byte of OOB data\n");
write(sockfd, "89", 2);
printf("wrote 2 bytes of normal data\n");
exit(0);
}
接收端程序:
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <unistd.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <sys/select.h>
extern int my_listen(const char *, const char *, socklen_t *);
extern void err_quit(const char *, ...);
extern int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int listenfd, connfd;
int
main(int argc, char **argv)
{
int n;
int on = 1;
char buff[100];
/* 服務器套接字處於監聽狀態 */
if (argc == 2)
listenfd = my_listen(NULL, argv[1], NULL);
else if (argc == 3)
listenfd = my_listen(argv[1], argv[2], NULL);
else
err_quit("usage: tcprecv01 [ <host> ] <port#>");
/* 設置SO_OOBINLINE套接字選項,表示希望在線接收帶外數據 */
setsockopt(listenfd, SOL_SOCKET, SO_OOBINLINE, &on, sizeof(on));
/* 接受來自客戶端的連接請求 */
connfd = Accept(listenfd, NULL, NULL);
sleep(5);/* sleep以接收來自發送進程的所有數據 */
for ( ; ; ) {
/* 檢測套接字接收緩衝區是否處於帶外標記 */
if(sockatmark(connfd))
printf("at OOB mark\n");
/* 讀取數據,並顯示這些數據 */
if ( (n = read(connfd, buff, sizeof(buff)-1)) == 0) {
printf("received EOF\n");
exit(0);
}
buff[n] = 0; /* null terminate */
printf("read %d bytes: %s\n", n, buff);
}
}
輸出結果:從結果可以知道,第一個帶外標記並沒有並輸出,這驗證了一個現象:一個給定 TCP 連接只有一個帶外標記,若前面的帶外標記不被進程及時接收,則會被後來新到達的帶外標記覆蓋。
$ ./recv03 127.0.0.1 9877 &
[1] 19344
$ ./send 127.0.0.1 9877
wrote 3 bytes of normal data
wrote 1 byte of OOB data
wrote 2 bytes of normal data
wrote 1 byte of OOB data
wrote 2 bytes of normal data
read 6 bytes: 123456
at OOB mark
read 3 bytes: 789
received EOF
[1]+ Done ./recv03 127.0.0.1 9877
總結
TCP 沒有真正的帶外數據,不過提供緊急模式和緊急指針,一旦發送端進入緊急模式,緊急指針就出現在發送端的報文段的 TCP 首部中。連接的對端接收取該指針是在告知接收進程 發送端已經進入緊急模式,而且該指針指向緊急數據的最後一個字節。套接字 API 把 TCP 的緊急模式映射成帶外數據。發送端進程通過指定 MSG_OOB 標誌調用 send 讓發送端進入緊急模式。該調用中的最後一個數據字節被認定爲帶外字節。接收端 TCP 收到新的緊急指針後,或者 通過發送 SIGURG 信號,或者通過由 select 返回套接字有異常事件待處理,讓接收端進程得以通知。
帶外數據概念實際上向接收端傳達三個不同的消息:
- 發送端進入緊急模式這個事實。接收端得以通知這個事實的方式有兩個:I/O 複用調用 select 函數產生的異常事件 和 SIGURG 信號。本通知在發送進程發送帶外字節後由發送端 TCP 立即發送,即使接收端的任何數據發送因流量控制而停止,TCP 仍然發送本通知。
- 帶外字節的位置。也就是它相對於來自發送端的其餘數據的發送位置:帶外標記。
- 帶外字節的實際值。
對於TCP的緊急模式,我們可以認爲 URG 標誌時通知(信息1),緊急指針是帶外標記(信息2),數據字節是其本身(信息3)。
與這個帶外數據概念相關的問題有:
- 每個連接只有一個TCP緊急指針;
- 每個連接只有一個帶外標記;
- 每個連接只有一個單字節的帶外緩衝區(該緩衝區只有在數據非在線讀入時才需考慮)。如果帶外數據時在線讀入的,那麼當新的帶外數據到達時,先前的帶外字節並未丟失,不過他們的標記卻因此被新的標記取代而丟失了。
總之,帶外數據是否有用取決於應用程序使用它的目的。如果目的是告知對端丟棄直到標記處得普通數據,那麼丟失一箇中間帶外字節及其相應的標記不會有什麼不良後果。但是如果不丟失帶外字節本身很重要,那麼必須在線收到這些數據。另外,作爲帶外數據發送的數據字節應該區別於普通數據,因爲當前新的標記到達時,中間的標記將被覆寫,從而事實上把帶外字節混雜在普通數據之中。
參考資料:
《Unix 網絡編程》