218-I/O复用函数的使用(select)

I/O复用函数的使用(select)

I/O 复用使得程序能同时监听多个文件描述符,这对于提高程序的性能至关重要。通常,网络程序在下列情况下需要使用 I/O 复用技术:
◼ TCP 服务器同时要处理监听套接字和连接套接字。
◼ 服务器要同时处理 TCP 请求和 UDP 请求。
◼ 程序要同时处理多个套接字。
◼ 客户端程序要同时处理用户输入和网络连接。
◼ 服务器要同时监听多个端口。
(同时处理多个描述符)

需要指出的是,I/O 复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依处理其中的每一个文件描述符,这使得服务器看起来好像是串行工作的。如果要提高并发处理的能力,可以配合使用多线程或多进程等编程方法。

在这里插入图片描述
TCP中,第一个客户端和服务器端建立连接,向服务器端不发数据,服务器端就在recv阻塞住,无法继续执行;如果有第二个客户端与服务器端建立连接,就在已完成三次握手的队列中放着,等着accept处理它,由于我们的代码阻塞在recv,没有机会去执行accept,导致第二个客户端得不到响应,我们之前的解决方法是accept之后的代码在子线程中执行。多线程多进程解决此问题。
select poll epoll都是系统调用

那么今天我们就要用I/O复用的select方法解决此问题!

I/O复用解决描述符过多的情况。(开销问题)
我们在一个线程内完成多个客户端连接服务器端。

如何做到的?在单个进程内同时处理多个描述符?
举个例子,假如学校要给你们发一本书,一种情况下,所有人到图书馆门前等,每个人就是一个线程,每个人都想要自己的数据,每个人都在等,阻塞住,如果谁的书到了,点名,谁就上去把书拿了,线程退出,线程往下执行。那么多人等,都阻塞住。还有一种情况,就留一个人在那等,谁的书发下来,那个人就给你打电话,你就去图书馆领取,这就是留一个人在那里等,就一个人阻塞住,先看有你没有数据,如果有数据,就通知你来处理这个数据,相当于代码执行recv,不会阻塞,因为你的数据已经到达了。

就是我们用I/O函数先去检测所有的描述符,挑谁上面有数据,把这些检测出来,对这些有数据的描述符来个循环,依次处理,依次recv处理,都不会阻塞,因为都有数据,select检测没有数据的描述符我们不去recv处理。

我们首先给出一个集合
把关系的描述符都加进去(3,4,6,7),然后select前去检测,谁上面有数据,select返回的n的值为-1,就是失败了。n为0,就是超时了,select可以永久阻塞也可以设置一个超时时间,时间到了提醒我们。n>0,集合中有n个元素上面有数据。接下来我们要找到是哪个,比如说n=2,说明3,4,6,7中有2个有数据。select返回只是告诉我们有n个有数据,不会告诉我们是哪个。select返回以后,我们去检测哪个有数据,比如说4,6有数据,我们把4,6处理,然后再把 这些数据重新添加到集合中,再select处理。
在这里插入图片描述
在这里插入图片描述

select 的接口介绍
select 系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符的可读、可写和异常等事件。
select 系统调用的原型如下:
存放描述符是以位来存储的!
在这里插入图片描述
这个集合有1024个位。比这里有描述符3,4,6,7,如果我们按一个整数去存,一个整数占4个字节,很占空间。所以我们在这里用偏移量去算(按位存)。比如描述符是0,把第一个值0置为1,如果描述符是2,把第三个0改成1。这个集合可以存放0-1023的描述符。
当我们把这些描述符添加进去,我们把集合交给select,select返回2,就说明2个有数据。原来我们把第4个,第五个,第七个,第八个位置为1,分别代表描述符,3,4,6,7,假如3和6上有数据,我们就把4和7对应的位置从1置为0。select把没有数据的描述符从1置为0。但是select不会告诉我们是哪个有数据。接下来我们用一个方法去测试,把1从头向右移,按位与,都为1为1。
测出3和6上有数据,系统提供了4个方法:FD_ZERO,把集合空间全部清空置为0,FD_SET,把描述符添加到相应的位置,FD_CLR,清除某个描述符的位置,int FD_ISSET,测试描述符在集合中有没有被设置(1按位与)。

#include <sys/select.h>

int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct ti
		   meval *timeout);
