带外数据
什么是带外数据
带外数据用于迅速告诉对方本端发生的重要事情。它比普通数据有更高的优先级,它应该总是立即被发送,不论发送缓冲区中是否有排队等待发送的普通数据。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。udp没有实现带外数据传输,TCP也没有真正的带外数据,但是它通过其他方式实现了类似的效果。
TCP的带外数据
TCP利用其头部的紧急指针标志和紧急指针两个字段,给应用程序提供了一种紧急方式。其利用传输普通数据的连接来传输带外数据。
TCP带外数据的发送过程
假设一个进程已经往某个TCP连接的发送缓冲区中写入了N个字节的普通数据,并等待发送。在数据发送前,该进程又往发送缓冲区中写入了3个字节的带外数据"abc"。此时,待发送的TCP报文头部将被设置URG标志,并且紧急指针被设置为指向最后一个带外数据的下一个字节(进一步减去当前TCP报文段的序号值得到其头部中的紧急偏移值),如下图:
发送端一次发送的多字节的带外数据中只有最后一个字节被当做带外数据(字母c),而其他数据被当成了普通数据(字母a和b),在后面的例子中我们可以看到这一现象。如果TCP用多个报文来发送上图所示发送缓冲区中的内容,则每个TCP报文段都将设置URG标志,并且它们的紧急指针指向同一个位置(数据流中带外数据的下一个位置),但只有一个TCP报文段真正携带带外数据。
TCP带外数据的接受过程
默认接受方式
TCP接收端只有在接收到紧急指针标记时才检查紧急指针,然后根据紧急指针所指的位置确定带外数据的位置,并将它读入只有一个字节的带外缓存中。如果上层应用没有及时将带外数据从带外缓存中读出,则后续的带外数据将覆盖它。
另一种接收方式
如果我们给TCP连接设置了SO_OOBINLINE选项,则带外数据和普通数据一样被TCP模块存放在TCP接受缓冲区中。此时应用程序需要向读取普通数据一样来读取带外数据,socket编程接口我们提供了系统调用sockatmark来识别带外数据。其函数原型为:
#include <sys/socket.h>
int sockatmark(int sockfd);
其判断sockfd是否处于带外标记,即下一个被读到的数据是否是带外数据。如果是 ,sockatmark返回1。如果不是,则返回0。
应用程序如何接受和发送带外数据
应用数据通过recv和send的MSG_OOB标志来接受和发送带外数据。
发送带外数据:
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<arpa/inet.h>
int main(int argc,const char* argv[])
{
if(argc < 3)
{
printf("usage: %s ip_address port_number\n",argv[0]);
exit(1);
}
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in connaddr;
socklen_t connaddrlen = sizeof(connaddr);
connaddr.sin_family = AF_INET;
connaddr.sin_port = htons(port);
inet_pton(AF_INET,ip,&connaddr.sin_addr);
int connfd = socket(AF_INET,SOCK_STREAM,0);
int ret = connect(connfd,(struct sockaddr*)&connaddr,sizeof(connaddr));
if(ret == -1)
{
perror("connect error:");
exit(1);
}
sleep(1);/*连接后等待一秒,使服务器将工作准备好,比如SIGURG信号函数的注册*/
const char* oob_data = "abc";
const char* ordinary_data = "123";
ret = send(connfd,ordinary_data,strlen(ordinary_data),0);
ret = send(connfd,oob_data,strlen(oob_data),MSG_OOB);
ret = send(connfd,ordinary_data,strlen(ordinary_data),0);
sleep(1);/*退出前停留一秒,避免服务器因为对端关闭而结束,导致读不到带外数据*/
close(connfd);
return 0;
}
接受带外数据:
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<arpa/inet.h>
int main(int argc,const char* argv[])
{
if(argc < 3)
{
printf("usage: %s ip_address port_number\n",argv[0]);
exit(1);
}
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in listenaddr,connaddr;
socklen_t connaddrlen = sizeof(connaddr);
socklen_t listenaddrlen = sizeof(listenaddr);
listenaddr.sin_family = AF_INET;
listenaddr.sin_port = htons(port);
inet_pton(AF_INET,ip,&listenaddr.sin_addr);
int listenfd = socket(AF_INET,SOCK_STREAM,0);
bind(listenfd,(struct sockaddr*)&listenaddr,sizeof(listenaddr));
listen(listenfd,5);
int connfd = accept(listenfd,(struct sockaddr*)&connaddr,&connaddrlen);
if(connfd == -1)
{
perror("accpet error:");
exit(1);
}
char buf[BUFSIZ];
memset(buf,'\0',sizeof(buf));
int ret = recv(connfd,buf,sizeof(buf),0);
printf("get %d bytes of ordinary data '%s'\n",ret,buf);
memset(buf,'\0',sizeof(buf));
ret = recv(connfd,buf,sizeof(buf),MSG_OOB);
printf("get %d bytes of oob data '%s'\n",ret,buf);
memset(buf,'\0',sizeof(buf));
ret = recv(connfd,buf,sizeof(buf),0);
printf("get %d bytes of ordinary data '%s'\n",ret,buf);
close(connfd);
close(listenfd);
return 0;
}
运行程序得到以下结果:
由此可见,客户端发送给服务器的3个字节的带外数据"abc"中,只有最后一个字符"c"被服务器当成真正的带外数据接受,并且服务器对正常的数据的接收将被带外数据截断。
通过tcpdump抓包后,带有URG标记的TCP报文段内容如下:
Flags里的U说明该TCP报文头部标志位里有URG标志,urg 3 是紧急偏移值,它指出带外数据在字节流中的位置的下一个字节位置是7(3+4,其中4是该TCP报文的序号值,即图中的seq 4),所以带外数据是字节流中的第6字节,即字符’c’。
应用程序检查带外数据是否到达的方法
I/O复用系统调用的异常事件
socket上接收到带外数据会使,I/O复用中的慢速系统调用返回异常事件,下例代码示范了select是如何同时处理普通数据和带外数据:
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<arpa/inet.h>
int main(int argc,const char* argv[])
{
if(argc < 3)
{
printf("usage: %s ip_address port_number\n",argv[0]);
exit(1);
}
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in listenaddr,connaddr;
socklen_t connaddrlen = sizeof(connaddr);
socklen_t listenaddrlen = sizeof(listenaddr);
listenaddr.sin_family = AF_INET;
listenaddr.sin_port = htons(port);
inet_pton(AF_INET,ip,&listenaddr.sin_addr);
int listenfd = socket(AF_INET,SOCK_STREAM,0);
bind(listenfd,(struct sockaddr*)&listenaddr,sizeof(listenaddr));
listen(listenfd,5);
int connfd = accept(listenfd,(struct sockaddr*)&connaddr,&connaddrlen);
if(connfd == -1)
{
perror("accpet error:");
exit(1);
}
char buf[BUFSIZ];
fd_set read_fds;
fd_set excption_fds;
FD_ZERO(&read_fds);
FD_ZERO(&excption_fds);
while(1)
{
FD_SET(connfd,&read_fds);
FD_SET(connfd,&excption_fds);
int ret = select(connfd + 1,&read_fds,NULL,&excption_fds,NULL);
if(ret == -1)
{
printf("selec error:");
exit(1);
}
if(FD_ISSET(connfd,&read_fds))
{
memset(buf,'\0',sizeof(buf));
ret = recv(connfd,buf,sizeof(buf),0);
if(ret <= 0)
break;
printf("get %d bytes of normal data: '%s'\n",ret,buf);
}
if(FD_ISSET(connfd,&excption_fds))
{
memset(buf,'\0',sizeof(buf));
ret = recv(connfd,buf,sizeof(buf),MSG_OOB);
if(ret <= 0)
break;
printf("get %d bytes of oob data: '%s'\n",ret,buf);
}
}
close(connfd);
close(listenfd);
return 0;
}
运行结果为:
SIGURG信号
在sockfd上到来的带外数据会触发SIGURG信号,我们需要指定那个进程或进程组来收到由sockfd上的带外数据触发的SIGURG信号。
#include<stdio.h>
#include<errno.h>
#include<signal.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<assert.h>
static int connfd;
void handler(int signum)
{
char buf[BUFSIZ];
memset(buf,'\0',sizeof(buf));
int ret = recv(connfd,buf,sizeof(buf),MSG_OOB);
printf("get %d bytes of oob data: '%s'\n",ret,buf);
}
void addsig(int sig,void (*handler)(int)){
struct sigaction sa;
memset(&sa,'\0',sizeof(sa));
sa.sa_handler = handler;
assert(sigaction(sig,&sa,NULL) != -1);
}
int main(int argc,const char* argv[])
{
if(argc < 3)
{
printf("usage: %s ip_address port_number\n",argv[0]);
exit(1);
}
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in listenaddr,connaddr;
socklen_t connaddrlen = sizeof(connaddr);
socklen_t listenaddrlen = sizeof(listenaddr);
listenaddr.sin_family = AF_INET;
listenaddr.sin_port = htons(port);
inet_pton(AF_INET,ip,&listenaddr.sin_addr);
int listenfd = socket(AF_INET,SOCK_STREAM,0);
bind(listenfd,(struct sockaddr*)&listenaddr,sizeof(listenaddr));
listen(listenfd,5);
connfd = accept(listenfd,(struct sockaddr*)&connaddr,&connaddrlen);
if(connfd == -1)
{
perror("accpet error:");
exit(1);
}
addsig(SIGURG,handler);
/*使用SIGURG前,必须指定socket的宿主进程或进程组,意思是当
该socket上有带外数据时,将SIGURG信号发送给那个进程或者进程组*/
int ret = fcntl(connfd,F_SETOWN,getpid());
if(ret == -1)
{
printf("fcntl error:");
}
char buf[BUFSIZ];
while(1)
{
memset(buf,'\0',sizeof(buf));
int ret = recv(connfd,buf,sizeof(buf),0);
if(ret <= 0)
break;
printf("get %d bytes of normal data: '%s'\n",ret,buf);
}
close(connfd);
close(listenfd);
return 0;
}
运行结果如下:
参考:Linux高性能服务器编程 游双