select、poll、epoll、reactor总结

网络解决的是机器与机器之间通信的问题。

tcp类似服务员,从建立链接开始,tcp一直为客户端服务。

listend类似在门口迎宾的。

一点击链接(listend),在内核(协议栈里)发生三次握手

为什么两个能链接成功?

因为listenfd处于listen状态,三次握手是在协议栈里面完成的,。与应用层没有关系。。

三次握手发生在哪个api里面?是协议栈本身完成的,是在处于listen状态的时候被动完成的,不由应用程序管理,所以三次握手不发生在任何api。是在处于listen之后,,三次握手完成之后,accept取一个节点处理。

如果在客户量不多的时候,可以采用while(1)recev并分配一个线程去接收数据,。即采用多线程的方式。多线程的方式优点:逻辑简单;

缺点:不是后大量客户。(随着不断收发数据,内存会不断增加,最后重启);

客户量多的时候无法准确知道哪个客户端的信息需不需要处理,最好的方法就是把listenfd放到一个组件里 。采用iO多路复用组件->select。一个客户端一个listenfd。,多个listenfd。

选择有事件可以处理的。

多路是指多个连接,复用是指对线程的复用。用select去监视多个连接。

select带5个参数。。

select相当于一个服务员集中处理多桌客人。,在集体中挑选出需要处理的来处理。

fd_set:是fd的集合。每个fd通过用0或1表示是否有事件处理。此集合是个比特位。先把所有fd放到一个集合里

socket创建出来的listenfd是依次增加的,这次是2,下次是3。

select需要对每个fd状态做判断。然后选择。

select先对readfds、writefds和exceptfds中每个位置的fd进行探测,记录事件发生的总数量并返回。只记录了数量,并不知道哪个fd发生了事件。然后再对每个fd判断是否可读写。

fd_set  rfds, rset, wset;

FD_ZERO(&rfds);清空

FD_SET(listenfd,rfds);//两个集合,一个是需要带到select里面去的,去监控,一个是从被监控集合里提取出有具体数据可处理的集合。即一个是带到select去的,一个是select出来的。

两个动作,一个copy进去(copy到内核里),一个是copy出来(copy出需要处理的)。

int max_fd=listenfd// max_fd表示最大的id值是多少,即需要遍历的fd_set里面最大(最多)的位置

int nready=select(max_fd+1,&rset,&wset,NULL,NULL);/select相当于行政人员,需要统计带饭的多少人,出去吃的多少人,点外卖的多少人,多长时间统计一次。最后一个参数是多长时间统计一次。第二个参数是读集合,第三是写集合,第四是出错的集合

返回值nready是总共的数量。。select自带阻塞功能,如果传进去的参数是NULL,就阻塞,一直等。

对于一请求一线程的方式(线程数量有限制,超不过C10K这个限制)。一个select可做1024fd,多做几个select可突破C10K,但无法突破C1000K。因为select需要copy进出(如需要把rset集合copy进去,然后选择后copy出来。)这一进一出,对大量的fd管理,数量是有局限的。

最后释放的时候,要清除fd。这里有两部分fd,一个是负责连接的fd,一个是监听的fd

poll是另一种管理方式,

poll和select差不多,也是挨个遍历查看是否有事件发生,返回一个总数量,然后对每个fd进行判断。主要区别就是在对fd的储存上。select采用的三个集合。每个集合里放的表示fd的位置。用0和1表示有无事件发生。而poll把fd放到一个数组里面。

struct  pollfd  fds[POLL_SIZE]//POLL_SIZE是需要放的fd的最大的数量是多少。poll底层用的和select是一样的(即虽然系统api不一样,但系统调用的流程是一样的)

select里面有的流程,poll里面也有。

select/poll/epoll都是事件驱动。

epoll:特点:有select/poll一个函数改成了三个函数。

epoll把每个fd放到红黑树里面,每个结点放的键值对。key即fd。当有事件发生的时候就会有回调自动把对应的fd放进一个链表。epoll返回这个链表。直接对链表进行处理。

