Unix网络编程(12):带外数据
许多传输层有带外数据 ( $out-of-band$ $data$ ) 的概念,有时也被称为经加速数据 ( $expedited$ $data$ )。当一个连接的某端发生了重要的事情,而且该端希望迅速告知对端,这意味着这些数据应该在已经排队等待发送的任何普通数据之前发送。带外数据并不要求在客户和服务器之间再创建一个连接,而是使用现有的连接。不幸的是,几乎每个传输层都有各自不同的带外数据的实现。UDP
是一个极端的例子,没有带外数据。
1. TCP
带外数据
TCP
并没有真正的带外数据,不过提供了紧急模式 ( $urgent$ $mode$ )。以下为TCP
发送缓冲区。
进程以 $MSG_-OOB$ 标志发送一个带外数据:
send(fd, "a", 1, MSG_OOB);
TCP
将这个数据放置在发送缓冲区的下一个可用位置,并把该连接的TCP
紧急指针设置为下一个可用位置。
TCP
紧急指针对应一个TCP
序列号,它是使用 $MSG_-OOB$ 标志写出的最后一个数据字节 ( 即带外数据字节 ) 对应的序列号加 $1$ 。发送端TCP
将为待发送的下一个分节在TCP
首部设置URG
标志,并把TCP
紧急偏移 ( $urgent$ $offset$ ) 字段设置为紧急指针指向的字节,这个分节可能包含也可能不含之前发送的带外数据。$16$ 位的TCP
序列号加上 $16$ 位的紧急偏移字段即可得到 $32$ 位的紧急指针。TCP
紧急模式的一个重要特点是TCP
首部指出发送端已经进入紧急模式 ( 设置URG
标志 ),但是紧急指针指向的实际数据字节却不一定发送。
从接收端的角度来看,发生了以下事件:
- 收到
URG
分节,接收端检查紧急指针,确定它是否指向带外数据,也就是判断本分节是不是首个到达的紧急模式分节。发送端TCP
往往发送多个URG
分节并且其紧急指针都会指向同一个数据字节的分节,只有第一个到达的分节会通知接收进程有新的带外数据到达; - 当有新的紧急指针到达时,不论由紧急指针指向的实际数据字节是否已经到达,接收进程会收到通知。内核给接收套接字的进程发送 $SIGURG$ 信号,前提是接收进程 ( 或其他进程 ) 曾调用 $fcntl$ 或 $ioctl$ 为这个套接字建立了属主,而且该属主今后层已经为 $SIGURG$ 信号建立了信号处理函数。其次,如果接收进程阻塞在 $select$ 调用中,$select$ 调用就立即返回;
- 当由紧急指针指向的实际数据字节到达接收端
TCP
时,该数据字节可能被拉出带外,也可能留在带内。$SO_-OOBINLINE$ 套接字选项默认是禁止的,对于这样的套接字,该数据字节并不立即放入接收缓冲区,而是放入该连接的一个独立的单字节带外缓冲区。接收进程从这个单字节缓冲区中读入数据的唯一方法是指定 $MSG_-OOB$ 标志调用 $recv$ 、$recvfrom$ 或 $recvmsg$ 。如果新的带外字节在就旧的带外字节被读取之前到达,旧的带外字节会被丢弃。如果开启了 $SO_-OOBINLINE$ ,接收进程不能通过 $MSG_-OOB$ 标志读入带外数据,而是通过检查连接的带外标记获悉何时访问到这个字节。
这个过程可能会发送以下错误:
- 接收进程通过 $MSG_-OOB$ 标志请求读入带外数据,但是对端并未发送带外数据,读入操作会返回 $EINVAL$ ;
- 接收进程被告知对端发送了一个带外字节的前提下,如果接收进程试图读入该节字,但是该字节尚未到达,读入操作会返回 $EWOULDBLOCK$ ;
- 如果接收进程试图多次读入同一个带外字节,读入操作将返回 $EINVAL$ ;
- 如果接收进程已经开启了 $SO_-OOBINLINE$ 套接字选项,后来试图通过 $MSG_-OOB$ 标志读入带外数据,读入操作会返回 $EINVAL$ 。
在使用 $select$ 监听时,要注意将监听带外数据的描述符置于异常集合中。当监听异常条件时,要注意先进行一次普通的读,然后再监听异常集合,并且在读入带外数据后清除异常集合。
2. sockatmark
函数
每当收到一个带外数据时,就有一个与之关联的带外标记 ( $out-of-band$ $mark$ ),这是发送进程发送带外字节时该字节在发送端普通数据流中的位置。在从套接字读入期间,接收进程通过调用 $sockatmark$ 函数确定是否处于带外标记。
#include <sys/socket.h>
// 带外标记返回1,非带外标记返回0,出错返回-1
int sockatmark(int sockfd);
不管接收进程在带内 ( $SO_-OOBINLINE$ 选项 ) 还是带外 ( $MSG_-OOB$ 标志 ) 接收带外数据,都可以使用带外标记。带外标记的常见用法之一就是接收进程特殊地对待所有带外数据,直到越过带外数据。
带外标记总是指向普通数据最后一个字节之后的位置。如果带外数据在线接收,如果下一个待读入字节是使用 $MSG_-OOB$ 标志发送的,$sockatmark$ 就返回真。如果带外数据带外接收,那么当下一个待读入字节是带外数据的第一个字节时,$sockatmark$ 就返回真。读操作总是停在带外标记上,从而进程能通过 $sockatmark$ 判断缓冲区指针是否处于带外标记。
3. 客户/服务器心跳函数
实现TCP
心跳函数,有些人会想到使用TCP
的保活计时器 ( $SO_-KEEPALIVE$ 选项 ) 来提供这种功能,然而TCP
要在连接闲置 $2$ 小时后才发送存活探测分节。尽管缩短TCP
的保活计时器参数在许多系统上可行,但是这些参数通常是按照内核而不是按照每个套接字维护的,因此会影响到所有开启该选项的套接字。其次,两个端系统之间短暂的连接丢失并非总是坏事,TCP
从一开始就支持临时断连,源自 $Berkeley$ 的TCP
实现将重传 $8 \sim 10$ 分钟才会放弃某个连接。开发人员必须审查引入心跳机制的具体应用,有些系统需要这种功能,但是大多数并不需要。
#include "unp.h"
static int servfd;
static int nsec; // seconds between each alarm
static int maxnprobes; // probes w/no response before quit
static int nprobes; // probes since last server response
static void sig_urg(int), sig_alrm(int);
void heartbeat_cli(int servfd_arg, int nsec_arg, int maxnprobes_arg) {
servfd = servfd_arg; // set globals for signal handlers
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(SIGALRM, 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; // reset counter
return; // may interrupt client code
}
stativ 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; // may interrupt client code
}
在这个例子中,客户每隔 $1$ 秒向服务器发送一个带外字节,服务器收取该字节将导致它向客户发回一个带外字节。每端都需要知道对端是否不复存在或不再可达。
#include "unp.h"
static int servfd;
static int nsec; // seconds between each alarm
static int maxnalarms; // alaram w/no client probe before quit
static int nprobes; // alarms since last client probe
static void sig_urg(int), sig_alrm(int);
void heartbeat_serv(int esrvfd_arg, int nsec_arg, int maxnalarms_arg) {
servfd = servfd_arg; // set globals for signal handlers
if ((nsec = nsec_arg) < 1)
nsec = 1;
if ((maxnalarms = maxnalarms_arg) < nsec)
maxnalarms = nsec;
Signal(SIGURG, sig_urg);
Fcntl(servfd, F_SETOWN, getpid());
Signal(SIGALRM, 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); // echo back out-of-band type
nprobes = 0; // reset counter
return; // may interrupt server code
}
static void sig_alrm(int signo) {
if (++nprobes > maxnalarms) {
printf("no probes from client\n");
exit(0);
}
alarm(nsec);
return; // may interrupt server code
}