天天看點

關于TCP帶外資料(OOB,Out Of Bound)

 定義帶 外 資料 

想 像一下在銀行人們排起隊待等處置他們的帳單。在這個步隊中每個人最後會都移到面前由出納員行進服務。在現想像一下一個走入銀行,超出個整步隊,然後用槍抵 住出納員。這個就可以看作為帶 外 資料 。這個盜強超出個整步隊,是因為這把槍給了他淩駕于世人的權利。出納員也集會中注意力于這個盜強身上,因為他曉得後以 的勢形是很緊迫的。

應相的,一個連接配接的流式套口接上的帶 外 資料 的作工理原也與此相似。常通情況下,資料 由連接配接的一端流到另一端,并且為認 資料 的全部節字都是确精排序的。晚寫入的節字絕不會早于先寫入的節字到達。然而套口接API念概性的供給了一些實用程式,進而可以使得一串資料 無阻的先于 常通的資料 到達收接端。這就是所謂的發送帶 外 資料 。

從技術上來講,一個TCP 流不可以發送帶 外 資料 。而他所持支的隻是一個念概性的緊迫資料 ,這些緊迫資料作為帶 外 資料 映射到套口接API。 這就帶 來了多許制限,這些我們會在面前行進探讨。

盡管我們可以立刻享受到在銀行中超出個整步隊的好處,但是我們也會認識到用使槍來到達這樣的的目是反社會的為行。一個TCP 流常通希望以完善的隊列來發送資料 節字,那麼亂序的發送資料 就仿佛與流的念概相違背。那麼為什麼要供給帶 外 資料 的套口接方法呢?

也 許我們已意識到了,偶然資料 會以定一的式方變得緊迫。一個流套口接會有一個量大的資料 隊列待等發送到網絡。在程遠端點,也會有量大已收接的,卻還沒有被 程式取讀的資料 。如果發送用戶端程式由于一些原因須要消取已寫入伺服器的請求,那麼他就須要向伺服器緊迫發送一個識标消取的請求。如果向程遠伺服器發送 消取請求失敗,那麼就會無謂的費浪伺服器的資源。 

使 用帶 外 資料 的際實程式例子就是telnet,rlogin,ftp指令。前兩個程式會将中斷字元作為緊迫資料 發送到程遠端。這會答應程遠端沖刷全部未處置 的入輸,并且棄丢全部未發送的終端出輸。這會倏地中斷一個向我們螢幕發送量大資料 的行運程序。ftp指令用使帶 外 資料 來中斷一個件文的輸傳。

套口接與帶 外 資料 

新重調強套口接口接本身并非制限因素是很主要的。帶 外 資料 的念概際實上映射到 TCP /IP信通的緊迫資料模式。在明天,TCP 流對于網絡是很主要的,而在這一章我們僅專一于帶 外 資料 适應于TCP 緊迫資料 的套口接用使。

實作上的化變 

很可憐,TCP 的實在現緊迫資料 就如何處置上有兩種不同的解釋。這些别區我們将會本章的面前行進具體的探讨。這些不同的解釋是:

TCP 緊迫指針的RFC793解釋

TCP 緊迫指針的BSD解釋

現 在已湧現了中分的狀态,因為原始的TCP 規格答應兩種解釋。進而,一個"主機須要"的RFC識标準确的解釋。然而,大多數的實作都基于BSD源碼,而在 明天BSD方法還是一個通用的用法。從持支兩種解釋的角度而言,Linux處于決裂的狀态。然而,Linux認默用使BSD解釋。 

在現我們稍做頓停,來檢測一個我們Linux系統的後以設定。這決了我們這一章的例子是不是可以發生一樣的結果。

$ cat /proc/sys/net/ipv4/tcp_stdurg

$

這裡示顯的出輸為0。這示表後以起作的為BSD解釋。如果我們失掉其他的出輸結果(例如1),那麼如果我們希望失掉也本章的例子同相的結果,我們應将其為改0。

上面列出了tcp_stdurg設定可能的取值。tcp_stdurg值可以在Shell本腳中行進詢查和設定,包含啟動與關閉本腳。

/proc/sys/net/ipv4_stdurg的設定值:

0   BSD解釋(Linux認默)

1   RFC793解釋

如果我們須要将其設定為改0,我們須要root權限,然後入輸上面的指令:

# echo 0 >/proc/sys/net/ipv4/tcp_stdurg

#

行進重雙檢測老是很明知的,是以在變改後以再列出其值來定确變改是不是為核内所受接。我們也可以在上面的例子中用使cat指令來示顯0值。

編寫帶 外 資料 

一個write調用将會寫入一個我們已習氣的帶 内資料 。應相的,必須用使一個新的函數來寫入帶 外 資料 。為了這個的目,在這裡列出send函數地型原:

#include <sys/types.h>

#include <sys/socket.h>

int send(int s, const void *msg, int len, unsigned int flags);

這個函數須要四個參數,分别為:

1 要寫入的套口接s

2 存放要寫入的消息的緩沖位址msg

3 消息長度(len)

4 發送選項flags

send函數與write函數相相似,所不同的隻是供給了額外的flags參數。這是際實的部分。send函數傳回寫入的節字數,如果發生錯誤則會傳回-1,檢測errno可以失掉錯誤原因。

要發送帶 外 資料 ,與write調用相似,用使前三個參數。如果我們為flags參數指定了C語言宏MSG_OOB,則資料 是作為帶 外 資料 發送的,而不是常通的帶 内資料 ,如上面的例子代碼:

char buf[64];

int len;     

int s;       

. . .

send(s,buf,len,MSG_OOB); 

如果所供給的flags參數沒有MSG_OOB位,那麼資料 是作為常通資料 寫入的。這就答應我們用使同一個函數同時發送帶 内資料 與帶 外 資料 。我們隻須要簡單的在程式控制中變改flags參數值來到達這個的目。

取讀帶 外 資料 

帶 外 資料 可以用兩種不同的方法行進取讀:

