码迷,mamicode.com
首页 > 其他好文 > 详细

带外数据

时间:2019-11-29 21:03:34      阅读:102      评论:0      收藏:0      [点我收藏+]

标签:connect   决定   要求   分布   val   不同的   print   注意   err   

  许多传输层有带外数据的概念,它有时也称为经加速数据。其想法是一个连接的某端发生了重要的事情,而且该端希望迅速通告其对端。这里“迅速”意味着这种通知应该在已排队等待发送的任何“普通”(有时称为“带内”)数据之前发送。也就是说,带外数据被认为具有比普通数据更高的优先级。带外数据并不需要在客户和服务器之间再使用一个连接,而是被映射到已有的连接中。

  不幸的是,一旦超越普通概念光临现实世界,我们发现几乎每个传输层都各自有不同的带外数据实现。而UDP作为一个极端的例子,没有实现带外数据。

TCP带外数据

  TCP并没有真正的带外数据,不过提供了我们接着要说的紧急模式。假设一个进程已经往一个TCP套接字写出N字节数据,而且TCP把这些数据排队在该套接字的发送缓冲区中,等着发送到对端。如图展示了这样的套接字发送缓冲区,并且标记了从1到N的数据字节。

技术图片

  该进程接着以MSG_OOB标志调用send函数写出一个含有ASCII字符a的单字节带外数据:

send(fd,"a",1,MSG_OOB);

  TCP把这个数据放置在该套接字发送缓冲区的下一个可用位置,并把该连接的TCP紧急指针设置成再下一个可用位置。如图展示了此时的套接字发送缓冲区,并且把带外字节标记为:“OOB”。

技术图片

  TCP紧急指针对应一个TCP序列号,它是使用MSG_OOB标志写出的最后一个数据字节(即带外字节)对应的序列号加1。

       给定的上图所示的TCP套接字发送缓冲区状态,发送端TCP将为待发送的下一个分节在TCP首部中设置URG标志,并把紧急偏移字段设置为指向带外字节之后的字节,不过该分节可能含也可能不含我们标记的OOB的那个字节。OOB字节是否发送取决于在套接字发送缓冲区中先于它的字节数,TCP准备发送给对端的分节大小以及对端通告的当前窗口。

       这是TCP紧急模式的一个重要特点:TCP首部指出发送端已经进入紧急模式(即伴随紧急偏移的URG标志已经设置),但是由紧急指针所指的实际数据字节却不一定随同送出。事实上即使发送端TCP因流量控制而暂停发送数据(接收端的套接字接收缓冲区已满,导致其TCP想发送端TCP通告了一个值为0 的窗口),紧急通知照样不伴随任何数据的发送。这也是应用进程使用TCP紧急模式(即带外数据)的一个原因:即便数据的流动会因为TCP的流量控制而停止,紧急通知却总是无障碍的发送到对端TCP。

  紧急指针和紧急偏移在tcp层面是不同的,tcp首部中的16位值称为紧急指针,他必须加上同一个首部的序列号字段才能获得32位的紧急指针,只有在同一个首部中称为URG标志的位已经设置的前提下,tcp才会检查紧急偏移。

       如果我们发送多个字节的带外数据,情况又会任何呢?例如:

