天天看點

UNIX網絡程式設計——帶外資料

       許多傳輸層有帶外資料的概念,它有時也稱為經加速資料。其想法是一個連接配接的某端發生了重要的事情,而且該端希望迅速通告其對端。這裡“迅速”意味着這種通知應該在已排隊等待發送的任何“普通”(有時稱為“帶内”)資料之前發送。也就是說,帶外資料被認為具有比普通資料更高的優先級。帶外資料并不需要在客戶和伺服器之間再使用一個連接配接,而是被映射到已有的連接配接中。

       不幸的是,一旦超越普通概念光臨現實世界,我們發現幾乎每個傳輸層都各自有不同的帶外資料實作。而UDP作為一個極端的例子,沒有實作帶外資料。

1.TCP帶外資料

       TCP并沒有真正的帶外資料,不過提供了我們接着要說的緊急模式。假設一個程序已經往一個TCP套接字寫出N位元組資料,而且TCP把這些資料排隊在該套接字的發送緩沖區中,等着發送到對端。如圖展示了這樣的套接字發送緩沖區,并且标記了從1到N的資料位元組。

UNIX網絡程式設計——帶外資料

       該程序接着以MSG_OOB标志調用send函數寫出一個含有ASCII字元a的單位元組帶外資料:

       TCP把這個資料放置在該套接字發送緩沖區的下一個可用位置,并把該連接配接的TCP緊急指針設定成再下一個可用位置。如圖展示了此時的套接字發送緩沖區,并且把帶外位元組标記為:“OOB”。

UNIX網絡程式設計——帶外資料

       TCP緊急指針對應一個TCP序列号,它是使用MSG_OOB标志寫出的最後一個資料位元組(即帶外位元組)對應的序列号加1。

       給定的上圖所示的TCP套接字發送緩沖區狀态,發送端TCP将為待發送的下一個分節在TCP首部中設定URG标志,并把緊急偏移字段設定為指向帶外位元組之後的位元組,不過該分節可能含也可能不含我們标記的OOB的那個位元組。OOB位元組是否發送取決于在套接字發送緩沖區中先于它的位元組數,TCP準備發送給對端的分節大小以及對端通告的目前視窗。

       這是TCP緊急模式的一個重要特點:TCP首部指出發送端已經進入緊急模式(即伴随緊急偏移的URG标志已經設定),但是由緊急指針所指的實際資料位元組卻不一定随同送出。事實上即使發送端TCP因流量控制而暫停發送資料(接收端的套接字接收緩沖區已滿,導緻其TCP想發送端TCP通告了一個值為0 的視窗),緊急通知照樣不伴随任何資料的發送。這也是應用程序使用TCP緊急模式(即帶外資料)的一個原因:即便資料的流動會因為TCP的流量控制而停止,緊急通知卻總是無障礙的發送到對端TCP。

       如果我們發送多個位元組的帶外資料,情況又會任何呢?例如:

       在這個例子中,TCP的緊急指針指向最後那個位元組緊後的位置,也就是說最後那個位元組(字母c)被認為是帶外位元組,注意僅僅是一個位元組。

       至此我們已經講述了帶外資料的發送,下面從接收端的角度檢視一下:

(1)當收到一個設定了URG标志的分節時,接收端TCP檢查緊急指針,确實它是否指向新的帶外資料,也就是判斷本分節是不是首個到達的引用從發送端到接收端的資料流中特定位元組的緊急模式分布。發送端TCP往往發送多個含有URG标志且緊急指針指向同一個資料位元組的分節(通常是在已小段時間内)。這些分節中隻有第一個到達的會導緻通知接收程序有新的帶外資料到達。

(2)當有新的緊急指針到達時,接收程序被通知到。首先,核心給接收套接字的屬主程序發送SIGURG信号,前提是接收程序(或其他程序)曾調用fcntl或ioctl為這個套接字建立了屬主,而且該屬主程序已為這個信号建立了信号處理函數。其次,如果接收程序阻塞在select調用中以等待這個套接字描述符出現一個異常條件,select調用就傳回。

       一旦有新的緊急指針到達,不論由緊急指針指向的實際資料位元組是否已經到達接收端TCP,這兩個潛在通知接收程序的手段就發生動作。

       隻有一個OOB标記,如果新的OOB位元組在舊的OOB位元組被讀取之前就到達,舊的OOB位元組會被丢棄。