單獨取讀帶 外 資料 

與帶 内資料 混合取讀 

為了與常通資料 流分開單獨取讀帶 外 資料 ,我們須要用使recv函數。如果我們猜想recv函數與read函數相似,隻是有一個額外的flags參數,那麼我們的猜想是準确的。這個函數的型原如下:

#include <sys/types.h>

#include <sys/socket.h>

int recv(int s, void *buf, int len, unsigned int flags);

recv函數受接四參數,分别為:

1 要從中收接資料 的套口接s(帶 内資料 或帶 外 資料 )

2 要放置所收接的資料 的緩沖區位址buf

3 收接緩沖區的最大長度

4 調用所需的flags參數

正如我們所看到的,recv函數是與send函數調用相對應的函數。為要收接帶 外 資料 ,在flags參數中指定C宏MSG_OOB。沒有MSG_OOB标志位,recv函數所收接的為常通的帶 内資料 ,就像常通的read調用一樣。

recv函數傳回所收接到的節字數,如果出錯則傳回-1,檢測errno可以失掉錯誤原因。

上面的代碼例子示範了如何取讀帶 外 資料 :

char buf[128];  

int n;     

int s;            

int len;        

. . .

n = recv(s,buf,len,MSG_OOB);

盡管指出帶 外 資料 可以與常通資料 相混合還為時尚早,但是我們會在面前行進相關的探讨。

了解SIGURG 信号 

當帶 外 數所到在時,收接程序須要收到通知。如果須要與常通資料 流分開取讀時更是如此。這樣做的一個方法就是當帶 外 資料 到達時,使Linux核内向我們的程序發送一個SIGURG 信号。

用使SIGURG 信号通知須要兩個先決條件:

我們必須擁有套口接

我們必須為SIGURG 建立一個信号處置器 

要收接SIGURG 信号,我們的程序必須為套口接的全部者。要建立這樣的擁有關系,我們可以用使fcntl函數。其函數型原如下: 

#include <unistd.h>

#include <fcntl.h>

int fcntl(int fd, int cmd, long arg);

函數參數如下:

1 要在其上執行控制函數的件文描述符fd(或是套口接)

2 要執行的控制函數cmd

3 要設定的值arg

函數的傳回值依賴于fcntl所執行的控制函數。對于課外閱讀感興趣的讀者,fcntl的Linux man手冊頁具體的描述了cmd的F_SETOWN操作。

要将我們的程序建立為套口接的全部者,收接程式須要用使上面的代碼:

int z;

int s;

z = fcntl(s,F_SETOWN,getpid());

if ( z == -1 ) {

    perror("fcntl(2)");

    exit(1);

}

F_SETOWN操作會使得fcntl函數成功時傳回0,失敗時傳回-1。

另外一個先決條件是程式必須準備好收接SIGURG 信号,這是通過為信号建立一個信号處置器來做到的。我們很快就會看到這樣的一個例子。

收接SIGURG 信号 

移開了這些煩瑣的作工後以,在現我們可以來探索有趣的帶 外 資料 的念概了。上面所列的程式代碼就是我們用來收接資料 和當帶 外 資料 到達時處置帶 外 資料 的程式。他設計用使BSD解釋來處置帶 外 資料 ,而這也正是Linux的認默情況。

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <signal.h> #include <fcntl.h> #include <sys/types.h> #include <sys/socket.h> extern void bail(char *on_what); extern int BindAccept(char *addr); static int s = -1; /* Socket */ /* * SIGURG signal handler: */ static void sigurg (int signo) { int n; char buf[256]; n = recv(s,buf,sizeof buf,MSG_OOB); if(n<0) bail("recv(2)"); buf[n] = 0; printf("URG ''%s'' (%d) /n",buf,n); signal(SIGURG ,sigurg ); } int main(int argc,char **argv) { int z; /* Status */ char buf[256]; /* * Use a server address from the command * line,if one has been provided. * Otherwise,this program will default * to using the arbitrary address * 127.0.0.1: */ s = BindAccept(argc >=2 ?argv[1] :"127.0.0.1:9011"); /* * Establish owership: */ z = fcntl(s,F_SETOWN,getpid()); if(z==-1) bail("fcntl(2)"); /* * Catch SIGURG : */ signal(SIGURG ,sigurg ); for(;;) { z = recv(s,buf,sizeof buf,0); if(z==-1) bail("recv(2)"); if(z==0) break; buf[z] = 0; printf("recv ''%s'' (%d) /n",buf,z); } close(s); return 0; }           

    然而,在我們将收接程式投入用使之前,我們還須要一個發送程式。

    發送帶 外 資料 

    上面列出的程式示範了一個簡短的發送程式,他隻可以輸傳一些小的字元串,然後停止發送

    帶

    外

    資料

     。這個程式為了在收接端管理傳送塊用使了多許的sleep(3)調用。

/* * oobsend.c * * Example OOB sender: */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> extern void bail(char *on_what); extern int Connect(char *addr); /* * Send in-band data: */ static void iband(int s,char *str) { int z; z = send(s,str,strlen(str),0); if(z==-1) bail("send(2)"); printf("ib: ''%s'' (%d) /n",str,z); } /* * Send out-of-band data: */ static void oband(int s,char *str) { int z; z = send(s,str,strlen(str),MSG_OOB); if(z==-1) bail("send(2)"); printf("OOB ''%s'' (%d)/n",str,z); } int main(int argc,char **argv) { int s = -1; s = Connect(argc >=2 ? argv[1] : "127.0.0.1:9011"); iband(s,"In the beginning"); sleep(1); iband(s,"Linus begat Linux,"); sleep(1); iband(s,"and the Penguins"); sleep(1); oband(s,"rejoiced"); sleep(1); iband(s,"exceedingly."); close(s); return 0; }           

    每日一道理 

