首先给出OSI
参考模型与TCP/IP
协议模型图:
理论
数据分两种,一种是带内数据,一种是带外数据。 带内数据就是我们平常传输或者说是口头叫的数据。带外数据就是我们接下来讲的内容。
许多的传输层都具有带外数据(也就是经加速数据)的概念,想法就是连端发生了重要的事情,希望迅速的通知给对端。这里的迅速是指这种通知应该在已经排队了的带内数据之前发送。也就是说,带外数据具有更高的优先级。带外数据不要求再启动一个连接进行传输,而是使用已有的连接进行传输。
为什么需要带外数据
有时数据 会以一定的方式变得紧急。一个流套接口会有一个大量的数据 队列等待发送到网络。
在远程端点,也会有大量已接收的,却还没有被 程序读取的数据 。
如果发送客户端程序由于一些原因需要取消已经写入服务器的请求,那么他就需要向服务器紧急发送一个标识取消的请求。
如果向远程服务器发送 取消请求失败,那么就会无谓的浪费服务器的资源。
哪里用到了带外数据
其中, UDP
没有实现带外数据
TCP中telnet,rlogin,ftp等应用(除了这样的远程非活跃应用之外,几乎很少有使用到带外数据的地方)
- telnet,rlogin命令会将中止字符作为紧急数据 发送到远程端。这会允许远程端冲洗所有未处理 的输入,并且丢弃所有未发送的终端输出。
- ftp命令使用带 外 数据 来中断一个文件的传输。
TCP带外数据
我们先来看看 TCP包的数据格式。
针对带外数据,我们只关心下面两个部分:
- 紧急字段
URG
:设置URD=1
,以告知系统此报文段中有紧急数据,应尽快传送。 - 紧急指针:指出在本报文段中的紧急数据的最后一个字节的序号,即指出带外数据字节在正常字节流中的位置。
所以当TCP发送带外数据时,他的TCP首部一定是设置了URG标志和紧急指针的 。而紧急指针就是用来指出带外数据字节在正常字节流中的位置的 。
发送端
- 紧急数据是插在正常数据流中进行传输的
- 一个紧急指针只指向一个字节的带外数据的后一个字节位置。比如我们要发送数据1,2,3,4,5,6 ,7,8,如果我们只发送一个字节的带外数据X,那么发送缓冲区就是(1,2,3,4,5,6,7,8,X),紧急指针置为10,X是带外数据字节 。如果我们发送多个字节的带外数据(X,Y,Z),那么发送缓冲区就是(1,2,3,4,5,6,7,8,X,Y,Z),紧急指针指向Z的后面,为12 ,Z 被当作带外数据字节。
- 假如由于发送窗口的关系,导致该发送缓冲区中的数据(1,2,3,4,5,6,7,8,X)分为多次或者两次发送。比如:发送窗口是6,那么就分为两个包发送,情况如下:第一个包紧急指针为10,传送六个字节(1,2,3,4,5,6),接收端记下接受的字节数并且发现紧急指针指向的紧急数据没有到达,所以继续等待下一个包,下一个包(紧急指针还是10)发过来 7,8,X ,接收端发现紧急指针指向的紧急数据在这个包里,所以将紧急数据进行处理即可。
- 带外字节会被标记为
OOB
- 即使发送端TCP因流量控制而暂停发送数据(接受缓冲区的套接字接受缓冲区已满,导致其TCP向发送端通告了一个值为0 的窗口),紧急通知照样不伴随任何数据的发送。也就是说:即使数据的流动会因为TCP的流量控制而停止,紧急通知却总是无障碍的发送到对端TCP。
接收端
一旦有一个新的紧急指针到达,那么无论由紧急指针指向的实际数据字节是否已经到达了接收端,以下两个动作都会发生:
- 当接收到一个设置了
URG
标志的分节时,接收端检查紧急指针,确定它是否指向新的带外数据,比如:前面发送了两个包,只有第一个才会通知接受进程有新的带外数据到达 - 当有新的紧急指针到达时,接受进程被通知到。首先,内核会给接收套接字的属主进程发送
SIGURG
信号,前提是接收进程调用了fcntl
或者ioctl
为这个套接字建立了属主,并且该属主进程为该信号建立了信号处理函数
带外标记
实际应用中,我们无法预期带外数据何时到来。而Linux内核检测到TCP紧急标记时,将通知应用程序有带外数据需要接受。内核通信应用程序带外数据到达的两种常见方式是:
- IO复用产生的异常事件
- SIGURG信号
但是,即使应用程序得到了有带外数据需要接受的通知,还需要知道带外数据在数据流中的具体位置,才能准确的接收带外数据。这一点可以通过下面系统调用实现:
#include <sys/socket.h>
/*
* 说明:判断下一个被读取到的数据是否是带外数据:
* 如果是,返回1.这时我们可以利用带MSG_OOB标记的recv调用来接收带外数据
* 如果不是,返回0
*/
int sockatmark(int sockfd);
实践
1:紧急指针的实现
TCP 的实现在紧急数据 就如何处理上有两种不同的解释:
- TCP 紧急指针的RFC793解释
- TCP 紧急指针的BSD解释
Linux默认使用BSD解释。
$ cat /proc/sys/net/ipv4/tcp_stdurg
0
0表示当前起作的为BSD解释(默认)
1表示 RFC793解释
当然我们也可以修改为RFC793解释。但是不建议修改
echo 0 >/proc/sys/net/ipv4/tcp_stdurg
2:TCP发送带外数据
1、服务器接收带外数据
#include <string.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <zconf.h>
#include <errno.h>
int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp)
{
int listenfd, n;
const int on = 1;
struct addrinfo hints, *res, *ressave;
bzero(&hints, sizeof(struct addrinfo));
hints.ai_flags = AI_PASSIVE;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0){
printf("tcp_listen error for %s, %s: %s",
host, serv, gai_strerror(n));
exit(0);
}
ressave = res;
do {
listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (listenfd < 0)
continue; /* error, try next one */
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0)
break; /* success */
close(listenfd); /* bind error, close and try next one */
} while ( (res = res->ai_next) != NULL);
if (res == NULL){
printf("tcp_listen error for %s, %s", host, serv);
exit(0);
} /* errno from final socket() or bind() */
listen(listenfd, 1024);
if (addrlenp)
*addrlenp = res->ai_addrlen; /* return size of protocol address */
freeaddrinfo(ressave);
return(listenfd);
}
int Tcp_listen(const char *host, const char *serv, socklen_t *addrlenp)
{
return(tcp_listen(host, serv, addrlenp));
}
#define BUF_SIZE 1024
int main(int argc, char *argv[])
{
if( argc <= 1 )
{
printf( "usage: %s ip_address port\n", basename( argv[0] ) );
return 1;
}
int socket, ret, connfd;
const char* ip = argv[1];
char buffer[BUF_SIZE];
struct sockaddr_in client;
socklen_t addrlen = strlen(ip);
socklen_t client_addrlength = sizeof(client);
socket = Tcp_listen(ip, argv[2], &addrlen);
if(socket < 0){
printf("%s(%d) listen error %s", __FUNCTION__ , __LINE__, strerror(errno));
return 0;
}
connfd = accept(socket, (struct sockaddr*)&client, &client_addrlength);
if(socket < 0){
printf("%s(%d) listen error %s", __FUNCTION__ , __LINE__, strerror(errno));
close(socket);
return 0;
}
memset(buffer, '\0', BUF_SIZE);
ret = recv(connfd, buffer, BUF_SIZE - 1, 0);
printf( "got %d bytes of normal data '%s'\n", ret, buffer );
memset( buffer, '\0', BUF_SIZE );
ret = recv( connfd, buffer, BUF_SIZE-1, MSG_OOB );
printf( "got %d bytes of oob data '%s'\n", ret, buffer );
memset( buffer, '\0', BUF_SIZE );
ret = recv( connfd, buffer, BUF_SIZE-1, 0 );
printf( "got %d bytes of normal data '%s'\n", ret, buffer );
close(connfd);
close(socket);
return 0;
}
启动:
2、客户端发送带外数据
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
struct sockaddr_in server_address;
bzero( &server_address, sizeof( server_address ) );
server_address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &server_address.sin_addr );
server_address.sin_port = htons( port );
int sockfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( sockfd >= 0 );
if ( connect( sockfd, ( struct sockaddr* )&server_address, sizeof( server_address ) ) < 0 )
{
printf( "connection failed\n" );
}
else
{
printf( "send oob data out\n" );
const char* oob_data = "abc";
const char* normal_data = "123";
send( sockfd, normal_data, strlen( normal_data ), 0 );
send( sockfd, oob_data, strlen( oob_data ), MSG_OOB );
send( sockfd, normal_data, strlen( normal_data ), 0 );
}
close( sockfd );
return 0;
}