概述
监听端口实战、epoll介绍及原理详析
一、监听端口
在创建worker进程之前就要执行函数ngx_open_listening_sockets(),先创建套接字,再setsockopt防止TIME_WAIT,再ioctl设置非阻塞,然后listen,bind,将监听的套接字放入m_ListenSocketList容器。
二、epoll技术概述
1、I/O多路复用:epoll就是一种典型的I/O多路复用技术,epoll技术的最大特点是支持高并发;
传统多路复用技术select,poll,在并发量达到1000-2000性能就会明显下降;epoll,从linux内核2.6引入的,2.6之前是没有的。
2、epoll和kqueue技术类似:单独一台计算机支撑少则数万,多则数十上百万并发连接的核心技术。
epoll技术完全没有性能会随着并发量提高而出现明显下降的问题。但是并发每增加一个,必定要消耗一定的内存去保存这个连接相关的数据;并发量总还是有限制的,不可能是无限的。
3、10万个连接同一时刻,可能只有几十上百个客户端给你发送数据,epoll只处理这几十上百个客户端。
4、很多服务器程序用多进程,每一个进程对应一个连接;也有用多线程做的,每一个线程对应一个连接;
epoll事件驱动机制,在单独的进程或者单独的线程里运行,收集/处理事件;没有进程/线程之间切换的消耗,高效。
5、适合高并发,开发难度极大。
三、epoll原理与函数介绍:三个函数(操作系统提供的,只会用就行)
1、epoll_create()函数
int epoll_create(int size); //size>0就行
【功能】创建一个epoll对象,返回该对象的文件描述符,这个描述符就代表这个epoll对象,最终要用close()关闭。
【原理】
a)struct eventpoll *ep = (struct eventpoll*)calloc(1, sizeof(struct eventpoll)); 分配一段内存
b)rbr结构成员:代表一颗红黑树的根节点[刚开始指向空]把rbr理解成红黑树的根节点的指针;
红黑树,用来保存键(数字)/ 值(结构),能够快速的通过key取出键值对。
c)rdlist结构成员:代表一个双向链表的表头指针;
双向链表:从头访问/遍历每个元素特别快,一直next就行。
d)总结:创建了一个eventpoll结构对象,被系统保存起来:
rbr成员被初始化成指向一颗红黑树的根(有了一个红黑树);
rdlist成员被初始化成指向一个双向链表的根(有了双向链表)。
2、epoll_ctl()函数
int epoll_ctl(int efpd,int op,int sockid,struct epoll_event *event);
【功能】把一个socket以及这个socket相关的事件添加到这个epoll对象描述符中去,目的就是通过这个epoll对象来监视这个socket(客户端的TCP连接)上数据的来往情况,当有数据来往时,系统会通知我们;我们把感兴趣的事件通过epoll_ctl()添加到系统,当这些事件来的时候,系统会通知我们。
【字段说明】
efpd:epoll_create()返回的epoll对象描述符;
op:动作,添加/删除/修改,对应数字是1,2,3,EPOLL_CTL_ADD、EPOLL_CTL_DEL、EPOLL_CTL_MOD。
EPOLL_CTL_ADD:添加事件,往红黑树上添加一个节点,每个客户端连入服务器后,服务器都会产生一个对应的socket,对应每个连接的这个socket值肯定都不重复;这个socket就是红黑树中的key,把这个节点(其实是一个结构)挂到红黑树上去。
EPOLL_CTL_MOD:修改事件,用了EPOLL_CTL_ADD把节点添加到红黑树上之后才存在修改。
EPOLL_CTL_DEL:是从红黑树上把这个节点干掉,这会导致这个socket(这个tcp链接)上无法收到任何系统通知事件。
sockid:想读时就是监听套接字,想写时就是从accept()返回的sockfd,这个sockid也就是红黑树里边的key。
event:事件信息,EPOLL_CTL_ADD和EPOLL_CTL_MOD都要用到这个event参数里边的事件信息。
【原理】假设是ADD,先判断有没有这个key,生成一个epitem对象(一个结点),把socket和事件保存在结点中,把结点加入红黑树。每一个epi就是一个指向epitem的指针,rbn中有三个指针,分别指向左孩子、右孩子和父亲。
epi = (struct epitem*)calloc(1, sizeof(struct epitem));
epi = RB_INSERT(_epoll_rb_socket, &ep->rbr, epi); 【EPOLL_CTL_ADD】,增加节点到红黑树中;
epi = RB_REMOVE(_epoll_rb_socket, &ep->rbr, epi);【EPOLL_CTL_DEL】,从红黑树中把节点干掉;
EPOLL_CTL_MOD,找到红黑树节点,修改这个节点中的内容。
【面试】
EPOLL_CTL_ADD:等价于往红黑树中增加节点
EPOLL_CTL_DEL:等价于从红黑树中删除节点
EPOLL_CTL_MOD:等价于修改已有的红黑树的节点
当事件发生,我们如何拿到操作系统的通知?
3、epoll_wait()函数
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
【功能】阻塞一小段时间并等待事件发生,返回事件集合,也就是获取内核的事件通知。
因为双向链表里记录的是所有有数据/有事件的socket(TCP连接),所以具体做法是遍历这个双向链表,把这个双向链表里边的节点数据拷贝出去,拷贝完毕的就从双向链表里移除。
【字段说明】
epfd:epoll_create()返回的epoll对象描述符;
events:是内存,也是数组,长度是maxevents,表示此次epoll_wait调用可以收集到的maxevents个已经就绪【已经准备好的】的读写事件,也就是返回的是实际发生事件的tcp连接数目。
参数timeout:阻塞等待的时长,没数据等待这么久,有数据直接返回;
epitem结构设计的高明之处:既能够作为红黑树中的节点,又能够作为双向链表中的节点(rdlink)。
epoll_wait的作用就是从双向链表中把有事件发生的连接取出来再read或write等,只有发生某个事件的连接才在双向链表中。
【原理】等一段时间,这段时间是用来把sockfd扔到双向链表中,取得事件数量(给的空间和来的数量取小的),每次从双向链表头中一个一个取(但在红黑树中还存在),rdy=0表示不在双向链表中,把事件拷贝到提供的events中。
4、内核向双向链表增加节点
一般有四种情况,会使操作系统把节点插入到双向链表中:
a)客户端完成三路握手;服务器要accept()从已完成队列中取走连接;
b)当客户端关闭连接,服务器也要调用close()关闭;
c)客户端发送数据来的;服务器要调用read(),recv()函数来收数据;
d)当可以发送数据时;服务器可以调用send(),write()来发送数据。
通讯代码精粹之epoll函数实战1
一、配置文件的修改
增加worker_connections项:允许连接的最大并发数(1024)
二、epoll函数实战
epoll_create()、epoll_ctl()、epoll_wait()系统提供的函数调用
1、ngx_epoll_init函数内容
1)epoll_create()
创建一个epoll对象,创建了一个红黑树,还创建了一个双向链表;
2)创建连接池
【连接池】就是一个数组,元素数量就是worker_connections(1024),每个数组元素类型为 ngx_connection_t(结构)。
为什么要引入这个数组?2个监听套接字, 用户连入进来,每个用户多出来一个套接字(sockfd);套接字只是一个数字,把套接字数字跟一块内存捆绑,达到的效果就是将来我通过这个套接字,就能够把这块内存拿出来。
把数组中每一项的一个内容指向下一个数组元素,相当于串起来的一个空闲链表,这样当来了一个连接,需要取一个套接字时,就不需要遍历1024个连接找空的了,直接把链表头的连接取出应对客户端连接,用完了再放回链表头。
3)遍历所有监听端口,为每个端口分配一个连接池中的连接来对应
调用ngx_get_connection()重要函数从连接池中找空闲连接,非常快!
4)epoll_ctl的EPOLL_CTL_ADD来往红黑树里加结点
lsof -i:80 命令可以列出哪些进程在监听80端口
2、ngx_epoll_init函数的调用(要在子进程中执行)
【具体过程】父进程先监听80和443端口,创建子进程过程中进行epoll相关初始化,调用ngx_epoll_init函数为每个进程开辟一个1024个元素的数组,遍历所有socket,将其对应c[0](空闲池的一个位置),相当于每个socket都对应了连接池的一项。然后,调ngx_epoll_add_event(里面是epolll_ctl)这种函数来往对应socket上增加事件,可能是读可能是写,然后就不用管了,这样如果有客户端来连接内核就会识别到从epoll_wait中往下走。
具体是怎么走的呢?
客户端连接请求到来时,内核从红黑树中找对应的文件描述符,将对应的结点(epitem)加入双向链表,然后epoll_wait将改变的事件放入events(epoll_wait返回events),再遍历events调用accept函数完成三次握手。所以对于写程序的人,只需要在子进程中循环调用epoll_wait并对返回的events遍历,调相关的如accpet一类针对监听套接字的函数即可。
通讯代码精粹之epoll函数实战2
一、ngx_epoll_process_events函数调用位置
这个函数,仍旧是在子进程中被调用,被放在了子进程的for (;;),意味着这个函数会被不断的调用。
二、ngx_epoll_process_events函数内容
用户三次握手成功连入进来,这个“连入进来”这个事件对于服务器来讲,就是一个监听套接字上的可读事件。
三:ngx_event_accept函数内容
epoll_wait走下来就去执行这个函数去accept,生成用来传数据的服务端套接字!
a)accept4/accept:注意设置成非阻塞;
b)ngx_get_connection:从连接池中取一项,对新套接字绑定一块内存;
c)ngx_epoll_add_event:对新套接字绑定处理函数rhandler
【epoll的两种工作模式:LT和ET】
LT:level trigged, 水平触发,这种工作模式是低速模式(效率差),epoll缺省用此模式。
ET:edge trigged,边缘触发/边沿触发,这种工作模式是高速模式(效率好)
水平触发:来一个事件,如果你不处理它,那么这个事件就会一直被触发;具体,因为是水平触发而且worker死循环里调用epoll_wait,所以epoll_wait每次都触发了事件,每次都走下来,如果不用函数处理,每次都提醒你有三次握手连入;
边缘触发:只对非阻塞socket有用,来一个事件,内核只会通知你一次(不管是否处理,内核都不再次通知);边缘触发模式,提高系统运行效率,编码的难度加大;因为只通知一次,所以接到通知后,你必须要保证把该处理的事情处理利索。比如客户端发来了100B的数据,结果只处理了70B,还有30B在收缓冲区中没读,这时,如果是水平触发,就还会收到去读数据的事件,会不停提醒直到读完,但如果是边缘触发的话,自己没处理干净,就得等下次再有别的什么东西触发这个事件去读那30B。
现状:所有的监听套接字用的都是水平触发;所有的接入进来的用户套接字都是边缘触发。
小tips:有可能第一个事件是关闭连接的,这时c->fd = -1,然后第三个事件是跟第一个事件操作相同的套接字,这时要判断一下,如果c->fd 等于-1就continue,这样可以过滤过期事件。
四、总结和测试
1、服务器能够感知到客户端发送过来abc字符了
2、来数据会调用ngx_wait_request_handler(),在屏幕上打印一行222222222(往errno打印)
五、事件驱动总结:nginx所谓的事件驱动框架(面试可能问到)
【总结事件驱动框架/事件驱动架构】
所谓事件驱动框架,就是由一些事件发生源(三次握手内核通知或客户端发来数据,事件发生源就是客户端,),通过事件收集器来收集和分发事件(调用函数处理),事件收集器:epoll_wait()函数,ngx_event_accept(),ngx_wait_request_handler(),都属于事件处理器,用来消费事件。
ngx_epoll_process_events就是中间的圆柱!
六、一道腾讯后台开发的面试题
【问题】使用Linux epoll模型,水平触发模式;当socket可写时,会不停的触发socket可写的事件,如何处理(就是发缓冲区发满,系统就会告诉你可写可写可写)?
【答案】第一种最普遍的方式:需要向socket写数据的时候才把socket加入epoll(红黑树),等待可写事件。接受到可写事件后,调用write或者send发送数据,当所有数据都写完后,把socket移出epoll。这种方式的缺点是,即使发送很少的数据,也要把socket加入epoll,写完后在移出epoll,有一定操作代价。
一种改进的方式:开始不把socket加入epoll,需要向socket写数据的时候,直接调用write或者send发送数据。如果返回EAGAIN(可能数据多了就返回这个?),把socket加入epoll,在epoll的驱动下写数据,全部数据发送完毕后,再移出epoll。这种方式的优点是:数据不多的时候可以避免epoll的事件处理,提高效率。
ET、LT深释,服务器设计、粘包解决
一、’ET,LT模式深入分析及测试
【回顾】
LT:水平触发/低速模式,这个事件没处理完,就会被一直触发;
ET:边缘触发/告诉模式,这个事件通知只会出现一次。
普遍认为ET比LT效率高一些,但是ET编程难度比LT大一些;
【测试】
//ET测试代码
unsigned char buf[10]={0};
memset(buf,0,sizeof(buf));
do
{
int n = recv(c->fd,buf,2,0); //每次只收两个字节
if(n == -1 && errno == EAGAIN)
break; //数据收完了
else if(n == 0)
break;
ngx_log_stderr(0,"OK,收到的字节数为%d,内容为%s",n,buf);
}while(1);*/
//LT测试代码
unsigned char buf[10]={0};
memset(buf,0,sizeof(buf));
int n = recv(c->fd,buf,2,0);
if(n == 0)
{
//连接关闭
ngx_free_connection(c);
close(c->fd);
c->fd = -1;
}
ngx_log_stderr(0,"OK,收到的字节数为%d,内容为%s",n,buf);
客户端发abcdefg,ET模式下,如果每次触发只用recv收2个字节,那么需要触发4次才可以全部收完(注意字符串最后有'