共和國迎來了她五十誕辰。五十年像一條長河,有急流也有緩流;五十年像一幅長卷,有冷色也有暖色;五十年像一首樂曲,有低音也有高音;五十年像一部史詩,有痛苦也有歡樂。長河永遠奔流,畫卷剛剛展開,樂曲漸趨高潮,史詩還在續寫。我們的共和國正邁着堅定的步伐,跨入新時代。

    編譯程式:

$ make oobrecv oobsend

gcc -c -D_GNU_SOURCE -Wall -Wreturn-type -g oobrecv.c

gcc -c -D_GNU_SOURCE -Wall -Wreturn-type -g mkaddr.c

gcc -c -D_GNU_SOURCE -Wall -Wreturn-type -g bindacpt.c

gcc oobrecv.o mkaddr.o bindacpt.o -o oobrecv

gcc -c -D_GNU_SOURCE -Wall -Wreturn-type -g oobsend.c

gcc oobsend.o mkaddr.o bindacpt.o -o oobsend

$

在編譯完成後以,我們失掉兩個可執行程式:

oobrecv 是收接程式(一個伺服器)

oobsend 是發送程式(一個用戶端)

在現我們已準備好來調用這兩個程式了。

測試oobrecv與oobsend程式

最好是在兩個不同的終端會話上行運這兩個程式。用使兩個不同的xterm視窗,或是兩個不同的終端會話。首先在第一個終端會話中啟動收接程式:

$ ./oobrecv

如果我們希望指定我們的以太網位址而不是用使認默的回環位址,那麼這兩個程式都收接一個位址與端口号對。例如,上面的将會作工在一個NIC卡位址為192.168.0.1的系統上:

$ ./oobrecv 192.168.0.1:9023

這會啟動伺服器在192.168.0.1的9023端口上監聽。然而,為了示範,我們可以不指定參數來行運這個程式。

在現在第二個終端會話中啟動發送程式:

$ ./oobsend

ib: ''In the beginning'' (16)

ib: ''Linus begat Linux,'' (18)

ib: ''and the Penguins'' (16)

OOB ''rejoiced'' (8)

ib: ''exceedingly.'' (12)

$

以ib:開始的行表明寫入的帶 内資料 。以OOB開始的行表明''rejoiced''是作為帶 外 資料 寫入套口接的。

如果我們可以同時觀察兩個終端,我們就會發現收接程式報告資料 稍晚于發送程式發送資料 。其會話出輸相似于上面的樣子:

$ ./oobrecv

rcv ''In the beginning'' (16)

rcv ''Linus begat Linux,'' (18)

rcv ''and the Penguins'' (16)

URG ''d'' (1)

rcv ''rejoice'' (7)

rcv ''exceedingly.'' (12)

$

在這個終端會話中示顯的以rcv開始的行表明收接到的常通的帶 内資料 。以URG開始的行表明收接到SIGURG 信号,并且信号處置程式被調用。在信号處置器中,緊迫資料 被取讀并報告。我們應注意到一個很奇怪的事情--隻有d節字被作為帶 外 資料 收接。為什麼是這樣? 

了解緊迫指針 

在這一章面前,套口接口接供給了一個常通的網絡口接。這就包含他如何處置帶 外 資料 。然而,緊迫資料的TCP 實作卻達不到帶 外 資料 所包含的常通念概。 盡管個整字元串''rejoiced''用使send作為帶 外 資料 發送,但是在收接端我們可以觀察到下列内容:

隻有d字元作為帶 外 資料 被收接

d字元在其餘的''rejoice''之前到達

d字元在''rejoice''之前被收接的事實确實示範了d字元更為緊迫的事實。他表明節字順序已被一個緊迫元素所打亂。

了解TCP 緊迫模式 

隻有一個節字被作為帶 外 資料 被收接的事實與一個TCP 協定念概到一個套口接念概的映射有關。TCP 緊迫模式被映射到更為常通的帶 外 資料 的套口接念概。 

TCP 協定本身并不供給帶 外 資料 程式。最接近于這個套接式方的念概就是信通的TCP 緊迫模式。為了使得我們了解緊迫模式是如何作工,在這裡有必要行進一些TCP 協定的探讨。

當設定了MSG_OOB位用使send套口接口接函數時,資料 寫入了TCP 的外行隊列,并且建立了一個緊迫指針。這個指針的确切位置是由我們在面前所說的tcp_stdurg來決定的。 下表列出回顧了我們面前所說的兩種解釋,并且表明了緊迫指針的位置:

值       解釋       緊迫指針

0       BSD解釋       緊迫節字之後

1       RFC793解釋   緊迫節字之前

下圖示顯了send調用在将字元串''rejoiced''排列為帶 外 資料 傳回之後,TCP 發送緩沖區的可視化情況。盡管我們并不對BSD解釋感興趣,但是在這個圖中同時示顯了兩種解釋的情況。

對于BSD解釋,用使MSG_OOB标志調用send所發生的事件隊列為:

1 資料 放入TCP 的外行隊列(在這種情況為空TCP 緩沖區的開始處)

2 開啟TCP 緊迫模式(一個TCP URG位設定為真)

3 計算緊迫指針,指向入輸外行TCP 隊列的最後一個節字之後。

在例子程式oobsend.c中,send調用之後跟随着了一個sleep調用。這個動作會使得Linux核内執行下列操作:

1 發送目前為止在TCP 緩沖區中已排隊的資料 ,而不是待等更多的資料 。

2 在現由TCP 協定所建立的資料 標頭設定了URG位。這就表明用使TCP 緊迫模式(這是因為設定了MSG_OOB位來調用send函數)

3 計算一個TCP 緊迫指針并且放在資料 標頭中。在這個例子中(tcp_stdurg=0),這個指針指向已排隊的帶 外 資料 的最後一個節字之後。

4 包含URG位,緊迫指針以及全部待等發送的資料 包的資料 標頭在現作為一個實體資料 包發送到網絡口接裝置。

執行完這些步驟之後,資料 包立刻加帶 傳遞到網絡的收接主機。這個資料 在程遠端被收接,念概上如下圖所示。