send(fd,"abc",3,MSG_OOB);

  在这个例子中,TCP的紧急指针指向最后那个字节紧后的位置,也就是说最后那个字节(字母c)被认为是带外字节,注意仅仅是一个字节。

  至此我们已经讲述了带外数据的发送,下面从接收端的角度查看一下:

  1.当收到一个设置了URG标志的分节时,接收端TCP检查紧急指针,确实它是否指向新的带外数据,也就是判断本分节是不是首个到达的引用从发送端到接收端的数据流中特定字节的紧急模式分布。发送端TCP往往发送多个含有URG标志且紧急指针指向同一个数据字节的分节(通常是在已小段时间内)。这些分节中只有第一个到达的会导致通知接收进程有新的带外数据到达。

  2.当有新的紧急指针到达时,接收进程被通知到。首先,内核给接收套接字的属主进程发送SIGURG信号,前提是接收进程(或其他进程)曾调用fcntl或ioctl为这个套接字建立了属主,而且该属主进程已为这个信号建立了信号处理函数。其次,如果接收进程阻塞在select调用中以等待这个套接字描述符出现一个异常条件,select调用就返回。

  一旦有新的紧急指针到达,不论由紧急指针指向的实际数据字节是否已经到达接收端TCP,这两个潜在通知接收进程的手段就发生动作。

  只有一个OOB标记,如果新的OOB字节在旧的OOB字节被读取之前就到达,旧的OOB字节会被丢弃。

  3.当由紧急指针指向的实际数据字节到达接收端TCP,该数据字节既可能被拉出带外,也可能被留在带内,即在线留存。SO_OOBINLINE套接字选项默认情况下是禁止的,对于这样的接收端套接字,该数据字节并不放入套接字接收缓冲区,而是被放入该连接的一个独立的单字节带外缓冲区。接收进程从这个单字节缓冲区读入数据的唯一方法是制定MSG_OOB标志调用recv,recvfrom或recvmsg。如果新的OOB字节在旧的OOB字节被读取之前就到达,旧的OOB字节会被丢弃。

       然而如果接收进程开启了SO_OOBINLINE套接字选项,那么由TCP紧急指针指向的实际数据字节将被留在通常的套接字接收缓冲区中。这种情况下,接收进程不能指定MSG_OOB标志读入该数据字节。相反,接收进程通过检查该连接的带外标记以获悉何时访问到这个数据字节。 

       发生一些错误是可能的:

  1. 如果接收进程请求读入带外数据(通过指定MSG_OOB标志),但是对端尚未发送任何带外数据,读入操作将返回EINVAL。
  2. 在接收进程已被告知对端发送了一个带外字节(通过SIGURG或select手段)的前提下,如果接收进程读入该字节,但是该字节尚未到达,读入操作将返回EWOULDBLOCK。接收进程此时能做的仅仅是从套接字接收缓冲区读入数据(要是没有存放这些数据的空间,可能还得丢弃他们),以便在该缓冲区中腾出空间,继而允许对端TCP发送出那个带外字节。
  3. 如果接收进程试图多次读入同一个带外字节,读入操作将返回EINAVL。(后面有说明:select改进)
  4. 如果接收进程已经开启SO_OOBINLINE套接字选项,后来试图通过指定的MSG_OOB标志读入带外数据,读入操作将返回EINVAL。

使用SIGURG的简单例子

  发送和接受带外数据的例子

int main(int argc, char **argv)
{
    int        sockfd;

    if (argc != 3)
        err_quit("usage: tcpsend01 <host> <port#>");

    sockfd = Tcp_connect(argv[1], argv[2]);

    Write(sockfd, "123", 3);
    printf("wrote 3 bytes of normal data\n");
    sleep(1);//间以停顿的目的是让每个write或send的数据作为单个TCP分节在本端发送并在对端接受

    Send(sockfd, "4", 1, MSG_OOB);
    printf("wrote 1 byte of OOB data\n");
    sleep(1);

    Write(sockfd, "56", 2);
    printf("wrote 2 bytes of normal data\n");
    sleep(1);

    Send(sockfd, "7", 1, MSG_OOB);
    printf("wrote 1 byte of OOB data\n");
    sleep(1);

    Write(sockfd, "89", 2);
    printf("wrote 2 bytes of normal data\n");
    sleep(1);

    exit(0);
}

  该程序共发送9个字节,每个输出操作之间有一个1S的sleep。停顿的目的是让每个write或send的数据作为单个TCP分节在本端发送并在对端接收。程序运行结果:

wrote 3 bytes of normal date
wrote 1 bytes of OOB data
wrote 2 bytes of normal date
wrote 1 bytes of OOB data
wrote 2 bytes of normal date

  下面是接受程序

