您好,欢迎来到易妖游戏网。
搜索
您的当前位置:首页小结下select模型,poll模型和epoll模型

小结下select模型,poll模型和epoll模型

来源:易妖游戏网

关于selece,poll和epoll模型的总结

   首先select,poll,epoll都是IO多路复用的机制,先是监听多个文件的描述符fd,一旦某个fd就绪后,就可以进行相应的读写操作。select,poll,epoll从本质上讲都是同步I/O,都需要在读写时间就绪之后自己负责读写,这个过程时阻塞的。

1.select模型
select用于io复用,用于监视多个文件描述符的集合,判断是否有符合条件的事件发生

1.1原理
使用select函数可以先对需要操作的文件描述符进行查询,查看是否目标文件描述符可以进行读写或者错误操作,然后当文件描述符满足操作的条件时才进行真正的io操作

1.2函数原型
int select(int nfds,//最大文件描述符+1
fd_set *readfds,//监控的所有读文件描述符集合
fd_set *writefds,//写集合
fd_set *exceptfds,//异常集合
struct timeval *timeout);//超时时间
其返回值:> 0 正常,== 0 超时,== -1 发生错误

从某个文件描述符的集合中取出某个文件描述符
void FD_CLR(int fd, fd_set *set);

测试某个文件描述符是否在某个集合中
int FD_ISSET(int fd, fd_set *set);

向某个文件描述符集合中加入文件描述符
void FD_SET(int fd, fd_set *set);

清理文件描述符集合
void FD_ZERO(fd_set *set);
注意:文件描述符的集合存在最大的,其最大值为FD_SETSIZE=1024

1.3使用方法
用fd_set(一个数组)来保存要监视的fd,用FD_SET来往里添加想要监视的fd,轮询(死循环)调用select,当监视的信息发生变化了,select解除阻塞状态走到下面,再遍历所有fd,用FD_ISSET确定是想要的fd发生了变化(比如当i=serv_sock时我就调accept去创建用于传数据的套接字,如果不等于就说明是有要接收的数据,直接read就行)。注意:如果新建了一个套接字,需要也用FD_SET把他加入fd_set作为监视的fd。

例如用select来实现echo服务器

客户端和服务端的功能如下:
客户端从标准输入读入一行,发送到服务端
服务端从网络读取一行,然后输出到客户端
客户端收到服务端的响应,输出这一行到标准输出echo服务器和客户端

server.c  服务端

#include  <unistd.h>
#include  <sys/types.h>       /* basic system data types */
#include  <sys/socket.h>      /* basic socket definitions */
#include  <netinet/in.h>      /* sockaddr_in{} and other Internet defns */
#include  <arpa/inet.h>       /* inet(3) functions */
#include <sys/select.h>       /* select function*/

#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

#define MAXLINE 10240

void handle(int * clientSockFds, int maxFds, fd_set* pRset, fd_set* pAllset);

