网络解决的是机器与机器之间通信的问题。
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数据一次性能读完,就任务是小数据,读不完就认为是大数据。
百万连接的功劳在于epoll。
读数据从读缓冲区中读,写数据写到写缓冲区(每个连接都有个读缓冲区和写缓冲区)。写都写缓冲区后,由内核中的协议栈完成发送到对端。
网络编程关注的四个问题:连接的建立,链接的断开,消息的到达。数据的发送完毕。数据的发送完毕是指发送到写缓冲区,数据的到达是指从读缓冲区中读取出来,取到用户空间中来。
若是使用的阻塞io;则read会阻塞,等待read buffer中有数据,即等待协议栈从网络中收取数据放入read 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的通知机制需要很多函数回调。