数据收发的拓展用法

问题

write() 和 send() 都可以发送数据,有什么区别?

read() 和 recv() 都可以接收数据,有什么区别?

  • send() 和 recv() 比 write() 和 read() 多了一个 flags 参数,用于描述收发网络数据时的选项

数据收发选项

数据收发的拓展用法

flags - 收发数据时指定的可选项信息

数据收发的拓展用法

当调用 send()、recv() 函数,将 flags 参数指定为 0 时:

调用 send() 函数时,首先查看发送缓冲区的数据是否被发送出去了,等发送缓冲区的数据都被发送完成后,再把网络数据拷贝到发送缓冲区中,然后就返回。这和调用 write() 函数的效果是一样的。

调用 recv() 函数时,首先去查看接收缓冲区是否有数据,有数据则将数据拷贝到用户空间中用户定义的缓冲区中,无数据则阻塞等待数据的到来。 这和调用 read() 函数的效果是一样的。

flags 选项信息

数据收发的拓展用法

注意:

不同的操作系统对上述可选项的支持不同,实际工程开发时,需要事先对目标系统中支持的可选项进行调研。

MSG_OOB (带外数据,紧急数据) 

原生定义

  • 使用与普通数据不同的通道独立传输的数据
  • 带外数据优先级比普通数据高 (优先传输,对端优先接收)

TCP 中的带外数据

  • 由于原生设计的限制,TCP 无法提供真正意义上的带外数据
  • TCP 中仅能通过传输协议消息头中的标记,传输紧急数据,且长度仅 1 字节

TCP 带外数据实现原理

数据收发的拓展用法

 URG 指针指向紧急消息的下一个位置。即:URG 指针指向位置的前一个字节存储了紧急消息。

TCP 带外数据处理策略

由于 TCP 设计为流式数据,因此,无法做到真正的带外数据

被标记的紧急数据可被提前接收,进入特殊缓冲区 (仅 1 字节)

  • 每个 TCP 包最多只有一个紧急数据
  • 特殊缓冲区仅存放最近的紧急数据 (不及时接收将丢失)

用下面的方式收发数据会发生什么?

发送普通数据,普通方式接收

  • 有数据到来则接收,无数据到来则阻塞等待

发送普通数据,紧急方式接收

  • 会去特殊缓冲区去取数据,由于发送的是普通数据,特殊缓冲区并无数据,所以会出错返回

发送紧急数据,普通方式接收

  • 会去接收缓冲区去取数据,由于发送的是紧急数据,紧急数据存放在接收缓冲区,所以会阻塞等待接收缓冲区的数据到来

发送紧急数据,紧急方式接收

  • 发送的数据将存放到特殊缓冲区,接收方去特殊缓冲区中取数据

TCP 紧急数据的发送与接收

client.c

#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main()
{
	int sock = -1;
	struct sockaddr_in addr = {0};
	char* test = "D.T.Software";

	sock = socket(AF_INET, SOCK_STREAM, 0);

	if(sock == -1)
	{
		printf("socker error\n");

		return -1;
	}

	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	addr.sin_port = htons(8888);

	if(connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1)
	{
		printf("connect error\n");

		return -1;
	}

	printf("connect succeed\n");

	send(sock, test, strlen(test), MSG_OOB);

	getchar();

	close(sock);

	return 0;
}

server.c

#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>

int main()
{
	int server = 0;
	struct sockaddr_in saddr = {0};
	int client = 0;
	struct sockaddr_in caddr = {0};
	socklen_t csize = 0;
	char buf[64] = {0};
	int r = 0;

	server = socket(AF_INET, SOCK_STREAM, 0);

	if(server == -1)
	{
		printf("server socket error\n");

		return -1;
	}

	saddr.sin_family = AF_INET;
	saddr.sin_addr.s_addr = htonl(INADDR_ANY);
	saddr.sin_port = htons(8888);

	if(bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == -1)
	{
		printf("server bind error\n");

		return -1;
	}

	if(listen(server, 1) == -1)
	{
		printf("server listen error\n");

		return -1;
	}

	printf("start to accept\n");

	while(1)
	{
		csize = sizeof(caddr);

		client = accept(server, (struct sockaddr*)&caddr, &csize);

		if(client == -1)
		{
			printf("server accept error\n");
		
			return -1;
		}

		printf("client = %d\n", client);

		do
		{
			r = recv(client, buf, sizeof(buf), MSG_OOB);	

			if(r > 0)
			{
				buf[r] = '\0';
				printf("OOB: %s\n", buf);
			}

			r = recv(client, buf, sizeof(buf), 0);	

			if(r > 0)
			{
				buf[r] = '\0';
				printf("Receive: %s\n", buf);
			}

		}while(r > 0);

		printf("\n");

		close(client);
	}

	close(server);

	return 0;
}

客户端把 D.T.Software 作为紧急数据发送给服务端;服务端接收紧急数据和普通数据,并打印出来。

程序运行结果如下所示:

数据收发的拓展用法

我们想把多个字节以紧急数据的方式发送出去,由于一个数据包只能携带一个字节的紧急数据,所以 D.T.Software 最后一个字节 e 会作为紧急数据发送出去,其他数据以普通数据的方式被发送出去。