當一個URG位被設定為真的資料 包被收接到時,Linux核内會用使信号SIGURG 通知擁有這個套接品的程序。之是以這樣做,是因為資料 包包含一個緊迫指針(這也就是為什麼要在TCP 頭設定URG位的原因)。

程 序oobrecv.c,一旦處置SIGURG 信号,就會設定MSG_OOB标志,通過recv調用來取讀帶 外 資料 。這會使得Linux核内隻傳回帶 外 數 據 。因為TCP 并不會記錄帶 外 資料 的起始位置,套口接API隻會傳回資料 包内緊迫指針之前的一個節字(假設tcp_stdurg=0)。 應相的,在我們的 例子中,隻有節字d作為帶 外 資料 傳回。任何帶 内資料 的取讀隊列會取讀其餘的''rejoice''節字,以及緊迫節字之後的資料 (如果存在)。

盡管帶 外 資料 并非在信号處置函數中取讀,隻會取讀''rejoice''節字以及非緊迫資料 序列。d節字會被阻止在常通的帶 内資料 中傳回,是因為他已被識标為帶 外 資料 。 

tcp_stdurg=1的緊迫模式

空 間并不答應我們具體探讨這種情況,但是一些小的評論還是值得的。當tcp_stdurg=1時,常通會發生一件奇怪的事情,常通會進入緊迫模式,而其應相 的緊迫指針也會被收接,但是卻并不會取讀應相的緊迫資料 。如果緊迫指針正如位于資料 包中最後一個資料 節字之後,那麼也許在其後就會再收接到任何資料 節字。 緊迫資料也 許會在其後的一個資料 包中。正是因為這個原因,當用使這種模式時,當收到SIGURG 信号時,設定了MSG_OOB位的recv調用并不須要必須為TCP 傳回一個帶 外 資料 。

要處置緊迫資料 不可得的情況,我們必須執行上面的操作(記住,這适用于tcp_stdurg=1的情況):

1 在一個标記中記錄SIGURG 事件(也就是一個名為urg_mode=1的變量)。

2 由我們的信号處置器中傳回。

3 繼續取讀我們程式中的帶 内資料 。

4 當urg_mode的值為真時,試着用使設定了MSG_OOB标記位的recv函數來取讀帶 外 資料 。

5 如果步驟4失掉資料 ,那麼設定urg_mode=0,并且傳回常通的處置。重複步驟3。

6 如果步驟4沒有失掉任何帶 外 資料 ,将urg_mode設定為真繼續處置。重複步驟3。

再一次,必須調強我們也許不會為Linux代碼執行這些步驟,除非Linux變改了方向。Linux認默用使BSD(tcp_stdurg=0)緊迫資料 模式,而這是較為容易處置的。 

收接内聯帶 外 資料 

在面前,我們已談到,也可以在常通的帶 内資料 混合收接帶 外 資料 。偶然用這樣的式方來處置會更為友善。要為一個特定的套口接打開這種操作模式,我們必須設定SO_OOBINLINE套口接選項:

例如

int z;                  

int s;                  

int oobinline =1;     

z = setsockopt(s,

    SOL_SOCKET,       

    SO_OOBINLINE,     

    &oobinline,      

    sizeof oobinline);

警告

如果我們為一個套口接打開了這個選項,我們就不可以用使設定了MSG_OOB标記位的recv函數。如果我們這樣做了,我們就會傳回一個錯誤,而變量errno會被設定為EINVAL。

注意

如果我們覺得有用,也可以用使SIGURG 信号。這是通過一個用使F_SETOWN的fcntl函數來建立了。

定确緊迫指針 

無論我們是不是正在用使内聯資料 的式方行進收接,當我們收接到後以資料 流中的數指針時,我們都可以自由的用使一個函數來通知我們。這可以通過準确的參數來調用ioctl(2)來定确。

例如

#include <sys/ioctl.h>

. . .

int z;    

int s;    

int flag;

z = ioctl(s, SIOCATMARK,&flag);

if ( z == -1 )

     abort();        

if ( flag != 0 )

     puts("At Mark");

else

     puts("Not at mark.");

在現我們已了解了面前所介紹地功能,上面我們将用使一個修改的oobrecv程式來示範收接内聯資料 ,并且在收接到資料 時測試緊迫資料 标記。

用使内聯帶 外 資料 

上面示範的是一個新版本的oobinline.c程式,他會同時收接帶 内資料 與帶 外 資料 。同時他包含一個經過修改的SIGURG 信号處置器,這樣他就會在緊迫資料 到達時報告。這就會答應我們觀察多許事件。

/* * oobinline.c * * OOB inline receiver: */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <signal.h> #include <fcntl.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/socket.h> extern void bail(char *on_what); extern int BindAccept(char *addr); /* * SIGURG signal handler: */ static void sigurg (int signo) { write(1,"[SIGURG ]/n",9); signal(SIGURG ,sigurg ); } /* * Emulate the IEEE Std 1003.1g * standard function sockatmark(3): */ static int Sockatmark(int s) { int z; int flag; z = ioctl(s,SIOCATMARK,&flag); if( z == -1 ) return -1; return flag ? 1 : 0; } int main(int argc,char **argv) { int z; /* Status */ int s; /* Socket */ int oobinline= 1; /* oob inline */ char buf[256]; /* * use a server address from the command * line,if one has been provided. * otherwise,this program will default * to using the arbitrary address * 127.0.0.1; */ s = BindAccept(argc >= 2 ? argv[1] : "127.0.0.1:9011"); /* * Establish ownership: */ z = fcntl(s,F_SETOWN,getpid()); if(z==-1) bail("fcntl(2)"); /* * Catch SIGURG : */ signal(SIGURG ,sigurg ); /* * Receive the OOB data inline: */ z = setsockopt(s, SOL_SOCKET, SO_OOBINLINE, &oobinline, sizeof oobinline); if(z==-1) bail("setsockopt(2)"); for(;;) { printf("/n[%s]/n", Sockatmark(s) ? "AT MARK" : " NO MARK"); z = recv(s,buf,sizeof buf,0); if(z==-1) bail("recv(2)"); if(z==0) break; buf[z]=0; printf("rcv ''%s''(%d)/n", buf,z); } close(s); return 0; }            

