概述
许多传输层有带外数据的概念,有时候也称经加速数据。其想法是一个连接的某端发生了重要的事情,而且该端希望迅速通告其对端。这里的“迅速”意味着这种通知在已经排队等待发送的任何“普通”数据之前发送,也就是优先级更高。
-
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)被认为是带外字节。
有一些错误是可能的:
- 如果接收进程请求读入带外数据(通过指定MSG_OOB标志),但是对端尚未发送任何带外数据,读入操作将返回EINVAL;
- 在接收进程已被告知对端发送了一个带外字节(通过SIGURG或select手段)的前提下,如果接收进程试图读入该字节,但是该字节尚未到达,读入操作将返回EOULDBLOCK。接收进程此时能做的仅仅是从套接字接收缓冲区读入数据(要是没有存放这些数据的空间,可能还得丢弃它们),以便在该缓冲区中腾出空间,继而允许对端TCP发送那个带外字节;
- 如果接收进程试图多次读入同一个带外字节,读入操作将返回EINVAL;
- 如果接收进程已经开启了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);
}
-
例子
现在给出一个简单的例子说明带外标记的以下两个特性:
- 带外标记总是指向普通数据最后一个字节紧后的位置。这意味着,如果带外数据在线接收,那么如果下一个待读入的字节是使用MSG_OOB标志发送的,sockatmark就返回真。而如果SO_OOBINLINE套接字选项没有开启,那么,若下一个待读入的字节是跟在带外数据后发送的第一个字节,sockatmark就返回真;
- 读操作总是停在带外标记上。也就是说,如果在套接字接收缓冲区中有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 ;
}