php反射实现Ioc-Di及注解

PHP5之后提供了完整的反射API,添加了对类、接口、函数、方法和扩展进行反向工程的能力。此外,反射API提供了方法来取出函数、类和方法的文档注释。

Ioc/Di大家应该都不陌生,但是对小白来说呢听起来就挺高大上的,下面就用代码来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php
/**
* @author gaobinzhan <gaobinzhan@gmail.com>
*/

class Foo
{
public function getClassName()
{
return 'this is Foo';
}
}

class Bar
{
public function getClassName()
{
return 'this is Bar';
}
}

class Test
{
public function __construct(Foo $foo, Bar $bar)
{
var_dump($foo->getClassName());
var_dump($bar->getClassName());
}

public function index(Foo $foo)
{
var_dump($foo->getClassName());
}
}

// 反射Test
$reflect = new ReflectionClass(Test::class);
// 获取是否有构造函数
$constructor = $reflect->getConstructor();
if ($constructor) {
// 如果存在构造 获取参数
$constructorParams = $constructor->getParameters();
// 初始化注入的参数
$args = [];
// 循环去判断参数
foreach ($constructorParams as $param) {
// 如果为class 就进行实例化
if ($param->getClass()) {
$args[] = $param->getClass()->newInstance();
} else {
$args[] = $param->getName();
}
}
// 实例化注入参数
$class = $reflect->newInstanceArgs($args);
} else {
$class = $reflect->newInstance();
}


// 假设我们要调用index方法 在此之前自己判断下方法是否存在 我省略了
$reflectMethod = new ReflectionMethod($class, 'index');
// 判断方法修饰符是否为public
if ($reflectMethod->isPublic()) {
// 以下代码同等上面
$args = [];
$methodParams = $reflectMethod->getParameters();
foreach ($methodParams as $param) {
if ($param->getClass()) {
$args[] = $param->getClass()->newInstance();
} else {
$args[] = $param->getName();
}
}
$reflectMethod->invokeArgs($class, $args);
}

以上就简单的实现了依赖注入,下面我们接着封装一下。

Ioc.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php
/**
* @author gaobinzhan <gaobinzhan@gmail.com>
*/

class Ioc
{
public static function getInstance($className)
{
$args = self::getMethodParams($className);
return (new ReflectionClass($className))->newInstanceArgs($args);
}

public static function make($className, $methodName, $params = []) {

// 获取类的实例
$instance = self::getInstance($className);

// 获取该方法所需要依赖注入的参数
$args = self::getMethodParams($className, $methodName);

return $instance->{$methodName}(...array_merge($args, $params));
}

protected static function getMethodParams($className, $methodsName = '__construct')
{

// 通过反射获得该类
$class = new ReflectionClass($className);
$args = []; // 记录注入的参数

// 判断该类是否有构造函数
if ($class->hasMethod($methodsName)) {
// 构造函数存在 进行获取
$construct = $class->getMethod($methodsName);

// 获取构造函数的参数
$params = $construct->getParameters();

// 构造函数无参数 直接返回
if (!$params) return $args;

// 判断参数类型
foreach ($params as $param) {

// 假设参数为类
if ($paramClass = $param->getClass()) {

// 获得参数类型名称
$paramClassName = $paramClass->getName();
// 如果注入的这个参数也是个类 就要继续判断是否存在构造函数
$methodArgs = self::getMethodParams($paramClassName);

// 存入数组中
$args[] = (new ReflectionClass($paramClass->getName()))->newInstanceArgs($methodArgs);
}
}
}
// 返回参数
return $args;
}
}

以上代码实现了构造函数的依赖注入及方法的依赖注入,下面进行测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class Bar
{
public function getClassName()
{
return 'this is Bar';
}
}

class Test
{
public function __construct(Foo $foo, Bar $bar)
{
var_dump($foo->getClassName());
var_dump($bar->getClassName());
}

public function index(Foo $foo)
{
var_dump($foo->getClassName());
}
}
Ioc::getInstance(Test::class);
Ioc::make(Test::class,'index');

以上呢,就简单的通过php的反射机制实现了依赖注入。

继基于swoole的微服务框架出现,注解呢就开始慢慢出现在我们的视角里。据说php8也加入了注解支持:

1
2
3
4
5
6
7
8
9
10
use \Support\Attributes\ListensTo;