在現編譯這個程式:

                                         M

$ make oobinline

gcc -c -D_GNU_SOURCE -Wall -Wreturn-type -g oobinline.c

                                          FL

gcc oobinline.o mkaddr.o bindacpt.o -o oobinline

$

執行下列步驟來行進測試:

1 在第一個終端會話中,啟動oobinline程式。

2 在第二個終端會話中,啟動我們面前所用的oobsend程式。

發送程式的終端會話出輸如下所示:

$ ./oobsend

ib: ''In the beginning'' (16)

ib: ''Linus begat Linux,'' (18)

ib: ''and the Penguins'' (16)

OOB ''rejoiced'' (8)

ib: ''exceedingly.'' (12)

$

這個終端會話與面前的例子同相。然而,收接終端會話的出輸如下所示:

$ ./oobinline

[No Mark]

rev In the beginning (16)

[No Mark]

rev ''Linus begat Linux, (18)

[No Mark]

rev ''and the Penguins'' (16)

[No Mark]

[SIGURG ]

rev ''rejoice'' (7)

[AT MARK]

rev ''d'' (1)

[No Mark]

rev ''exceedingly.'' (12)

[No Mark]

$

注意,當收接字元串''rejoiced''時,與面前的例子相似也會啟動SIGURG 信号。然而注意,标記在直到先取讀''rejoice''節字才到達。然後到達标記,并且收接到額外的内聯節字(d)。在這裡須要注意幾點:

與沒有用使内聯緊迫資料 取讀時一樣,SIGURG 信号要盡早到達。

帶 内資料 必須在取讀帶 外 資料 之前順序取讀。

盡管所傳送的資料 包作為一個整體包含個整''rejoiced''字元串,而recv函數會在緊迫資料 節字所處的位置停止(收接在d節字處停止)。

接下來須要調用recv函數取讀緊迫資料 。對于TCP ,在這個例子中隻是一個單一節字。

常通,資料 由一個流式套口接中取讀,而不必指定資訊邊界。然而,我們會發現,當緊迫資料 由内聯取讀時,确實形成了一個邊界。取讀會在緊迫資料 處停止。如果不是這樣,我們就會很容易讀過标記。

緊迫指針的制限 

到在現為止,我們已示範了TCP 确實隻供給了一個帶 外 資料 節字。這是因為他是用使協定的TCP 緊迫模式特性來實作的。

我們很容易會為認TCP 緊迫模式及其緊迫指針應使得他可以标記緊迫資料 的邊界。然而,際實上并非這樣的,因為緊接着的帶 外 資料 的發送會覆寫收接者原始的緊迫資料 标記,而這個标記也許還沒有行進處置。 

如果我們修改oobsend.c程式就可以行進示範。移除全部的sleep(3)函數調用,在oband(s,"rejoiced")之後插入一個oband(s,"very")調用。主程式看起來如下所示:

int

main(int argc,char **argv) {

    int s = -1;     

    s = Connect(argc >= 2

        ? argv[1]

        : "127.0.0.1:9011");

    iband(s,"In the beginning");

    iband(s,"Linus begat Linux,");

    iband(s,"and the Penguins");

    oband(s,"rejoiced");

    oband(s,"very");

    iband(s,"exceedingly.");

    close(s);

    return 0;

}

當再一次行運這個測試時,在一個倏地的系統上,我們會收到如下的結果:

$ ./oobinline

[No Mark]

rcv ''In the beginning'' (16)

[No Mark]

rcv ''Linus begat Linux,'' (18)

[No Mark]

[SIGURG ]

rcv ''and the Penguinsrejoicedver'' (27)

[AT MARK]

rcv ''yexceedingly.'' (13)

[No Mark]

在這裡須要的注意的幾點:

隻收接到一個SIGURG 信号。

隻有一個緊迫資料 标記,盡管在發送端寫入了兩個帶 外 資料 。

在字元串''yexceedingly''中第一個y是一個帶 外 資料 節字。接下來的節字隻是簡單的随後發送的帶 内資料 節字。

面前的測試依賴于sleep(3)所供給的到實體資料 控制集合的延遲。

正 如我們面前的注意所指出的,我們的結果與許會與我們例子出輸略有不同。當由一個低速的486系統向一個倏地的Pentium III 系統發送時會示顯出更多的不同。當由一個倏地的CPU向一個慢速的CPU發送時會觀察到另外一個收接模式,其他的因素會決定資料 包如何行進分割。

用使select(2)處置帶 外 資料 

在這一章還有一些空間來探讨這個特殊的話題,但隻是一些簡單的建議看起來也許會更合适。

對于select函數調用,帶 外 資料 的念概是一個異常。我們也許可以記起第11章,"并發用戶端伺服器",select會阻塞,直到上面的一個或是多個事件發生:

一個讀事件(要取讀的資料 已到達)

一個寫事件(資料 在現可以寫入)

一個異常(帶 外 資料 到達)

我們的程式可以在select調用中捕獲這個異常。然後,可以在必要時用使設定了MSG_OOB标記位的recv函數來處置帶 外 資料 。

A,TCP支援帶外資料OOB嗎?與緊急模式URG有什麼關系?

     TCP支援帶外資料,但是隻有一個OOB位元組,TCP的帶外資料是通過緊急模式URG實作的.

