1.基本概念
IO多路复用是指在一个进程或线程中监控多个文件描述符,可以用select和epoll函数实现
select函数声明:
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int epoll_create(int size);//创建一个epoll的句柄,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);
2.select与epoll比较
select最多能同时监控1024个文件描述符
epoll可监控的文件描述符个数没有限制
epoll通过回调函数的机制,可以不用遍历所有的文件描述符
epoll通过mmap内存映射机制,可以不用在用户空间和内核空间来拷贝文件描述符
3.示例代码(代码来自网上的博客)
单线程select服务器代码:
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/select.h>
const static int MAXLINE = 1024;
const static int SERV_PORT = 10001;
int main()
{
int i , maxi , maxfd, listenfd , connfd , sockfd ;
/*nready 描述字的数量*/
int nready ,client[FD_SETSIZE];
int n ;
/*创建描述字集合,由于select函数会把未有事件发生的描述字清零,所以我们设置两个集合*/
fd_set rset , allset;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in cliaddr , servaddr;
/*创建socket*/
listenfd = socket(AF_INET , SOCK_STREAM , 0);
/*定义sockaddr_in*/
memset(&servaddr , 0 ,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listenfd, (struct sockaddr *) & servaddr , sizeof(servaddr));
listen(listenfd , 100);
/*listenfd 是第一个描述字*/
/*最大的描述字,用于select函数的第一个参数*/
maxfd = listenfd;
/*client的数量,用于轮询*/
maxi = -1;
/*init*/
for(i=0 ;i<FD_SETSIZE ; i++)
client[i] = -1;
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
for (;;)
{
rset = allset;
/*只select出用于读的描述字,阻塞无timeout*/
nready = select(maxfd+1 , &rset , NULL , NULL , NULL);
if(FD_ISSET(listenfd,&rset))
{
clilen = sizeof(cliaddr);
connfd = accept(listenfd , (struct sockaddr *) & cliaddr , &clilen);
/*寻找第一个能放置新的描述字的位置*/
for (i=0;i<FD_SETSIZE;i++)
{
if(client[i]<0)
{
client[i] = connfd;
break;
}
}
/*找不到,说明client已经满了*/
if(i==FD_SETSIZE)
{
printf("Too many clients , over stack .\n");
return -1;
}
FD_SET(connfd,&allset);//设置fd
/*更新相关参数*/
if(connfd > maxfd) maxfd = connfd;
if(i>maxi) maxi = i;
if(nready<=1) continue;
else nready --;
}
for(i=0 ; i<=maxi ; i++)
{
if (client[i]<0) continue;
sockfd = client[i];
if(FD_ISSET(sockfd,&rset))
{
n = read(sockfd , buf , MAXLINE);
if (n==0)
{
/*当对方关闭的时候,server关闭描述字,并将set的sockfd清空*/
close(sockfd);
FD_CLR(sockfd,&allset);
client[i] = -1;
}
else
{
buf[n]='\0';
printf("Socket %d said : %s\n",sockfd,buf);
write(sockfd,buf,n); //Write back to client
}
nready --;
if(nready<=0) break;
}
}
}
return 0;
}
单线程epoll服务器代码:
#include <stdio.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <netdb.h>
#include <errno.h>
#define MAX_EVENT 20
#define READ_BUF_LEN 256
/**
* 设置 file describe 为非阻塞模式
* @param fd 文件描述
* @return 返回0成功,返回-1失败
*/
static int make_socket_non_blocking (int fd) {
int flags, s;
// 获取当前flag
flags = fcntl(fd, F_GETFL, 0);
if (-1 == flags) {
perror("Get fd status");
return -1;
}
flags |= O_NONBLOCK;
// 设置flag
s = fcntl(fd, F_SETFL, flags);
if (-1 == s) {
perror("Set fd status");
return -1;
}
return 0;
}
int main() {
// epoll 实例 file describe
int epfd = 0;
int listenfd = 0;
int result = 0;
struct epoll_event ev, event[MAX_EVENT];
// 绑定的地址
const char * const local_addr = "127.0.0.1";
struct sockaddr_in server_addr = { 0 };
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == listenfd) {
perror("Open listen socket");
return -1;
}
/* Enable address reuse */
int on = 1;
// 打开 socket 端口复用, 防止测试的时候出现 Address already in use
result = setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) );
if (-1 == result) {
perror ("Set socket");
return 0;
}
server_addr.sin_family = AF_INET;
inet_aton (local_addr, &(server_addr.sin_addr));
server_addr.sin_port = htons(10001);
result = bind(listenfd, (const struct sockaddr *)&server_addr, sizeof (server_addr));
if (-1 == result) {
perror("Bind port");
return 0;
}
result = make_socket_non_blocking(listenfd);
if (-1 == result) {
return 0;
}
result = listen(listenfd, 200);
if (-1 == result) {
perror("Start listen");
return 0;
}
// 创建epoll实例
epfd = epoll_create1(0);
if (1 == epfd) {
perror("Create epoll instance");
return 0;
}
ev.data.fd = listenfd;
ev.events = EPOLLIN | EPOLLET /* 边缘触发选项。 */;
// 设置epoll的事件
result = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
if(-1 == result) {
perror("Set epoll_ctl");
return 0;
}
for ( ; ; ) {
int wait_count;
// 等待事件
wait_count = epoll_wait(epfd, event, MAX_EVENT, -1);
int i=0;
for ( i = 0 ; i < wait_count; i++) {
uint32_t events = event[i].events;
// IP地址缓存
char host_buf[NI_MAXHOST];
// PORT缓存
char port_buf[NI_MAXSERV];
int __result;
// 判断epoll是否发生错误
if ( events & EPOLLERR || events & EPOLLHUP || (! events & EPOLLIN)) {
printf("Epoll has error\n");
close (event[i].data.fd);
continue;
} else if (listenfd == event[i].data.fd) {
// listen的 file describe 事件触发, accpet事件
for ( ; ; ) { // 由于采用了边缘触发模式,这里需要使用循环
struct sockaddr in_addr = { 0 };
socklen_t in_addr_len = sizeof (in_addr);
int accp_fd = accept(listenfd, &in_addr, &in_addr_len);
if (-1 == accp_fd) {
perror("Accept");
break;
}
__result = getnameinfo(&in_addr, sizeof (in_addr),
host_buf, sizeof (host_buf) / sizeof (host_buf[0]),
port_buf, sizeof (port_buf) / sizeof (port_buf[0]),
NI_NUMERICHOST | NI_NUMERICSERV);
if (! __result) {
printf("New connection: host = %s, port = %s\n", host_buf, port_buf);
}
__result = make_socket_non_blocking(accp_fd);
if (-1 == __result) {
return 0;
}
ev.data.fd = accp_fd;
ev.events = EPOLLIN | EPOLLET;
// 为新accept的 file describe 设置epoll事件
__result = epoll_ctl(epfd, EPOLL_CTL_ADD, accp_fd, &ev);
if (-1 == __result) {
perror("epoll_ctl");
return 0;
}
}
continue;
} else {
// 其余事件为 file describe 可以读取
int done = 0;
// 因为采用边缘触发,所以这里需要使用循环。如果不使用循环,程序并不能完全读取到缓存区里面的数据。
for ( ; ;) {
ssize_t result_len = 0;
char buf[READ_BUF_LEN] = { 0 };
result_len = read(event[i].data.fd, buf, sizeof (buf) / sizeof (buf[0]));
if (-1 == result_len) {
if (EAGAIN != errno) {
perror ("Read data");
done = 1;
}
break;
} else if (! result_len) {
done = 1;
break;
}
write(STDOUT_FILENO, buf, result_len);
}
if (done) {
printf("Closed connection\n");
close (event[i].data.fd);
}
}
}
}
close (epfd);
return 0;
}
客户端代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
int main(int argc, char *argv[])
{
int sockfd;
int n;
struct sockaddr_in serv_addr;
char buffer[256];
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
printf("opening socket error! \n");
exit(-1);
}
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(10001);
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
{
printf("connecting error! \n");
exit(-1);
}
printf("Please enter the message: ");
bzero(buffer, 256);
fgets(buffer, 255, stdin);
n = write(sockfd, buffer, strlen(buffer));
if (n < 0)
printf("error writing to socket! \n");
bzero(buffer, 256);
n = read(sockfd, buffer, 255);
if (n < 0)
printf("error reading from socket! \n");
printf("%s \n",buffer);
return 0;
}