引言
_本文章为学习笔记,仅供参考 _
在Linux下进行网络编程时,常见的有同步阻塞IO(Blocking IO)、同步非阻塞IO(Non-blocking IO)、IO多路复用(IO Multiplexing)、信号驱动IO(Signal driven IO )和异步IO(Asynchronous IO)五种IO模型。而IO多路复用有select多路复用、poll多路复用和epoll多路复用,下面主要介绍select、poll和epoll。
select多路复用
1. select()函数:该函数允许进程指示内核等待多个事件(文件描述符)中的任何一个发生,并只在一个或多事件发生或经历一段时间后才唤醒它,然后接下来判断究竟是哪个文件描述符发生了事件并进行相应的处理。
#include <sys/select.h>
#include <sys/time.h>
struct timeval
{
long tv_sec; //seconds秒
long tv_usec; //microseconds微秒
};
void FD_ZERO(fd_set *set); //清空集合
void FD_SET(int fd, fd_set *set); //将给定描述符加入集合
int FD_ISSET(int fd, fd_set *set); //判断指定描述符是否在集合中
void FD_CLR(int fd, fd_set *set); //将给定的文件描述符从文件中删除
int select(int maxfd, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
- 第一个参数max_fd指待测试的fd的总个数,它的值是待测试的最大文件描述符加1。
- 其中exceptfds为文件描述符异常,我们一般不关心writefds和exceptfds可设置为NULL。
- selcet监视并等待多个文件描述符的属性发生变化,它监视的属性分3类,分别是readfds(文件描述符有数据到来可读)、writefds(文件描述符可写)和exceptfds(文件描述符异常)。调用select函数会阻塞,直到有文件描述符就绪(有数据可读、可写或异常),又或者超时(timeout 指定等待时间)发生函数才返回。
- 当select()函数返回后,可以通过遍历fdset找到究竟是哪些文件描述符就绪。
如果select成功返回,返回值是三个集合的fd总数;如果返回0表示超时;如果返回-1表示出错,通过errno获得错误原因。 - select最多支持1024个连接,Posix接口限制为1024个文件描述符,FD_SET超过1024的值,会造成越界,只是这个越界可能不会有致命的后果。
//select() can monitor only file descriptors numbers that are less than FD_SETSIZE;
// include/uapi/linux/posix_types.h
#define __FD_SETSIZE 1024
typedef struct {
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;
2. select流程:
3. sock_serv_select示例程序:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <getopt.h>
#include <netinet/in.h>
#include <libgen.h>
#include <ctype.h>
#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
static inline void msleep(unsigned long ms);//inline内联函数,省去函数调用的开销
static inline void print_usage(char *progname)
{
printf("usage:%s [OPTION]...\n",progname );
printf("-b(--daemon): set program running on background\n");
printf("-p(--port): sepcify Server listen port\n");
printf("-h(--help): print this help information.\n");
printf("\nExample: %s -b -p 8000\n", progname);
return ;
}
//将socket抽象成一个函数
int socket_server_init(char *listen_ip,int listen_port);
int main(int argc,char **argv)
{
int listenfd,connfd;
int serv_port = 0;
char *progname = NULL;
int daemon_run = 0;
int ch;
int i,j;
fd_set rdset;
int rv;
int found;
int maxfd=0;
char buf[1024];
int fds_array[1024];
struct option long_options[] = {
{"daemon",no_argument,NULL,'b'}, //后台运行
{"port",required_argument,NULL,'p'},
{"help",no_argument,NULL,'h'},
{NULL,0,NULL,0}
};
progname = basename(argv[0]);
//parser the command line parameters解析命令行参数
while ((ch=getopt_long(argc, argv, "bp:h",long_options,NULL)) != -1)
{
switch(ch)
{
case 'b':
daemon_run=1;
break;
case 'p':
serv_port=atoi(optarg);
break;
case 'h':
print_usage(argv[0]);
return EXIT_SUCCESS;
default:
break;
}
}
if ( !serv_port )
{
print_usage(progname);
return -1;
}
listenfd=socket_server_init(NULL, serv_port);
if(listenfd < 0)
{
printf("ERROR:%S server listen on port %d failur\n",argv[0],serv_port);
return -2;
}
printf("%s server start to listen on port %d\n",argv[0],serv_port);
//后台运行程序
if( daemon_run)
{
daemon(0, 0);
}
for (i = 0;i < ARRAY_SIZE(fds_array); i++)
{
fds_array[i] = -1;
}
fds_array[0] = listenfd;
for( ; ; )
{
FD_ZERO(&rdset);
for(i=0;i< ARRAY_SIZE(fds_array); i++)
{
if( fds_array[i] < 0)
{
continue;
}
maxfd = fds_array[i]>maxfd ? fds_array[i] : maxfd;
FD_SET(fds_array[i],&rdset);
}
/*program will block here*/
rv = select(maxfd+1,&rdset,NULL,NULL,NULL);
if(rv <0)
{
printf("select failure :%s\n",strerror(errno));
break;
}
else if(rv == 0)
{
printf("select get timeout\n");
continue;
}
if( FD_ISSET(listenfd,&rdset) )
{
if( (connfd=accept(listenfd,(struct sockaddr *)NULL,NULL)) < 0)
{
printf("accept new client failure :%s\n",strerror(errno));
continue;
}
found = 0;
for(i=0;i< ARRAY_SIZE(fds_array); i++)
{
//fds_array[i] < 0说明该位置还未被占用
if(fds_array[i] < 0)
{
printf("accept new client[%d] and add it into array\n",connfd);
fds_array[i] = connfd;
found = 1;
break;
}
}
if( !found )
{
printf("accept new client[%d] but it full,so refused\n",connfd);
close(connfd);
}
}
else //已连接客户端
{
for(i=0;i< ARRAY_SIZE(fds_array); i++)
{
if(fds_array[i] < 0 || !FD_ISSET(fds_array[i],&rdset) )
continue;
if( (rv = read(fds_array[i],buf,sizeof(buf))) < 0)
{
printf("socket[%d] read failure or get disconnect.\n",fds_array[i]);
close(fds_array[i]);
fds_array[i] = -1;
}
else
{
printf("socket [%d] read %d bytes data :%s\n",fds_array[i],rv,buf);
for(j= 0;j<rv;j++)
{
buf[j] = toupper(buf[j]);
}
if( write(fds_array[i],buf,rv) < 0)
{
printf("socket[%d] write failure:%s\n",fds_array[i],strerror(errno));
close(fds_array[i]);
fds_array[i] = -1;
}
}
}
}
}
CleanUp:
close(listenfd);
return 0;
}
//通过select实现毫秒延时
static inline void msleep(unsigned long ms)
{
struct timeval tv;
tv.tv_sec = ms/1000;
tv.tv_usec = (ms%1000)*1000;
select(0,NULL,NULL,NULL,&tv);
};
int socket_server_init(char *listen_ip,int listen_port)
{
struct sockaddr_in servaddr;
int rv = 0;
int on = 1;
int listenfd;
if( (listenfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
printf("Use socket() to create a TCP socket failure: %s\n",strerror(errno));
return -1;
}
//解决Address already in use(地址被占用)问题
setsockopt(listenfd, SOL_SOCKET, SO_RCVTIMEO, &on, sizeof(on));
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(listen_port);
//监听所有IP
if( !listen_ip )
{
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
}
else
{
if( inet_pton(AF_INET,listen_ip,&servaddr.sin_addr) <=0)
{
printf("inet_pton() set listen IP address failure.\n");
rv = -2;
goto CleanUp;
}
}
if( bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0)
{
printf("Use bind() to bind the TCP socket failure: %s\n",strerror(errno));
rv = -3;
goto CleanUp;
}
if(listen(listenfd,13) < 0)
{
printf("Use bind() to bind the TCP socket failure: %s\n",strerror(errno));
rv = -4;
goto CleanUp ;
}
CleanUp:
if(rv < 0)
close(listenfd);
else
rv = listenfd;
return rv;
}
测试结果:
- Server
- Client1
- Client2
4.select 优缺点
- 优点:
- 可单进程执行可以为多个客户端服务,这样可以减少创建线程或进程所需要的CPU时间片或内存资源的开销;
- 可移植性较好,可以跨平台。
- 缺点:
- 单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过setrlimit()、修改宏定义甚至重新编译内核等方式来提升这一限制,但是这样也会造成效率的降低;
- select每次调用时都要将文件描述符集合从用户态拷贝到内核态,开销较大;
用户态和内核态 - select返回的就绪文件描述符集合,需要用户遍历监听的所有文件描述符是否在该集合中,当监听描述符数量很大时效率较低;
- select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。
poll多路复用
select()和poll()系统调用的本质一样,管理多个描述符也是进行轮询,但是 poll() 没有最大文件描述符数量的限制(但是数量过大后性能也是会下降)。
1. poll()函数:
#include <poll.h>
struct pollfd
{
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生了的事件 */
} ;
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 第一个参数用来指向一个struct pollfd类型的数组,每一个pollfd结构体指定了一个被监视的文件描述符,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域,events域中请求的任何事件都可能在revents域中返回。
- 第二个参数 nfds 指定数组中监听的元素个数。
- 第三个参数 timeout指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout为负数表示无线等待,timeout为0表示调用后立即返回。
- 该poll()函数返回fds集合中就绪的读、写,或出错的描述符数量,返回0表示超时,返回-1表示出错。
2. poll流程:
3. sock_serv_poll示例程序:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <getopt.h>
#include <netinet/in.h>
#include <libgen.h>
#include <ctype.h>
#include <poll.h>
#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
static inline void print_usage(char *progname)
{
printf("usage:%s [OPTION]...\n",progname );
printf("-b(--daemon): set program running on background\n");
printf("-p(--port): sepcify Server listen port\n");
printf("-h(--help): print this help information.\n");
printf("\nExample: %s -b -p 8000\n", progname);
return ;
}
//将socket抽象成一个函数
int socket_server_init(char *listen_ip,int listen_port);
int main(int argc,char **argv)
{
int listenfd,connfd;
int serv_port = 0;
char *progname = NULL;
int daemon_run = 0;
int ch;
int i,j;
int rv;
int found;
int max;
char buf[1024];
struct pollfd fds_array[1024];
struct option long_options[] = {
{"daemon",no_argument,NULL,'b'}, //后台运行
{"port",required_argument,NULL,'p'},
{"help",no_argument,NULL,'h'},
{NULL,0,NULL,0}
};
progname = basename(argv[0]);
//parser the command line parameters解析命令行参数
while ((ch=getopt_long(argc, argv, "bp:h",long_options,NULL)) != -1)
{
switch(ch)
{
case 'b':
daemon_run=1;
break;
case 'p':
serv_port=atoi(optarg);
break;
case 'h':
print_usage(argv[0]);
return EXIT_SUCCESS;
default:
break;
}
}
if ( !serv_port )
{
print_usage(progname);
return -1;
}
listenfd=socket_server_init(NULL, serv_port);
if(listenfd < 0)
{
printf("ERROR:%S server listen on port %d failur\n",argv[0],serv_port);
return -2;
}
printf("%s server start to listen on port %d\n",argv[0],serv_port);
//后台运行程序
if( daemon_run)
{
daemon(0, 0);
}
for (i = 0;i < ARRAY_SIZE(fds_array); i++)
{
fds_array[i].fd = -1;
}
fds_array[0].fd = listenfd;
fds_array[0].events = POLLIN;
max = 0;
for( ; ; )
{
/*program will block here*/
rv = poll(fds_array,max+1,-1);
if(rv <0)
{
printf("poll failure :%s\n",strerror(errno));
break;
}
else if(rv == 0)
{
printf("poll get timeout\n");
continue;
}
if( fds_array[0].revents & POLLIN )
{
if( (connfd=accept(listenfd,(struct sockaddr *)NULL,NULL)) < 0)
{
printf("accept new client failure :%s\n",strerror(errno));
continue;
}
found = 0;
for(i=1;i< ARRAY_SIZE(fds_array); i++)
{
//fds_array[i] < 0说明该位置还未被占用
if(fds_array[i].fd < 0)
{
printf("accept new client[%d] and add it into array\n",connfd);
fds_array[i].fd = connfd;
fds_array[i].events = POLLIN;
found = 1;
break;
}
}
if( !found )
{
printf("accept new client[%d] but it full,so refused\n",connfd);
close(connfd);
continue;
}
max = i>max ? i : max;
if(--rv <= 0 )
continue;
}
else //已连接客户端
{
for(i=1;i< ARRAY_SIZE(fds_array); i++)
{
if(fds_array[i].fd < 0 )
continue;
memset(buf, 0, sizeof(buf));
if( (rv = read(fds_array[i].fd,buf,sizeof(buf))) <= 0)
{
printf("socket[%d] read failure or get disconnect.\n",fds_array[i].fd);
close(fds_array[i].fd);
fds_array[i].fd = -1;
}
else
{
printf("socket [%d] read %d bytes data:%s \n",fds_array[i].fd,rv,buf);
for(j= 0;j<rv;j++)
{
buf[j] = toupper(buf[j]);
}
if( write(fds_array[i].fd,buf,rv) < 0)
{
printf("socket[%d] write failure:%s\n",fds_array[i],strerror(errno));
close(fds_array[i].fd);
fds_array[i].fd = -1;
}
}
}
}
}
CleanUp:
close(listenfd);
return 0;
}
int socket_server_init(char *listen_ip,int listen_port)
{
struct sockaddr_in servaddr;
int rv = 0;
int on = 1;
int listenfd;
if( (listenfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
printf("Use socket() to create a TCP socket failure: %s\n",strerror(errno));
return -1;
}
//解决Address already in use(地址被占用)问题
setsockopt(listenfd, SOL_SOCKET, SO_RCVTIMEO, &on, sizeof(on));
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(listen_port);
//监听所有IP
if( !listen_ip )
{
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
}
else
{
if( inet_pton(AF_INET,listen_ip,&servaddr.sin_addr) <=0)
{
printf("inet_pton() set listen IP address failure.\n");
rv = -2;
goto CleanUp;
}
}
if( bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0)
{
printf("Use bind() to bind the TCP socket failure: %s\n",strerror(errno));
rv = -3;
goto CleanUp;
}
if(listen(listenfd,13) < 0)
{
printf("Use bind() to bind the TCP socket failure: %s\n",strerror(errno));
rv = -4;
goto CleanUp ;
}
CleanUp:
if(rv < 0)
close(listenfd);
else
rv = listenfd;
return rv;
}
测试结果:
- Server
- Client1
- Client2
个人感觉在运行多个客户端时可以明显感受到服务器阻塞于poll()。
5. poll 优缺点
- 优点:
- poll使用链表保存文件描述符,所以没有最大数量限制。
poll链表保存文件描述符 - poll向内核传递pollfd数组,然后对pollfd中的每个描述符进行poll,相比处理fdset来说,poll效率更高。
- 缺点:
- 与select一样,poll每次都是通过轮询来获得就绪文件描述符,同样需要把大量的pollfd结构从用户态拷贝到内核中,造成开销。
- 虽然没有最大数量限制,但是数量过大后性能也还是会下降。
epoll多路复用
- epoll(event poll)提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。且它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait /epoll_pwait的调用,提高应用程序效率。LT事件不会丢弃,而是只要读buffer里面有数据可以让用户读,则不断的通知你。而ET则只在事件发生之时通知。LT模式只要有事件未处理就会触发,而ET则只在高低电平变换时(即状态从1到0或者0到1)触发。
- 在linux上,2.4内核前主要是select和poll,自Linux 2.6内核正式引入epoll以来,epoll已经成为了目前实现高性能网络服务器的必备技术。
- epoll在底层实现了自己的高速缓存区,并且建立了一个红黑树用于存放socket,另外维护了一个链表用来存放准备就绪的事件。(红黑树是二叉搜索树的一种,红与黑是红黑树的实现者才关心的内容,对于我们使用者来说不用关心颜色,Java 中的 TreeMap 底层就是红黑树,红黑树可以快速能插入(注册过程)、查询(通知过程) Socket 的文件描述符)
1.epoll 函数与流程
epoll通过在Linux内核中申请一个简易的文件系统,把原先的select/poll调用分成了3
个部分:
- 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源) ----创建epoll 实例
- 调用epoll_ctl向epoll对象中添加这100万个连接的套接字 ----修改epoll的兴趣列表
- 调用epoll_wait收集发生的事件的连接 ----事件等待
- 在执行epoll_ create时,创建了红黑树和就绪链表,执行epoll_ ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
//用户数据载体
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
//fd装载入内核的载体
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
//三板斧api
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
- epoll_create()
系统调用epoll_create()创建了一个新的epoll实例,其对应的兴趣列表初始化为空。若成功返回文件描述符,若出错返回-1。参数size指定了我们想要通过epoll实例来检查的文件描述符个数。从Linux2.6.8版以来,size参数被忽略不用。从2.6.27版内核以来,Linux支持了一个新的系统调用epoll_create1()。该系统调用执行的任务同epoll_create()一样,但是去掉了无用的参数size,并增加了一个可用来修改系统调用行为的flags参数。目前只支持一个flag标志:EPOLL_CLOEXEC,它使得内核在新的文件描述符上启动了执行即关闭标志。 - epoll_ctl()
系统调用epoll_ctl()将fd添加/删除于epoll_create返回的epfd中,其中epoll_event是用户态和内核态交互的结构,定义了用户态关心的事件类型和触发时数据的载体epoll_data,若成功返回0,若出错返回-1。
第二个参数op用来指定需要执行的操作,它可以是如下几种值:
- EPOLL_CTL_ADD:将描述符fd添加到epoll实例中的兴趣列表中去。
- POLL_CTL_MOD:修改描述符上设定的事件,需要用到由ev所指向的结构体中的信息。如果我们试图修改不在兴趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误;
- EPOLL_CTL_DEL:将文件描述符fd从epfd的兴趣列表中移除,该操作忽略参数ev。关闭一个文件描述符会自动将其从所有的epoll实例的兴趣列表移除;
第三个参数fd指明了要修改兴趣列表中的哪一个文件描述符的设定。该参数可以是代表管道、FIFO、套接字、POSIX消息队列、inotify实例、终端、设备,甚至是另一个epoll实例的文件描述符。但是,这里fd不能作为普通文件或目录的文件描述符;
第四个参数event是指向结构体epoll_event的指针,结构体的定义如下:
typedef union epoll_data
{
void *ptr; /* Pointer to user-defind data */
int fd; /* File descriptor */
uint32_t u32; /* 32-bit integer */
uint64_t u64; /* 64-bit integer */
} epoll_data_t;
struct epoll_event
{
uint32_t events; /* epoll events(bit mask) */
epoll_data_t data; /* User data */
};
- epoll_wait()
系统调用epoll_wait()返回epoll实例中处于就绪态的文件描述符信息,单个epoll_wait()调用能够返回多个就绪态文件描述符的信息。调用成功后epoll_wait()返回数组evlist中的元素个数,如果在timeout超时间隔内没有任何文件描述符处于就绪态的话就返回0,出错时返回-1并在errno中设定错误码以表示错误原因。
第一个参数epfd是epoll_create()的返回值;
第二个参数evlist所指向的结构体数组中返回的是有关就绪态文件描述符的信息,数组evlist的空间由调用者负责申请;
第三个参数maxevents指定所evlist数组里包含的元素个数;
第四个参数timeout用来确定epoll_wait()的阻塞行为,有如下几种:
- 如果timeout等于-1,调用将一直阻塞,直到兴趣列表中的文件描述符上有事件产生或者直到捕获到一个信号为止。
- 如果timeout等于0,执行一次非阻塞式地检查,看兴趣列表中的描述符上产生了哪个事件。
- 如果timeout大于0,调用将阻塞至多timeout毫秒,直到文件描述符上有事件发生,或者直到捕获到一个信号为止。
2.sock_serv_epoll示例程序
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <getopt.h>
#include <netinet/in.h>
#include <libgen.h>
#include <sys/epoll.h>
#include <sys/resource.h>
#include <ctype.h>
#define MAX_EVENTS 512
#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
static inline void print_usage(char *progname)
{
printf("usage:%s [OPTION]...\n",progname );
printf("-b(--daemon): set program running on background\n");
printf("-p(--port): sepcify Server listen port\n");
printf("-h(--help): print this help information.\n");
printf("\nExample: %s -b -p 8000\n", progname);
return ;
}
//将socket抽象成一个函数
int socket_server_init(char *listen_ip,int listen_port);
void set_socket_rlimit(void);
int main(int argc,char **argv)
{
int listenfd,connfd;
int serv_port = 0;
char *progname = NULL;
int daemon_run = 0;
int ch;
int i,j;
int rv;
int found;
int max;
char buf[1024];
int epollfd;
struct epoll_event event;
struct epoll_event event_array[MAX_EVENTS];
int events; //发生了的事件
struct option long_options[] = {
{"daemon",no_argument,NULL,'b'}, //后台运行
{"port",required_argument,NULL,'p'},
{"help",no_argument,NULL,'h'},
{NULL,0,NULL,0}
};
progname = basename(argv[0]);
//parser the command line parameters解析命令行参数
while ((ch=getopt_long(argc, argv, "bp:h",long_options,NULL)) != -1)
{
switch(ch)
{
case 'b':
daemon_run=1;
break;
case 'p':
serv_port=atoi(optarg);
break;
case 'h':
print_usage(argv[0]);
return EXIT_SUCCESS;
default:
break;
}
}
if ( !serv_port )
{
print_usage(progname);
return -1;
}
set_socket_rlimit();//解除Linux内核最大限制
listenfd=socket_server_init(NULL, serv_port);
if(listenfd < 0)
{
printf("ERROR:%S server listen on port %d failur\n",argv[0],serv_port);
return -2;
}
printf("%s server start to listen on port %d\n",argv[0],serv_port);
//后台运行程序
if( daemon_run)
{
daemon(0, 0);
}
if( (epollfd = epoll_create(MAX_EVENTS)) < 0)
{
printf("epoll_create() failur:%s\n",strerror(errno));
return -3;
}
event.events = EPOLLIN;
event.data.fd = listenfd;
if(epoll_ctl(epollfd,EPOLL_CTL_ADD,listenfd,&event) < 0)
{
printf("epoll add listen socket failure :%s\n",strerror(errno));
return -4;
}
for( ; ; )
{
/*program will block here*/
events = epoll_wait(epollfd, event_array, MAX_EVENTS, -1);
if(events <0)
{
printf("epoll failure :%s\n",strerror(errno));
break;
}
else if(events == 0)
{
printf("epoll get timeout\n");
continue;
}
for(i=0;i<events;i++)
{
if((event_array[i].events&EPOLLERR) || event_array[i].events&EPOLLHUP)
{
printf("epoll_wait get error on fd[%d]:%s\n",event_array[i].data.fd,strerror(errno));
epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd,NULL);
close(event_array[i].data.fd);
}
/* listen socket get event means new client start connect now */
if( event_array[i].data.fd == listenfd)
{
if( (connfd=accept(listenfd,(struct sockaddr *)NULL,NULL)) < 0)
{
printf("accept new client failure :%s\n",strerror(errno));
continue;
}
event.data.fd = connfd;
event.events = EPOLLIN;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event) < 0)
{
printf("epoll add client socket failure:%s\n",strerror(errno));
close(event_array[i].data.fd);
continue;
}
printf("epoll add new client socket[%d] successfully.\n",connfd);
}
else //已连接客户端
{
memset(buf,0,sizeof(buf));
if( (rv=read(event_array[i].data.fd, buf, sizeof(buf))) <= 0)
{
printf("socket[%d] read failure or getdisconnect and will be removed.\n",event_array[i].data.fd);
epoll_ctl(epollfd,EPOLL_CTL_DEL,event_array[i].data.fd,NULL);
close(event_array[i].data.fd);
continue;
}
else
{
printf("socket [%d] read %d bytes data:%s \n",event_array[i].data.fd,rv,buf);
for(j= 0;j<rv;j++)
{
buf[j] = toupper(buf[j]);
}
if( write(event_array[i].data.fd,buf,rv) < 0)
{
printf("socket[%d] write failure:%s\n",event_array[i].data.fd,strerror(errno));
epoll_ctl(epollfd,EPOLL_CTL_DEL,event_array[i].data.fd,NULL);
close(event_array[i].data.fd);
}
}
}
}
}
CleanUp:
close(listenfd);
return 0;
}
int socket_server_init(char *listen_ip,int listen_port)
{
struct sockaddr_in servaddr;
int rv = 0;
int on = 1;
int listenfd;
if( (listenfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
printf("Use socket() to create a TCP socket failure: %s\n",strerror(errno));
return -1;
}
//解决Address already in use(地址被占用)问题
setsockopt(listenfd, SOL_SOCKET, SO_RCVTIMEO, &on, sizeof(on));
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(listen_port);
//监听所有IP
if( !listen_ip )
{
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
}
else
{
if( inet_pton(AF_INET,listen_ip,&servaddr.sin_addr) <=0)
{
printf("inet_pton() set listen IP address failure.\n");
rv = -2;
goto CleanUp;
}
}
if( bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0)
{
printf("Use bind() to bind the TCP socket failure: %s\n",strerror(errno));
rv = -3;
goto CleanUp;
}
if(listen(listenfd,13) < 0)
{
printf("Use bind() to bind the TCP socket failure: %s\n",strerror(errno));
rv = -4;
goto CleanUp ;
}
CleanUp:
if(rv < 0)
close(listenfd);
else
rv = listenfd;
return rv;
}
void set_socket_rlimit(void)
{
struct rlimit limit = {0};
getrlimit(RLIMIT_NOFILE, &limit);
limit.rlim_cur = limit.rlim_max;
setrlimit(RLIMIT_NOFILE, &limit);
printf("set socket open fd max count to %d\n",limit.rlim_max);
}
测试结果:
- Server
- Client1
- Client2
5. epoll 优缺点
- 优点:
- 支持一个进程打开大数目的socket描述符(FD)
- IO效率不随FD数目增加而线性下降
- 使用mmap加速内核与用户空间的消息传递
总结
select与poll都是用数组来维护客户端相关信息,然后进行遍历,而epoll是直接将其注册给内核,无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了,这也就是epoll的高性能的原因。
_ 注:部分资料来自网络 _