/*
select 成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内
没有任何文件描述符就绪,select 将返回 0。select 失败是返回-1.如果在 select 等待
期间,程序接收到信号,则 select 立即返回-1,并设置 errno 为 EINTR。

maxfd 参数指定的被监听的文件描述符的总数。它通常被设置为 select 监听的所
有文件描述符中的最大值+1,就是把前面几个位关注,后面的位就不关注了

readfds、writefds 和 exceptfds 参数分别指向可读、可写和异常等事件对应的文件
描述符集合。应用程序调用 select 函数时,通过这 3 个参数传入自己感兴趣的文件
描述符。select 返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪
*/

//fd_set 结构如下:
#define __FD_SETSIZE 1024//数组有2014个位的空间
typedef long int __fd_mask;
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
typedef struct
{
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];//多少个元素
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
1 # define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;


//通过下列宏可以访问 fd_set 结构中的位:
FD_ZERO(fd_set *fdset); // 清除 fdset 的所有位
FD_SET(int fd, fd_set *fdset); // 设置 fdset 的位 fd
FD_CLR(int fd, fd_set *fdset); // 清除 fdset 的位 fd
int FD_ISSET(int fd, fd_set *fdset);// 测试 fdset 的位 fd 是否被设置

/*
timeout 参数用来设置 select 函数的超时时间。它是一个 timeval 结构类型的指
针,采用指针参数是因为内核将修改它以告诉应用程序 select 等待了多久。timeval
结构的定义如下:
struct timeval
{
long tv_sec; //秒数
long tv_usec; // 微秒数
};
如果给 timeout 的两个成员都是 0,则 select 将立即返回。如果 timeout 传递
NULL,则 select 将一直阻塞,直到某个文件描述符就绪
*/

读事件:一开始是没有就绪的,因为接收缓存区没有数据,对方发了才有
写事件:一开始是就绪的,因为发送缓冲区没有数据,是空闲的

select(检查键盘是否有数据)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
为什么每次都要去清空,添加呢?
因为select返回会去修改fdset的内容,下次我们要关注哪些描述符就得重新添加,包括tv,select返回以后会去修改tv的值,如果5秒超时,3秒的时候数据到达了,它会返回告诉你描述符上有数据,此时会把tv的值改为2秒,因为用了3秒
运行程序
在这里插入图片描述
在这里插入图片描述
select检测有数据,才去read读。

看下图,第一次,集合中只有一个描述符,sockfd,select检测如果有数据,n只可能是1,如果sockfd有数据,我们调用accept,得到新的c,把c添加进集合,集合中有2个描述符了,如果select返回1,只有一个有数据,使用FED_ISSET,检测,发现仍然是sockfd有数据,调用accept,产生一个新的c,添加进集合,集合中有3个描述符,select返回1,检测,发现是c有数据,就调动recv处理。
但是select返回之后,会把没有数据的描述符删掉,所以我们有必要定义一个数组,事先把这些描述符先存起来!
下标就是描述符的值,方便寻找查询!或者把数组的值初始化为-1,代表空闲的。是-1就可以把描述符存进去。
在这里插入图片描述

select 处理多个客户端

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
封装好,我们书写主函数
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
客户端的代码在此!
在这里插入图片描述
在这里插入图片描述

运行服务器和客户端
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最后关闭的时候,一定要移除,如果不移除,下一轮会把这个描述符继续添加到集合中,但是对方已经关闭了,如果我们去检测这个描述符有没有数据,但是它执行了return -1,有数据,就有问题了。所以我们select只是去检测“还在通话的”。

我们把接收数据从127改为1,一次只收1个字符

在这里插入图片描述
全读到了,意味着我们把hello发出去,select,有数据,recv,再一次select,缓冲区上有数据,recv。当对方发了数据,如果我们没有收完,下一次select检测,这个描述符还是有数据,再recv,不会丢数据,还会再次提醒你。

使用 select 实现的 TCP 服务器代码如下:

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

#define MAX_FD 128
#define DATALEN 1024

// 初始化服务器端的 sockfd 套接字
int InitSocket()
{
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd == -1) return -1;

	struct sockaddr_in saddr;
	memset(&saddr, 0, sizeof(saddr));
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(6000);
	saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

	int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
	if(res == -1) return -1;

	res = listen(sockfd, 5);
	if(res == -1) return -1;

	return sockfd;
}

