前言
服务器的重要作用是对多种外部事件或者请求作出反应。对于多个外部请求,可以通过多进程进行处理,即每来一个连接,我就fork一个子进程,用这个子进程专门针对这个请求进行处理,包括读写操作,与其他进程通讯,有数据库打交道等。这种方式是多个进程并行进行处理。但是,并行的程序,由于需要fork子进程,进行进程管理、进程切换等,需要耗费较多的资源,是否有串行的方式解决这个问题呢?有,那就是IO复用。IO复用是把所有需要进行处理的IO的读写注册成事件,如果哪个IO可以读写了,就触发相应的事件,通过复用函数返回结果,然后主进程(这里只有一个进程)根据返回的结果进行判断,分析是哪个IO事件,然后再做具体的处理。通过这种方式,我们可以把多个外部连接请求注册在一起去监听,当某个请求有内容到达时,触发这个文件描述符的可读事件,服务器端的主进程判断,发现是这个客户有内容要发送,且内容已经准备好了,于是调用read()函数,读取内容,把读到的内容交给核心业务处理函数,进行进一步处理(要求这里的处理,一般是非阻塞的处理),处理结束后,继续监听所有注册的文件描述符。然后循环上面那个过程。
1.IO复用函数
IO复用函数主要有select,poll和epoll三个。我看了select和epoll两个。现在可能epoll用的更多一些。具体的函数的使用方法,可以看man手册(我一般在ubuntu下开发,在window上写博客,现在不太方便调用man)。当然,也可以直接参考我写的小例子。这样更清晰明了。
2.select函数使用方法(待补)
3.epoll函数的使用方法(待补)
4.例子
4.1阻塞版
通过对select和epoll的学习,我写了下面的小例子。例子的功能是 echoback服务器,既需要监听客户端的连接,连接建立后,对客户端发送的数据直接写回去。该服务器还要接受本端标准输入,并把标准输入的内容写到标准输出。通过例子1我们发现,这个需求是比较难做到的,因为不IO复用的话,需要一直阻塞在accept或read函数,要么就一直阻塞在scanf函数。例子见http://git.oschina.net/mengqingxi89/codelittle/blob/master/codes/echoserver/block_io.cpp。使用方法,编译后,起一个terminal运行./block_io 127.0.0.1 12345,服务器就启动了,在起一个terminal,用telnet 127.0.0.1 12345 作为客户端,就可以连接到服务器了。我们发现,客户端连接后,在服务器的terminal就不能输入了,这是因为阻塞造成的,下面我们用IO复用的方法来解决这个问题,包括两个版本,select版和epoll版的。
4.2select版
select版是把listenfd和conn套接字都进行监听,把标准输入套接字0也进行监听,哪个套接字有事件,则处理哪个。从而实现了复用。
具体的代码见
核心部分是
while(run_flag)
{
FD_SET(conn,&read_fds);
FD_SET(conn,&exception_fds);
FD_SET(0,&read_fds); //标准输入
FD_SET(0,&exception_fds);
//int ret=select(maxfd(allsockets,2)+1,&read_fds,&write_fds,&exception_fds,);
//int ret=select(conn+1,&read_fds,&write_fds,&exception_fds,&timeout);
int ret=select(conn+1,&read_fds,&write_fds,&exception_fds,NULL);
if(ret<0)
{
printf("selection error\n");
break;
}
if(FD_ISSET(conn,&read_fds))
{
dowork(users[user_number]);
}
else if (FD_ISSET(conn,&exception_fds))
{
printf("conn excetpion\n");
break;
}
if(FD_ISSET(0,&read_fds))
{
printf("yes\n");
//getline(&ioinput,BUF_SIZE,stdin);
//gets(ioinput);
scanf("%s",ioinput);
printf("%s\n",ioinput);
}
4.3 epoll版本
epoll版本和select版本差不多,只不过epoll的效率可能更高一些,因为他只在内核维护一个事件注册表,而且返回时返回的ready事件也极大的方便了后续的处理。
关键部分是
int addfd(int epollfd, int fd)
{
epoll_event event;
event.data.fd=fd;
event.events=EPOLLIN|EPOLLET;
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);
setnonblock(fd);
}
*************************************************
epoll_event events [ MAX_EVENT_NUMBER];
int epollfd=epoll_create(5);
assert(epollfd!=-1);
//注册listenfd上的可读事件
addfd(epollfd,listenfd);
addfd(epollfd,0);
**************************************************
while(run_flag)
{
int number=epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1);
if(number<0)
{
printf("selection error\n");
break;
}
for(int i=0;i
{
int sockfd=events[i].data.fd;
if(sockfd==listenfd)
{
int acfd=accept(listenfd,NULL,NULL);
if(acfd>=0)
{
conn=acfd;
user_number++;
/*users[user_number] 表示一个conn,唯一标识这个用户链接*/
users[user_number].conn=acfd;
addfd(epollfd,acfd);
}
else
{
printf("accept error\n");
break;
}
}
else if (sockfd==0)
{
scanf("%s",ioinput);
printf("%s\n",ioinput);
}
else
dowork(users[user_number]);
}
}
5总结
有了前面几篇日志的基础,我们就可以做点更大的事了。做一个功能健全的echoback服务器,采用epollIO复用,采用多进程处理每个客户端的请求,在此过程中,对重要信号进行处理。下一个博客就是完成上面的任务。敬请期待。