步骤:把所有客户端的fd放到集合里。

有两个集合,一个是所有住户,一个是快递集合(需要寄快递的用户)。

epoll_create。相当于创建一个集合(创建一栋楼,蜂巢);

epoll_ctl。相当于把住户搬到楼里,有三个过程,一个是往楼里面搬用户,一个是删除,一个是修改。

epoll_ctl(ADD,DEL,MOD)

epoll_wait快递员多长时间取一次快递。

epoll增加并发。大并发底层离不开epoll。服务器底层逻辑就是while(1)循环,但围绕这个循环有很多种做法。

底层做循环的做法:1、一请求一线程2、select,3、poll4、epoll。服务器底层离不开这四种。

epoll_create中参数size只分小于0和大于零。大于零都一样,因为是动态链表实现的。多大size都一样。

struct  epoll_event  events[POLL_SIZE]={0};events相当于快递员的袋子,POLL_SIZE是能装的大小。

struct  epoll_event  ev;

ev.events=EPOLLIN;

ev.data.fd=listenfd;

windows里没有epoll。

创建一个socket便加入到epoll里面去监听。

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout):阻塞等待注册的事件发生,返回事件的数目,并将触发的事件写入events数组中。

处于ready状态的那些文件描述符会被复制进ready list中,epoll_wait用于向用户进程返回ready list。events和maxevents两个参数描述一个由用户分配的struct epoll event数组,调用返回时,内核将ready list复制到这个数组中,并将实际复制的个数作为返回值。注意,如果ready list比maxevents长,则只能复制前maxevents个成员;反之,则能够完全复制ready list。

另外,struct epoll event结构中的events域在这里的解释是:在被监测的文件描述符上实际发生的事件。

timeout = -1表示调用将一直阻塞,直到有文件描述符进入ready状态或者捕获到信号才返回;

timeout = 0用于非阻塞检测是否有描述符处于ready状态,不管结果怎么样,调用都立即返回;

timeout > 0表示调用将最多持续timeout时间,如果期间有检测对象变为ready状态或者捕获到信号则返回,否则直到超时。

struct eventpoll结构体中有红黑树和链表,红黑树用来保存键值对,其中键即每个socket。

epoll_wait说白了就是遍历这个双向链表,把这个双向链表里边的节点数据拷贝出去,拷贝完毕的就从双向链表里移除。因为双向链表里记录的是所有有数据/有事件的socket。

socket创建出来的listenfd是依次增加的,这次是2,下次是3.。。。

在一个进程里面,fd是连续的,并不是整台机器中。

select、poll、epoll都是基于io(或者说fd,socket)的。epoll并不是真正基于io的

reactor:把每个io集中起来管理,每个io对应不同的事件走不同的回调函数

单独的reactor与多线程和多进程没关系。

所有的fd都交给epoll管理。对于每个fd,关注是否可读是否可写。每个fd都有对应的事件,当epoll中管理的每个fd发生事件是,就会发生相应的反应,调用相应的回调函数。实现epoll 的时候,把fd与对应的可读事件、可写事件。每个事件对应一个回调函数。

reactor即通过epoll把fd集中管理,每个fd不同的事件调用不同的回调函数

反应堆即reactor中每个fd有哪些属性即结构体组成?。

struct  nevent{//或者用nitem表示。表示一个元素。每个item对应一个元素(即fd)

int  fd;//这个是item内部的fd。除此之外还用能通过fd找到对应的item

int  events;//结合对应的事件,不同的事件走不同的回调函数,每次把指针指向改变一下即可。 //这样不同的回调函数写成一个就行.可以写一个回调函数也可以写多个回调 函数

int (*callback)(int fd,int event, void*arg);//写成一个回调函数的话,在回调函数内通过epoll_ctl可改变对应的事件。

}

有没有可能在同一时刻,epoll_wait返回的fd,即可读又可写?有可能

