文章开篇先脑补一些知识,有助于阅读,本篇文章主要以select为住,介绍select实现原理,并利用select来实现一个单进程阻塞复用的网络服务器。
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程,目前支持I/O多路复用有 select,poll,epoll,I/O多路复用就是经过一种机制,一个进程能够监视多个描述符,一旦某个描述符就绪(通常是读就绪或者写就绪),可以通知程序进行相应的读写操做,IO多路复用适用以下场合:php
与多进程和多线程技术相比,I/O多路复用技术的最大优点是系统开销小,系统没必要建立进程/线程,也没必要维护这些进程/线程,从而大大减少了系统的开销。html
监视并等待多个文件描述符的属性变化(可读、可写或错误异常)。
select函数监视的文件描述符分 3 类,分别是writefds、readfds、和 exceptfds。
调用后 select会阻塞,直到有描述符就绪(有数据可读、可写、或者有错误异常),或者超时( timeout 指定等待时间),函数才返回。
当 select()函数返回后,能够经过遍历 fdset,来找到就绪的描述符,而且描述符最大不能超过1024数组
poll的机制与select相似,与select在本质上没有多大差异,管理多个描述符也是进行轮询,根据描述符的状态进行处理,可是poll没有最大文件描述符数量的限制。poll和select一样存在一个缺点就是,包含大量文件描述符的数组被总体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增长而线性增大。服务器
select/poll问题很明显,它们须要循环检测链接是否有事件。若是服务器有上百万个链接,在某一时间只有一个链接向服务器发送了数据,select/poll须要作循环100万次,其中只有1次是命中的,剩下的99万9999次都是无效的,白白浪费了CPU资源。网络
epoll是在2.6内核中提出的,是以前的select和poll的加强版本。相对于select和poll来讲,epoll更加灵活,没有描述符限制,无需轮询。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中。
简单点来讲就是当链接有I/O流事件产生的时候,epoll就会去告诉进程哪一个链接有I/O流事件产生,而后进程就去处理这个事件。多线程
单进程阻塞复用的网络服务器 ,以下图所示socket
服务监听流程如上
一、保存全部的socket,经过select系统调用,监听socket描述符的可读事件
二、select会在内核空间监听一旦发现socket可读,会从内核空间传递至用户空间,在用户空间经过逻辑判断是服务端socket可读,仍是客户端的socket可读
三、若是是服务端的socket可读,说明有新的客户端创建,将socket保留到监听数组当中
四、若是是客户端的socket可读,说明当前已经能够去读取客户端发送过来的内容了,读取内容,而后响应给客户端。
缺点:
一、select模式自己的缺点(一、循环遍历处理事件、二、内核空间传递数据的消耗)
二、单进程对于大量任务处理乏力tcp
class Worker{ //监听socket protected $socket = NULL; //链接事件回调 public $onConnect = NULL; //接收消息事件回调 public $onMessage = NULL; public $workerNum=4; //子进程个数 public $allSocket; //存放全部socket public function __construct($socket_address) { //监听地址+端口 $this->socket=stream_socket_server($socket_address); stream_set_blocking($this->socket,0); //设置非阻塞 $this->allSocket[(int)$this->socket]=$this->socket; } public function start() { //获取配置文件 $this->fork(); } public function fork(){ $this->accept();//子进程负责接收客户端请求 } public function accept(){ //建立多个子进程阻塞接收服务端socket while (true){ $write=$except=[]; //须要监听socket $read=$this->allSocket; //状态谁改变 stream_select($read,$write,$except,60); //怎么区分服务端跟客户端 foreach ($read as $index=>$val){ //当前发生改变的是服务端,有链接进入 if($val === $this->socket){ $clientSocket=stream_socket_accept($this->socket); //阻塞监听 //触发事件的链接的回调 if(!empty($clientSocket) && is_callable($this->onConnect)){ call_user_func($this->onConnect,$clientSocket); } $this->allSocket[(int)$clientSocket]=$clientSocket; }else{ //从链接当中读取客户端的内容 $buffer=fread($val,1024); //若是数据为空,或者为false,不是资源类型 if(empty($buffer)){ if(feof($val) || !is_resource($val)){ //触发关闭事件 fclose($val); unset($this->allSocket[(int)$val]); continue; } } //正常读取到数据,触发消息接收事件,响应内容 if(!empty($buffer) && is_callable($this->onMessage)){ call_user_func($this->onMessage,$val,$buffer); } } } } } } $worker = new Worker('tcp://0.0.0.0:9805'); //链接事件 $worker->onConnect = function ($fd) { //echo '链接事件触发',(int)$fd,PHP_EOL; }; //消息接收 $worker->onMessage = function ($conn, $message) { //事件回调当中写业务逻辑 $content="回复的消息"; $http_resonse = "HTTP/1.1 200 OK\r\n"; $http_resonse .= "Content-Type: text/html;charset=UTF-8\r\n"; $http_resonse .= "Connection: keep-alive\r\n"; //链接保持 $http_resonse .= "Server: php socket server\r\n"; $http_resonse .= "Content-length: ".strlen($content)."\r\n\r\n"; $http_resonse .= $content; fwrite($conn, $http_resonse); }; $worker->start(); //启动
在PHP中提供了一个很是方便的函数一次性建立、绑定端口、监听端口
stream_set_blocking ( resource $stream , int $mode ) : boolthis
为资源流设置阻塞或者阻塞模式,$mode 0非阻塞,1阻塞
接受由 stream_socket_server() 建立的套接字链接