栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

I/O 多路复用select、poll

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

I/O 多路复用select、poll

I/O 多路复用

I/O 多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是 读就绪 或者 写就绪 ),能够通知程序进行相应的读写操作。
select 、 poll 和 epoll 是 Linux API 提供的 I/O 复用方式。.
本质上就是,把本应该由程序员的事情交给内核负责:检测这些读写缓冲区是否可用(需要调用阻塞函数);检测一轮以后内核将可操作的fd告诉我们,我们再去操作就不会再阻塞。

select和poll的底层是线性表


服务器的文件描述符


服务器包含两种文件描述符,每一个都有两个缓存区:
1、监听LFD:用于标志是否有数据是否能够读写数据,需要调用accept系统调用。(有且仅有一个)
2、通讯FD1:读取和输出数据,调用read/recv、write/send方法。(N,每建立一次连接加1个)
问题在于该三个方法(系统调用)都是阻塞的,当全部在一个线程里就会出现一个阻塞都阻塞。

使用多路IO复用,四个buffer就不需要自己去维护了;交由内核维护。
当内核检测之后会通知那些缓冲区 可以读写,再去系统调用,由于已经就绪不会再有堵塞发生。
当处理数据并不是同时进行,而是优先到后进行的(因为是单线程)。


select 概述

可跨平台
select 系统调用是用来让我们的程序**监视多个文件句柄的状态变化的** 。
select 函数监视的文件描述符分 3 类:writefds 、 readfds 、和 exceptfds 。
调用后 select 函数会阻塞 ,直到有描述符就绪(有数据可读 、可写 、或者有 except ),或者超时(timeout 指定等待时间,如果立即返回设为 null 即可),函数返回 。

缺点
select 本质上是通过设置或者检查存放 fd 标志位的数据结构来进行下一步处理。这样所带来的缺点是:

1 、 单个进程可监视的 fd 数量被限制,即能监听端口的大小有限;

2 、 它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有**O(n)**的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

3 、需要维护一个用来存放大量 fd 的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

函数
int select (int n, fd_set *readfds , fd_set *writefds , fd_set *exceptfds , 
			struct timeval *timeout);

n:要检测的文件描述符集合里,最大的文件描述符的值+1;这是因为fd是线性表(数组)的下标;或者直接写1024。
readfds:读集合,检测一系列文件描述符的读缓冲区。
    传入传出参数,读集合(放需要写的fd) 一般情况下都是需要检测的,这样才知道通过哪个文件描述符接收数据。
writefds:写集合,检测一系列文件描述符的读缓冲区。
    传入传出参数,如果不需要使用这个参数可以指定为NULL。
exceptfds :异常集合

slect函数检测这些集合里的文件描述符,如果这些集合都没有满足条件(读集合里fd的读缓存区全为空,写集合里fd的写缓冲区全为慢的)select会一直在检测;timeout设置最多检测时间;timeout=0,select函数不阻塞,直接退出。

为什么类型是fd_set *?
因为我们需要把一块内存传给select,在函数体内部修改这个内存;也就是缺点3。

fd_set

占据1024bit,128Byte,int[32];=但是以bit去处理数据。
每一个文件标识符对应一个bit。
如果集合中标志位为 0代表不检测 这个fd状态;
如果集合中标志位为 1代表检测 这个fd状态;这个fd在fd_set里

fd3 6 9 10读缓存区有数据,记录下来并回传给用户空间;


遍历检查fs_set是否有为1的,为1的说明已经准备就绪,分辨一下是什么类型的缓存区。

