数据收发的拓展用法

问题

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 能够及时处理紧急数据,并区分普通数据

MSG_PEEK (数据窥探)

使用 MSG_PEEK 选项能够获取接收缓冲区数据的拷贝

  • recv() 专用选项,可用于数据预接收
  • 指定 MSG_PEEK 选项时,不会清空缓冲区
  • 可用于获取接收缓冲区中的数据量 (字节数)

MSG_PEEK 数据窥探示例

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

	sleep(2);

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

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

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

				r = recv(client, buf, sizeof(buf), 0);
				buf[r] = '\0';
				printf("Receive: %s\n", buf);
			}
			else
			{
				printf("no data in receive buf\n");
			}

		}while(r > 0);

		printf("\n");

		close(client);
	}

	close(server);

	return 0;
}

当调用 recv() 函数指定 flags 为 MSG_PEEK 时,当接收缓冲区中没有数据时就阻塞等待,接收缓冲区有数据时就返回,将接收缓冲区的数据拷贝到我们指定的 buf 中,但接收缓冲区的数据并没有清空,只是进行了数据窥探,这样我们就可以知道数据的长度和内容了,之后再调用 recv(),进行数据的接收。

程序运行结果如下所示:

服务端首先窥探出接收缓冲区的数据的长度和内容,随后进行再进行读取;当客户端退出后,reccv() 返回 0。

MSG_DONTWAIT (立即收发模式)

数据收发时不阻塞,立即返回

  • send() - 如果无法将数据送入发送缓冲区,那么直接错误返回
  • recv() -  如果接收缓冲区没有数据,那么直接错误返回

MSG_DONTWAIT 示例

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

	sleep(1);

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

	sleep(2);

	send(sock, "quit", 5, 0);

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

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

				if(strcmp(buf, "quit") == 0)
				{
					break;
				}
			}
			else
			{
				printf("no data receive\n");
				sleep(1);
			}

		}while(1);

		printf("\n");

		close(client);
	}

	close(server);

	return 0;
}

服务端在接收数据的时候使用的是不阻塞接收,无论接收缓冲区是否有数据都立即返回,如果接收缓冲区没有数据就返回 -1。

程序运行结果如下所示:

recv() 返回 -1 时,表示接收缓冲区并没有数据,我们休眠 1s 后,再进行读取。

再论阻塞发送模式 (flags => 0)

send()

再论阻塞接收模式 (flags => 0)

recv()

通信框架的迭代增强 

MSG_WAITALL (等待数据) 

接收专用,等待需要的数据量完全满足时,recv() 才返回

MSG_MORE (更多数据)

发送专用,指示内核不着急将发送缓冲区中的数据进行传输

数据收发拓展用法

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";
	int i = 0;

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

	for(i = 0; i < strlen(test); i++)
	{
		send(sock, test + i, 1, 0);

		if(i % 2)
		{
			sleep(1);
		}
	}

	test = "quit";

	for(i = 0; i < strlen(test) - 1; i++)
	{
		send(sock, test + i, 1, MSG_MORE);
		sleep(1);
	}

	send(sock, test + i, 1, 0);

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

		static int len[2] = {12, 4};

		do
		{
			for(int i = 0; i < 2; i++)
			{
				r = recv(client, buf, len[i], MSG_WAITALL);	

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

					if(strcmp(buf, "quit") == 0)
					{
						break;
					}
				}
			}	

		}while(r > 0);

		printf("\n");

		close(client);
	}

	close(server);

	return 0;
}

服务端在接收数据时指定 flags 为 MSG_WAITALL,当接收缓冲区中有我们指定多的数据长度后才返回。

客户端在发送 quit 时,前三个字节发送时指定了 flags 为 MSG_MORE,这会告诉内核先不要将发送缓冲区的数据发送出去,等第四个字节 t 写入到发送缓冲区后再进行发送。

程序运行结果如下所示:

服务端接收到数据的字节数一定等于想要获取到的数据字节数。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值