int  main(int argc, char **argv)
{
	//端口号
	int  servPort = 6888;

	//listenq指定的其实就是accept队列也就是ESTABLISHED状态的连接
	int listenq = 1024;

	int  listenfd, connfd;

	struct sockaddr_in cliaddr, servaddr;

	socklen_t socklen = sizeof(struct sockaddr_in);

	int nready, nread;

	char buf[MAXLINE];

	//存放和客户端通信的socket描述符
	int clientSockFds[FD_SETSIZE];

	//描述符集合
	fd_set allset, rset;

	//最大的描述符
	int maxfd;

	//创建socket
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if (listenfd < 0) {
		perror("socket error");
		return -1;
	}

	int opt = 1;

	//监听的端口变成可以复用的
	if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
		perror("setsockopt error");
	}

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(servPort);

	//绑定
	if (bind(listenfd, (struct sockaddr*)&servaddr, socklen) == -1) {
		perror("bind error");
		exit(-1);
	}

	//监听
	if (listen(listenfd, listenq) < 0) {
		perror("listen error");
		return -1;
	}

	//初始化数组
	int i = 0;
	for (i = 0; i< FD_SETSIZE; i++)
	{
		clientSockFds[i] = -1;
	}

	//清空描述符集合
	FD_ZERO(&allset);
	//将描述符加入集合
	FD_SET(listenfd, &allset);

	maxfd = listenfd;

	printf("echo server use select startup, listen on port %d\n", servPort);
	printf("max connection: %d\n", FD_SETSIZE);

	for (;;)  {
		rset = allset;

		//等待事件发生
		nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
		if (nready < 0) {
			perror("select error");
			continue;
		}

		//处理客户端的连接
		if (FD_ISSET(listenfd, &rset)) {
			connfd = accept(listenfd, (struct sockaddr*) &cliaddr, &socklen);
			if (connfd < 0) {
				perror("accept error");
				continue;
			}

			sprintf(buf, "accept form %s:%d\n", inet_ntoa(cliaddr.sin_addr), cliaddr.sin_port);
			printf(buf, "");

			//将客户端连接的描述符加入数组
			for (i = 0; i< FD_SETSIZE; i++) {
				if (clientSockFds[i] == -1) {
					clientSockFds[i] = connfd;
					break;
				}
			}
			if (i == FD_SETSIZE) {
				fprintf(stderr, "too many connection, more than %d\n", FD_SETSIZE);
				close(connfd);
				continue;
			}
			if (connfd > maxfd)
				maxfd = connfd;

			//将描述符加入集合
			FD_SET(connfd, &allset);

			if (--nready <= 0)
				continue;
		}

		//处理客户端的数据收发
		handle(clientSockFds, maxfd, &rset, &allset);
	}
}


void handle(int * clientSockFds, int maxFds, fd_set* pRset, fd_set* pAllset)
{
	int nread;
	int i;
	char buf[MAXLINE];
	for (i = 0; i< maxFds; i++) {
		if (clientSockFds[i] != -1) {
			if (FD_ISSET(clientSockFds[i], pRset)) {
				nread = read(clientSockFds[i], buf, MAXLINE);//读取客户端socket流
				if (nread < 0) {
					perror("read error");
					close(clientSockFds[i]);
					FD_CLR(clientSockFds[i], pAllset);
					clientSockFds[i] = -1;
					continue;
				}
				if (nread == 0) {
					printf("client close the connection\n");
					close(clientSockFds[i]);
					FD_CLR(clientSockFds[i], pAllset);
					clientSockFds[i] = -1;
					continue;
				}

				write(clientSockFds[i], buf, nread);//响应客户端有可能失败暂不处理
			}
		}
	}

}
client.c  客户端

#include  <unistd.h>
#include  <sys/types.h>       /* basic system data types */
#include  <sys/socket.h>      /* basic socket definitions */
#include  <netinet/in.h>      /* sockaddr_in{} and other Internet defns */
#include  <arpa/inet.h>       /* inet(3) functions */
#include <sys/select.h>       /* select function*/

#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

#define MAXLINE 10240
#define max(a,b)    ((a) > (b) ? (a) : (b))
//typedef struct sockaddr  SA;

void handle(int sockfd);