class ProductSubscriber
{
<<ListensTo(ProductCreated::class)>>
public function onProductCreated(ProductCreated $event) { /* … */ }

<<ListensTo(ProductDeleted::class)>>
public function onProductDeleted(ProductDeleted $event) { /* … */ }
}

就类似这样的,哈哈哈。

而我们现在的注解则是通过反射拿到注释去做到的解析。

接下来我们去用别人写好的组件去实现annotations

编写我们的composer.json

1
2
3
4
5
6
7
8
9
10
11
{
"require": {
"doctrine/annotations": "^1.8"
},
"autoload": {
"psr-4": {
"app\\": "app/",
"library\\": "library/"
}
}
}

接下来要执行啥???这个你要是再不会,真的我劝你回家种地吧!!哈哈哈 闹着玩呢!

composer install

然后我们接下来去创建目录:

1
2
3
4
5
6
7
8
9
10
11
12
- app // app目录
- Http
- HomeController.php
- library // 核心注解库
- annotation
- Mapping
- Controller.php
- RequestMapping.php
- Parser
- vendor
- composer.json
- index.php // 测试文件

php-annotation目录

害 图片有点大。。。。。咋整。。。算了,就这样吧!!!

温馨提示:在phpstrom里面,安装插件PHP Annotation写代码会更友好啊!!

创建library\Mapping\Controller.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
/**
* @author gaobinzhan <gaobinzhan@gmail.com>
*/


namespace library\annotation\Mapping;


use Doctrine\Common\Annotations\Annotation\Attribute;
use Doctrine\Common\Annotations\Annotation\Attributes;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;

/**
* Class Controller
* @package library\annotation\Mapping
* @Attributes({
* @Attribute("prefix", type="string"),
* })
* @Annotation
* @Target("CLASS")
*/
final class Controller
{
/**
* @Required()
* @var string
*/
private $prefix = '';
public function __construct(array $value)
{
if (isset($value['value'])) $this->prefix = $value['value'];
if (isset($value['prefix'])) $this->prefix = $value['prefix'];
}

public function getPrefix(){
return $this->prefix;
}
}

@Annotation表示这是个注解类,让IDE提示更加友好!

@Target表示这个注解类只能被类使用!

@Required表示这个属性是必须填写的!

@Attributes表示这个注解类有多少个属性!

创建library\Mapping\RequestMapping.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
/**
* @author gaobinzhan <gaobinzhan@gmail.com>
*/


namespace library\annotation\Mapping;


use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;

/**
* Class RequestMapping
* @package library\annotation\Mapping
* @Annotation
* @Attributes({
* @Attribute("route", type="string"),
* })
* @Target("METHOD")
*/
final class RequestMapping
{
/**
* @Required()
*/
private $route;
public function __construct(array $value)
{
if (isset($value['value'])) $this->route = $value['value'];
if (isset($value['route'])) $this->route = $value['route'];
}

public function getRoute(){
return $this->route;
}
}

这里的代码就不用再重复解释了吧!我偏不解释了!哈哈哈

创建app\Http\HomeController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
/**
* @author gaobinzhan <gaobinzhan@gmail.com>
*/


namespace app\Http;


use library\annotation\Mapping\Controller;
use library\annotation\Mapping\RequestMapping;

/**
* Class HomeController
* @package app\Http
* @Controller(prefix="/home")
*/
class HomeController
{
/**
* @RequestMapping(route="/test")
*/
public function test(){
echo 111;
}
}

这里代码没啥解释的。。。

哈哈,下面来测试我们的结果!

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php
/**
* @author gaobinzhan <gaobinzhan@gmail.com>
*/

$loader = require './vendor/autoload.php';

// 获取一个反射类
$refClass = new \ReflectionClass('\app\Http\HomeController');

// 注册load
\Doctrine\Common\Annotations\AnnotationRegistry::registerLoader([$loader, 'loadClass']);

// new 我们的注解读取器
$reader = new \Doctrine\Common\Annotations\AnnotationReader();

// 获取该类上的所有注解
$classAnnotations = $reader->getClassAnnotations($refClass);

