reactor -- 响应
reactor模式 -- 响应器模式
整体架构:
相较于epoll传统模型,reactor将整个epoll架构进行抽象,将read和write接口化,epoll整个机制封装在reactor中;简化程序编写。
具体实现就是基于epoll和回调函数的形式,从而在设置好回调函数之后,对应线程在epoll_wait返回时可以针对不同fd执行不同的注册函数操作;在此过程中还涉及到对epoll的设置的变化,设置相应的为读写状态;
同步与异步:
同步操作就是说流程A需要等待流程B完成之后再接着运行;实现就是直接等待;
异步操作就是说流程A执行过程中直接执行流程B,不需要等待流程B返回直接执行之后的流程,等到流程B完成后,流程A得到通知,执行流程A需要执行的部分;一般的措施就是回调函数。
关于epoll的ET与ET设置
这个设置可以在accept之后对套接字描述符使用fcntl对描述符直接设置,也可以在epoll_ctl的时候增加EPOLLET设置(LT是默认的,没有对应设置)。
边沿触发时当套接字缓冲区发生变化的时候就会被触发;而水平触发是只要缓冲区中有数据就会一直触发。也就是说使用边沿触发可以减少epoll_wait的触发,但是相应的处理相较于水平触发更为繁琐,需要在触发时就将socket缓冲区数据都读取完毕,好处就是整个过程中减少了内核与用户层的切换,提升程序执行效率。
还有个小的注意的点,就是说recv这个函数,如果recv的len参数设置为0,相应的recv也会返回0(不论是阻塞还是非阻塞的),此时需要区别与对端close的返回0的情况。
还有就是关于listen,listen如果设置了边沿触发,此时的listen就是当内核连接数量变化的时候才会触发listen,因此如果只是accept一个的话,就可能会造成连接丢失问题,因此需要将listenFd使用fcntl改为非阻塞套接字,然后使用while进行accept监听。另外,accept的功能就是从内核套接字连接池中提取连接,他和epoll_wait是相互独立的,也就是说在这种情况下,在accept处理过程中如果有新的连接,那么accept直接会处理,处理完毕后再次epoll_wait就不会触发条件了;而如果在accept处理过程中没有处理新连接的客户端,再次返回epoll_wait,边沿触发情况下依旧会触发epoll_wait.
那么如果在listen、epoll_add之后,epoll_wait之前有新的连接到来,测试发现他也是会触发epoll_wait的。
还有就是关于epoll_wait的接口中的epoll_event接收数组大小问题,这个本质上就是从内核的就绪链表中复制数据,即使传入数组大小无法一次性读取装在内核的就绪列表中的数据,但是他再次执行到epoll_wait的时候会继续转载,因此这个数组的大小无需设置太大。
关于select、poll和epoll
select实现了初步的IO复用,poll对单进程的IO复用监控描述符数量进行了解决,epoll对poll频繁的内核和用户层数据复制进行了解决。
关于65535个端口和百万连接
这里有个误区,就是说套接字描述符并不是和端口直接绑定的,套接字描述符是和五元组绑定的,本机端口只是五元组中的一个因素,当百万个客户端IP连接到服务器上的时候,服务器对每个连接都分配相同端口生成的套接字描述符都是有效的。
另外,在accept监听到客户端连接的时候会创建套接字描述符,而该连接套接字服务器的端口就是服务区对应监听的端口,这也可以一定程度上理解防火墙对端口的出入站规则。
百万连接的测试
首先,每个套接字都对应着一个文件描述符,关于文件描述符的数量限制就需要手动设定;有两个文件描述符的数量设定:一个是整个操作系统可以打开的文件描述符数量,另外一个就是进程可以打开的文件描述符数量;
后者使用ulimit -a查看,其中open file选项就是每个进程允许打开的文件描述符数量;通过ulimit -n去进行修改;这个过程中由于权限问题会修改失败,或者是sudo找不到ulimit,这种情况下可以使用su切换到管理员权限,然后进行ulimit设置。权限正常的话依旧报警"无法修改limit值:不允许的操作",那么就需要修改/etc/security/limits.conf文件,增加soft与hard参数,然后重启终端;
接下来就是修改协议栈相关的参数,tcp在使用过程中会在内核层创建缓冲区等,这个文件是/etc/sysctl.conf,具体参数搜索该文件,修改完毕之后使用sysctl -p将参数生效;
当然,测试的过程中考虑到硬件情况服务器端以及客户端可能都需要对以上参数进行修改;
关于端口绑定:
端口的绑定和本机的IP、端口、协议三个因素是有关的,tcp的话,协议固定,不同IP绑定相同端口也是可以进行通信的。相同IP、相同端口的情况下,TCP和UDP协议也是可以正常绑定套接字的。
客户端在发送的时候也可以使用bind对套接字进行绑定,绑定的话如果IP设置为0.0.0.0那么在发送数据的时候由内核指定IP,port如果绑定为0的话,那么就是由内核指定一个端口,且IP和port的bind是相互独立的,可以其中某个为0,也可以都为0;
关于端口复用:
当一个服务器要实现大量并发且监听一个端口的时候,首先遇到的问题就是集中性的accept问题和close问题,也就是惊群问题,处理这个问题一个是手动加锁,一个是端口复用。
关于为什么提升处理线程可以提升服务器效率问题:
个人认为:不论是否分解服务器服务到多个线程,最终要解决的服务都是一样的,因此这个问题和连接没有关系,应该和进程调度有关系,更多的线程可以获取到更大占比的CPU时间,从而提升服务器效率。因此分解服务到线程本质上就是向内核申请更多的时间。