(3)當由緊急指針指向的實際資料位元組到達接收端TCP,該資料位元組既可能被拉出帶外,也可能被留在帶内,即線上留存。SO_OOBINLINE套接字選項預設情況下是禁止的,對于這樣的接收端套接字,該資料位元組并不放入套接字接收緩沖區,而是被放入該連接配接的一個獨立的單位元組帶外緩沖區。接收程序從這個單位元組緩沖區讀入資料的唯一方法是制定MSG_OOB标志調用recv,recvfrom或recvmsg。如果新的OOB位元組在舊的OOB位元組被讀取之前就到達,舊的OOB位元組會被丢棄。

       然而如果接收程序開啟了SO_OOBINLINE套接字選項,那麼由TCP緊急指針指向的實際資料位元組将被留在通常的套接字接收緩沖區中。這種情況下,接收程序不能指定MSG_OOB标志讀入該資料位元組。相反,接收程序通過檢查該連接配接的帶外标記以獲悉何時通路到這個資料位元組。

       發生一些錯誤是可能的:

如果接收程序請求讀入帶外資料(通過指定MSG_OOB标志),但是對端尚未發送任何帶外資料,讀入操作将傳回EINVAL。

在接收程序已被告知對端發送了一個帶外位元組(通過SIGURG或select手段)的前提下,如果接收程序讀入該位元組,但是該位元組尚未到達,讀入操作将傳回EWOULDBLOCK。接收程序此時能做的僅僅是從套接字接收緩沖區讀入資料(要是沒有存放這些資料的空間,可能還得丢棄他們),以便在該緩沖區中騰出空間,繼而允許對端TCP發送出那個帶外位元組。

如果接收程序試圖多次讀入同一個帶外位元組,讀入操作将傳回EINAVL。(後面有說明:select改進)

如果接收程序已經開啟SO_OOBINLINE套接字選項,後來試圖通過指定的MSG_OOB标志讀入帶外資料,讀入操作将傳回EINVAL。

2.使用SIGURG的簡單例子

   現在給出一個發送和接收帶外資料的例子:

       該程式共發送9個位元組,每個輸出操作之間有一個1S的sleep。停頓的目的是讓每個write或send的資料作為單個TCP分節在本端發送并在對端接收。程式運作結果:

下面是接收程式:

建立信号處理函數和套接字屬主

15-16  建立SIGURG的信号處理函數,使用fcntl設定已連接配接套接字的屬主。

  (注意,我們直到accept傳回之後才建立信号處理函數,這麼做會錯過一些以小機率出現的帶外資料,他們在TCP完成三次握手之後但在

accept傳回之前到達。然而如果我們在調用accept之前信号處理函數并設定監聽套接字的屬主(本屬性将傳承給已連接配接套接字),那麼如果

帶外資料在accept之前到達,我們的信号處理函數将沒有真正的connfd值可用。如果這種情形對于應用程式确實重要,它就應該把connfd初

始化為-1,在信号處理函數中檢查該值是否為-1,若為真則簡單的設定一個标志,供主循環在accept傳回之後檢查。另一方面,這可能阻塞

accept調用周圍的信号)。

17-25   本程序從套接字中讀,顯示由read傳回的每個字元串。發送程序終止連接配接後,接收程序随後終止。

SIGURG處理函數

28-36   我們的信号處理函數調用printf,通過指定MSG_OOB标志讀入帶外位元組,然後顯示傳回的資料。注意,我們在recv調用中請求最多100個位元組。

運作結果:

       結果與我們預期的一緻。發送程序帶外資料的每次發送産生遞交給接收程序的SIGURG信号,後者接着讀入單個帶外位元組。

3.使用select的簡單例子

   我們現在改用select代替SIGURG信号重新編寫帶外接收程式。

有問題的:

19-25    調用select等待普通資料(讀集合rset)或帶外資料(異常集合xset)。每種情況下都顯示接收的資料。

      我先運作本程式,接着運作早先的發送程式,結果碰到如下錯誤:

       問題是select一直訓示一個異常條件,直到程序的讀入越過帶外資料。同一個帶外資料不能讀入多次,因為首先讀入之後,核心就清空這個單位元組的緩沖區。再次指定MSG_OOB标志調用recv時,它将傳回EINVAL。

       解決辦法是隻在讀入普通資料之後select異常條件。它正确的處理了上述情形,以下是正确的代碼:

4      聲明一個名為justreadoob的變量,用于訓示我們是否剛剛讀過帶外資料。這個标志決定是否select異常條件。

29-30    當設定justreadoob标志時,我們還得在異常描述符集中清除已連接配接套接字描述符對應的位。