// 这是个循环 说明$classAnnotations是个数组
foreach ($classAnnotations as $annotation){
// 因为我们定义了Controller注解类 要判断好啊
if ($annotation instanceof \library\annotation\Mapping\Controller){
// 获取我们的 prefix 这地方能看懂吧。。。
echo $routePrefix = $annotation->getPrefix().PHP_EOL;
// 获取类中所有方法
$refMethods = $refClass->getMethods();
}

// 进行循环
foreach ($refMethods as $method){
// 获取方法上的所有注解
$methodAnnotations = $reader->getMethodAnnotations($method);
// 循环
foreach ($methodAnnotations as $methodAnnotation){
if ($methodAnnotation instanceof \library\annotation\Mapping\RequestMapping){
// 输出我们的route
echo $methodAnnotation->getRoute().PHP_EOL;
}
}
}
}

执行结果:

1
2
/home
/test

之前我们不是建立了个Parser的目录嘛,可以在里面创建对应的解析类,然后去解析,把它们封装一下子!!

php yield关键字及协程实现

迭代器

迭代是指反复执行一个过程,每执行一次叫做迭代一次

php提供了统一的迭代器接口,之前文章我已经写过了。
通过实现Iterator接口,可以自行决定如何遍历。

生成器

相比迭代器,生成器提供了更容易的方法来简单实现对象的迭代,性能开销和复杂性大大降低。

一个生成器函数看起来更像一个普通的函数,不同的是普通函数返回的是一个值,而生成器可以yield生成许多个值。

生成器yield关键字不是返回值,而是返回Generator对象,不能被实力化,且继承了Iterator接口。

生成器优点:

  • 生成器会对php应用的性能有非常大的影响。

  • 代码运行时,节省大量内存。

  • 适合计算大量的数据。

颠覆常识的yield

大家都知道range函数创建一个包含指定范围的元素的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

$start = 1;
$end = 99999999999;

range($start,$end); // PHP Warning: range(): The supplied range exceeds the maximum array size: start=1 end=99999999999

function xrange($start, $end)
{
$result = [];
for ($i = $start; $i <= $end; $i++) {
$result[] = $i;
}
}
xrange(1, 99999999999);// Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 134217736 bytes)

以上代码,创建1-99999999999的数组,range报错,用for来创建,会内存溢出。

接下来看个好玩的!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

$start = 1;
$end = 99999999999;


function xrange($start, $end)
{
for ($i = $start; $i < $end; $i++) {
yield $i;
}
}

$result = xrange($start, $end);

echo $result->current(); // 输出 1

$result->next();

echo $result->current(); // 输出 2

却发现能够正常输出数值!

那下面这样呢???

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

$start = 1;
$end = 3;


function yrange($start, $end)
{
for ($i = $start; $i < $end; $i++) {
echo '输出1' . PHP_EOL;
yield $i;
echo '输出2' . PHP_EOL;
}
}

echo '输出这个对象!!!!' . PHP_EOL;
var_dump(yrange($start, $end));
echo '对象输出结束!!!!'.PHP_EOL.PHP_EOL;

echo '遍历一次开始!!!!'.PHP_EOL;
foreach (yrange($start, $end) as $value) {
echo '遍历的数据'.$value . PHP_EOL;
break; // 我们遍历一次 就停止循环
};
echo '遍历一次结束!!!!'.PHP_EOL.PHP_EOL;


echo '一直遍历开始!!!!'.PHP_EOL;
foreach (yrange($start, $end) as $value) {
echo '遍历的数据'.$value . PHP_EOL;
};
echo '一直遍历结束!!!!'.PHP_EOL.PHP_EOL;

来看下运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
输出这个对象!!!!
object(Generator)#1 (0) {
}
对象输出结束!!!!

遍历一次开始!!!!
输出1
遍历的数据1
遍历一次结束!!!!

一直遍历开始!!!!
输出1
遍历的数据1
输出2
输出1
遍历的数据2
输出2
一直遍历结束!!!!

是不是懵逼了!!

  • 调用函数返回,却发现for竟然没有执行。
  • 就遍历一次,发现只执行了echo '输出1' . PHP_EOL;,而且也没有循环3次。

yield就是这样,有yield的函数被称为生成器函数。

yield实现协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
function task1()
{
for ($i = 0; $i < 3; $i++) {
sleep(1); // 模拟耗时
echo "发短信{$i}\n";
}
}

function task2()
{
for ($i = 0; $i < 3; $i++) {
sleep(1); // 模拟阻塞
echo "发邮件{$i}\n";
}
}

task1();
task2();

以上代码可以看出,短信发完之后,才会发邮件,如果交替执行或者再添加任务应该怎么做呢。

多任务协作及调度器实现