B,我們知道send(sendfd,"ABC",3,MSG_OOB),将發送3個位元組的帶外資料OOB資料.但是這裡TCP又隻支援一個位元組的OOB,難道丢掉2個位元組?

     TCP将把緊急模式URG 置位,緊急指針定位第三個位元組("C")(這裡不管它的具體位置,緊急指針的作用就是提供定位那個OOB位元組的資訊),前兩個位元組("AB")當作普通位元組發送.其實TCP總是把最後一個位元組當作OOB資料,其他的當作普通位元組.不管你通過帶MSG_OOB标志的sendxxx函數發送多少位元組帶外資料OOB資料,發送端隻把最後一個位元組當作OOB資料,接收端也隻能收到一個位元組的OOB資料.

C,如果一定要發送多位元組的帶外資料,讓接收端能一次收到多個位元組的帶外資料.能不能做到?

     對于TCP協定,不能!

D,對于TCP,收到的帶外資料怎麼儲存?

     兩種模式:

     1,非OOBINLINE 模式,這是套接字的預設模式,把OOB位元組與普通位元組分開存放.存放在一個OOB緩沖區中,當然TCP隻有一個位元組,可以用一個位元組儲存OOB資料.

     2,OOBINLINE 模式,OOB位元組和普通位元組一起存放,它和普通位元組本來就是一起發送,當然可以一起存放.

E,recv(recvfd,buff,256,MSG_OOB).會有哪些結果?

     recvxxxx函數,在MSG_OOB模式下,将在OOB緩沖區中尋找資料。

     如果發送端沒發送OOB位元組,它傳回錯誤.

     如果發送端發送了OOB位元組:

     1,對于非OOBINLINE 模式,它傳回1位元組的OOB資料.

     2,對于OOBINLINE 模式,它傳回錯誤.因為OOB位元組沒有放到OOB緩沖區中.

F,如果發送端使用MSG_OOB模式,send(sendfd,sndbuff,64,MSG_OOB),發送了包含"OOB位元組"的64位元組資料,然後用非MSG_OOB模式,send(sendfd,sndbuff,64,0)發送64位元組,當接收端收到64+64位元組的資料後,用recv(recvfd,revbuff,256,0).會有哪些結果?

1,對于非OOBINLINE 模式,第一次recv(recvfd,revbuff,256,0)隻傳回前63位元組的普通資料,接收緩沖區剩下64位元組.要獲得1位元組的OOB資料,必須使用MSG_OOB模式的revxxx函數.再次recv(recvfd,revbuff,256,0),傳回第二次發送的64位元組.一次recvxxx不跨越urg-mark标記.

2,對于OOBINLINE 模式,第一次recv(recvfd,revbuff,256,0)隻傳回前63位元組的普通資料,接收緩沖區剩下65位元組(OOB+64位元組),第二次recv(recvfd,revbuff,256,0),對于windows,隻傳回一位元組的OOB位元組,需要第三次rev才能傳回最後的64位元組,對于linux/unix,第二次rev 就傳回65位元組(OOB+64位元組).總之與協定棧的實作有關.

G,如果OOB位元組沒被應用程式讀取,協定棧又收到了新的OOB位元組,會出現什麼情況?

    TCP協定對每個socket保持一個URG指針,此時直接重新整理URG指針,指向新的OOB位元組.

對于非OOBINLINE,舊的OOB位元組直接被丢棄,被新的OOB位元組覆寫.

對于OOBINLINE,舊的OOB位元組仍然在接收緩沖區中,但被當着普通資料看待,每個socket隻有一個URG指針,隻能定位一個OOB位元組.

許多傳輸層都支援帶外資料(Out-Of-Band data),有時候也稱為快速資料(Expedited

Data).之是以有帶外資料的概念,是因為有時候在一個網絡連接配接的終端想“快速”的告訴

網絡另一邊的終端一些資訊.這個“快速”的意思是我們的“提示”資訊會在正常的網絡

資料(有時候稱為帶内資料In-Band data)之前到達網絡另一邊的終端.這說明,帶外數

據擁有比一般資料高的優先級.但是不要以為帶外資料是通過兩條套接字連接配接來實作的.事

實上,帶外資料也是通過以有的連接配接來傳輸。

不幸的是,幾乎每個傳輸層都有不同的帶外資料的處理方法。我們下面研究的是TCP

模型的帶外資料,提供一個小小的例子來看看它是怎樣處理套接字的帶外資料,及調用套

接字API 的方法。

流套接字的抽象中包括了帶外資料這一概念,帶外資料是相連的每一對流套接字間一

個邏輯上獨立的傳輸通道。帶外資料是獨立于普通資料傳送給使用者的,這一抽象要求帶外

資料裝置必須支援每一時刻至少一個帶外資料消息被可靠地傳送。這一消息可能包含至少

一個位元組;并且在任何時刻僅有一個帶外資料資訊等候發送。對于僅支援帶内資料的通訊

協定來說(例如緊急資料是與普通資料在同一序列中發送的),系統通常把緊急資料從普

通資料中分離出來單獨存放。這就允許使用者可以在順序接收緊急資料和非順序接收緊急數

據之間作出選擇(非順序接收時可以省去緩存重疊資料的麻煩)。在這種情況下,使用者也

可以“偷看一眼”緊急資料。

某一個應用程式也可能喜歡線内處理緊急資料,即把其作為普通資料流的一部分。這

可以靠設定套接字選項中的SO_OOBINLINE 來實作。在這種情況下,應用程式可能希望

确定未讀資料中的哪一些是“緊急”的(“緊急”這一術語通常應用于線内帶外資料)。為

了達到這個目的,在Sockets 的實作中就要在資料流保留一個邏輯記号來指出帶外資料從

哪一點開始發送.

select()函數可以用于處理對帶外資料到來的通知。

6.11.1 TCP 的帶外資料

TCP 上沒有真正意義上的“帶外資料”。TCP 是由一種叫做“緊急模式”的方法來傳

輸帶外資料的。假設一個程序向一個TCP 套接字寫入了N 個位元組的資料,資料被TCP 套

接字的發送緩沖區緩存,等待被發送到網絡上面.我們在圖6-10 可以看見資料的排列。

第6 章berkeley 套接字- 191 -