// 初始化记录服务器套接字的数组
void InitFds(int fds[], int n)
{
	int i = 0;
	for (; i < n; ++i)
	{
		fds[i] = -1;
	}
}

// 将套接字描述符添加到数组中
void AddFdToFds(int fds[], int fd, int n)
{
	int i = 0;
	for (; i < n; ++i)
	{
		if (fds[i] == -1)
		{
			fds[i] = fd;
			break;
		}
	}
}

// 删除数组中的套接字描述符
void DelFdFromFds(int fds[], int fd, int n)
{
	int i = 0;
	for (; i < n; ++i)
	{
		if (fds[i] == fd)
		{
			fds[i] = -1;
			break;
		}
	}
}

//将数组中的套接字描述符设置到 fd_set 变量中,并返回当前最大的文件描述符值
int SetFdToFdset(fd_set *fdset, int fds[], int n)
{
	FD_ZERO(fdset);
	int i = 0, maxfd = fds[0];
	for (; i < n; ++i)
	{
		if (fds[i] != -1)
		{
			FD_SET(fds[i], fdset);
			if (fds[i] > maxfd)
			{
				maxfd = fds[i];
			}
		}
	}

	return maxfd;
}

void GetClientLink(int sockfd, int fds[], int n)
{
	struct sockaddr_in caddr;
	memset(&caddr, 0, sizeof(caddr));
	socklen_t len = sizeof(caddr);
	int c = accept(sockfd, (struct sockaddr*)&caddr, &len);
	if (c < 0)
	{
		return;
	}

	printf("A client connection was successful\n");
	AddFdToFds(fds, c, n);
}

// 处理客户端数据
void DealClientData(int fds[], int n, int clifd)
{
	char data[DATALEN] = { 0 };
	int num = recv(clifd, data, DATALEN - 1, 0);
	if (num <= 0)
	{
		DelFdFromFds(fds, clifd, n);
		close(clifd);
		printf("A client disconnected\n");
	}
	else
	{
		printf("%d: %s\n", clifd, data);
		send(clifd, "OK", 2, 0);
	}
}

// 处理 select 返回的就绪事件
void DealReadyEvent(int fds[], int n, fd_set *fdset, int sockfd)
{
	int i = 0;
	for (; i < n; ++i)
	{
		if (fds[i] != -1 && FD_ISSET(fds[i], fdset))
		{
			if (fds[i] == sockfd)
			{
				GetClientLink(sockfd, fds, n);
			}
			else
			{
				DealClientData(fds, n, fds[i]);
			}
		}
	}
}

int main()
{
	int sockfd = InitSocket();
	assert(sockfd != -1);

	fd_set readfds;
	int fds[MAX_FD];
	InitFds(fds, MAX_FD);
	AddFdToFds(fds, sockfd, MAX_FD);

	while ( 1 )
	{
		int maxfd = SetFdToFdset(&readfds, fds, MAX_FD);
		struct timeval timeout;
		timeout.tv_sec = 2; // 秒数
		timeout.tv_usec = 0; //微秒数
		int n = select(maxfd + 1, &readfds, NULL, NULL, &timeout);
		if (n < 0)
		{
			printf("select error\n");
			break;
		}
		else if (n == 0)
		{
			printf("time out\n");
			continue;
		}

		DealReadyEvent(fds, MAX_FD, &readfds, sockfd);
	}

	exit(0);
}

TCP客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int InitSocket()
{
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd == -1) return -1;

	struct sockaddr_in saddr;
	memset(&saddr, 0, sizeof(saddr));
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(6000);
	saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

	int res = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
	if(res == -1) return -1;

	return sockfd;
}

int main()
{
	int sockfd = InitSocket();
	assert(sockfd != -1);

	while ( 1 )
	{
		printf("please input: ");
		char buff[128] = { 0 };
		fgets(buff, 127, stdin);

		if (strncmp(buff, "bye", 3) == 0)
		{
			break;
		}

		int n = send(sockfd, buff, strlen(buff) - 1, 0);
		{
			printf("send error\n");
			break;
		}

		memset(buff, 0, 128);
		n = recv(sockfd, buff, 127, 0);
		if (n <= 0)
		{
			printf("send error\n");
			break;
		}
		printf("%s\n", buff);
	}

	close(sockfd);
	exit(0);
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林林林ZEYU

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

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值