带外数据

概述

许多传输层有带外数据的概念,有时候也称经加速数据。其想法是一个连接的某端发生了重要的事情,而且该端希望迅速通告其对端。这里的“迅速”意味着这种通知在已经排队等待发送的任何“普通”数据之前发送,也就是优先级更高。

 

  • TCP带外数据

TCP没有真正的带外数据,不过提供了紧急模式。假设一个进程已经往一个TCP套接字写出N字节数据,而且TCP把这些数据排队在该套接字的发送缓冲区中,等着发送到对端。即

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

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

TCP把这个数据放置在该套接字发送缓冲区的下一个可用位置,并把该链接的TCP紧急指针设置成再下一个可用位置,并把带外字节标记为“OOB”:

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

【注】

  • 紧急指针=TCP首部中的16位值
  • 32位紧急指针=16位值+同一首部中的序列号字段

如果发送多字节的带外数据,情况会如何呢?

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

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

有一些错误是可能的:

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

 

  • 例子:使用SIGURG的简单例子

#include "unp.h"

int main(int argc, char **argv){
    int sockfd;
    if(argc!=3)
        err_quit("usage: tcpsend01 <host> <port#>");
    
    write(sockfd, "123", 3);
    printf("wrote 3 bytes of normal data\n");
    sleep(1);

    send(sockfd, "4", 1, MSG_OOB);
    printf("wrote 1 bytes 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 bytes of OOB data\n");
    sleep(1);

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

    exit(0);
}

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

macosx % topsend01 freebsd4 9999
wrote 3 bytes of normal data
wrote 1 bytes of OOB data
wrote 2 bytes of normal data
wrote 1 bytes of OOB data
wrote 2 bytes of normal data
  • 接收程序

#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(listenfd, NULL, NULL);
    
    signal(SIGURG,sig_urg);

    //设置套接字的属主
    fcntl(connfd, F_SETOWN, getpid());

    for( ; ; ){
        if((n = read(connfd, buff, sizeof(buff)-1)) == 0){
            printf("received EOF\n");
            exit(0);
        }
        buff[n] = 0;
        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;
    printf("read %d OOB byte: %s\n", n, buff);
}

运行本程序,可得到结果如下:

freebsd4 % tcprecv01 9999
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
  • sockatmark函数

每当收到一个带外数据时,就有一个与之关联的带外标记。这是发送进程发送带外字节时该字节在发送端普通数据流中的位置。在从套接字读入期间,接收进程通过调用sockatmark函数确定是否处于带外标记。

#include <sys/socket.h>

int sockatmark(int sockfd);

//返回:若处于带外标记则为1,若不处于带外标记则为0,若出错则为-1

以下是常见的SIOCATMARK ioctl完成本函数的一个实现:

#include "unp.h"

int sockatmark(int fd){
    int flag;
    if(ioctl(fd, SIOCATMARK, &flag) < 0)
        return -1;
    return (flag != 0);
}
  • 例子

现在给出一个简单的例子说明带外标记的以下两个特性:

  1. 带外标记总是指向普通数据最后一个字节紧后的位置。这意味着,如果带外数据在线接收,那么如果下一个待读入的字节是使用MSG_OOB标志发送的,sockatmark就返回。而如果SO_OOBINLINE套接字选项没有开启,那么,若下一个待读入的字节是跟在带外数据后发送的第一个字节,sockatmark就返回
  2. 读操作总是在带外标记上。也就是说,如果在套接字接收缓冲区中有100个字节,不过在带外标记之前只有5个字节,而进程执行一个请求100个字节的read调用,那么返回的是带外标记之前的5个字节。这种在带外标记上强制停止读操作的做法使得进程能够调用sockatmark确定缓冲区指针是否处于带外标记;

 

  • 发送程序
#include "unp.h"

int main(int argc, char **argv){
    int sockfd;
    if(argc!=3)
        err_quit("usage: tcpsend04 <host> <port#>");

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

    write(sockfd, "123", 3);
    printf("wrote 3 bytes of normal data\n");
   
    send(sockfd, "4", 1, MSG_OOB);
    printf("wrote 1 bytes of OOB data\n");

    write(sockfd, "5", 1);
    printf("wrote 1 bytes of normal data\n");

    exit(0);
}
  • 接收程序
