UNIX网络编程——带外数据
许多传输层有带外数据的概念,它有时也称为经加速数据。其想法是一个连接的某端发生了重要的事情,而且该端希望迅速通告其对端。这里“迅速”意味着这种通知应该在已排队等待发送的任何“普通”(有时称为“带内”)数据之前发送。也就是说,带外数据被认为具有比普通数据更高的优先级。带外数据并不需要在客户和服务器之间再使用一个连接,而是被映射到已有的连接中。
不幸的是,一旦超越普通概念光临现实世界,我们发现几乎每个传输层都各自有不同的带外数据实现。而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网络编程——带外数据的更多相关文章
- UNIX网络编程——带外数据小结
TCP没有真正的带外数据,不过提供紧急模式和紧急指针.一旦发送端进入紧急模式,紧急指针就出现在发送到对端的分节中的TCP首部中.连接的对端收取该指针是在告知接收进程发送端已经进入紧急模式,而且该指针指 ...
- UNIX网络编程——TCP带外数据小结
带外数据概念实际上时向接收端传送三个不同的信息:(1)发送端进入紧急模式这个事实.接收进程得以通知这个事实的手段不外乎SIGURG信号或select调用.本通知在发送进程发送带外字节后由发送端TCP立 ...
- 网络IPC:套接字之带外数据
带外数据(Out-of-band data)是一些通信协议所支持的可选特征,允许更高优先级的数据比普通数据优先传输.即使传输队列已经有数据,带外数据先行传输.TCP支持带外数据,但是UDP不支持.套接 ...
- UNIX网络编程-基本API介绍(二)
参考链接:http://www.cnblogs.com/riky/archive/2006/11/24/570713.aspx 1.getsockname和getpeername getsocknam ...
- Unix网络编程--卷一:套接字联网API
UNIX网络编程--卷一:套接字联网API 本书面对的读者是那些希望自己编写的程序能够使用成为套接字(socket)的API进行彼此通信的人. 目录: 0.准备环境 1.简介 2.传输层:TCP.UD ...
- UNIX网络编程——网络IPC:套接字
UNIX网络编程——网络IPC:套接字 Contents 套接字接口 套接字描述符 寻址 字节序 地址格式 地址查询 绑定地址 建立连接 数据传输 套接字选项 带外数据 UNIX域套接字 使用套接字的 ...
- UNIX网络编程——客户/服务器心搏函数
阅读此博客时,可以参考以前的博客<<UNIX网络编程--socket的keep-alive>>和<<UNIX网络编程--套接字选项(心跳检测.绑定地址复用)> ...
- 【unix网络编程第三版】阅读笔记(五):I/O复用:select和poll函数
本博文主要针对UNP一书中的第六章内容来聊聊I/O复用技术以及其在网络编程中的实现 1. I/O复用技术 I/O多路复用是指内核一旦发现进程指定的一个或者多个I/O条件准备就绪,它就通知该进程.I/O ...
- UNIX网络编程——使用select函数编写客户端和服务器
首先看原先<UNIX网络编程--并发服务器(TCP)>的代码,服务器代码serv.c: #include<stdio.h> #include<sys/types.h> ...
随机推荐
- THUPC2017 抱大腿记
Day 0: 移步http://www.cnblogs.com/juruolty/p/6854848.html Day 1: 来到了清华大学. 见到了zrt巨巨. 又发了件衣服,我们开始看别的队的名字 ...
- Python【第一课】 Python简介和基础
本节内容 Python安装(windows) 第一个程序(windows中的python) 变量 字符编码 注释 用户输入 模块初步认识 数据类型 数据运算 表达式if...else 表达式for l ...
- centos7.2中文乱码解决办法
centos7.2 中文乱码解决办法 1.查看安装中文包: 查看系统是否安装中文语言包 (列出所有可用的公共语言环境的名称,包含有zh_CN) # locale -a |grep "zh_C ...
- TCP/UDP的区别
TCP与UDP区别 TCP提供的是面向连接的.可靠的数据流传输: UDP提供的是非面向连接的.不可靠的数据流传输. TCP提供可靠的服务,通过TCP连接传送的数据,无差错.不丢失,不重复,按序到达:U ...
- Java并发之BlockingQueue的使用
Java并发之BlockingQueue的使用 一.简介 前段时间看到有些朋友在网上发了一道面试题,题目的大意就是:有两个线程A,B, A线程每200ms就生成一个[0,100]之间的随机数, B线 ...
- 女儿开始bababababa的发声了
女儿八个半月,开始bababababa的发声了,而不是像以前总啊啊啊的.
- 70. Climbing Stairs(easy, 号称 Dynamic Programming 天下第一题)
You are climbing a stair case. It takes n steps to reach to the top. Each time you can either climb ...
- Luogu P2756 [网络流24题]飞行员配对方案问题_二分图匹配
二分图模板题 我用的是匈牙利 其实最大流也可以做 #include<iostream> #include<cstdio> #include<cstdlib> #in ...
- 关于熊猫认证软件IOS安装步骤教程(适用于其他软件)
IOS运行企业版应用教程 1.扫描二维码之后微信进入界面,如下图所示:点击右上角三个点 2.弹出分享界面,如图所示:点击苹果自带浏览器(sarfari) 3.进入苹果自带浏览器后如图所示, ...
- JavaScript switch 语句
switch 语句用于基于不同的条件来执行不同的动作. JavaScript switch 语句 请使用 switch 语句来选择要执行的多个代码块之一.你可以在JavaScript编程实战中了解怎么 ...