为了实现我们的多任务调度,首先实现“任务”–一个用轻量级的包装的协程函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
class Task
{
protected $taskId; // 任务id
protected $coroutine; // 生成器
protected $sendValue = null; // 生成器send值
protected $beforeFirstYield = true; // 迭代的指针是否为第一个

public function __construct($taskId, Generator $coroutine)
{
$this->taskId = $taskId;
$this->coroutine = $coroutine;
}

public function getTaskId()
{
return $this->taskId;
}

public function setSendValue($sendValue)
{
$this->sendValue = $sendValue;
}

public function run()
{
if ($this->beforeFirstYield) {
$this->beforeFirstYield = false;
return $this->coroutine->current();
} else {
$retval = $this->coroutine->send($this->sendValue);
$this->sendValue = null;
return $retval;
}
}

public function isFinished()
{
return !$this->coroutine->valid();
}
}

如代码,一个任务就是用任务ID标记的一个协程(函数)。

使用setSendValue()方法,你可以指定哪些值将被发送到下次的恢复(在之后你会了解到我们需要这个)。

run()函数确实没有做什么,除了调用send()方法的协同程序, 要理解为什么添加了一个 beforeFirstYieldflag变量, 需要考虑下面的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
function gen() {
yield 'foo';
yield 'bar';
}
$gen = gen();
var_dump($gen->send('something'));
// 如之前提到的在send之前, 当$gen迭代器被创建的时候一个renwind()方法已经被隐式调用
// 所以实际上发生的应该类似:
//$gen->rewind();
//var_dump($gen->send('something'));
//这样renwind的执行将会导致第一个yield被执行, 并且忽略了他的返回值.
//真正当我们调用yield的时候, 我们得到的是第二个yield的值! 导致第一个yield的值被忽略.
//string(3) "bar"

通过添加 beforeFirstYield我们可以确定第一个yield的值能被正确返回。

调度器现在不得不比多任务循环要做稍微多点了,然后才运行多任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
class Scheduler {
protected $maxTaskId = 0;
protected $taskMap = []; // taskId => task
protected $taskQueue;
public function __construct() {
$this->taskQueue = new SplQueue();
}
public function newTask(Generator $coroutine) {
$tid = ++$this->maxTaskId;
$task = new Task($tid, $coroutine);
$this->taskMap[$tid] = $task;
$this->schedule($task);
return $tid;
}
public function schedule(Task $task) {
$this->taskQueue->enqueue($task);
}
public function run() {
while (!$this->taskQueue->isEmpty()) {
$task = $this->taskQueue->dequeue();
$task->run();
if ($task->isFinished()) {
unset($this->taskMap[$task->getTaskId()]);
} else {
$this->schedule($task);
}
}
}
}

newTask()方法(使用下一个空闲的任务id)创建一个新任务,然后把这个任务放入任务map数组里,接着它通过把任务放入任务队列里来实现对任务的调度,接着run()方法扫描任务队列,运行任务。

如果一个任务结束了, 那么它将从队列里删除,否则它将在队列的末尾再次被调度。

让我们看看下面具有两个简单任务的调度器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
function task1()
{
for ($i = 0; $i < 3; $i++) {
sleep(1); // 模拟耗时
yield;
echo "发短信{$i}\n";
}
}

function task2()
{
for ($i = 0; $i < 3; $i++) {
sleep(1); // 模拟耗时
yield;
echo "发邮件{$i}\n";
}
}

$scheduler = new Scheduler;
$scheduler->newTask(task1());
$scheduler->newTask(task2());
$scheduler->run();

两个任务都仅仅回显一条信息,然后使用yield把控制回传给调度器。输出结果如下:

1
2
3
4
5
6
发短信0
发邮件0
发短信1
发邮件1
发短信2
发邮件2

输出确实如我们所期望的:对前五个迭代来说,两个任务是交替运行的。

调度器通信

上面实现了协程封装,但是调度器和任务直接缺少了通信,进行重新封装,使协程当中能够获取当前的任务id,新增任务,以及杀死任务。

系统调用

先封装系统调用:

1
2
3
4
5
6
7
8
9
10
11
<?php
class SystemCall {
protected $callback;
public function __construct(callable $callback) {
$this->callback = $callback;
}
public function __invoke(Task $task, Scheduler $scheduler) {
$callback = $this->callback;
return $callback($task, $scheduler);
}
}