#include "unp.h"

int main(int argc, char **argv){
    char buff[100];
    int listenfd, connfd, n, on=1;

    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: tcprecv04 [ <host> ] <port#>");

    setsockopt(listenfd, SOL_SOCKET, SO_OOBINLINE, &on, sizeof(on));
    connfd = accept(listenfd, NULL, NULL);
    sleep(5);    

    for( ; ; ){
        if(sockatmark(connfd))
            printf("at OOB mark\n");

        if((n = read(connfd, buff, sizeof(buff)-1)) == 0){
            printf("received EOF\n");
            exit(0);
        }
        buff[n] = 0;
        printf("read %d bytes: %s\n", n, buff);
    }
}

运行程序可得到如下输出:

freebsd4 % tcprecv04 6666
read 3 bytes: 123
at OOB mark
read 2 bytes: 45
recvived EOF
  • 客户/服务器心搏函数

心搏函数可以发现对端主机或到对端的通信路径的过早失效。也许会想到利用TCP的保持存活特性(SO_KEEPLIVE套接字选项)来提供这种功能,然后TCP得在连接已经闲置2小时之后才发送一个保持存活探测段。意识到这一点,需要解决的是如果把保持存活参数改为一个小得多的值(往往是在秒钟的量级),以便更快地检测到失效。

上述这个例子中,客户每隔1秒钟向服务器发送一个带外字节,服务器收取该字节将导致它向客户发送回一个带外字节。每端都需要知道对端是否不复存在或者不再可达。客户和服务器每1秒钟递增它们的cnt变量一次,每收到一个带外字节又把该变量重置为0。如果计数器达到5(也就是说本进程已有5秒钟没有收到来自对端的带外字节),那就认定连接失效。当有带外字节到达时,客户和服务器都使用SIGURG信号得以通知。

//客户程序心搏函数
static int servfd;
static int nsec;
static int maxnprobes;
static void sig_urg(int), sig_alrm(int);

void heartbeat_cli(int servfd_arg, int nsec_arg, int maxnprobbbes_arg){
    servfd = servfd_arg;
    if((nsec = nsec_Arg) < 1)
        nsec = 1;
    if((maxnprobes = maxnprobes_arg) < nsec)
        maxnprobes = nsec;
    nprobes = 0;

    signal(SIGURG, sig_urg);
    fcntl(servfd, F_SETOWN, getpid());

    signal(SIGHALRM, sig_alrm);
    alarm(nsec);
}

static void sig_urg(int signo){
    int n;
    char c;
    
    if((n = recv(servfd, &c, 1, MSG_OOB)) < 0){
        if(errno != EWOULDBLOCK)
            err_sys("recv error");
    }
    nprobes = 0;
    return ;
}

static void sig_alrm(int signo){
    if(++nprobes > maxnprobes){
        fprintf(stderr, "server is unreachable\n");
        exit(0);
    }

    send(servfd, "1", 1, MSG_OOB);
    alarm(nsec);
    return ;
}
//服务器程序心搏函数
static int servfd;
static int nsec;
static int maxnprobes;
static void sig_urg(int), sig_alrm(int);

void heartbeat_cli(int servfd_arg, int nsec_arg, int maxnprobbbes_arg){
    servfd = servfd_arg;
    if((nsec = nsec_Arg) < 1)
        nsec = 1;
    if((maxnprobes = maxnprobes_arg) < nsec)
        maxnprobes = nsec;

    signal(SIGURG, sig_urg);
    fcntl(servfd, F_SETOWN, getpid());

    signal(SIGHALRM, sig_alrm);
    alarm(nsec);
}

static void sig_urg(int signo){
    int n;
    char c;
    
    if((n = recv(servfd, &c, 1, MSG_OOB)) < 0){
        if(errno != EWOULDBLOCK)
            err_sys("recv error");
    }
    send(servfd, &c, 1, MSG_OOB);

    nprobes = 0;
    return ;
}

static void sig_alrm(int signo){
    if(++nprobes > maxnprobes){
        printf("no probes from client\n");
        exit(0);
    }
    alarm(nsec);
    return ;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值