圖6-10 TCP 資料的排列

現在程序使用以MSG_OOB 為參數的send()函數寫入一個單位元組的"帶外資料",包

含一個ASCII 字元"a":

send(fd, “a”, 1, MSG_OOB);

TCP 将資料放在下一個可用的發送緩沖區中,并設定這個連接配接的"緊急指針"(urgent

pointer)指向下一個可用的緩沖區空間.圖6-11 表示了我們描述的這個狀态,并将帶外數

據(Out-Of-Band)表示為"OOB"。

圖6-11 ODB 資料

TCP 的緊急指針的指向的位置是在程式發送的OOB 資料的後面。

由圖6-11 所表示的TCP 套接字的狀态,得知下一個将要發送的資料是TCP 的URG

(Urgent pointer)标志,發送完URG 标志,TCP 才會發送下面的帶外資料的那個位元組。

但是TCP 所一次發送的資料中,可能隻包含了TCP 的URG 标志,卻沒有包含我們所發送

的OOB 資料.是否會發生這種情況而取決于TCP 将要發送的資料隊列中,在OOB 資料

之前的資料的多少。如果在一次發送中,OOB 前的資料已經占滿了名額,則TCP 隻會發

送URG 标志,不會發送OOB資料

這是一個TCP 緊急資料狀态的重要特性:TCP 的資訊頭指出發送者進入了緊急模式(比

方說,在緊急偏移處設定了URG 标志),但是緊急偏移處的資料并沒有必要一定要發送出

去.事實上,如果一個TCP 套接字的流傳送停止後(可能是接收方的套接字的接收緩沖區

沒有空餘空間),為了發送帶外資料,系統會發送不包含資料的TCP 資料包,裡面标明這

是一個帶外資料.這也是我們使用帶外資料的一個有利點:TCP 連接配接就算是在不能向對方

- 192 - Linux網絡程式設計

發送資料的時候,也可以發送出一個帶外資料的信号。

如果我們像下面這樣發送一個多位元組的帶外資料:

send(fd, “abc”, 3, MSG_OOB);

在這個例子中, TCP 的緊急指針指向了資料最後一位的後面, 是以隻有最後一位數

據(“c”)才被系統認為是“帶外資料”。

//

//

我們上面大緻了解了發送方是怎樣發送“帶外資料”的了,下面我們來看一看接收方

是怎樣接收“帶外資料”的。

1.當TCP 收到一個包含URG 标志的資料段時,TCP 會檢查“緊急指針”來驗證是

否緊急指針所指的資料已經到達本地。也就是說,無論這次是否是TCP 緊急模式從發送方

到接收方的第一次傳輸帶外資料。一般來說,TCP 傳輸資料會分成許多小的資料包來傳輸

(每個包的到達時間也不同)。可能有好幾個資料包中都包含緊急指針,但是這幾個包中

的緊急指針都是指向同一個位置的,也就是說多個緊急指針指向一個資料。需要注意的是,

對于這一個帶外資料,雖然有多個指針指向它,但是隻有第一個緊急指針會通知程式注意。

2.接收程序收到另外一個帶外資料的通知的條件是:有另外一個帶外資料的指針到

達.注意這裡是“另外一個帶外資料”的指針,不是上面的“一個帶外資料”的另外一個

指針。首先, SIGURG 信号回發送給套接字的屬主,這個取決于是否已經使用fcntl()函數

或ioctl()函數設定套接字的屬主和這個程式對SIGURG 信号的具體操作函數。其次,如果

一個程式正阻塞與對這個套接字描述符的select()函數的調用中,則select()函數會産生一個

例外,然後傳回。

注意:程序收到帶外資料的通知的時候,并不會在乎帶外資料的真正資料是否到達。

3.當緊急指針所指的真正的帶外資料通過TCP 網絡到達接收端的時候,資料或者被

放入帶外資料緩沖區或是隻是簡單的和普通的網絡資料混合在一起。在預設的條件下,

SO_OOBINLINE 套接字選項是不會被設定的,是以這個單位元組的帶外資料并沒有被防在

套接字的接收緩存區中,而是被放入屬于這個套接字的一個單獨的帶外資料緩存區中。如

果這個程序想讀取這個帶外資料的具體内容的話,唯一的辦法就是調用recv,recvfrom,

或是recvmsg 函數,并且一定要指定MSG_OOB 标志。

4.如果一個程序将套接字設定為SO_OOBINLINE 屬性,則由緊急指針所指的,代表

帶外資料的那個位元組将回被放在正常套接字緩沖區中的最左邊.在這種情況下,程序不能

指定MSG_OOB 來讀取這個一個位元組的帶外資料,但是它可以知道帶外資料的到達時間:

通過檢查套接字的帶外資料标記.

有可能發生的一些錯誤:

5.如果當連接配接沒有發送帶外資料的時候程序來讀取帶外資料(比如說,通過MSG_OOB

參數來接收函數),則EINVAL 将會被傳回。

6.當真正的帶外資料到達之前的時候,程序被通知(SIGURG 或是select 函數)有帶

外資料到達(也就是說帶外資料的通知信号已經到達),如果程序嘗試讀取帶外資料,則

傳回EWOULDFBLOCK .程序所能做的隻是去讀取套接字的接收緩存區.(也許,由于

緩存區的資料以滿,帶外資料的那個位元組資訊無法傳輸過來,這樣的話也許你需要清理一

下接收緩存區來給帶外資料空出一些空間)

7.如果程序嘗試多次讀取同一個帶外資料,則EINVAL 将會被傳回。

8.如果程序将套接字屬性設定為SO_OOBINLINE ,然後嘗試通過指定MSG_OOB

第6 章berkeley 套接字- 193 -

标志來讀取帶外資料,則EINVAL 将會被傳回。

下面我們将前面的套接字例程做一些變動來測試帶外資料的發送與接收.

6.11.2 OOB 傳輸套接字例程(伺服器代碼Server.c)

