select poll epoll用法解析
通常,服务器分为循环服务器和并发服务器,循环服务器每次只能处理一个来自客户端的请求连接,直到该客户端完成所有的请求断开连接之后才能开始下一个客户端的服务。并发服务器可以同时处理多个客户端的请求,实现并发的方式有多线程,多进程方式,但是开辟多个线程或者多个进程虽然可以满足要求,但是性能不够好,系统开销比较大。因此,我们采用IO多路复用技术来实现并发。IO多路复用方式有三种,select , poll , epoll。
select
man手册中select函数的介绍
- select代码示例:
#include<stdio.h>
11 #include <sys/types.h> /* See NOTES */
12 #include <sys/socket.h>
13 #include <string.h>
14 #include <netinet/in.h>
15 /* According to POSIX.1-2001 */
16 #include <sys/select.h>
17 /* According to earlier standards */
18 #include <sys/time.h>
19 #include <unistd.h>
20 #define BUF_SIZE 100
int main()
24 {
25 //socket
26 int iServer=socket(AF_INET,SOCK_STREAM,0);
27 if(-1==iServer)
28 {
29 printf("create socket error\r\n");
30 return -1;
31 }
32 printf("create socket ok,iServer=%d\r\n",iServer);
33 //bind
34 struct sockaddr_in stServer;
35 stServer.sin_family=AF_INET;
36 stServer.sin_port=htons(8888);
37 stServer.sin_addr.s_addr=inet_addr("127.0.0.1");
38 int ret=bind(iServer,(struct sockaddr *)&stServer,sizeof(struct sockaddr));
if(-1==ret)
40 {
41 printf("bind error\r\n");
42 return -1;
43 }
44 printf("bind ok\r\n");
45 //listen
46 ret=listen(iServer,5);
47 if(-1==ret)
48 {
49 return -1;
50 }
51 printf("listen ok\r\n");
/*
前面的操作我们创建了一个socket套接字,并与主机的ip地址绑定,设定
了一个端。之后我们将此套接字设置为监听模式,监听队列的大小为5,下
面的例子即是select实现并发服务器的核心代码
*/
//accept
53 struct sockaddr_in stClient;
54 socklen_t len=sizeof(struct sockaddr_in);
55 char buf[BUF_SIZE];
56 fd_set stFdr; // 定义一个fd_set类型的变量,用于添加socket套接字描述符
57 FD_ZERO(&stFdr); // 清空stFdr
58 FD_SET(iServer,&stFdr); // 把监听套接字描述符加入stFdr中
59 int Max=iServer; //select 最大的监听套接字描述符
60 while(1)
61 {
62 fd_set stFdrTmp=stFdr; //将stFdr赋值给临时的变量,因为程序会修改stFdrTmp
63 ret=select(Max+1,&stFdrTmp,NULL,NULL,NULL);//监听最大的描述符为 Max + 1 ,临时的stFdrTmp,select是阻塞函数,如果有某个socket描述符有数据,即可返回,返回值为有数据的socket描述符的个数
64 if(ret<=0)
65 {
66 printf("select error\r\n");
67 continue;
68 }
69 printf("select ok,ret=%d\r\n",ret);
71 int i=0;
// 循环遍历每一个socket描述符,看一下哪一个被置位
72 for(;i<Max+1;i++)
73 {
//如果当前的文件描述符被置位
74 if(FD_ISSET(i,&stFdrTmp))
75 {
// 如果是服务器套接字,就接收一个新的连接,并把这个连接加入到stFdc中
76 if(i==iServer)
77 {
78 int iClient=accept(iServer,(struct sockaddr *)&stClient,&len);
79 if(-1==iClient)
80 {
81 continue;
82 }
83 printf("iclient=%d\r\n",iClient);
84 FD_SET(iClient,&stFdr); //添加新的描述符
85 if(Max<iClient)
86 {
87 Max=iClient; //设置监听的最大值
88 }
89 }
else
91 {
// 单独处理每一个连接的事务
ret=recv(i,buf,BUF_SIZE,0);
93 printf("recv data:%s\r\n",buf);
94 if(ret>0)
95 { send(i,buf,BUF_SIZE,0);
97 }
98 else
99 {
100 close(i);
101 FD_CLR(i,&stFdr);
102 }
103 }
104 }
105 }
106 }
107 return 0;
108 }
select的特点
- 1024的bitmap,因此select服务器只能同时处理1024个客户端的连接
- FDset 不可重用
- select维护1024位的bitmap,使用时从用户空间拷贝到内核空间,然后内核检测IO的数据情况,置位 bitmap,然后又从内核空间返回到用户空间
- 用户处理select返回,需要遍历bitmap,才能知道哪一个IO被置位,这个过程需要O(n)的时间复杂度
poll
poll是另一种IO多路复用技术
poll代码示例:
#include<stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <unistd.h>
#include <poll.h>
#define BUF_SIZE 100
/*
struct pollfd
{
int fd;
short events;
short revents;
};
*/
struct Polldata
{
struct pollfd pollfds[BUF_SIZE];
int size;
}list;
/*这里我们定义了一个Polldata类型的数据结构,其中包含两个数据成员,
一个是struct pollfd类型的数组,数组大小为BUF_SIZE,这个长度决定了
我们允许同时连接到该服务器的客户端的个数最大为MAX_SIZE,size成员
表示了我们目前连接到服务器的客户端的个数。
*/
int main()
{
//socket
int iServer=socket(AF_INET,SOCK_STREAM,0);
if(-1==iServer)
{
printf("create socket error\r\n");
return -1;
}
printf("create socket ok,iServer=%d\r\n",iServer);
//bind
struct sockaddr_in stServer;
stServer.sin_family=AF_INET;
stServer.sin_port=htons(8888);
stServer.sin_addr.s_addr=inet_addr("127.0.0.1");
int ret=bind(iServer,(struct sockaddr *)&stServer,sizeof(struct sockaddr));
if(-1==ret)
{
printf("bind error\r\n");
return -1;
}
printf("bind ok\r\n");
//listen
ret=listen(iServer,5);
if(-1==ret)
{
return -1;
}
printf("listen ok\r\n");
//之前我们创建了一个socket套接字,绑定了一个主机地址和端口号,并把该套接字设置为监听模式,监听队列的大小为5
//accept
struct sockaddr_in stClient;
socklen_t len=sizeof(struct sockaddr_in);
memset( &list , 0 , sizeof(list) );
char buf[BUF_SIZE];
int stlen;
int i;
int listSize;
list.size = 1;
list.pollfds[0].fd = iServer;
list.pollfds[0].events = POLLIN;
/*
我们将iServer加入监听队列,并初始化list的size为1
*/
while(1)
{
puts( "rount again" );
listSize = list.size;
printf( "total client %d\n" , listSize - 1 );
poll( list.pollfds , listSize , -1 );
/*
用poll函数,传参为pollfds的数组首地址,元素个数为当前加入监听的
数量,-1表示阻塞状态监听
*/
/*
循环遍历加入其中的元素,根据revents是否被内核置位来判断哪个IO口有消息输入
*/
for( i = 0 ; i < listSize ; i++ )
{
if( list.pollfds[i].revents )
{
/*
如果是iServer套接字,则表示新的客户端请求连接服务器
我们就要处理这个新的连接
*/
if( i == 0 )
{
if( list.size <= BUF_SIZE )
{
memset( &stClient , 0 , len );
ret = accept( iServer , (struct sockaddr*)&stClient , &stlen );
list.pollfds[list.size].fd = ret;
list.pollfds[list.size].events = POLLIN;
list.size++;
}
else
{
printf( "maxinum connections\n" );
}
}
else
{
/*
这里表示一个普通的客户端发消息给服务器,服务器接收这个消息即可
*/
memset( buf , 0 , BUF_SIZE );
ret = recv( list.pollfds[i].fd , buf , BUF_SIZE , 0 );
if( ret <= 0 )
{
printf( "recv %d error\n" , list.pollfds[i].fd );
close( list.pollfds[i].fd );
}
ret = send( list.pollfds[i].fd , buf , BUF_SIZE , 0 );
if( ret <= 0 )
{
printf( "send %d errror\n" , list.pollfds[i].fd );
close( list.pollfds[i].fd );
}
list.pollfds[i].revents = 0;
//给pollfd中的成员revents置位
}
}
}
}
return 0;
}
poll的特点
- 存在用户态到内核态的pollfd 结构体的传递和置位
- 需要轮询pollfd 结构体数组才能知道哪一个被置位
- pollfd结构体数组可以重复利用
- 同时连接到服务器的客户端的数目大大增加
== 注意,以上的代码存在一个bug,运行时可以自行测试,时间原因我没有处理这个bug,欢迎私信交流==
epoll
epoll对poll做了相应的改进先看代码示例:
#include<stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <unistd.h>
#include <sys/epoll.h>
#define BUF_SIZE 100
struct Polldata
{
struct epoll_event events[BUF_SIZE];
int size;
}list;
int main()
{
//socket
int iServer=socket(AF_INET,SOCK_STREAM,0);
if(-1==iServer)
{
printf("create socket error\r\n");
return -1;
}
printf("create socket ok,iServer=%d\r\n",iServer);
//bind
struct sockaddr_in stServer;
stServer.sin_family=AF_INET;
stServer.sin_port=htons(8888);
stServer.sin_addr.s_addr=inet_addr("127.0.0.1");
int ret=bind(iServer,(struct sockaddr *)&stServer,sizeof(struct sockaddr));
if(-1==ret)
{
printf("bind error\r\n");
return -1;
}
printf("bind ok\r\n");
//listen
ret=listen(iServer,5);
if(-1==ret)
{
return -1;
}
printf("listen ok\r\n");
//accept
struct sockaddr_in stClient;
socklen_t len=sizeof(struct sockaddr_in);
memset( &list , 0 , sizeof(list) );
char buf[BUF_SIZE];
int stlen;
int i;
int listSize;
struct epoll_event ev;
int epfd = epoll_create( 10 );
ev.data.fd = iServer;
ev.events = EPOLLIN;
epoll_ctl( epfd , EPOLL_CTL_ADD , iServer , &ev );
list.size = 1;
while(1)
{
puts( "rount again" );
listSize = list.size;
printf( "total client %d\n" , listSize - 1 );
int nfds = epoll_wait( epfd , list.events , list.size , -1 );
for( i = 0 ; i < nfds ; i++ )
{
if( list.events[i].data.fd == iServer )
{
if( list.size <= BUF_SIZE )
{
memset( &stClient , 0 , len );
ret = accept( iServer , (struct sockaddr*)&stClient , &stlen );
if( ret <= 0 )
{
printf( "accept error\n" );
continue;
}
ev.data.fd = ret;
ev.events = EPOLLIN;
epoll_ctl( epfd , EPOLL_CTL_ADD , ret , &ev );
list.size++;
}
else
{
printf( "maxinum connections\n" );
}
}
else
{
printf( "read from client\n" );
memset( buf , 0 , BUF_SIZE );
ret = recv( list.events[i].data.fd , buf , BUF_SIZE , 0 );
if( ret <= 0 )
{
printf( "recv %d error\n" , list.events[i].data.fd );
close( list.events[i].data.fd );
}
ret = send( list.events[i].data.fd , buf , BUF_SIZE , 0 );
if( ret <= 0 )
{
printf( "send %d errror\n" , list.events[i].data.fd );
close( list.events[i].data.fd );
}
}
}
}
return 0;
}
epoll的特点:
- 采用了epfd方式,节省了用户态与内核态之间交互的开销
- 不需要轮询,epoll_wait函数返回的是当前产生事件的描述符的个数,并且对epfd做了重新排序