它和其他任何可调用的对象(使用_invoke)一样的运行, 不过它要求调度器把正在调用的任务和自身传递给这个函数。
为了解决这个问题我们不得不微微的修改调度器的run方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
public function run() {
while (!$this->taskQueue->isEmpty()) {
$task = $this->taskQueue->dequeue();
$retval = $task->run();
if ($retval instanceof SystemCall) {
$retval($task, $this);
continue;
}
if ($task->isFinished()) {
unset($this->taskMap[$task->getTaskId()]);
} else {
$this->schedule($task);
}
}
}

获取任务

编写获取任务id函数:

1
2
3
4
5
6
7
<?php
function getTaskId() {
return new SystemCall(function(Task $task, Scheduler $scheduler) {
$task->setSendValue($task->getTaskId());
$scheduler->schedule($task);
});
}

重新编写我们的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
function task1()
{
$tid = (yield getTaskId()); // <-- here's the syscall!
for ($i = 0; $i < 3; $i++) {
sleep(1); // 模拟耗时
yield;
echo "发短信{$i}\n";
}
}

function task2()
{
$tid = (yield getTaskId()); // <-- here's the syscall!
for ($i = 0; $i < 3; $i++) {
sleep(1); // 模拟耗时
yield;
echo "发邮件{$i}\n";
}
}

$scheduler = new Scheduler;
$scheduler->newTask(task1());
$scheduler->newTask(task2());
$scheduler->run();

这段代码将给出与前一个例子相同的输出。请注意系统调用如何同其他任何调用一样正常地运行,只不过预先增加了yield。

新增任务

编写新增任务函数:

1
2
3
4
5
6
7
8
9
<?php
function newTask(Generator $coroutine) {
return new SystemCall(
function(Task $task, Scheduler $scheduler) use ($coroutine) {
$task->setSendValue($scheduler->newTask($coroutine));
$scheduler->schedule($task);
}
);
}

杀死任务

编写杀死任务函数:

1
2
3
4
5
6
7
8
9
<?php
function killTask($tid) {
return new SystemCall(
function(Task $task, Scheduler $scheduler) use ($tid) {
$task->setSendValue($scheduler->killTask($tid));
$scheduler->schedule($task);
}
);
}