#include "unp.h"
int listenfd, connfd;
void sig_urg(int);
int main(int argc, char * * argv)
{
  int n;
  char buff[100];
  if(argc == 2)
    listenfd = Tcp_listen(NULL, argv[1], NULL);
  else if(argc = 3)
           listenfd = Tcp_listen(argv[1], argv[2], NULL);
         else
           err_quit("usage: tcprecv01 [ <host> ] <port#>");
  connfd = Accept(listen, NULL, NULL);
  Signal(SIGURG, sig_urg); /* 建立SIGURG的信号处理程序 */
  Fcntl(connfd, F_SETOWN, getpid()); /* 并用fcntl设置已连接套接口的属主 */
  for( ; ; )/* 进程从套接口中读,输出每个由read返回的字符串,当发送者终止连接后,接收者也就终止了 */
  {
    if( (n = Read(connfd, buff, sizeof(buff)-1) ) == 0 )
    {
      printf("recived EOF\n");
      exit(0);
    }
    buff[n] = 0; /* null terminate */
    printf("read %d bytes: %s\n", n, buff);
  }
}
void sig_urg(int signo)
{
  int n;
  char buff[100];
  printf("SIGURG received\n");
  n = Recv(connfd, buff, sizeof(buff)-1, MSG_OOB);
  buff[n] = 0; /* null terminate */
  printf("read %d OOB byte: %s\n", n, buff);
}

  直到accept返回之后才建立信号处理函数,这么做会错过一些以小概率出现的带外数据,他们在TCP完成三次握手之后但在accept返回之前到达。然而如果我们在调用accept之前信号处理函数并设置监听套接字的属主(本属性将传承给已连接套接字),那么如果带外数据在accept之前到达,我们的信号处理函数将没有真正的connfd值可用。如果这种情形对于应用程序确实重要,它就应该把connfd初始化为-1,在信号处理函数中检查该值是否为-1,若为真则简单的设置一个标志,供主循环在accept返回之后检查。另一方面,这可能阻塞accept调用周围的信号。

  运行结果

read 3 bytes:123
SIGURG received
read 1 OOB byte:4
read 2 bytes:56
SIGURG received
read 1 OOB byte:7
read 2 bytes:89
received EOF

  结果与我们预期的一致。发送进程带外数据的每次发送产生递交给接收进程的SIGURG信号,后者接着读入单个带外字节。

使用select的简单例子

  select一直指示一个异常条件,直到进程的读入越过带外数据。同一个带外数据不能读入多次,因为首先读入之后,内核就清空这个单字节的缓冲区。再次指定MSG_OOB标志调用recv时,它将返回EINVAL

#include    "unp.h"
 
int
main(int argc, char **argv)
{
    int        listenfd, connfd, n;
    char    buff[100];
    fd_set    rset, xset;
 
    if (argc == 2)
        listenfd = Tcp_listen(NULL, argv[1], NULL);
    else if (argc == 3)
        listenfd = Tcp_listen(argv[1], argv[2], NULL);
    else
        err_quit("usage: tcprecv02 [ <host> ] <port#>");
 
    connfd = Accept(listenfd, NULL, NULL);
 
    FD_ZERO(&rset);
    FD_ZERO(&xset);
    for ( ; ; ) {
        FD_SET(connfd, &rset);
        FD_SET(connfd, &xset);
 
        Select(connfd + 1, &rset, NULL, &xset, NULL);
 
        if (FD_ISSET(connfd, &xset)) {
            n = Recv(connfd, buff, sizeof(buff)-1, MSG_OOB);
            buff[n] = 0;        /* null terminate */
            printf("read %d OOB byte: %s\n", n, buff);
        }
 
        if (FD_ISSET(connfd, &rset)) {
            if ( (n = Read(connfd, buff, sizeof(buff)-1)) == 0) {
                printf("received EOF\n");
                exit(0);
            }
            buff[n] = 0;    /* null terminate */
            printf("read %d bytes: %s\n", n, buff);
        }
    }
}

  上述代码是错误的,运行结果如下:

read 3 bytes:123
read 1 OOB byte:4
recv error:Invalid argument

   解决办法是只在读入普通数据之后select异常条件。它正确的处理了上述情形,以下是正确的代码