int main(int argc, char **argv)
{
	char * servInetAddr = "127.0.0.1";
	int servPort = 6888;
	char buf[MAXLINE];
	int connfd;
	struct sockaddr_in servaddr;

	if (argc == 2) {
		servInetAddr = argv[1];
	}
	if (argc == 3) {
		servInetAddr = argv[1];
		servPort = atoi(argv[2]);
	}
	if (argc > 3) {
		printf("usage: selectechoclient <IPaddress> <Port>\n");
		return -1;
	}

	connfd = socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(servPort);
	inet_pton(AF_INET, servInetAddr, &servaddr.sin_addr);

	if (connect(connfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
		perror("connect error");
		return -1;
	}
	printf("welcome to selectechoclient\n");
	handle(connfd);     /* do it all */
	close(connfd);
	printf("exit\n");
	exit(0);
}


void handle(int connfd)
{
	FILE* fp = stdin;
	char sendline[MAXLINE], recvline[MAXLINE];
	fd_set rset;
	FD_ZERO(&rset);
	int maxfds = max(fileno(fp), connfd) + 1;
	int nread;
	for (;;) {
		FD_SET(fileno(fp), &rset);
		FD_SET(connfd, &rset);

		if (select(maxfds, &rset, NULL, NULL, NULL) == -1) {
			perror("select error");
			continue;
		}

		if (FD_ISSET(connfd, &rset)) {
			//接收到服务器响应
			nread = read(connfd, recvline, MAXLINE);
			if (nread == 0) {
				printf("server close the connection\n");
				break;
			}
			else if (nread == -1) {
				perror("read error");
				break;
			}
			else {
				//server response
				write(STDOUT_FILENO, recvline, nread);
			}
		}

		if (FD_ISSET(fileno(fp), &rset)) {
			//标准输入可读
			if (fgets(sendline, MAXLINE, fp) == NULL) {
				//eof exit
				break;
			}
			else {
				write(connfd, sendline, strlen(sendline));
			}
		}

	}
}

2.poll模型
检测文件描述符上,是否有某些事件发生。poll本质上和select没有区别,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构

2.1 函数原型
需要包含头文件#include<poll.h>

*int poll(struct pollfd fds,unsigned int nfds,int timeout);
参数:
fds:是一个poll函数监听的struct pollfd结构类型的数组

pollfd结构体定义如下:
struct pollfd{
int fd;//文件描述符(socket描述符)
short events;//等待的事件
short revents;//实际发生了的事件
};
events和revents的取值是一样的,常用的事件:
POLLIN 有数据可读
POLLOUT 写数据不会导致阻塞
POLLMSGSIGPOLL 消息可用
POLLERR 指定的文件描述符发生错误

nfds :是fds的元素个数
timeout :表示poll函数的超时时间,单位是毫秒

注意:
timeout == 0 代表立即返回
timeout > 0 代表等待指定的毫秒数后,返回
timeout < 0 代表永不过期,就是阻塞
函数返回值同select

2.2使用步骤
a:pollfd的数组 fdlist;
b:初始化fdlist;
c:将监听fdlist中,并设置要监测的事件(POLLIN)
d:调用poll函数去监测
e:遍历fdlist集合,处理客户端连接或收发数据

例如一个简单的poll模型
程序将会等待3秒去监测是否有读事件发生,若满足触发条件就会在屏幕上输出所打字节,若3s内无读事件发生会显示超时

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<poll.h>
#include<strings.h>

int main()
{
    struct pollfd poll_fd;

    //设置要检测的描述符,0是标准输入
    poll_fd.fd = 0;

    //设置检测的事件,POLLIN是读事件
    poll_fd.events = POLLIN;

    while(1)
    {
        //3000代表等待3000毫秒(3s),等待检测的最大时间值
        int ret = poll(&poll_fd,1,3000);
    
    
        switch(ret)
        {
            case 0://超时
                 printf("poll timeout\n");
                 break;
            case -1://错误
                 printf("poll error\n");
                 break;
            default:
            {
                //事件判断
                if(poll_fd.revents == POLLIN)
                {
                    char buffer[1024];
                    bzero(buffer,sizeof(buffer));

                    int nread = read(poll_fd.fd,buffer,sizeof(buffer));

                    if(nread > 0)
                    {
                        printf("stdin input %s\n",buffer);
                    }
                }
            }
        }
    }
    return 0;
}

3.epoll模型
为实现高并发,在2.6内核中提出了epoll,是select和poll的增强版本,没有描述符。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件放到内核的一个事件表中,如此用户空间和内核空间的copy只需一次

3.1相关函数
epoll函数基于select和poll接口比较简单,一共3个函数。
a:创建一个epoll实例
int epoll_create(int size);
创建一个epoll的句柄,size是用来告诉内核所要监听的数目有多大
要注意的是,在创建好句柄后,其会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

b:注册socket对应的事件
**int epoll_ctl(int epfd,int op,int fd,struct epoll_event event);

epfd:epoll上下文的描述符,就是epoll_create的返回值;

op:操作命令
EPOLL_CTL_ADD 向epoll监听集合中添加socket描述符
EPOLL_CTL_DEL 从epoll监听集合中删除socket描述符
EPOLL_CTL_MOD 修改

fd :socket描述符,对TCP来说就是accept函数的返回值;

event :在向epoll监听集合当中添加socket描述符的同时,为这个描述符绑定一个触发事件。
可以是以下宏的集合:
1》EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
2》 EPOLLOUT: 表示对应的文件描述符可以写;
3》EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
4》EPOLLERR: 表示对应的文件描述符发生错误;
5》EPOLLHUP: 表示对应的文件描述符被挂断;
6》EPOLLET: 将 EPOLL设为边缘触发(Edge Triggered)模式(默认为水平触发),这是相对于水平触发(Level Triggered)来说的。
7》EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

c:等待epfd_代表的epoll实例中监听的事件发生,events指针返回已经准备好的事件,最多有maxevents个,参数maxevents必须大于0
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);//返回值是需要处理的事件数目

