前言
在上大学的时候,我就听说了 OOB 这个概念(Out Of Band 带外数据,又称紧急数据)。当时老师给的解释就是在当前处理的数据流之外的数据,用于紧急的情况。然后就没有然后了……
毕业这么多年了,回想一下,还真是没有接触过 OOB 的场景,更没有实地发送、接收过 OOB。那么到底该怎样处理 OOB 呢?OOB 在所谓的紧急情况下是否有用呢?下面一一道来。
发送 OOB
首先产生 OOB 是非常简单的,只需要在寻常 send 的最后一个参数,加入 MSG_OOB 标志位:
代码语言:javascript复制ret = send (sockfd, ptr, n, MSG_OOB);
如果考虑一个完整的测试场景,需要有惯常数据,中间夹带 OOB 数据,这样才能比较好的测试接收端是否能正确的区分他们,所以客户端可以写成这样:
代码语言:javascript复制 1 strcpy(buf, "abcdefghijklmn");
2 char const* ptr = buf;
3 if ((ret = send (sockfd, ptr, 2, 0)) < 0)
4 err_sys ("send normal head failed");
5 else
6 printf ("send normal head %dn", ret);
7
8 ptr = 2;
9 n = 1;
10 if ((ret = send (sockfd, ptr, n, MSG_OOB)) < 0)
11 err_sys ("send oob failed");
12 else
13 printf ("send oob %dn", ret);
14
15 ptr = n;
16 if ((ret = send (sockfd, ptr, 2, 0)) < 0)
17 err_sys ("send normal tail failed");
18 else
19 printf ("send normal tail %dn", ret);
算法比较简单,先发送 2 字节惯常数据,接着 1 字节 OOB,最后 2 字节惯常数据结尾。需要注意的是,目前只有 TCP 支持 OOB,UDP 没所谓顺序,更没所谓带内带外之分,所以也没有 OOB;另外 TCP 目前大多数实现只支持 1 字节 OOB,大于 1 字节的 OOB,只有最后一字节会被当为 OOB 处理,之前的作为普通数据。
接收 OOB
接收 OOB 共有三种方法。
1. 使用 SIGURG 信号专门处理 OOB
这种方法是将 OOB 与惯常数据分开处理,具体步骤如下:
a) 进程起始时,建立 SIGURG 信号处理器
代码语言:javascript复制1 struct sigaction sa;
2 sa.sa_handler = on_urg;
3 sa.sa_flags |= SA_RESTART;
4 sigemptyset (&sa.sa_mask);
5 sigaction (SIGURG, &sa, NULL);
b) 建立新连接时,设置连接句柄的信号处理进程(为当前进程)
代码语言:javascript复制1 fcntl (clfd, F_SETOWN, getpid ());
c) 在信号处理器中使用 MSG_OOB 接收带外数据
代码语言:javascript复制 1 int g_fd = 0;
2 void on_urg (int signo)
3 {
4 int ret = 0;
5 char buf[BUFLEN] = { 0 };
6 ret = recv (g_fd, buf, sizeof (buf), MSG_OOB);
7 if (ret > 0)
8 buf[ret] = 0;
9 else
10 strcpy (buf, "n/a");
11
12 printf ("got urgent data on signal %d, len %d, %sn", signo, ret, buf);
13
14 }
d) 惯常数据,可以在主处理流程中使用不带 MSG_OOB 的 recv,像以前那样处理
代码语言:javascript复制1 ret = recv (clfd, buf, sizeof(buf), 0);
2 if (ret > 0)
3 buf[ret] = 0;
4 else
5 strcpy (buf, "n/a");
6
7 printf ("recv %d: %sn", ret, buf);
由于惯常数据的接收,会被 OOB 打断,因此这里可能需要一个循环,不断接收惯常数据。下面是方法 1 的接收输出:
代码语言:javascript复制hostname length: 64
get hostname: localhost.localdomain
setup SIGURG for oob data
setown to 31793
got urgent data on signal 23, len 1, c
recv 2: ab
has oob!
recv -1: n/a
recv 2: de
write back 70
recv 2: ab
recv 2: ab
got urgent data on signal 23, len 1, c
has oob!
recv -1: n/a
recv 2: de
write back 70
recv 2: ab
no oob!
got urgent data on signal 23, len 1, c
recv 2: de
write back 70
recv 2: ab
recv 2: ab
got urgent data on signal 23, len 1, c
has oob!
recv -1: n/a
recv 2: de
write back 70
^C
可以看到信号处理器中接收到的总是 OOB 数据 'c',而普通 recv 只能读到非 OOB 数据 'a' / 'b' / 'd' / 'e'。而且普通数据的接收,会被 OOB 数据打断成两块,无法一次性读取。
2. 使用 SO_OOBINLINE 标志位将 OOB 作为惯常数据处理
这种方法是将 OOB 数据当作惯常数据接收,在接收前通过判断哪些是普通数据哪些是 OOB 数据,具体步骤如下:
a) 新连接建立时,设置套接字选项 SO_OOBINLINE
代码语言:javascript复制1 setsockopt (fd, SOL_SOCKET, SO_OOBINLINE, &oil, sizeof (oil));
b) 在接收数据前,先判断下一个字节是否为 OOB,如果是,则接收 1 字节 OOB 数据(注意不使用 MSG_OOB 标志)
代码语言:javascript复制 1 if (sockatmark (clfd))
2 {
3 printf ("has oob!n");
4 ret = recv (clfd, buf, sizeof(buf), 0);
5 if (ret > 0)
6 buf[ret] = 0;
7 else
8 strcpy (buf, "n/a");
9
10 printf ("recv %d: %sn", ret, buf);
11 }
12 else
13 printf ("no oob!n");
这里 sockatmark 当下个字节为 OOB 时返回 1,否则返回 0。
c) 如果不是,按惯常数据接收
代码语言:javascript复制1 ret = recv (clfd, buf, sizeof(buf), 0);
2 if (ret > 0)
3 buf[ret] = 0;
4 else
5 strcpy (buf, "n/a");
6
7 printf ("recv %d: %sn", ret, buf);
同理,由于惯常数据会被 OOB 打断,上述代码总是可以正确的分离 OOB 与普通数据。下面是方法 2 的接收输出:
代码语言:javascript复制hostname length: 64
get hostname: localhost.localdomain
setown to 31883
recv 2: ab
no oob!
recv 3: cde
write back 70
recv 2: ab
has oob!
recv 1: c
recv 2: de
write back 70
recv 2: ab
has oob!
recv 1: c
recv 2: de
write back 70
recv 2: ab
no oob!
recv 3: cde
write back 70
recv 2: ab
has oob!
recv 1: c
recv 2: de
write back 70
^C
可以看出,有时候 OOB 数据不能被正常的识别,会被当作普通数据处理掉。而且这种方式也不能体现 OOB 紧急的意义,没有给予它优先的处理权。
3. 使用 select/epoll 多路事件分离
这种方法是利用 select 或 epoll,将 OOB 数据作为 exception 事件与普通数据的 read 事件相分离,这里以 select 为例:
a) 建立 select 事件处理循环
代码语言:javascript复制1 for (;;) {
2 // must set it in every loop.
3 memcpy (&rdds, &cltds, sizeof (cltds));
4 memcpy (&exds, &cltds, sizeof (cltds));
5 FD_SET(sockfd, &rdds);
6 ret = select (FD_SIZE 1, &rdds, NULL, &exds, NULL);
7 ……
8 }
b) 建立连接时,将连接 fd 加入待监听 fd_set
代码语言:javascript复制 1 if (FD_ISSET(clfd, &rdds))
2 {
3 if (clfd == sockfd)
4 {
5 // the acceptor
6 printf ("poll accept inn");
7 clfd = accept (sockfd, NULL, NULL);
8 if (clfd < 0) {
9 printf ("accept error: %d, %sn", errno, strerror (errno));
10 exit (1);
11 }
12
13 print_sockopt (clfd, "new accepted client");
14 // remember it
15 FD_SET(clfd, &cltds);
16 printf ("add %d to client setn", clfd);
17 }
18 else
19 {
20 ……
21 }
22 }
c) 连接上有数据到达时,如果是 read 事件,使用 recv 接收数据
代码语言:javascript复制 1 if (FD_ISSET(clfd, &rdds))
2 {
3 if (clfd == sockfd)
4 {
5 ……
6 }
7 else
8 {
9 // the normal client
10 printf ("poll read inn");
11 ret = recv (clfd, buf, sizeof(buf), 0);
12 if (ret > 0)
13 buf[ret] = 0;
14 else
15 sprintf (buf, "errno %d", errno);
16
17 printf ("recv %d from %d: %sn", ret, clfd, buf);
18 if (ret <= 0) {
19 FD_CLR(clfd, &cltds);
20 printf ("remove %d from client setn", clfd);
21 }
22 }
23 }
d) 如果是 exception 事件,使用 recv(..,MSG_OOB) 接收带外数据
代码语言:javascript复制 1 if (FD_ISSET(clfd, &exds))
2 {
3 // the oob from normal client
4 printf ("poll exception inn");
5 if (sockatmark (clfd))
6 {
7 printf ("has oob!n");
8 ret = recv (clfd, buf, 1, MSG_OOB);
9 if (ret > 0)
10 buf[ret] = 0;
11 else
12 sprintf (buf, "errno %d", errno);
13
14 printf ("recv %d from %d on urgent: %sn", ret, clfd, buf);
15 if (ret > 0) {
16 // let clfd cleared in sig_cld
17 do_uptime (clfd);
18 }
19 else
20 {
21 FD_CLR(clfd, &cltds);
22 printf ("remove %d from client setn", clfd);
23 }
24 }
25 else
26 printf ("no oob!n");
27 }
此时,仍可使用 sockatmark 来判断是否为 OOB 数据,另外,如果在连接建立时设定了 OOB_INLINE 标志位,则此处应使用不带 MSG_OOB 的 recv 接收数据,因为 OOB 数据已经被当作惯常数据来处理了,此处与方法 2是一致的。下面是方法 3 的输出:
代码语言:javascript复制setup handler for SIGCHLD ok
hostname length: 64
get hostname: localhost.localdomain
got event 1
poll accept in
add 4 to client set
got event 2
poll read in
recv 2 from 4: ab
poll exception in
has oob!
recv 1 from 4 on urgent: c
start worker process 4511
goto serve next client..
got event 1
poll read in
recv 2 from 4: de
got event 1
poll accept in
add 5 to client set
got event 2
poll read in
recv 2 from 5: ab
poll exception in
has oob!
recv 1 from 5 on urgent: c
start worker process 4513
goto serve next client..
got event 1
poll read in
recv 2 from 5: de
got event 1
poll accept in
add 6 to client set
got event 2
poll read in
recv 2 from 6: ab
poll exception in
has oob!
recv 1 from 6 on urgent: c
start worker process 4516
goto serve next client..
got event 1
poll read in
recv 2 from 6: de
SIGCHLD received
wait child 4511 return 0
find clfd 4 for that pid
remove 4 from client set
interrupted by signal, some child process done ?
SIGCHLD received
wait child 4513 return 0
find clfd 5 for that pid
remove 5 from client set
interrupted by signal, some child process done ?
SIGCHLD received
wait child 4516 return 0
find clfd 6 for that pid
remove 6 from client set
interrupted by signal, some child process done ?
^C
需要注意的是,在某些场景下,OOB 会被识别为惯常数据,此时 exception 事件在处理时将得不到 OOB 数据,不过这有一定的随机性,不是每次都能复现。
结语
最后,总结一下 OOB 这个功能。这么多年来没有遇到OOB的处理,可能本身就说明了大家对它的态度——就是挺鸡肋的一功能,而且即使真的需要紧急处理了,1 字节的限制也导致不能传递什么更多的信息,且 OOB 本身的处理又有些复杂和局限性,例如:
- 使用信号处理器,如果有多个连接,我怎么知道是哪个连接上的 OOB ?
- 如果使用 SO_OOBINLINE,OOB 被当作普通数据,这里面如果有个结构体被生生插入一个 OOB 字节,而且还没有正确识别出来,这里面的对齐问题可要了老命了。
所以最后的结论是:OOB是过时的,请不要使用它
下载
文件中的测试程序可以通过下面的链接下载:
测试程序1
测试程序2
测试程序3