我们一共测试了 5 次。4 次先打印紧急数据,1 次先打印普通数据。

先收到紧急数据是因为:当接收方调用 recv 接收紧急数据时,此时接收方的特殊缓冲区已经有数据了,所以把接收缓冲区的一字节数据拷贝到用户缓冲区,然后打印出来,随后处理普通数据。

先打印普通数据是因为:当接收方调用 recv 接收紧急数据时,此时接收方的特殊缓冲区并没有数据,随后返回 -1,去接收普通数据,由于接收普通数据是阻塞接收,所以一定会接收到,接收到普通数据后,紧急数据也已经到达接收缓冲区了,这时调用 recv 就能成功收到紧急数据了。

小问题

实际开发中,如何高效的接收 TCP 紧急数据?

使用 select 接收紧急数据

socket 收到普通数据和紧急数据都会使得 select 立即返回

  • 普通数据:socket 处于数据可读状态 (可读取普通数据)
  • 紧急数据:socket 处于异常状态 (可读取紧急数据)

紧急数据接收示例

数据收发的拓展用法

使用 select 接收紧急数据 

client.c

#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main()
{
	int sock = -1;
	struct sockaddr_in addr = {0};
	char* test = "D.T.Software";

	sock = socket(AF_INET, SOCK_STREAM, 0);

	if(sock == -1)
	{
		printf("socker error\n");

		return -1;
	}

	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	addr.sin_port = htons(8888);

	if(connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1)
	{
		printf("connect error\n");

		return -1;
	}

	printf("connect succeed\n");

	send(sock, test, strlen(test), MSG_OOB);

	getchar();

	close(sock);

	return 0;
}

select-server.c

#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <stdio.h>
#include <string.h>

int server_handler(int server)
{
	struct sockaddr_in addr = {0};
	int asize = sizeof(addr);

	return accept(server, (struct sockaddr*)&addr, &asize);
}

int client_handler(int client)
{
	int ret = -1;
	char buf[32] = {0};

	ret = recv(client, buf, sizeof(buf) - 1, 0);

	if(ret > 0)
	{
		buf[ret] = '\0';

		printf("Receive: %s\n", buf);

		if(strcmp(buf, "quit") != 0)
		{
			ret = write(client, buf, strlen(buf));
		}
		else
		{
			ret = -1;
		}
	}

	return ret;
}

int main()
{
	int server = 0;
	struct sockaddr_in saddr = {0};
	int max = 0;
	int num = 0;
	fd_set reads = {0};
	fd_set temps = {0};
	fd_set except = {0};
	struct timeval timeout = {0};

	server = socket(PF_INET, SOCK_STREAM, 0);

	if(server == -1)
	{
		printf("server socket error\n");

		return -1;
	}

	saddr.sin_family = AF_INET;
	saddr.sin_addr.s_addr = htonl(INADDR_ANY);
	saddr.sin_port = htons(8888);

	if(bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == -1)
	{
		printf("server bind error\n");

		return -1;
	}

	if(listen(server, 1) == -1)
	{
		printf("server listen error\n");

		return -1;
	}

	FD_ZERO(&reads);
	FD_SET(server, &reads);

	max = server;

	printf("start to accept\n");

	while(1)
	{
		timeout.tv_sec = 0;
		timeout.tv_usec = 10000;

		temps = reads;
		except = reads;

		num = select(max + 1, &temps, NULL, &except, &timeout);

		if(num > 0)
		{
			for(int i = 0; i <= max; i++)
			{
				if(FD_ISSET(i, &except))
				{
					if(i != server)
					{
						char buf[2] = {0};

						int r = recv(i, buf, sizeof(buf) - 1, MSG_OOB);

						if(r > 0)
						{
							printf("OOB: %s\n", buf);
						}		
					}
				}

				if(FD_ISSET(i, &temps))
				{
					if(i == server)
					{
						int client = server_handler(server);

						if(client >= 0)
						{
							FD_SET(client, &reads);

							max = (max > client) ? max : client;

							printf("accept client: %d\n", client);
						}
					}
					else
					{
						if(client_handler(i) == -1)
						{
							FD_CLR(i, &reads);

							close(i);
						}
					}
				}
			}
		}
	}

	close(server);

	return 0;
}

当有紧急数据到来时,与客户端通信 socket 会产生一个异常事件;我们通过 select 来监听对应描述符上的异常事件,当 select 监听到异常事件后,如果不是服务端上的异常事件,我们就通过 recv 来读取紧急数据。这样就实现了紧急数据的高效读取。

程序运行结果如下所示:

数据收发的拓展用法

当一个数据包同时存在普通数据和紧急数据时,客户端的紧急数据会优先发送出去,所以服务端首先会收到紧急数据,接着会收到普通数据。

小结论

read() / write() 可用于收发普通数据 (不具备拓展功能)

send() / recv() 可通过选项信息拓展更多功能

TCP 紧急数据可标识 256 种紧急事件 (异常事件)

通过 select 能够及时处理紧急数据,并区分普通数据

上一篇:数据结构初学


下一篇:六十:权限提升-MY&MS&ORA等SQL数据库提权