void FD_CLR(int fd, fd_set *set);
// 判断文件描述符fd是否在set集合中 == 读一下fd对应的标志位到底是0还是1
int  FD_ISSET(int fd, fd_set *set);
// 将文件描述符fd添加到set集合中 == 将fd对应的标志位设置为1
void FD_SET(int fd, fd_set *set);
// 将set集合中, 所有文件文件描述符对应的标志位设置为0, 集合中没有添加任何文件描述符
void FD_ZERO(fd_set *set);
select流程
  1. 先将需要监控读、写或异常的fd分别存入对应的集合中;
  2. 这三个集合会被拷入进内核中;内核基于一个线性表去轮询这些集合的事件;
  3. 内核发现出现了事件(可以读写或者出现异常),传出三个集合;
  4. 传出的三个集合会被内核写到传入的三个集合的地址上(readfds 、writefds 、exceptfds 被内核修改)。

举个栗子:

集合传入传出
readfds5、6、7、8、95、6、7、8、9
writefds7、8、9、109、10
exceptfds5-10空

传出和传入的集合在同一块内存上,由内核负责改写。
读集合中有5和6号描述符,判断一下到底是用于监听的还是数据的;如果是监听的调用accept,与客户端建立链接;读数据就是调用read/rev。
写集合有9、10,调用send。

服务器代码示例
  1. 初始化fd_set,服务器的套接字fd并绑定
  2. while(1)
  3. 运行select,并把select之后的fd_set提出来进行遍历,分辨是监听的还是数据的
  4. 如果是监听的,将新的fd加入fd_set中

初始化服务器套接字并绑定

	
	int iSocketServer= socket(AF_INET, SOCK_STREAM, 0);
	
	
	int opt = 1;
	setsockopt(iSocketServer, SOL_SOCKET, SO_REUSEADDR, (void*)&opt, sizeof(opt));
	
	
	struct sockaddr_in tSocketServerAddr;
	
	struct sockaddr_in tSocketClientAddr;
	
	
	tSocketServerAddr.sin_family      = AF_INET;
	tSocketServerAddr.sin_port        = htons(SERVER_PORT);
	
	tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;
	
	memset(tSocketServerAddr.sin_zero,0,8);
	
	
	
	iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr,
			 sizeof(struct sockaddr));
   
    //设置监听
    listen(iSocketServer, 128);

iSocketServer为fd,套接字;tSocketServerAddr负责储存IP端口信息,并通过bind实现于iSocketServer绑定。
tSocketServerAddr主要有四块需要设置的:

  1. sin_family通讯类型;
  2. sin_port服务器端口,注意需要通过htons()转变为二进制;
  3. sin_addr.s_addr历史遗留问题。
  4. memset(tSocketServerAddr.sin_zero,0,8); 预留位置0。

服务器的设置

  1. iSocketServer = socket(AF_INET, SOCK_STREAM, 0); 得到文件描述符;
  2. struct sockaddr_in tSocketServerAddr; 包含IP和端口信息,主要初始化以下四个:
    sin_family:链接类型;sin_port:客户端端口号;sin_addr.s_addr:客户端IP。
  3. bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr,
    sizeof(struct sockaddr)); 绑定tSocketServerAddr和iSocketServer。

设置fd_set 集合

    // 初始化检测的读集合
    fd_set rdset;
    fd_set rdtemp;
    
    // 清零
    FD_ZERO(&rdset);
    
    // 将监听的lfd设置到检测的读集合中
    FD_SET(iSocketServer, &rdset);

rdtemp主要用于拷贝使用,作为rdset的备份;
清空集合,并把iSocketServer加入集合中;由于目前集合只有一个iSocketServer,所以iSocketServer值必然是当前最大的。

while循环下的select

