Linux Socket学习(十四)
带外数据
这一章我们将使用TCP来处理带外数据。我们将会学习为什么要提供带外数据以及他的缺点。然而,最重要的是,我们可以正确的应用他所支持的各种协议来处理通信的带外模式。
这一章,我们将会学到下列内容:
定义什么是带外数据
为什么需要带外数据
套接口使用带外数据的相关问题
在TCP/IP实现中有哪些变化
如何中配合TCP流套接口使用带外数据
定义带外数据
想 像一下在银行人们排起队等待处理他们的帐单。在这个队伍中每个人最后都会移到前面由出纳员进行服务。现在想像一下一个走入银行,越过整个队伍,然后用枪抵 住出纳员。这个就可以看作为带外数据。这个强盗越过整个队伍,是因为这把枪给了他凌驾于众人的权力。出纳员也会集中注意力于这个强盗身上,因为他知道当前 的形势是很紧急的。
相应的,一个连接的流式套接口上的带外数据的工作原理也与此类似。通常情况下,数据由连接的一端流到另一端,并且认为 数据的所有字节都是精确排序的。晚写入的字节绝不会早于先写入的字节到达。然而套接口API概念性的提供了一些实用程序,从而可以使得一串数据无阻的先于 通常的数据到达接收端。这就是所谓的发送带外数据。
从技术上来说,一个TCP流不可以发送带外数据。而他所支持的只是一个概念性的紧急数据,这些紧急数据作为带外数据映射到套接口API。这就带来了许多限制,这些我们会在后面进行讨论。
理解带外数据的需要
尽管我们可以立刻享受到在银行中越过整个队伍的利益,但是我们也会认识到使用枪来达到这样的目的是反社会的行为。一个TCP流通常希望以完美的队列来发送数据字节,那么乱序的发送数据就似乎与流的概念相违背。那么为什么要提供带外数据的套接口方法呢?
也 许我们已经意识到了,有时数据会以一定的方式变得紧急。一个流套接口会有一个大量的数据队列等待发送到网络。在远程端点,也会有大量已接收的,却还没有被 程序读取的数据。如果发送客户端程序由于一些原因需要取消已经写入服务器的请求,那么他就需要向服务器紧急发送一个标识取消的请求。如果向远程服务器发送 取消请求失败,那么就会无谓的浪费服务器的资源。
也许对于这个设计问题我们还有其他的答案,例如认为套接口会提早关闭,或是通过另外的套接口连接来发送所需要的信息。这两种方案都会由于不同的原因而不能实现:
如果有多个通信在继续,那么关闭一个套接口并不会允许修复。
当有成百上千的用户连接到我们的服务器,建立额外的连接会是相当的昂贵。
企业防火墙的限制迫切要求我们在一个套接口连接中执行所有的网络请求。
使 用带外数据的实际程序例子就是telnet,rlogin,ftp命令。前两个程序会将中止字符作为紧急数据发送到远程端。这会允许远程端冲洗所有未处理 的输入,并且丢弃所有未发送的终端输出。这会快速中断一个向我们屏幕发送大量数据的运行进程。ftp命令使用带外数据来中断一个文件的传输。
套接口与带外数据
现 在阐述当使用套接口带外数据时,TCP流会有一些缺陷还为时尚早。重新强调套接口接口本身并不是限制因素是很重要的。带外数据的概念实际上映射到 TCP/IP通信的紧急数据模式。在今天,TCP流对于网络是很重要的,而在这一章我们仅专注于带外数据适应于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值。
使用带外数据
尽管我们对于带外数据也许还有许多疑问,不要烦恼。一些简单的例子程序会比文本更清楚的帮助我们来演示这些概念。在这些演示中将会进行相应的解释。
更多的关于带外数据I/O的基本讨论将会在下面进行。
编写带外数据
一个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信号,这是通过为信号创建一个信号处理器来做到的。我们很快就会看到这样的一个例子。
支持子程序
为了减少所提供的例子程序中复制代码的数量,一些通常的函数将会组织在一个模块中,而我们将会在这里提供。这会有助于我们专注于带外数据处理的重要概念。
下面提供了模块bindacpt.c,其中包含建立与接受套接口连接的通用代码:
/*
* bindacpt.c
*
* socket,bind,listen & accept:
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
extern int
mkaddr(void *addr,
int *addrlen,
char *str_addr,
char *protocol);
/*
* This function reports the error and
* exits back to the shell:
*/
void
bail(const char *on_what)
{
if(errno !=0 )
{
fputs(strerror(errno),stderr);
fputs(": ",stderr);
}
fputs(on_what,stderr);
fputc('/n',stderr);
exit(1);
}
/*
* Call socket,bind,
* listen and accept:
*
* return socket.
*/
int
BindAccept(char *addr)
{
int z;
int s;
struct sockaddr_in adr_srvr;
struct sockaddr_in adr_clnt;
int len_inet;
/*
* Create a TCP/IP socket to use:
*/
s =socket(PF_INET,SOCK_STREAM,0);
if(s==-1)
bail("socket()");
/*
* Bind the server address:
*/
len_inet = sizeof adr_srvr;
z = mkaddr(&adr_srvr,&len_inet,addr,"tcp");
if(z!=0)
{
puts("bad address/port");
exit(1);
}
/*
* Bind server address:
*/
z = bind(s,(struct sockaddr *)&adr_srvr,len_inet);
if( z==-1)
bail("bind(2)");
/*
* Set listen mode:
*/
if(listen(s,10) == -1)
bail("listen(2)");
/*
* Wait for a connet:
*/
len_inet = sizeof adr_clnt;
z = accept(s,(struct sockaddr *)&adr_clnt,&len_inet);
if(z==-1)
bail("accept");
close(s); /* No longer needed */
return z; /* Connected socket */
}
int Connect(char *addr)
{
int z;
int s;
struct sockaddr_in adr_srvr;
int len_inet;
/*
* Create a TCP/IP socket to use:
*/
s = socket(PF_INET,SOCK_STREAM,0);
if(s==-1)
bail("socket()");
/*
* Bind the server address:
*/
len_inet = sizeof adr_srvr;
z = mkaddr(&adr_srvr,&len_inet,addr,"tcp");
if(z!=0)
{
puts("Bad address/port");
exit(1);
}
/*
* Connect to server:
*/
len_inet = sizeof adr_srvr;
z = connect(s,(struct sockaddr_in *)&adr_srvr,len_inet);
if(z==-1)
bail("connect()");
return s; /* Connected socket */
}
在这里我们并不会详细讨论这个源码模块,因为我们应可以很好的理解其中所包含的操作了。
在这里我们无改动的使用上一章的mkaddr.c源码模块,所以我们不会在在这里重复。
接收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函数来处理带外数据。