#include <stdio.h>

#include <stdlib.h>

#include <errno.h>

#include <string.h>

#include <sys/types.h>

#include <netinet/in.h>

#include <sys/socket.h>

#include <sys/wait.h>

#define MYPORT 4000

#define BACKLOG 10

void

sig_urg(int signo);

main()

{

int sock_fd, new_fd ;

void * old_sig_urg_handle ;

struct sockaddr_in my_addr;

struct sockaddr_in their_addr;

int sin_size;

int n ;

char buff[100] ;

if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)

{

- 194 - Linux網絡程式設計

perror(“socket”);

exit(1);

}

my_addr.sin_family = AF_INET;

my_addr.sin_port = htons(MYPORT);

my_addr.sin_addr.s_addr = INADDR_ANY;

bzero(&(my_addr.sin_zero), 8);

if (bind(sockfd, (struct sockaddr *)&my_addr,

sizeof(struct sockaddr)) == -1)

{

perror(“bind”);

exit(1);

}

if (listen(sockfd, BACKLOG) == -1)

{

perror(“listen”);

exit(1);

}

old_sig_urg_handle = signal(SIGURG, sig_urg);

fcntl(sockfd, F_SETOWN, getpid());

while(1)

{

第6 章berkeley 套接字- 195 -

sin_size = sizeof(struct sockaddr_in);

if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1)

{

perror(“accept”);

continue;

}

printf(“server: got connection from %s/n”, inet_ntoa(their_addr.sin_addr));

if (!fork())

while(1)

{

if((n = recv(new_fd, buff, sizeof(buff)–1)) == 0)

{

printf(“received EOF/n”);

break ;

}

buff[n] = 0 ;

printf(“Recv %d bytes: %s/n”, n, buff);

}

close(new_fd);

}

}

while(waitpid(-1,NULL,WNOHANG) > 0);

signal(SIGURG, old_sig_urg_handle);

}

void

sig_urg(int signo)

{

- 196 - Linux網絡程式設計

int n;

char buff[100] ;

printf(“SIGURG received/n”);

n = recv(new_fd, buff, sizeof(buff)– 1, MSG_OOB);

buff [ n ] = 0 ;

printf(“recv %d OOB byte: %s/n” , n,buff);

}

6.11.3 OOB 傳輸套接字例程(用戶端代碼Client.c)

下面是用戶端程式:

#include <stdio.h>

#include <stdlib.h>

#include <errno.h>

#include <string.h>

#include <netdb.h>

#include <sys/types.h>

#include <netinet/in.h>

#include <sys/socket.h>

#define PORT 4000

#define MAXDATASIZE 100

int

main(int argc, char *argv[])

{

int sockfd, numbytes;

char buf[MAXDATASIZE];

struct hostent *he;

struct sockaddr_in their_addr;

if (argc != 2)

{

第6 章berkeley 套接字- 197 -

fprintf(stderr,“usage: client hostname/n”);

exit(1);

}

if ((he=gethostbyname(argv[1])) == NULL)

herror(“gethostbyname”);

exit(1);

}

if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {

perror(“socket”);

exit(1);

}

their_addr.sin_family = AF_INET;

their_addr.sin_port = htons(PORT);

their_addr.sin_addr = *((struct in_addr *)he->h_addr);

bzero(&(their_addr.sin_zero), 8);

if(connect(sockfd, (struct sockaddr *)&their_addr, sizeof(struct sockaddr)) == -1)

{

perror(“connect”);

exit(1);

}

if (send(new_fd, “123”, 3, 0) == -1)

{

perror(“send”);

close(new_fd);

- 198 - Linux網絡程式設計

exit(0);

}

printf(“Send 3 byte of normal data/n”);

sleep(1);

if (send(new_fd, “4”, 1, MSG_OOB)== -1)

{

perror(“send”);

close(new_fd);

exit(0);

}

printf(“Send 1 byte of OOB data/n”);

sleep(1);

if (send(new_fd, “56”, 2, 0) == -1)

{

perror(“send”);

close(new_fd);

exit(0);

}

printf(“Send 2 bytes of normal data/n”);

sleep(1);

if (send(new_fd,“7”, 1, MSG_OOB)== -1)

{

perror(“send”);

close(new_fd);

exit(0);

}

printf(“Send 1 byte of OOB data/n”);

sleep(1);

if (send(new_fd, “89”, 2, MSG_OOB)== -1)

{

perror(“send”);

close(new_fd);

exit(0);

}

printf(“Send 2 bytes of normal data/n”);

sleep(1);

第6 章berkeley 套接字- 199 -

close(sockfd);

return 0;

}

6.11.4 編譯例子

注意:你顯然需要在運作client 之前啟動server.否則client 會執行出錯(顯示“Connection

refused”).

當隻有一個連接配接的時候(因為這個伺服器是多程序的,是以如果有多個連接配接同時存在

可能會導緻螢幕輸出混亂),可以得到下面的結果:(注意是使用我們下面的客戶程式來連

接的,并且假設你運作我們的伺服器程式是在本地機器上面)

[email protected]# gcc –o server server.c

[email protected]# gcc –o client client.c

[email protected]# ./server

[email protected]# ./client 127.0.0.1

Send 3 bytes of normal data <- Client輸出

Recv 3 bytes: 123 <- Server輸出

Send 1 byte of OOB data <- Client輸出

SIGURG received <- Server輸出

Recv 1 OOB byte: 4 <- Server輸出

Send 2 bytes of normal data <- Client輸出

Recv 2 bytes: 56 <- Server輸出

Send 1 byte of OOB data <- Client輸出

SIGURG Received <- Server輸出

Recv 1 OOB byte: 7 <- Server輸出

received EOF <- Server輸出

這個結果正是我們想要的。每一個用戶端發送的帶外資料都導緻伺服器端産生了

SIGURG 信号,伺服器端收到SIGURG 信号後,就去讀取帶外資料了。

繼續閱讀