while(1){
	rdtemp = rdset;
	int num = select(maxfd+1, &rdtemp, NULL, NULL, NULL);

	//select会修改rdtemp值,现在去检查集合的fd。
	//因为用于监视的fd就只有一个,只用看iSocketServer现在还在不在集合里
	if(FD_ISSET(iSocketServer, &rdtemp))
        {
            // 接受连接请求, 这个调用不阻塞
            int cliLen = sizeof(tSocketClientAddr);
            int iSocketClient = accept(iSocketServer, 
            						(struct sockaddr*)&tSocketClientAddr, &cliLen);

            // 得到了有效的文件描述符
            // 通信的文件描述符添加到读集合
            // 在下一轮select检测的时候, 就能得到缓冲区的状态
            FD_SET(iSocketClient , &rdset);
            // 重置最大的文件描述符
            maxfd = cfd > maxfd ? cfd : maxfd;
        }
        
        for(int i=0; i
			// 判断除监听用fd外,其他fd是否在集合中
            if(i != lfd && FD_ISSET(i, &rdtemp))
            {
                char buf[10] = {0};
                // 一次只能接收10个字节, 客户端一次发送100个字节
                // 一次是接收不完的, 文件描述符对应的读缓冲区中还有数据
                // 下一轮select检测的时候, 内核还会标记这个文件描述符缓冲区有数据 -> 再读一次
                // 	循环会一直持续, 知道缓冲区数据被读完位置
                int len = read(i, buf, sizeof(buf));
                if(len == 0)
                {
                    printf("客户端关闭了连接...n");
                    // 将检测的文件描述符从读集合中删除
                    FD_CLR(i, &rdset);
                    close(i);
                }
                else if(len > 0)
                {
                    // 收到了数据
                    // 发送数据
                    write(i, buf, strlen(buf)+1);
                }
                else
                {
                    // 异常
                    perror("read");
                }
            }
        }
    }
        

1、accept(int sockfd,struct sockaddr *addr,socklen_t addrlen); 
  fd服务器套接字,addr用来存放客户端的IP端口信息,addrlen是addr的大小;其中(struct sockaddr) &sockaddr_in直接进行类型转换得到。
  返回的客户端的fd,iSocketClient。
2、将新的客户端fd加入rdset集合中,更新集合;
3、 maxfd = cfd > maxfd ? cfd : maxfd; 更新最大的fd值。
4、如果缓冲区一次没有被读完,内核仍标记这个文件描述符缓冲区有数据,下一次依旧会被加入集合。
5、int len = read(i, buf, sizeof(buf)); 当len=0时,说明数据已经被读完了。


客户端代码

客户端不需要考虑select的问题,和普通的client一样即可

	int iSocketClient;
	iSocketClient = socket(AF_INET, SOCK_STREAM, 0);
	
	unsigned char ucSendBuf[1000];

	struct sockaddr_in tSocketSever;
	tSocketSever.sin_family      = AF_INET;
	tSocketSever.sin_port        = htons(SERVER_PORT);

	//int inet_aton(const char *cp,struct in_addr *inp)
	
	//tSocketSeverAddr.sin_addr.s_addr = INADDR_ANY;
	if( 0 == inet_aton(argv[1],&tSocketSever.sin_addr))
		{
			printf("invalid server_ip!n");
			return -1;
		}
	else
		printf("Server IP : %d n",tSocketSever.sin_addr.s_addr);
	
	memset(tSocketSever.sin_zero,0,8);
		
	
//	connect(iSocketClient, (const struct sockaddr *)&tSocketSever,
//					   sizeof(tSocketSever));

	if( -1 == connect(iSocketClient, (const struct sockaddr *)&tSocketSever,
                   sizeof(tSocketSever)))
		{
			printf("connet error!n");
			return -1;
		}

	
	while(1)
		{
			
			if(fgets(ucSendBuf, 999 ,stdin))
				{
					
					
					ssize_t iSendlen = send(iSocketClient, ucSendBuf, 
											strlen(ucSendBuf), 0);
					if(iSendlen <= 0)
						{
							close(iSocketClient);
							return -1;
						}
				}
		}

1、iSocketClient = socket(AF_INET, SOCK_STREAM, 0); 设置客户端fd;
2、struct sockaddr_in tSocketSever; 定义所链接服务器的性质;
3、connect(iSocketClient, (const struct sockaddr *)&tSocketSever,sizeof(tSocketSever)); 申请链接;

poll

