一、带外数据
- 有些传输层协议具有带外(Out Of Band,OOB)数据的概念,用于迅速通告对方本端发生的重要事件。因此,带外数据比普通数据(也称为带内数据)有更高的优先级,它应该总是立即被发送,而不论发送缓冲区中是否有排队等待发送的普通数据。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中
- 实际应用中,带外数据的使用很少见,已知的仅有telnet、ftp等远程非活跃程序
- UDP没有实现带外数据传输,TCP也没有真正的带外数据(其实现过程见下)
TCP实现原理
- TCP利用其头部中的紧急指针标志和紧急指针两个字段,给应用程序提供了一种紧急方式
- TCP的紧急方式利用传输普通数据的连接来传输紧急数据。这种紧急数据的含义和带外数据类似,因此后文也将TCP紧急数据成为带外数据
- 带外数据覆盖问题:TCP接收端只有在接收到紧急指针标志时才检查紧急指针,然后根据紧急指针所指的位置确定带外数据的位置,并将它读入一个特殊的缓存中,这个缓存只有1字节,称为带外缓存。如果上层应用程序没有及时将带外数据从带外缓冲中读出,则后续的带外数据将覆盖它
演示案例:
- 假设一个进程已经向某个TCP连接的发送缓冲区写入了N字节的普通数据,并等待其发送。在数据发送前,该进程又向这个连接写入了3字节的带外数据“abc”。此时,待发送的TCP报文段的头部将被设置URG标志,并且紧急指针被设置为指向最后一个带外数据的下一字节(进一步减去当前TCP报文段的序号值得到其头部中的紧急偏移值),如下所示
- 在上图中,发送端一次发送的多字节的带外数据中只有最后一个字节被当作带外数据(字母c),而其他数据“a和b”被当成了普通数据
- 如果TCP模块以多个TCP报文段来发送上图所示的内容,则每个TCP报文段都将被设置URG标志,并且它们的紧急指针指向同一位置(数据流中带外数据的下一个位置),但只有一个TCP报文真正携带带外数据
- SO_OOBINLINE套接字选项可以设置带外数据标志,recv、send等函数也可以实现(本文介绍的)
二、代码实现
- 下面演示了客户端发送带外数据,服务端接收带外数据的演示案例
客户端
- 客户端在调用了3个send函数,第二个send函数指定了MSG_OOB选项标志
//cli_send.c #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> #include <libgen.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; }
服务端
- 服务端使用了3个recv函数阻塞等待服务端发送消息,其中第2个recv函数指定了MSG_OOB选项
//ser_recv.c #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <libgen.h> #define BUF_SIZE 1024 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 address; bzero( &address, sizeof( address ) ); address.sin_family = AF_INET; inet_pton( AF_INET, ip, &address.sin_addr ); address.sin_port = htons( port ); int sock = socket( PF_INET, SOCK_STREAM, 0 ); assert( sock >= 0 ); int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) ); assert( ret != -1 ); ret = listen( sock, 5 ); assert( ret != -1 ); struct sockaddr_in client; socklen_t client_addrlength = sizeof( client ); int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength ); if ( connfd < 0 ) { printf( "errno is: %d\n", errno ); } else { char buffer[ BUF_SIZE ]; 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 ); 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, 0 ); printf( "got %d bytes of normal data '%s'\n", ret, buffer ); close( connfd ); } close( sock ); return 0; }
演示结果
- 运行服务端和客户端
- 查看服务端接收到的消息:可以看到客户端发送给副武器的3字节的带外数据“abc”中,仅有最后一个字符“c”被服务器当成真正的带外数据接收。并且,服务器对正常数据的接收将被带外数据截断,即前一部分正常数据“123ab”和后续的正常数据“123”是不能被一个recv调用全部读出的
- 我们对54321端口进行抓包,结果显示如下