带外数据:TCP紧急模式分析

网络 专栏收录该内容
2 篇文章 0 订阅

TCP并未提供真正意义上的带外数据,而是紧急模式。TCP并未建立新的连接,或者使用独立的逻辑通道,而只是通过紧急模式的机制,在已有的TCP连接上传输带外数据。


发送端

TCP协议栈会为每个套接字维护一个发送端紧急模式标志和一个发送端紧急指针。一旦调用sendMSG_OOB)后,则TCP发送端进入紧急模式。此后,协议栈中的每个TCP输出操作都会为数据分段设置URG标志和紧急偏移。也即紧急模式的发送不受流量控制机制的影响。

紧急数据只有一个字节,也即sendMSG_OOB)的最后一个字节,它混在普通数据中(也即发送缓冲区),这个字节由紧急指针(大多数实现需要减1)指示。紧急数据的发送与普通数据没有任何区别,同样受流量控制的影响。只有当紧急数据真正发送出去,TCP发送端才解除紧急模式。

 

接收端

TCP协议栈也会为每个套接字维护一个接收端紧急模式标志和一个接收端紧急指针。一旦接收到一个URG标志的数据分段,则TCP接收端会开启此接收端紧急模式标志,并保存接收端紧急指针,并且此刻,其将通知接收进程(发送信号SIGURG,或者给阻塞的select调用返回异常)。

在接收端紧急模式下,TCP会监视此后接收到的每个数据分段,如果其中包含之前接收端紧急指针的字节,那么默认情况下,此字节将放在单独的带外缓冲中(独立于接收缓冲区)。如果接收端对套接字调用setsockopt开启了SO_OOBINLINE,此字节将混在普通数据中,称为在线接收。其由接收端紧急指针(大多数实现需要减1)指示,称之为OOB字节。


在接收进程读取数据时,可使用sockatmark可以判断带外标记,按照两种情况:

  • 开启了SO_OOBINLINE:如果下一个待读字节是OOB字节,那么sockatmark返回1。此处recv将被强制停止,下一个recv将返回从OOB字节开始的指定长度数据。
  • (默认)没有开启SO_OOBINLINE:如果下一个待读字节是发送OOB字节的下一个字节,那么sockatmark返回1。此处recv也将被强制停止,使用recvMSG_OOB)可以读取此OOB字节。请注意,在读取OOB字节后,下一个待读字节仍是发送OOB字节的下一个字节,所以此时sockatmark仍将返回1,只有再recv一段普通数据后,sockatmark才返回0。当然,OOB字节也可以不读取,或者之后再读取。

只有在下一个待读字节越过OOB字节之后,也即sockatmark返回0之后,接收端紧急模式才被解除。接收进程只会在进入接收端紧急模式的一刻,才会接收到SIGURG信号,但是在紧急模式下,所有的select都将返回异常。


示例

服务端代码(recv.c):
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>

int main(int argc, char ** argv)
{
	int sock = socket(AF_INET, SOCK_STREAM, 0);
	if(sock < 0)
	{
		perror("socket:");
		return 1;
	}
	
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(8000);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	memset(&(addr.sin_zero), 0, sizeof(addr.sin_zero));

	if(bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)
	{
		perror("bind:");
		return 2;
	}

	if(listen(sock, 1) < 0)
	{
		perror("listen:");
		return 3;
	}

	int csock = accept(sock, NULL, NULL);
	if(csock < 0)
	{
		perror("accept:");
		return 4;
	}
	if(close(sock) < 0)
	{
		perror("close 1:");
		return 5;
	}

	int on = 1;
	if(setsockopt(csock, SOL_SOCKET, SO_OOBINLINE, &on, sizeof(on)) < 0)
	{
		perror("setsockopt:");
		return 6;
	}

	char buf[10];
	while(1)
	{
		int oob = sockatmark(csock);
		if(oob < 0)
		{
			perror("sockatmark:");
			return 7;
		}
		
		int ret = -1;
		memset(buf, 0, sizeof(buf));
	
		ret = recv(csock, buf, sizeof(buf) - 1, MSG_DONTWAIT);

		if(ret < 0)
		{
			if(errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
				continue;
			if(oob == 0)
				perror("recv data:");
			else
				perror("recv oob:");
			return 8;
		}
		else if(ret == 0)
			break;
		else
		{
			if(oob == 0)
				printf("data: %s\n", buf);
			else
				printf("oob:  %s\n", buf);
		}
	}

	if(close(csock) < 0)
	{
		perror("close 2:");
		return 9;
	}
	
	return 0;
}
客户端代码(send.c):
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

int main(int argc, char **argv)
{
	int sock = socket(AF_INET, SOCK_STREAM, 0);
	if(sock < 0)
	{
		perror("socket:");
		return 1;
	}
	
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(8000);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	memset(&(addr.sin_zero), 0, sizeof(addr.sin_zero));

	if(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)
	{
		perror("connect:");
		return 2;
	}

	char buf[] = "abcdef";
	
	if(send(sock, buf, 2, 0) != 2)
	{
		perror("send 1:");
		return 3;
	}
	if(send(sock, "OOB", 3, MSG_OOB) != 3)
	{
		perror("send OOB 1:");
		return 4;
	}
	if(send(sock, "XXD", 3, MSG_OOB) != 3)
	{
		perror("send OOB 2:");
		return 5;
	}
	if(send(sock, buf + 2, 2, 0) != 2)
	{
		perror("send 2:");
		return 6;
	}
	
	if(close(sock) < 0)
	{
		perror("close:");	
		return 7;
	}

	return 0;
}
Makefile:
all: recv send

recv: recv.o
	gcc -o recv recv.o

recv.o: recv.c
	gcc -c -o recv.o recv.c

send: send.o
	gcc -o send send.o

send.o: send.c
	gcc -c -o send.o send.c

clean:
	-rm *.o recv send
运行结果:
infinova@ubuntu:~/Study/oob$ ./recv &
[1] 9676
infinova@ubuntu:~/Study/oob$ ./send 
data: ab
data: OOBXX
oob:  Dcd
[1]+  Done                    ./recv
infinova@ubuntu:~/Study/oob$ 

总结

TCP的紧急模式还是很繁杂的,很多细节都可能导致错误。我个人归结为以下原因:
  • 紧急模式标志URG独立于紧急数据发送
  • 紧急数据混杂在普通数据发送
这种设计导致了非常不人性化的操作接口,而之所以会有这种看来不甚合理的设计,我个人猜想是由于TCP为了保证流量控制和拥塞控制,导致很难去实现真正的带外数据机制所导致的。
  • 2
    点赞
  • 0
    评论
  • 1
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

aprilweet

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值