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

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

1.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。

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

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标志读入该数据字节。相反,接收进程通过检查该连接的带外标记以获悉何时访问到这个数据字节。

发生一些错误是可能的:

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

2.使用SIGURG的简单例子

现在给出一个发送和接收带外数据的例子:

#include	"unp.h"

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); 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);
}

建立信号处理函数和套接字属主
15-16  建立SIGURG的信号处理函数,使用fcntl设置已连接套接字的属主。
  (注意,我们直到accept返回之后才建立信号处理函数,这么做会错过一些以小概率出现的带外数据,他们在TCP完成三次握手之后但在

accept返回之前到达。然而如果我们在调用accept之前信号处理函数并设置监听套接字的属主(本属性将传承给已连接套接字),那么如果

带外数据在accept之前到达,我们的信号处理函数将没有真正的connfd值可用。如果这种情形对于应用程序确实重要,它就应该把connfd初

始化为-1,在信号处理函数中检查该值是否为-1,若为真则简单的设置一个标志,供主循环在accept返回之后检查。另一方面,这可能阻塞

accept调用周围的信号)。
17-25   本进程从套接字中读,显示由read返回的每个字符串。发送进程终止连接后,接收进程随后终止。

SIGURG处理函数
28-36   我们的信号处理函数调用printf,通过指定MSG_OOB标志读入带外字节,然后显示返回的数据。注意,我们在recv调用中请求最多100个字节。

运行结果:

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信号,后者接着读入单个带外字节。

3.使用select的简单例子

我们现在改用select代替SIGURG信号重新编写带外接收程序。

有问题的:

#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);
}
}
}

19-25    调用select等待普通数据(读集合rset)或带外数据(异常集合xset)。每种情况下都显示接收的数据。

我先运行本程序,接着运行早先的发送程序,结果碰到如下错误:

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

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

解决办法是只在读入普通数据之后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;
}
}
}

4      声明一个名为justreadoob的变量,用于指示我们是否刚刚读过带外数据。这个标志决定是否select异常条件。

29-30    当设置justreadoob标志时,我们还得在异常描述符集中清除已连接套接字描述符对应的位。

UNIX网络编程——带外数据的更多相关文章

  1. UNIX网络编程——带外数据小结

    TCP没有真正的带外数据,不过提供紧急模式和紧急指针.一旦发送端进入紧急模式,紧急指针就出现在发送到对端的分节中的TCP首部中.连接的对端收取该指针是在告知接收进程发送端已经进入紧急模式,而且该指针指 ...

  2. UNIX网络编程——TCP带外数据小结

    带外数据概念实际上时向接收端传送三个不同的信息:(1)发送端进入紧急模式这个事实.接收进程得以通知这个事实的手段不外乎SIGURG信号或select调用.本通知在发送进程发送带外字节后由发送端TCP立 ...

  3. 网络IPC:套接字之带外数据

    带外数据(Out-of-band data)是一些通信协议所支持的可选特征,允许更高优先级的数据比普通数据优先传输.即使传输队列已经有数据,带外数据先行传输.TCP支持带外数据,但是UDP不支持.套接 ...

  4. UNIX网络编程-基本API介绍(二)

    参考链接:http://www.cnblogs.com/riky/archive/2006/11/24/570713.aspx 1.getsockname和getpeername getsocknam ...

  5. Unix网络编程--卷一:套接字联网API

    UNIX网络编程--卷一:套接字联网API 本书面对的读者是那些希望自己编写的程序能够使用成为套接字(socket)的API进行彼此通信的人. 目录: 0.准备环境 1.简介 2.传输层:TCP.UD ...

  6. UNIX网络编程——网络IPC:套接字

    UNIX网络编程——网络IPC:套接字 Contents 套接字接口 套接字描述符 寻址 字节序 地址格式 地址查询 绑定地址 建立连接 数据传输 套接字选项 带外数据 UNIX域套接字 使用套接字的 ...

  7. UNIX网络编程——客户/服务器心搏函数

    阅读此博客时,可以参考以前的博客<<UNIX网络编程--socket的keep-alive>>和<<UNIX网络编程--套接字选项(心跳检测.绑定地址复用)> ...

  8. 【unix网络编程第三版】阅读笔记(五):I/O复用:select和poll函数

    本博文主要针对UNP一书中的第六章内容来聊聊I/O复用技术以及其在网络编程中的实现 1. I/O复用技术 I/O多路复用是指内核一旦发现进程指定的一个或者多个I/O条件准备就绪,它就通知该进程.I/O ...

  9. UNIX网络编程——使用select函数编写客户端和服务器

    首先看原先<UNIX网络编程--并发服务器(TCP)>的代码,服务器代码serv.c: #include<stdio.h> #include<sys/types.h> ...

随机推荐

  1. BZOJ3052(树上带修莫队)

    树上莫队的基本思路是把树按dfs序分块,然后先按x所在块从小到大排序,再按y所在块从小到大排序,处理询问即可. 这道题带修改,再加一个时间维即可. 设块大小为T,那么时间复杂度为$O(nT+\frac ...

  2. C语言程序设计第二次作业--顺序结构

    C语言程序设计第二次作业--顺序结构 1.输出带框文字:在屏幕上输出以下3行信息. ************* Welcome ************* 源程序 #include <stido ...

  3. 我在 B 站学习深度学习(生动形象,跃然纸上)

    我在 B 站学习深度学习(生动形象,跃然纸上) 视频地址:https://www.bilibili.com/video/av16577449/ tensorflow123 http://tensorf ...

  4. Java web 前端面试知识点总结

    经过几家大厂面试,目前成功拿到唯品会offer,分享一下我的面试知识点总结: 耦合性:也称块间联系.指软件系统结构中各模块间相互联系紧密程度的一种度量.模块之间联系越紧密,其耦合性就越强,模块的独立性 ...

  5. html取地址栏参数的方法

    现在的人写的博客真是日常挖坑 闲的蛋疼 想把所有东西都转成jstl格式 有个界面是取地址栏的信息的 之前用的是 <%--不用jstl的方法直接传递--%> <%--String ro ...

  6. angular+ionic前后端分离开发项目中的使用

    Ionic基于AngularJS构建而成,所以学习一些AngularJS的知识很有必要.Ionic并没有独立开发一套完整的Web应用框架,而是对AngularJS进行了扩展,给它添加了大量界面组件和其 ...

  7. github windows pycharm 设置

    Window 上pycharm数据上传到github 在window上操作 1),安装git(百度) 进入git , bin目录执行 git-bash.exe 1)  gengyantao@DESKT ...

  8. OpenCV设置摄像头分辨率及全屏显示

    OpenCV3.0下 设置摄像头分辨率为1920*1440,并全屏显示图像窗口. int _tmain(int argc, _TCHAR* argv[]) { Mat frame; VideoCapt ...

  9. 01_自动化构建工具之Maven

    目前技术中存在问题(为什么使用Maven): 一个项目就是一个工程: 缺陷:如果项目太过庞大,就不适合使用package来划分层次,最好是一个模块就是一个工程,利于分工协作. 解决:Maven可以将一 ...

  10. ACM hdu 3336 Count the string

    [题意概述] 给定一个文本字符串,找出所有的前缀,并把他们在文本字符串中的出现次数相加,再mod10007,输出和. [题目分析] 利用kmp算法的next数组 再加上dp [存在疑惑] 在分析nex ...