epfd epoll上下文描述符
events 用于保存监听集合当中所有的触发事件
maxevents 最大的监听数
timeout 监听等待超时(ms)
设置timeout_为-1则epoll_wait()会一直阻塞,设置为0则会立即返回。

3.2关于TCP服务器的思路
1》创建服务器的socket
2》创建epoll实例
3》使用epoll_ctl设置
4》监听服务器socket是否有新的客户端建立连接(即是否可读)

while(true) 
{
	epoll_wait();
	for 每个事件
        { 
		if(服务器socket可读)
		{
			accept()  
         
			把新的客户端socket加入epoll监听事件   
		}
         
		else 
		{  
			read()
          
			send()                  
		}
	}
}

此刻发送数据的时候,调用了send函数,若用epoll_wait等待socket可写了后再发数据,就不会造成阻塞了

客户端
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void Usage() 
{
  printf("usage: ./client [ip] [port]\n"); //输入ip地址和端口
}

int main(int argc, char* argv[]) {
  if (argc != 3) {
    Usage();
    return 1;
  }
  //服务器socket地址结构
  struct sockaddr_in addr;
  addr.sin_family = AF_INET;
  addr.sin_addr.s_addr = inet_addr(argv[1]);
  addr.sin_port = htons(atoi(argv[2]));
  int fd = socket(AF_INET, SOCK_STREAM, 0);

  if (fd < 0) {
    perror("socket");
    return 1;
  }
  //连接服务器
  int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
  if (ret < 0) {
    perror("connect");
    return 1;
  }
  
  while(1){
    printf("> ");
    fflush(stdout);//刷新输出缓冲区,即将缓冲区内容输出到屏幕上
    char buf[1024] = {0};
    read(0, buf, sizeof(buf) - 1);
    ssize_t write_size = write(fd, buf, strlen(buf));//写函数
    if (write_size < 0) {
      perror("write");
      continue;
    }
    ssize_t read_size = read(fd, buf, sizeof(buf) - 1);//读函数
    if (read_size < 0) {
      perror("read");
      continue;
    }
    if (read_size == 0) {
      printf("server close\n");
      break;
    }
    printf("server say: %s", buf);
  }
  close(fd);
  return 0;
}
服务器
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <string.h>