select和poll差不多,但是由于不能跨平台,但是一般不使用。

和 select 函数一样,监听多个文件描述符,直到条件满足或超时的时候 poll 返回,通过 遍历文件描述符来获取已经就绪的描述符。

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.。

不同于 select 使用三个位图来表示三个 fdset 的方式, poll 使用 一个 pollfd 的指针实现。

缺点
缺点基本与 select 函数一致

1 . poll 返回后 通过遍历文件描述符来获取已经就绪的描述符 。

2 . 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降 。

概述
#include 
// 每个委托poll检测的fd都对应这样一个结构体
struct pollfd {
    int   fd;         
    short events;     
    short revents;    
};

struct pollfd myfd[n];
int poll(struct pollfd *fds, nfds_t nfds, int timeout);


event:说明需要内核监视什么事件;
revents:内核检测到事件(由event决定是什么事件)就会储存在revents;

struct pollfd myfd[n]; 每个文件描述符需要一个pollfd,所以当有多个n个文件描述符时候,需要有n个pollfd,索性写为数组。
nfds_t nfds:数组(最大的)有效元素下标+1;
timeout:最长阻塞时长;
返回值:准备就绪的文件描述符个数。

poll的实现
unsigned int (*poll) (struct file *, struct poll_table_struct *);
//返回值:event事件

void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table * p);

typedef struct poll_table_struct {
	poll_queue_proc _qproc;
	unsigned long _key;
} poll_table;
//poll_table_struct 和 poll_table是同一个结构体
服务器代码
......
 struct pollfd fds[1024];
    // 初始化
    for(int i=0; i<1024; ++i)
    {
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }
    fds[0].fd = lfd;

    int maxfd = 0;
    while(1)
    {
        // 委托内核检测
        ret = poll(fds, maxfd+1, -1);
        if(ret == -1)
        {
            perror("select");
            exit(0);
        }

        // 检测的度缓冲区有变化
        // 有新连接
        if(fds[0].revents & POLLIN)
        {
            // 接收连接请求
            struct sockaddr_in sockcli;
            int len = sizeof(sockcli);
            // 这个accept是不会阻塞的
            int connfd = accept(lfd, (struct sockaddr*)&sockcli, &len);
            // 委托内核检测connfd的读缓冲区
            int i;
            for(i=0; i<1024; ++i)
            {
                if(fds[i].fd == -1)
                {
                    fds[i].fd = connfd;
                    break;
                }
            }
            maxfd = i > maxfd ? i : maxfd;
        }
        // 通信, 有客户端发送数据过来
        for(int i=1; i<=maxfd; ++i)
        {
            // 如果在集合中, 说明读缓冲区有数据
            if(fds[i].revents & POLLIN)
            {
                char buf[128];
                int ret = read(fds[i].fd, buf, sizeof(buf));
                if(ret == -1)
                {
                    perror("read");
                    exit(0);
                }
                else if(ret == 0)
                {
                    printf("对方已经关闭了连接...n");
                    close(fds[i].fd);
                    fds[i].fd = -1;
                }
                else
                {
                    printf("客户端say: %sn", buf);
                    write(fds[i].fd, buf, strlen(buf)+1);
                }
            }
        }
    }
    close(lfd);
......

主要是对pollfd fds[1024]的处理:
1、全部fd置为-1,并设置events ,因为fd>0的;说明为-1的都是可用空位;
2、由于第一个肯定都是监听的,所以fds[0]=lfd;
3、ret = poll(fds, maxfd+1, -1) timeout:-1,会一直阻塞直到有数据;
4、通过for循环找到第一个为-1的位置,填充上新的链接的fd;
5、通过收发的read函数的返回值判断。


select和poll的问题

1、select需要给内核传递三个集合;一次select需要三个拷贝;
2、poll需要传递一整个pollfd[]的数组;
3、由于是线性检测,所以待检测的套接字越多越慢。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/998948.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号