上节课主要讲了非阻塞模型的实现,stream_set_blocking主要影响fwrite和fread,不会影响accept连接;然后说了stream_select函数,查询可读可写的套接字,默认可读;这个函数主要是实现多路复用模型的,以及多路复用模型的问题。
暴力实现长连接:主要是通过服务端不主动关闭连接,客户端通过不断轮询的方式实现的。
下面是用阻塞IO模型暴力实现长连接的代码及演示结果:
Worker.php
namespace zjl\io\Blocking;
/**
* 用户连接服务
*/
class Worker
{
/**
* 自定义服务的事件注册函数
* 闭包函数
* @var null
*/
public $onReceive = null;
public $onConnect = null;
public $onClose = null;
//连接
public $socket = null;
public function __construct($socket_address)
{
$this->socket = stream_socket_server($socket_address);
echo $socket_address."\n";
}
/**
* 处理事情
*/
public function accept()
{
//接收连接,处理事情
while(true)
{
$this->debug("accept start");//这里只是打印信息,做断点
//监听的过程是阻塞的
$client = @stream_socket_accept($this->socket);
//判断是不是闭包
if(is_callable($this->onConnect)){
//执行闭包函数
($this->onConnect)($this,$client);
}
$data = fread($client,65535);
if(is_callable($this->onReceive)){
//执行闭包函数
($this->onReceive)($this,$client,$data);
}
$this->debug("accept end");
//处理完关闭连接
//fclose($client);
}
}
public function send($conn,$data)
{
$header = "HTTP/1.1 200 OK\r\n";
$header .= "Content-Type:text/html;charset=UTF-8\r\n";
$header .= "Connection:keep-alive\r\n";
$header .= "Content-length:".strlen($data)."\r\n\r\n";
$data =$header.$data;
fwrite($conn,$data);
}
public function debug($data,$flag = false)
{
if($flag){
var_dump($data);
} else {
echo "==== >>>> :".$data."\n";
}
}
//启动服务
public function start()
{
$this->accept();
}
}
server.php
require __DIR__."/../../vendor/autoload.php";
use zjl\io\Blocking\Worker;
$host = "tcp://0.0.0.0:9501";
$server = new Worker($host);
//连接服务
$server->onConnect = function ($server,$client){
echo "有一个客户端连接进来\n";
var_dump($client);
};
//接收和处理信息
$server->onReceive = function ($server,$client,$data){
echo "给客户端连接发送信息\n";
$server->send($client,"hello world client \n");
};
$server->start();
client.php
//建立连接
$client = stream_socket_client("tcp://127.0.0.1:9501");
$new = time();
while(true)
{
echo "===》准备发送信息\n";
fwrite($client,"hello world");
echo "===》信息发送成功\n";
echo "===》准备接收信息\n";
var_dump(fread($client,65535));
echo "===》信息接收成功\n";
//sleep(2);
}
//关闭连接
//fclose($client);
疑问:实现的长连接,客户端是通过轮询的方式,为什么就只循环一次呢?
因为这是个阻塞IO模型。第一次循环,需要创建主worker连接,还没有与客户建立连接,是可用的,不阻塞;第二次循环,没有新的连接进来,上一次的连接在忙,不可用,就会一直阻塞在那里。
通常长连接都是通过多路复用IO模型实现的,因为多路复用模型是非阻塞的,且是多路复用的。这里服务端就采用多路复用的代码。
但是多路复用是串行的,单线程执行程序,并不是异步的。
代码初体验:
namespace zjl\io\Signal;
/**
* 用户连接服务
*/
class Worker
{
/**
* 自定义服务的事件注册函数
* 闭包函数
* @var null
*/
public $onReceive = null;
public $onConnect = null;
public $onClose = null;
//连接
public $socket = null;
public function __construct($socket_address)
{
$this->socket = stream_socket_server($socket_address);
echo $socket_address."\n";
}
/**
* 处理事情
*/
public function accept()
{
//接收连接,处理事情
while(true)
{
$this->debug("accept start");
//监听的过程是阻塞的
$client = @stream_socket_accept($this->socket);
//当这个链接建立好了之后,需要对这个链接设置个信号,
pcntl_signal(SIGIO,$this->sigHandle($client));
posix_kill(posix_getpid(),SIGIO);
pcntl_signal_dispatch();
$this->debug("accept end");
//处理完关闭连接
//fclose($client);
}
}
public function sigHandle($client)
{
return function($sig) use ($client){
//判断是不是闭包
if(is_callable($this->onConnect)){
//执行闭包函数
($this->onConnect)($this,$client);
}
$data = fread($client,65535);
if(is_callable($this->onReceive)){
//执行闭包函数
($this->onReceive)($this,$client,$data);
}
};
}
public function send($conn,$data)
{
$header = "HTTP/1.1 200 OK\r\n";
$header .= "Content-Type:text/html;charset=UTF-8\r\n";
$header .= "Connection:keep-alive\r\n";
$header .= "Content-length:".strlen($data)."\r\n\r\n";
$data =$header.$data;
fwrite($conn,$data);
}
public function debug($data,$flag = false)
{
if($flag){
var_dump($data);
} else {
echo "==== >>>> :".$data."\n";
}
}
//启动服务
public function start()
{
$this->accept();
}
}
这种模型不会大量的使用,因为缺点。
缺点
信号I/O在大量IO操作时可能会因为信号队列溢出导致没法通知信号驱动I/O尽管对于处理UDP套接字来说有用,即这种信号通知意味着到达一个数据报,或者返回一个异步错误。但是,对于TCP而言,信号驱动的I/O方 式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源,与前几种方式相比优势尽失
异步IO模型:用户空间设置了一个事件,内核空间只要到了事件点就会触发。类似于 web前端 jquery 点击鼠标移动事件。这个事件可以有 连接 、发送、写的 事件。
那php 究竟如何操作 linux事件呢?
可以通过event类来操作。
php7 =》event 类
php5 =》 libevent 类
event 类:用于监听socket 写 、读、超时、信号发生。
事件标志类型:
Event::READ 该标志指示一个事件,当提供的文件描述符(通常是流资源或套接字)可以读取时,该事件变为活动状态。
Event::WRITE 标志表示当提供的文件描述符(通常是流资源或套接字)准备好写入时变为活动的事件。
Event::SIGNAL 用于实现信号检测。请参阅下面的“构造信号事件”。
Event::TIMEOUT 表示事件在超时后变为活动状态。Event::TIMEOUT 构造事件时,将忽略该 标志:添加事件时可以设置超时,也可以不设置超时 。 发生超时时,在回调函数的$ what参数中设置它 。
在php手册里 下载安装包event-2.3.0.tgz,解压,编译安装,然后在php.ini添加扩展。然后查看是否安装成功。
use \Event as Event;
use \EventBase as EventBase;
$eventBase = new EventBase();
$event = new Event($eventBase,-1,Event::PERSIST | Event::TIMEOUT,function (){
echo "hello world event \n";
});
$event1 = new Event($eventBase,-1,Event::PERSIST | Event::TIMEOUT,function (){
echo "hello world event --02 \n";
});
$event1->add(0.2);//里面是参数不传也是可以的
$event->add(0.1);
$eventBase->loop();//调用设置在eventBase中的事件 相当于 swoole中start启动事件
//event是一个事件 =》add 添加一个事件
//eventBase 是事件库 =》存储所创建的事件
//通过add 将创建的事件 存储在eventBase库中,然后通过eventBase库 去执行loop事件
//Event::PERSIST 表示事件循环执行 =》只针对回调函数使用,不针对event对象
//Event::TIMEOUT 表示间隔多久执行 =》只针对回调函数使用,不针对event对象
// | 表示多个累加
//$fd
// -1:计时器
// 信号:信号的标识 SIGIO ,SIGHUP
// socket:传递socket资源
那event如何与socket结合使用呢?
简单体验演示:
服务端:
use \Event as Event;
use \EventBase as EventBase;
$socket_address = "tcp://0.0.0.0:9501";
$server = stream_socket_server($socket_address);
$eventBase = new EventBase();
$event = new Event($eventBase,$server,Event::PERSIST | Event::READ | Event::WRITE,function ($socket){
//这里注意 回调函数里的参数,Event内部会处理,自动将$server 传进回调函数中,相当于$server是实参,$socket是形参
//在闭包函数中function ($socket)中的$socket就是在构造函数中传递的$server这个属性
echo "连接开启\n";
$client = stream_socket_accept($socket);
var_dump(fread($client,65535));
fwrite($client,"hello world\n");
fclose($client);
echo "连接关闭\n";
});
$event->add();
$eventBase->loop();//调用设置在eventBase中的事件 相当于 swoole中start启动事件
客户端
curl http://127.0.0.1:9501
但是由于异步回调函数中的程序是同步的,那么是不是可以将回调函数中的读和写也放在异步回调函数中呢?下面演示一下:
use \Event as Event;
use \EventBase as EventBase;
$socket_address = "tcp://0.0.0.0:9501";
$server = stream_socket_server($socket_address);
$eventBase = new EventBase();
$event = new Event($eventBase,$server,Event::PERSIST | Event::READ | Event::WRITE,function ($socket) use ($eventBase){
//这里注意 回调函数里的参数,Event内部会处理,自动将$server 传进回调函数中,相当于$server是实参,$socket是形参
//在闭包函数中function ($socket)中的$socket就是在构造函数中传递的$server这个属性
echo "连接开启\n";
$client = stream_socket_accept($socket);
//这里嵌套了一个事件event
$event1 = new Event($eventBase,$client,Event::PERSIST | Event::READ | Event::WRITE,function ($socket){
var_dump(fread($socket,65535));
fwrite($socket,"hello world\n");
fclose($socket);
});
$event1->add();
echo "连接关闭\n";
});
$event->add();
$eventBase->loop();//调用设置在eventBase中的事件 相当于 swoole中start启动事件
发现运行失败,这是为啥呢?
eventSocket.php
require "e.php";
use \Event as Event;
use \EventBase as EventBase;
$socket_address = "tcp://0.0.0.0:9501";
$server = stream_socket_server($socket_address);
$eventBase = new EventBase();
//记录创建的事件,让eventBase可以找得到这个事件
$count = [];//变量没有要求 随便定义
$event = new Event($eventBase,$server,Event::PERSIST | Event::READ | Event::WRITE,function ($socket) use ($eventBase,&$count){
//这里注意 回调函数里的参数,Event内部会处理,自动将$server 传进回调函数中,相当于$server是实参,$socket是形参
//在闭包函数中function ($socket)中的$socket就是在构造函数中传递的$server这个属性
echo "连接开启\n";
$client = stream_socket_accept($socket);
(new e($eventBase,$client,$count))->handler();
echo "连接关闭\n";
});
$event->add();
$count[(int)$server][Event::PERSIST | Event::READ | Event::WRITE] = $event;
$eventBase->loop();//调用设置在eventBase中的事件 相当于 swoole中start启动事件
e.php
use \Event as Event;
class e
{
protected $socket;
protected $eventBase;
protected $count;
function __construct($eventBase,$client,&$count)
{
$this->socket = $client;
$this->eventBase = $eventBase;
$this->count = $count;
}
public function handler()
{
$event = new Event($this->eventBase,$this->socket,Event::PERSIST | Event::READ | Event::WRITE,function ($socket){
var_dump(fread($socket,65535));
fwrite($socket,"hello world\n");
fclose($socket);
});
$event->add();
$this->count[(int)$this->socket][Event::PERSIST | Event::READ | Event::WRITE] = $event;
}
}
为啥在同一个文件里不能这么做呢?
Event在同一个文件中不能嵌套定义,而转为引用方式的嵌套
event:因为在同一个作用域中,如果嵌套可能会存在覆盖的问题,因此不支持。
但是多个客户端连接时,后面的连接连上后,前面的连接就会不能用了。
这个时候还需要把event给 free()一下
use \Event as Event;
class e
{
protected $socket;
protected $eventBase;
protected $count;
function __construct($eventBase,$client,&$count)
{
$this->socket = $client;
$this->eventBase = $eventBase;
$this->count = $count;
}
public function handler()
{
$event = new Event($this->eventBase,$this->socket,Event::PERSIST | Event::READ | Event::WRITE,function ($socket){
var_dump(fread($socket,65535));
fwrite($socket,"hello world\n");
fclose($socket);
($this->count[(int)$this->socket][Event::PERSIST | Event::READ | Event::WRITE])->free();
});
$event->add();
$this->count[(int)$this->socket][Event::PERSIST | Event::READ | Event::WRITE] = $event;
}
}