//处理客户端连接accept
void handleConnect(int fdListen,int epfd)
{
    struct sockaddr_in addr;
    socklen_t nLen = sizeof(addr);

    bzero(&addr,sizeof(addr));

    int fdClient = accept(fdListen,(struct sockaddr*)&addr,&nLen);

    if(fdClient < 0)
    {
        perror("accept");
        return;
    }

    //显示登录信息
    printf("client %s:%d connected!\n",inet_ntoa(addr.sin_addr),
           ntohs(addr.sin_port));

    struct epoll_event event;
    event.data.fd = fdClient;
    event.events = EPOLLIN;

    //注册事件
    int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,fdClient,&event);

    if(ret < 0)
    {
        perror("epoll_ctl");
        return;
    }

    return;
}

//处理客户端数据收发
void handleClientRW(int fdClient,int epfd)
{
    char buffer[1024] = {0};

    int nsize = read(fdClient,buffer,sizeof(buffer));

    if(nsize < 0)
    {//error
        perror("read");
        return;
    }

    if(nsize == 0)
    {//对方close
        close(fdClient);

        //将事件删除
        epoll_ctl(epfd,EPOLL_CTL_DEL,fdClient,NULL);
        printf("client say good bye!\n");
        return;
    }


    printf("client say:%s",buffer);


    //将数据发送给客户端
    write(fdClient,buffer,strlen(buffer));
}




int main()
{
    int fdListen;

    struct sockaddr_in  srvaddr;
    srvaddr.sin_family = AF_INET;
    srvaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    srvaddr.sin_port = htons(6888);


    //创建socket
    fdListen = socket(PF_INET,SOCK_STREAM,0);

    if(fdListen < 0)
    {
        perror("socket error:");
        return -1;
    }

    //绑定
    int nRet = bind(fdListen,(struct sockadd*)&srvaddr,sizeof(srvaddr));

    if(nRet < 0)
    {
        perror("bind error:");
        return -1;
    }


    //监听
    int nListen = listen(fdListen,5);

    if(nListen < 0)
    {
        perror("listen error:");
        return -1;
    }


    printf("server listening at 6888 port!\n");

    //创建epoll实例
    int epfd = epoll_create(10);

    if(epfd < 0)
    {
        close(fdListen);
        perror("epoll_create:");
        return -1;
    }

    //用于添加事件
    struct epoll_event event;
    event.data.fd = fdListen;
    event.events = EPOLLIN;

    //注册事件
    int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,fdListen,&event);

    if(ret < 0)
    {
        close(fdListen);
        perror("epoll_ctl:");
        return -1;
    }//处理客户端数据收发


    //存放检测到的事件
    struct epoll_event events[10];


    while(1)
    {
        //等待检测的事件发生,返回值是发生事件的数量
        int nNums = epoll_wait(epfd,&events,10,-1);

        if(nNums < 0)
        {
            perror("epoll_wait");
            continue;
        }

        if(nNums == 0)
        {
            perror("epoll_wait timeout");
            continue;
        }


        int i;

        for(i=0;i<nNums;++i)
        {
            if(events[i].data.fd == fdListen)
            {
                //处理客户端连接accept
                handleConnect(fdListen,epfd);

            }
            else
            {
                //处理客户端数据收发
                handleClientRW(events[i].data.fd,epfd);

            }
        }

    }
    //关闭句柄
    close(epfd);

    return 0;
}

总结:基于以上内容小结三者的主要区别
1.较于select,poll和epoll没有socket的FD_SETSIZE(1024)个数的
2.不同于select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现
3.内核中select和poll的实现采用轮询来处理,轮询的fd越多耗时越长
4.epoll的实现基于回调的,如果fd有期望的事件发生就通过回调函数将其加入到epoll的就绪队列中去,即epoll只关心活跃的fd(因此效率高)
5.内核/用户空间拷贝问题,当内核把fd消息通知给用户空间时,select和poll采用内存拷贝的方法,而epoll采用的是内存共享的方式

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- vipyiyao.com 版权所有 湘ICP备2023022495号-8

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务