同样我们也需要往调度器里面,增加一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
public function killTask($tid) {
if (!isset($this->taskMap[$tid])) {
return false;
}
unset($this->taskMap[$tid]);
// This is a bit ugly and could be optimized so it does not have to walk the queue,
// but assuming that killing tasks is rather rare I won't bother with it now
foreach ($this->taskQueue as $i => $task) {
if ($task->getTaskId() === $tid) {
unset($this->taskQueue[$i]);
break;
}
}
echo "任务 $tid 被杀死\n";
return true;
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
function childTask()
{
$tid = (yield getTaskId());
while (true) {
echo "任务 $tid 执行\n";
yield;
}
}

function task1()
{
$tid = (yield getTaskId()); // <-- here's the syscall!
for ($i = 0; $i < 3; $i++) {
echo "发短信{$i}\n";
yield;
}
}

function task2()
{
$tid = (yield getTaskId()); // <-- here's the syscall!
$childTId = (yield newTask(childTask()));
for ($i = 0; $i < 3; $i++) {
echo "发邮件{$i}\n";
yield;
if ($i == 2) {
yield killTask($childTId);
}
}
}

$scheduler = new Scheduler;
$scheduler->newTask(task1());
$scheduler->newTask(task2());
$scheduler->run();

swoole实现协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
Swoole\Coroutine::create(function (){
$start = time();
$wait = new Swoole\Coroutine\WaitGroup();
function task1()
{
for ($i = 0; $i < 3; $i++) {
echo "发短信{$i}\n";
Swoole\Coroutine::sleep(1); // 协程切换
}
}

function task2()
{
for ($i = 0; $i < 3; $i++) {
echo "发邮件{$i}\n";
Swoole\Coroutine::sleep(1); // 协程切换
}
}
$wait->add();
Swoole\Coroutine::create('task1');
$wait->add();
Swoole\Coroutine::create('task2');
$wait->wait();
echo '耗时' . (time() - $start);
});

以上代码可看出,耗时为6s,但运行结果确实3s,这就体现了协程的好处。

下一篇文章会具体写出swoole协程的用法。

以上yield实现协程 部分内容来自于 https://www.laruence.com/2015/05/28/3038.html


php用select实现I/O复用

前言

在Linux Socket服务器短编程时,为了处理大量客户的连接请求,需要使用非阻塞I/O和复用,select、poll和epoll是Linux API提供的I/O复用方式,其实I/O多路复用就是通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。现在比较受欢迎的的nginx就是使用epoll来实现I/O复用支持高并发,所以理解好select,poll,epoll对于nginx如何应对高并发还是很有帮助的。

select调用过程

select

select缺点

  1. 单个进程监控的文件描述符有限,通常为1024*8个文件描述符。

    当然可以改进,由于select采用轮询方式扫描文件描述符。文件描述符数量越多,性能越差。

  2. 内核/用户数据拷贝频繁,操作复杂。

    select在调用之前,需要手动在应用程序里将要监控的文件描述符添加到fed_set集合中。然后加载到内核进行监控。用户为了检测时间是否发生,还需要在用户程序手动维护一个数组,存储监控文件描述符。当内核事件发生,在将fed_set集合中没有发生的文件描述符清空,然后拷贝到用户区,和数组中的文件描述符进行比对。再调用selecct也是如此。每次调用,都需要了来回拷贝。

  3. 轮回时间效率低

    select返回的是整个数组的句柄。应用程序需要遍历整个数组才知道谁发生了变化。轮询代价大。

  4. select是水平触发

    应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作。那么之后select调用还是会将这些文件描述符返回,通知进程。

代码实现

Worker.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?php
/**
* @author gaobinzhan <[email protected]>
*/

class Worker
{

private $socket;

public $onReceive;

public $onConnect;

public $onClose;

private $socketList = [];

private $events = [
'connect' => 'onConnect',
'receive' => 'onReceive',
'close' => 'onClose'
];

public function __construct($host, $port, $type)
{
$this->socket = stream_socket_server("{$type}://{$host}:{$port}");
stream_set_blocking($this->socket,0); // Set non blocking
$this->socketList[(int)$this->socket] = $this->socket;
return $this->socket;
}


private function accept()
{
while (true) {
$write = $except = [];
$read = $this->socketList;
stream_select($read,$write,$except,60);
foreach ($read as $socket) $socket === $this->socket ? $this->createSocket() : $this->receive($socket);
}
}

private function createSocket(){
//Establish a connection with the client
$client = stream_socket_accept($this->socket);
(!empty($client && is_callable($this->onConnect))) && call_user_func_array($this->onConnect, [$this->socket, $client]);
$this->socketList[(int)$client] = $client;
}

private function receive($client){

$buffer = fread($client, 65535);
if (empty($buffer) && (feof($client) || !is_resource($client))) { fclose($client); unset($this->socketList[(int)$client]); }
!empty($buffer) && is_callable($this->onReceive) && call_user_func_array($this->onReceive, [$this->socket, $client, $buffer]);


//because:IO Multiplexing
/*$close = fclose($v);
if (!empty($close) && is_callable($this->onClose)) call_user_func_array($this->onClose, [$v]);*/
}

public function send($client, $data)
{
$response = "HTTP/1.1 200 OK\r\n";
$response .= "Content-Type: text/html;charset=UTF-8\r\n";
$response .= "Connection: keep-alive\r\n";
$response .= "Content-length: ".strlen($data)."\r\n\r\n";
$response .= $data;
fwrite($client, $response);
}

public function on($event, $callback)
{
$event = $this->events[$event] ?? null;
$this->$event = $callback;
}

public function start()
{
$this->accept();
}
}

server.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
/**
* @author gaobinzhan <[email protected]>
*/

require 'Worker.php';


class Server{

public $server;

public function __construct($host, $port, $type)
{
$this->server = new Worker($host, $port, $type);
$this->server->on('connect',[$this,"onConnect"]);
$this->server->on('receive',[$this,"onReceive"]);
$this->server->on('close',[$this,"onClose"]);

$this->server->start();
}

public function onConnect($server,$fd){
echo 'onConnect '.$fd.PHP_EOL;
}

public function onReceive($server,$fd,$data){
echo 'onReceive '.$fd.PHP_EOL;
$this->server->send($fd,'this is server !!!');
}

public function onClose($fd){
echo 'onClose '.$fd.PHP_EOL;
}
}
new Server('0.0.0.0','9501','tcp');

cli模式下运行server.php

浏览器第一次访问http://127.0.0.1:9501

发现第一次连接符为7

第一次

第二访问 发现为7的连接符被复用了

第二次

可以用ab测试工具 更能体现出io复用

ab -c 100 -n 100000 -k http://127.0.0.1:9501/