事件驱动模型
在网络编程里的模型里面,有一类叫事件驱动(Event-driven)模型。(在其他的资料里面也叫 : io多路复用 )。
这类io模型的好处是:在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
解释:
当用户进程调用了select/ poll/ epoll,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
这个图和blocking IO的并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select/ poll/ epoll的优势在于它可以同时处理多个connection。
这里有一个讲的很好的教程,推荐 :
http://lifeofzjs.com/blog/2015/05/16/how-to-write-a-server/
https://segmentfault.com/a/1190000003063859
如果有这么一个函数,在某个fd可以读的时候告诉我,而不是反复地去调用read。(这句话说的太好了,反复多读读)这种方式叫做事件驱动,在linux下可以用select/poll/epoll这些I/O复用的函数来实现,因为要不断知道哪些fd是可读的,所以要把这个函数放到一个loop里,这个就叫事件循环(event loop)。
示例代码如下:
image.png
在这个while里,调用epoll_wait会将进程阻塞住,直到在epoll里的fd发生了当时注册的事件。需要注明的是,select/poll不具备伸缩性,复杂度是O(n),而epoll的复杂度是O(1),在Linux下工业程序都是用epoll。
为什么epoll更好?
select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销
image.png
在写server的时候的时候,经常要写一个client作为调代码的工具,后来发现一个好用的linux命令 ncat
#这个表明本地的8000端口建立tcp连接,然后下面就可以输入要给server发送的消息了。
ncat 127.0.0.1 8000
给一个epoll的代码的例子:
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>
#define OPEN_MAX 100
int main(int argc, char *argv[])
{
struct epoll_event event; // 告诉内核要监听什么事件
struct epoll_event wait_event; //内核监听完的结果
//1.创建tcp监听套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
//2.绑定sockfd
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8002);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
//3.监听listen
listen(sockfd, 10);
//4.epoll相应参数准备
int fd[OPEN_MAX];
int i = 0, maxi = 0;
memset(fd,-1, sizeof(fd));
fd[0] = sockfd;
int epfd = epoll_create(10); // 创建一个 epoll 的句柄,参数要大于 0, 没有太大意义
if( -1 == epfd ){
perror ("epoll_create");
return -1;
}
event.data.fd = sockfd; //监听套接字
event.events = EPOLLIN; // 表示对应的文件描述符可以读
//5.事件注册函数,将监听套接字描述符 sockfd 加入监听事件
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
if(-1 == ret){
perror("epoll_ctl");
return -1;
}
//6.对已连接的客户端的数据处理
while(1)
{
// 监视并等待多个文件(标准输入,udp套接字)描述符的属性变化(是否可读)
// 没有属性变化,这个函数会阻塞,直到有变化才往下执行,这里没有设置超时
ret = epoll_wait(epfd, &wait_event, maxi+1, -1);
printf("ret is %d \n",ret);
//6.1监测sockfd(监听套接字)是否存在连接
if(( sockfd == wait_event.data.fd )
&& ( EPOLLIN == wait_event.events & EPOLLIN ) )
{
struct sockaddr_in cli_addr;
int clilen = sizeof(cli_addr);
//6.1.1 从tcp完成连接中提取客户端
int connfd = accept(sockfd, (struct sockaddr *)&cli_addr, &clilen);
//6.1.2 将提取到的connfd放入fd数组中,以便下面轮询客户端套接字
for(i=1; i<OPEN_MAX; i++)
{
if(fd[i] < 0)
{
fd[i] = connfd;
event.data.fd = connfd; //监听套接字
event.events = EPOLLIN; // 表示对应的文件描述符可以读
//6.1.3.事件注册函数,将监听套接字描述符 connfd 加入监听事件
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event);
if(-1 == ret){
perror("epoll_ctl");
return -1;
}
break;
}
}
//6.1.4 maxi更新
if(i > maxi)
maxi = i;
//6.1.5 如果没有就绪的描述符,就继续epoll监测,否则继续向下看
if(--ret <= 0)
continue;
}
//6.2继续响应就绪的描述符
for(i=1; i<=maxi; i++)
{
if(fd[i] < 0)
continue;
if(( fd[i] == wait_event.data.fd )
&& ( EPOLLIN == wait_event.events & (EPOLLIN|EPOLLERR) ))
{
int len = 0;
char buf[128] = "";
//6.2.1接受客户端数据
if((len = recv(fd[i], buf, sizeof(buf), 0)) < 0)
{
if(errno == ECONNRESET)//tcp连接超时、RST
{
close(fd[i]);
fd[i] = -1;
}
else
perror("read error:");
}
else if(len == 0)//客户端关闭连接
{
close(fd[i]);
fd[i] = -1;
}
else//正常接收到服务器的数据
{
send(fd[i], buf, len, 0);
printf("buf is : %s",buf);
}
//6.2.2所有的就绪描述符处理完了,就退出当前的for循环,继续poll监测
if(--ret <= 0)
break;
}
}
}
return 0;
}
#编译的话
gcc test.c
#调试的话,可以使用ncat
ncat localhost 8002
相关文章
一个基于python的单线程的echo服务器
http://www.jianshu.com/p/8f1941c4a549
基于python的一个多线程echo服务器
http://www.jianshu.com/p/2ffde49b55c3
一个基于select模型的echo服务器
http://www.jianshu.com/p/8a360a3f13aa
关于select,poll,epoll3个网络编程异步模型的区别
http://www.jianshu.com/p/3bf72e232fb8