最近在学习《Linux高性能服务器编程》 这本书,书中零零散散的讲了TCP带外数据的一些知识,在这里把这些知识总结以下,方便自己,也方便他人。
本文主要分为以下四个方面总结,分别为 TCP带外数据的概念,如何发送和接收带外数据,怎么检测带外数据的到达,最后介绍相关函数以及代码实现。
第一部分: TCP带外数据的概念
有很多传输层此协议都具有带外数据(OUT Of Band) 的概念,其作用是迅速通告通信的另一方本段发生的重要事件。带外数据具有比普通数据更高的优先级,理论上应该被立即发送和立即接收。带外数据可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。
UDP并没有实现带外数据,TCP也没有真正的带外数据。
TCP利用其头部中的紧急指针标志以及紧急指针字段,给应用程序提供里一种紧急方式。所以TCP是利用传输普通数据的连接来传输带外数据。
第二部分:TCP带外数据的发送和接收
1. TCP带外数据的发送过程:
假设一个进程已经往某个TCP连接的发送缓冲区写入里N字节的普通数据,并等待其发送。在数据发送前,该进程又向这个连接写入里3个字节的带外数据“abc”。 此时,待发送的TCP报文段头部将被蛇者URG标志,并且紧急指针指向带外数据的下一个字节。
发送端一次发送的多字节带外数据只有最后一个字节被当作是带外数据, 其他数据还是被当作普通数据。
2. TCP带外数据的接收过程
TCP接收端只有在接收到具有紧急标志的TCP报文端时才检查紧急指针,然后根据紧急指针所指的位置确定带外数据的位置,并将其读入一个特殊的缓存中。这个缓存只有一个字节,称为带外缓存。
第三部分:内核如何通知应用程序带外数据的到来
主要有两种方法:
1. IO复用(select方式复用)系统的调用报告一个异常事件;
select系统调用的用途是:在指定的一段时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件。同样,select系统调用可以监听socket上的可读、可写和异常时间。在网络程序中,select能处理的异常情况只有一种:socket上接收到带外数据。
其具体步骤是:首先设置select监听我们关心的socket描述符,当有事件发生时,判断时间类型,如果时间类型是socket异常时间,则表明有紧急数据到来,可以读取紧急数据里。
2. SIGURG信号
其实现方法是:在进程中注册SIGURG信号以及其信号处理函数,当进程接收到SIGURG信号时,在SIGURG的信号处理函数内接收紧急数据。此时信号的模式要为SA_RESTART,表示被信号中断的系统调用在信号返回时继续进行。
第四部分:相关函数及代码实现
1.相关函数
(1) 发送接收带外数据和发送接收普通数据一样,都可以使用send()和recv()函数。其区别在于函数的最后一个参数 int flags不同。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd,void* buf,size_t len,int flags);
ssize_t send(int sockfd,const void* buf,size_t len,int flags);
//将fags 设置为 MSG_OOB 表示结束和发送带外数据。
(2) select 系统调用相关函数
#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
//nfds参数表示监听的文件描述符总数,一般取监听的最大文件描述符 + 1。
//readfds指向需要监听可读事件的描述符集合,writefds指向需要监听可写事件的描述符集合,exceptfds指向需要监听异常//的描述符集合
//
FD_ZERO(fd_set* fdset);//清除描述符集fdset中的所有描述符
FD_SET(int fd,fd_set* fdset);//向描述符集fdset添加描述符fd
FD_CLR(int fd,fd_set* fdset);//在描述符集fdset中清除描述符fd
int FD_ISSET(int fd,fd_set* fdset);//测试描述符集fdset中描述符fd是否被置位
(3) SIGUSR 信号处理相关函数
#include <signal.h>
#include <fcntl.h>
int sigaction (int sig, const struct sigaction* act, struct sigaction* oact);
//sig参数指定要绑定的信号,在这里就是SIGUSR
//act 参数绑定了信号处理函数指针,指定里信号的处理方式以及设置里信号掩码
//其中act.sa_handler 赋值为信号处理函数的指针
//使用SIGUSR之前 ,必须设置socket的宿主进程或者进程组,使用下面的函数
fcntl(sockfd,F_SETOWN,getpid());
2.代码实现
代码实现一个客户端和一个服务器端,客户端既发送带外数据,又发送普通数据,服务器端分别用两种方式实现既接收带外数据又接收普通数据并显示,可通过启动时设置不同的参数来使用不同方式接收。
客户端依次发送普通数据 123,带外数据abc,普通数据123,客户端输入两个参数,参数1服务器地址,参数2服务器监听端口。服务器端接收数据,服务器程序有三个输入参数,参数1 服务器的绑定地址,参数2服务器的绑定端口,参数3为1时用select复用接收,参数3为2时用SIGUSR信号方式处理。
客户端代码:
#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[])//参数1为服务器IP地址,参数2为服务器的监听端口
{
if(argc<=2)
{
printf("Usage: %s ip_address port_number\n",basename(argv[0]));
return 1;
}
const char* ip = argv[1];//获取IP参数
int port = atoi (argv[2]);//获取端口参数
struct sockaddr_in server_address;
memset(&server_address,0,sizeof(server_address));
server_address.sin_family = AF_INET;
inet_pton(AF_INET,ip,&server_address.sin_addr);//设置地址结构体IP
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
{
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;
}
服务器端代码
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <signal.h>
static int connfd;
void sig_urg(int sig)//信号处理函数,在信号处理函数里接收带外数据
{
int save_errno = errno;
char buffer[1024];
memset(buffer,0,1024);
int ret = recv (connfd,buffer,1023,MSG_OOB);//接收带外数据
printf("got %d bytes of oob data ‘%s‘\n",ret,buffer);
errno = save_errno;
}
void addsig(int sig,void(*sig_handler)(int))//注册信号处理函数,设置信号处理方式以及掩码
{
struct sigaction sa;
memset(&sa,0,sizeof(sa));
sa.sa_handler = sig_handler;
sa.sa_flags |= SA_RESTART;
sigfillset(&sa.sa_mask);
assert(sigaction(sig,&sa,NULL)!=-1);
}
int main(int argc, char* argv[])
{
if(argc <= 3)
{
printf("Usage: %s ip_address port_number method\n",basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int method = atoi(argv[3]);
int ret = 0;
struct sockaddr_in address;
memset (&address,0,sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip ,&address.sin_addr);
address.sin_port = htons(port);
int listenfd = socket(PF_INET,SOCK_STREAM,0);//生成套接字
assert(listenfd>=0);
ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address));//绑定套接字
assert(ret!=-1);
ret = listen(listenfd,5);//设置监听
assert(ret!=-1);
struct sockaddr_in client_address;//客户端地址
socklen_t client_addrlength = sizeof(client_address);
connfd = accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength);//接收客户端连接
if(connfd<0)
{
printf("errno is: %d\n",errno);
close(listenfd);
}
if(method == 1)//select复用模式接收带外数据
{
char buf[1024];
fd_set read_fds;//需要监听可读信息的描述符集
fd_set exception_fds;//需要监听异常信息的描述符集
FD_ZERO(&read_fds);//清空描述符集
FD_ZERO(&exception_fds);//清空异常描述符集
while(1)
{
memset(buf,‘\0‘,sizeof(buf));
FD_SET(connfd,&read_fds);//将套接字加入监听可读描述符集
FD_SET(connfd,&exception_fds);//将套接字加入监听异常描述符集
ret = select(connfd+1,&read_fds,NULL,&exception_fds,NULL);//开始复用监听
if(ret < 0)
{
printf("selection failure\n");
break;
}
if(FD_ISSET(connfd,&read_fds))//有正常数据到来可读
{
memset(buf,‘\0‘,sizeof(buf));
ret = recv(connfd,buf,sizeof(buf)-1,0);
if(ret <= 0)
{
break;
}
printf("get %d bytes of normal data: %s\n",ret,buf);
}
if(FD_ISSET(connfd,&exception_fds))//有异常发生,即有带外数据到来
{
FD_CLR(connfd,&exception_fds);//清除描述符集中的相应位,否则,程序工作不正常
memset(buf,‘\0‘,sizeof(buf));
ret = recv(connfd,buf,sizeof(buf)-1,MSG_OOB);
if(ret <= 0)
{
break;
}
printf("get %d bytes of oob data: %s\n",ret,buf);
FD_SET(connfd,&exception_fds);//重新加入描述符集
}
}
close(connfd);//关机套接字
close(listenfd);//关闭监听套接字
return 0;
}
else if(method == 2)//利用SIGURG检测带外数据是否到达
{
addsig(SIGURG,sig_urg);//注册信号集信号处理函数
fcntl(connfd,F_SETOWN,getpid());//设置套接字的主进程
char buffer[1024];
while(1)
{
memset(buffer,0,1024);
ret = recv(connfd,buffer,1023,0);
if(ret <= 0)
{
break;
}
printf("got %d bytes of normal data ‘%s‘\n",ret,buffer);
}
close(connfd);
close(listenfd);
return 0;
}
}
以上就是我对TCP带外数据相关知识的总结,由于我是个学生,没有实际的经验,纸上得来终觉浅,所以借总结写一写来加深印象。欢迎批评指正!