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字节被作为带外数据接收。为什么是这样?