定义带 外 数据
想 像一下在银行人们排起队待等处置他们的帐单。在这个步队中每个人最后会都移到面前由出纳员行进服务。在现想像一下一个走入银行,超出个整步队,然后用枪抵 住出纳员。这个就可以看作为带 外 数据 。这个盗强超出个整步队,是因为这把枪给了他凌驾于世人的权利。出纳员也集会中注意力于这个盗强身上,因为他晓得后以 的势形是很紧迫的。
应相的,一个连接的流式套口接上的带 外 数据 的作工理原也与此相似。常通情况下,数据 由连接的一端流到另一端,并且为认 数据 的全部节字都是确精排序的。晚写入的节字绝不会早于先写入的节字到达。然而套口接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节字被作为带 外 数据 收接。为什么是这样?
理解紧迫指针
在这一章面前,套口接口接供给了一个常通的网络口接。这就包含他如何处置带 外 数据 。然而,紧迫数据的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; /* Status */
int s; /* Socket */
int oobinline =1; /* TRUE */
z = setsockopt(s,
SOL_SOCKET, /* Level */
SO_OOBINLINE, /* Option */
&oobinline, /* ptr to value */
sizeof oobinline); /* Size of value */
警告
如果我们为一个套口接打开了这个选项,我们就不可以用使设置了MSG_OOB标记位的recv函数。如果我们这样做了,我们就会返回一个错误,而变量errno会被设置为EINVAL。
注意
如果我们觉得有用,也可以用使SIGURG 信号。这是通过一个用使F_SETOWN的fcntl函数来建立了。
定确紧迫指针
无论我们是不是正在用使内联数据 的式方行进收接,当我们收接到后以数据 流中的数指针时,我们都可以自由的用使一个函数来通知我们。这可以通过准确的参数来调用ioctl(2)来定确。
例如
#include <sys/ioctl.h>
. . .
int z; /* Status */
int s; /* Socket */
int flag; /* True when at mark */
z = ioctl(s, SIOCATMARK,&flag);
if ( z == -1 )
abort(); /* Error */
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; /* Socket */
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
/* 能够同时接受多少没有accept 的连接*/
#define BACKLOG 10
void
sig_urg(int signo);
main()
{
/* 在sock_fd 上进行监听,new_fd 接受新的连接*/
int sock_fd, new_fd ;
/* 用于存储以前系统缺省的SIGURL 处理器的变量*/ void * old_sig_urg_handle ;
/* 自己的地址信息*/
struct sockaddr_in my_addr;
/* 连接者的地址信息*/
struct sockaddr_in their_addr;
int sin_size;
int n ;
char buff[100] ;
/* 这里就是我们一直强调的错误检查.如果调用socket() 出错,则返回*/
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);
/* 将运行程序机器的IP 填充入s_addr */
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)
{
/* 如果调用bind()失败,则给出错误提示,退出*/
perror(“bind”);
exit(1);
}
/* 这里是我们一直强调的错误检查!! */
if (listen(sockfd, BACKLOG) == -1)
{
/* 如果调用listen 失败,则给出错误提示,退出*/
perror(“listen”);
exit(1);
}
/* 设置SIGURG 的处理函数 sig_urg */
old_sig_urg_handle = signal(SIGURG, sig_urg);
/* 更改connfd 的属主*/
fcntl(sockfd, F_SETOWN, getpid());
while(1)
{
第6 章berkeley 套接字- 195 -
/* 这里是主accept()循环*/
sin_size = sizeof(struct sockaddr_in);
/* 这里是我们一直强调的错误检查!! */
if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1)
{
/* 如果调用accept()出现错误,则给出错误提示,进入下一个循环*/
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);
}
/* 关闭new_fd 代表的这个套接字连接*/
close(new_fd);
}
}
/* 等待所有的子进程都退出*/
while(waitpid(-1,NULL,WNOHANG) > 0);
/* 恢复系统以前对SIGURG 的处理器*/
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)
/* 如果gethostbyname()发生错误,则显示错误信息并退出*/
herror(“gethostbyname”);
exit(1);
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
/* 如果socket()调用出现错误则显示错误信息并退出*/
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)
{
/* 如果connect()建立连接错误,则显示出错误信息,退出*/
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”);
/* 睡眠1 秒*/
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”).
当只有一个连接的时候(因为这个服务器是多进程的,所以如果有多个连接同时存在
可能会导致屏幕输出混乱),可以得到下面的结果:(注意是使用我们下面的客户程序来连
接的,并且假设你运行我们的服务器程序是在本地机器上面)
root@bbs# gcc –o server server.c
root@bbs# gcc –o client client.c
root@bbs# ./server
root@bbs# ./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 信号后,就去读取带外数据了。