reactor结构体中存储epollfd以及所有的item(即fd);每个fd对应一个item即event。epoll管理所有的fd;listenfd又accept管理,其余fd由recv或send管理,当listenfd有事件可读的时候,由accept处理,accept分配新的fd用于对这个连接的后续操作。

struct reactor{

int  epfd;//即创建的epoll

//struct  nitem  *items;

strcut itemblock*head;//指向第一个

//strcut itemblock**last;//。*last指向的是一个itemblock。**last指向itemblock中的next。 next 是一个指针

}

可采用红黑树或链表存储item。课程中采用链表。

每个fd对应一个item,如何知道epoll_wait返回的fd对应哪个item呢?可之间采用下标存储。直接用数组存放item。下标为fd。。可通过fd下标找到对应的item。由于fd是连续依次增长的,因此用数组存放item比较好。

struct itemblock{struct  itemblock  *next;  struct  nitem  *items;}可以把多个item数组连起来。

三种cb(回调函数),可读、可写、可accept

reactor是对事件的管理,而不太关心fd

1.reactor

epoll水平/边缘触发

没数据是0,有数据是1。

水平触发:有数据的时候就一直触发,一直到没数据才停止。

从没数据到有数据这个变化叫边缘。从无数据到有数据这个变化时触发为边缘触发。

边缘触发:从无数据到有数据这个变化时触发。即使原来缓存中有数据,再接收新数据的时候也会触发。只要接收新的数据就会触发边缘触发。

默认是水平触发。

若边缘触发时,第一次没把缓存中的数据读取完,再次收到新数据时还会触发,继续读取缓存区剩余的。。

对recv来说,最好循环读,一直读取直到读完。

哪些场景使用水平触发?:

小数据用边缘触发。数据量大的用水平触发。

对于多个客户端连接的时候。listenfd用水平触发。因为accept每次只处理一个。

大数据和小数据的分界点:如果recv中buffer数据一次性能读完,就任务是小数据,读不完就认为是大数据。select、poll、epoll、reactor总结_第1张图片select、poll、epoll、reactor总结_第2张图片

 百万连接的功劳在于epoll。

读数据从读缓冲区中读,写数据写到写缓冲区(每个连接都有个读缓冲区和写缓冲区)。写都写缓冲区后,由内核中的协议栈完成发送到对端。

网络编程关注的四个问题:连接的建立,链接的断开,消息的到达。数据的发送完毕。数据的发送完毕是指发送到写缓冲区,数据的到达是指从读缓冲区中读取出来,取到用户空间中来。

  1. 连接 的建立:首先accept来自客户端的连接,然后服务端作为客户端去connect连接数据库或某内部服务等第三方服务。
  2. 连接的断开:主动断开连接,用close。或用shutdown(fd)关闭一端(读端和写端的一端)。。被动断开:客户端close的时候,服务端read返回0;另一种情况:write=-1&&errno=EPIPE也表示被断开连接。
  3. 消息的达到:read,recv。如果读缓冲区并没有数据,并且连接正常,如果是非阻塞io:则read=-1&&errno=wouldblock。

若是使用的阻塞io;则read会阻塞,等待read buffer中有数据,即等待协议栈从网络中收取数据放入read buffer

  1. 数据的发送完毕:两种接口,write 和send,差异不大。如果write buffer已经满了,则会出现的情况:如果是非阻塞io。则会出现write=-1,errno wouldblock(可能会阻塞)。若是阻塞io。则会阻塞等待,等待协议栈把write buffer中的数据发送到对端,write buffer有空间的时候,再写入到write buffer

write、accept、read、recv等第一个参数都是fd。都是对某个文件操作的。使用的是否是阻塞io由连接的fd决定。由创建fd时是否设置为阻塞决定的。默认为阻塞的。,如果是非阻塞的,则调用的函数也是阻塞的。

阻塞与非阻塞的具体差异:io函数在数据未到达时是否立刻返回

