歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

TCP帶外數據詳解

傳輸層協議使用帶外數據(out-of-band,OOB)來發送一些重要的數據,如果通信一方有重要的數據需要通知對方時,協議能夠將這些數據快速地發送到對方.為了發送這些數據,協議一般不使用與普通數據相同的通道,而是使用另外的通道.linux系統的套接字機制支持低層協議發送和接受帶外數據.但是TCP協議沒有真正意義上的帶外數據.為了發送重要協議,TCP提供了一種稱為緊急模式(urgentmode)的機制.TCP協議在數據段中設置URG位,表示進入緊急模式.接收方可以對緊急模式采取特殊的處理.很容易看出來,這種方式數據不容易被阻塞,可以通過在我們的服務器端程序裡面捕捉SIGURG信號來及時接受數據或者使用帶OOB標志的recv函數來接受.

 定義帶外數據

想 像一下在銀行人們排起隊等待處理他們的帳單。在這個隊伍中每個人最後都會移到前面由出納員進行服務。現在想像一下一個走入銀行,越過整個隊伍,然後用槍抵 住出納員。這個就可以看作為帶外數據。這個強盜越過整個隊伍,是因為這把槍給了他凌駕於眾人的權力。出納員也會集中注意力於這個強盜身上,因為他知道當前 的形勢是很緊急的。

相應的,一個連接的流式套接口上的帶外數據的工作原理也與此類似。通常情況下,數據由連接的一端流到另一端,並且認為 數據的所有字節都是精確排序的。晚寫入的字節絕不會早於先寫入的字節到達。然而套接口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
$

這裡顯示的輸出為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]; /* Data */
int len;      /* Bytes */
int s;        /* Socket */
. . .
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];  /* Buffer */
int n;      /* No. of bytes */
int s;            /* Socket */
int len;        /* Max bytes */
. . .
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; /* Status */
int s; /* Socket */
z = fcntl(s,F_SETOWN,getpid());
if ( z == -1 ) {
    perror("fcntl(2)");
    exit(1);
}

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

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


接收SIGURG信號

移開了這些煩瑣的工作以後,現在我們可以來探索有趣的帶外數據的概念了。下面所列的程序代碼就是我們用來接收數據和當帶外數據到達時處理帶外數據的程序。他設計使用BSD解釋來處理帶外數據,而這也正是Linux的默認情況。
/*
* oobrec.c
*
* Example OOB receiver:
*/
#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字節被作為帶外數據接收。為什麼是這樣?

Copyright © Linux教程網 All Rights Reserved