#include "unp.h"
int main(int argc, char * * argv)
{
  int listenfd, connfd, n, justreadoob = 0; /* 声明了一个叫做justreadoob的变量来指示我们是否刚读过带外数据,这个标志决定是否select异常条件 */
  char buff[100];
  fd_set rset, xset;
  if(argc == 2)
    listenfd = Tcp_listen(NULL, argv[1], NULL);
  else if(argc == 3)
          listenfd = Tcp_listen(argv[1], argv[2], NULL);
        else
          err_quit("usage: tcprecv03 [ <host> ] <port#>");
  connfd = Accept(listenfd, NULL, NULL);
  FD_ZERO(&rset);
  FD_ZERO(&xset);
  for( ; ; )
  {
    FD_SET(connfd, &rset);
    if(justreadoob == 0)
      FD_SET(connfd, &xset);
    
    Select(connfd+1, &rset, NULL, &xset, NULL);
    
    if(FD_ISSET(connfd, &xset))
    {
      n = Recv(connfd, buff, sizeof(buff)-1, MSG_OOB);
      buff[n] = 0; /* null terminate */
      printf("read %d OOB bytes: %s\n", n, buff);
      justreadoob = 1; /* 当我们设置justreadoob标志时,我们还必须清除这个描述字在异常集合中的那一位 */
      FD_CLR(connfd, &xset);
    }
    if(FD_ISSET(connfd, &rset))
    {
      if( ( n = Read(connfd, buff, sizeof(buff)-1) ) == 0 )
      {
        printf("received EOF\n");
        exit(0);
      }
      buff[n] = 0;  /* null terminate */
      printf("read %d bytes: %s \n", n, buff);
      justreadoob = 0;
    }
  }
}

 

总结

  1. 发送端进入紧急模式这个事实。接收进程得以通知这个事实的手段不外乎SIGURG信号或select调用。本通知在发送进程发送带外字节后由发送端TCP立即发送,即使往接收端的任何数据发送因流量控制而停止了,TCP仍然发送本通知。本通知可能导致接收端进入某种特殊处理模式,以处理接收的任何后继数据。
  2. 带外字节的位置,也就是它相对于来自发送端的其余数据的发送位置:带外标记。
  3. 带外字节的实际值。既然TCP是一个不解释应用进程所发送数据的字节流协议,带外字节就可以是任何8位值。

  对于TCP的紧急模式,我们可以认为URG标志时通知(信息1),紧急指针是带外标记(信息2),数据字节是其本身(信息3)。
  与这个带外数据概念相关的问题有:

  1. 每个连接只有一个TCP紧急指针;
  2. 每个连接只有一个带外标记;
  3. 每个连接只有一个单字节的带外缓冲区(该缓冲区只有在数据非在线读入时才需考虑)。如果带外数据时在线读入的,那么当心的带外数据到达时,先前的带外字节字节并未丢失,不过他们的标记却因此被新的标记取代而丢失了。

  带外数据的一个常见的用途体现在rlogin程序中。当客户中断运行在服务器主机上的程序时,服务器需要告知客户丢弃所有已在服务器排队的输出,因为已经排队等着从服务器发送到客户的输出最多有一个窗口的大小。服务器向客户发送一个特殊字节,告知后者清刷所有这些输出(在客户看来是输入),这个特殊字节就作为带外数据发送。客户收到由带外数据引发的SIGURG信号后,就从套接字中读入直到碰到带外数据发送。客户收到由带外数据引发的SIGURG信号后,就从套接字中读入直到碰到带外标记,并丢弃到标记之前的所有数据。这种情形下即使服务器相继地快速发送多个带外字节,客户也不受影响,因为客户只是读到最后一个标记为止,并丢弃所有读入的数据。

  总之,带外数据是否有用取决于应用程序使用它的目的。如果目的是告知对端丢弃直到标记处得普通数据,那么丢失一个中间带外字节及其相应的标记不会有什么不良后果。但是如果不丢失带外字节本身很重要,那么必须在线收到这些数据。另外,作为带外数据发送的数据字节应该区别于普通数据,因为当前新的标记到达时,中间的标记将被覆写,从而事实上把带外字节混杂在普通数据之中。举例来说,telnet在客户和服务器之间普通的数据流中发送telnet自己的命令,手段是把值为255的一个字节作为telnet命令的前缀字节。(值为255的单个字节作为数据发送需要2个相继地值为255的字节。)这么做使得telnet能够区分其命令和普通用户数据,不过要求客户进程和服务器进程处理每个数据字节以寻找命令。

 

带外数据

标签:connect   决定   要求   分布   val   不同的   print   注意   err   

原文地址:https://www.cnblogs.com/tianzeng/p/11960218.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!