引入:
上一篇博文中的预派生子进程模式的服务器虽然能解决一定程度的并发,但是太依赖进程的数量了,实际运行中一个进程只能处理一个连接,那有没有办法能让一个进程能同时处理很多个(最好理论上能处理无限个)连接呢?并且一个进程能处理N个连接后,在多fork几个这样的子进程,那是不是就能突破进程的限制呢?没错,只有你内存够大,CPU处理能力没问题,就能达到有多少个文件描述符(fd)就能保持多少个连接!(文件描述符是一个自增数字,范围是1 ~ 1600万,fd超过1600万后会自动从1开始进行复用)
知识预备:
什么是IO复用
Linux中select的原理
PHP调用linux的select函数
要点解析:
要点1:怎么实现进程复用?
要实现进程复用IO就得让这个进程找到多个连接,即便处理响应的地方阻塞了(程序要执行逻辑,绝大部分都要等待处理结果,阻塞是很正常的,如果需要处理比较耗时的任务就得到交给异步线程了),但是监听连接的地方不会阻塞,来一个连接,记录一个,然后利用linux的select机制去触发回调,这样一个进程不就能同时处理N个请求了吗?
要点2:怎么判断是该服务端发送消息了还是有新的客户端连接进入了?
这个就是上面知识预备中select的原理了,select会在linux系统中去监听,一旦发现socket可读,会从内核空间传递至用户空间,在用户空间通过逻辑判断是服务端socket可读,还是客户端的socket可读,逻辑是这样的,我们预先存储好服务端的socket连接信息,select遍历这些socket时就判断可读的socket是不是属于我们存储好的服务端socket,如果不是服务端的socket就说明是一个新的socket,新的socket从哪里来?别的地方来的,那就是客户端的socket,既然客户端的socket可读了,那我们就去读客户端的内容,进行自己的逻辑处理,然后做出相应;反过来,如果是服务端的socket可读了,说明有新的连接进来了,就与新进来的socket建立连接并监听。
代码实现:
<?php
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:9800');
//连接事件
$worker->onConnect = function ($fd) {
//echo '连接事件触发',(int)$fd,PHP_EOL;
};
//消息接收
$worker->onMessage = function ($conn, $message) {
//事件回调当中写业务逻辑
//var_dump($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(); //启动
测试
运行
这次来波更多的,请求9W,并发1k(注意,系统还是1G运行内存,而且全是长连接,并且是单进程,可以比较上一篇博客的测试结果)
完全没问题,一个进程的情况下,没用什么逻辑处理吞吐量就能达到了9W+
但是这个模式的服务器依旧存在缺点:这个也是select机制的问题,因为select会遍历每一个socket,直到找到可读的那个,如果有100万socket,select需要做循环100万次,其中只有1次是命中的,剩下的99万9999次都是无效的,白白浪费了CPU资源;而且从linux的内核空间传递到我们php的用户空间的数据传递也会消耗资源的。