write/read可直接操作网络数据,也可以检测网络状态(通过返回值和错误码)。

非阻塞io:需要我们主动去探测数据是否准备好。因为我们不知道数据什么时候准备好,非阻塞io无论数据是否准备都会立刻返回。优点是如果数据没有准备可以去处理别的io。

阻塞io在阻塞期间不能处理其它io。。阻塞io不需要主动探测。缺点是阻塞线程。

这两种都优缺点,因此用io多路复用。

阻塞和非阻塞io主要区别在数据准备阶段(即数据有没有就绪阶段。)在数据拷贝阶段都会阻塞。

io多路复用:多路是指多条连接,复用的是一个线程,即用一个线程去检测多个连接的数据状态

select./poll/epoll是io多路复用,可用一个线程(当然也可以用多个线程)去检测多个连接的数据状态,直接作用于数据准备阶段,为准备变会阻塞,同时检测多个连接,返回数据就绪的fd

epoll_ctl向epoll对象中添加、修改或删除事件,即告诉epoll对哪些fd哪些事件进行管理。

epoll_wait是阻塞调用,通过一个epoll_wait去检测多路数据状态。

epoll与select、poll的对比:

1. 用户态将文件描述符传入内核的方式

select:创建3个文件描述符集并拷贝到内核中,分别监听读、写、异常动作。这里受到单个进程可以打开的fd数量限制,默认是1024。

poll:将传入的struct pollfd结构体数组拷贝到内核中进行监听。

epoll:执行epoll_create会在内核的高速cache区中建立一颗红黑树以及就绪链表(该链表存储已经就绪的文件描述符)。接着用户执行的epoll_ctl函数添加文件描述符会在红黑树上增加相应的结点。

2. 内核态检测文件描述符读写状态的方式

select:采用轮询方式,遍历所有fd,最后返回一个描述符读写操作是否就绪的mask掩码,根据这个掩码给fd_set赋值。

poll:同样采用轮询方式,查询每个fd的状态,如果就绪则在等待队列中加入一项并继续遍历。

epoll:采用回调机制。在执行epoll_ctl的add操作时,不仅将文件描述符放到红黑树上,而且也注册了回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数,该回调函数将文件描述符放在就绪链表中。

3. 找到就绪的文件描述符并传递给用户态的方式

select:将之前传入的fd_set拷贝传出到用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。

poll:将之前传入的fd数组拷贝传出用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。

epoll:epoll_wait只用观察就绪链表中有无数据即可,最后将链表的数据返回给数组并返回就绪的数量。内核将就绪的文件描述符放在传入的数组中,所以只用遍历依次处理即可。

4. 重复监听的处理方式

select:将新的监听文件描述符集合拷贝传入内核中,继续以上步骤。

poll:将新的struct pollfd结构体数组拷贝传入内核中,继续以上步骤。

epoll:无需重新构建红黑树,直接沿用已存在的即可。

epoll更高效的原因:

select和poll的动作基本一致,只是poll采用链表来进行文件描述符的存储,而select采用fd标注位来存放,所以select会受到最大连接数的限制,而poll不会。

select、poll、epoll虽然都会返回就绪的文件描述符数量。但是select和poll并不会明确指出是哪些文件描述符就绪,而epoll会。造成的区别就是,系统调用返回后,调用select和poll的程序需要遍历监听的整个文件描述符找到是谁处于就绪,而epoll则直接处理即可。

select、poll都需要将有关文件描述符的数据结构拷贝进内核,最后再拷贝出来。而epoll创建的有关文件描述符的数据结构本身就存于内核态中。

select、poll采用轮询的方式来检查文件描述符是否处于就绪态,而epoll采用回调机制。造成的结果就是,随着fd的增加,select和poll的效率会线性降低,而epoll不会受到太大影响,除非活跃的socket很多。

epoll的边缘触发模式效率高,系统不会充斥大量不关心的就绪文件描述符

虽然epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

 

你可能感兴趣的:(网络)