首页
关于
Search
1
给你10个市场数据调研报告的免费下载网站!以后竞品数据就从这里找!
183 阅读
2
php接口优化 使用curl_multi_init批量请求
144 阅读
3
《从菜鸟到大师之路 ElasticSearch 篇》
107 阅读
4
2024年备考系统架构设计师
104 阅读
5
PHP 文件I/O
92 阅读
php
thinkphp
laravel
工具
开源
mysql
数据结构
总结
思维逻辑
令人感动的创富故事
读书笔记
前端
vue
js
css
书籍
开源之旅
架构
消息队列
docker
教程
代码片段
redis
服务器
nginx
linux
科普
java
c
ElasticSearch
测试
php进阶
php基础
登录
Search
标签搜索
php函数
php语法
性能优化
安全
错误和异常处理
问题
vue
Composer
Session
缓存
框架
Swoole
api
并发
异步
正则表达式
php-fpm
mysql 索引
开发规范
协程
dafenqi
累计撰写
786
篇文章
累计收到
31
条评论
首页
栏目
php
thinkphp
laravel
工具
开源
mysql
数据结构
总结
思维逻辑
令人感动的创富故事
读书笔记
前端
vue
js
css
书籍
开源之旅
架构
消息队列
docker
教程
代码片段
副业
redis
服务器
nginx
linux
科普
java
c
ElasticSearch
测试
php进阶
php基础
页面
关于
搜索到
560
篇与
的结果
2023-08-07
在PHP中使用协程实现多任务调度
在PHP中使用协程实现多任务调度PHP5.5一个比较好的新功能是加入了对迭代生成器和协程的支持.对于生成器,PHP的文档和各种其他的博客文章已经有了非常详细的讲解.协程相对受到的关注就少了,因为协程虽然有很强大的功能但相对比较复杂, 也比较难被理解,解释起来也比较困难.这篇文章将尝试通过介绍如何使用协程来实施任务调度, 来解释在PHP中的协程.我将在前三节做一个简单的背景介绍.如果你已经有了比较好的基础,可以直接跳到“协同多任务处理”一节.迭代生成器(迭代)生成器也是一个函数,不同的是这个函数的返回值是依次返回,而不是只返回一个单独的值.或者,换句话说,生成器使你能更方便的实现了迭代器接口.下面通过实现一个xrange函数来简单说明:<?phpfunction xrange($start, $end, $step = 1) {for ($i = $start; $i <= $end; $i += $step) { yield $i; }}foreach (xrange(1, 1000000) as $num) {echo $num, "\n";}上面这个xrange()函数提供了和PHP的内建函数range()一样的功能.但是不同的是range()函数返回的是一个包含值从1到100万0的数组(注:请查看手册). 而xrange()函数返回的是依次输出这些值的一个迭代器, 而不会真正以数组形式返回.这种方法的优点是显而易见的.它可以让你在处理大数据集合的时候不用一次性的加载到内存中.甚至你可以处理无限大的数据流.当然,也可以不同通过生成器来实现这个功能,而是可以通过继承Iterator接口实现.但通过使用生成器实现起来会更方便,不用再去实现iterator接口中的5个方法了.生成器为可中断的函数要从生成器认识协程, 理解它内部是如何工作是非常重要的: 生成器是一种可中断的函数, 在它里面的yield构成了中断点.还是看上面的例子, 调用xrange(1,1000000)的时候, xrange()函数里代码其实并没有真正地运行. 它只是返回了一个迭代器:<?php$range = xrange(1, 1000000);var_dump($range); // object(Generator)#1var_dump($range instanceof Iterator); // bool(true)?>这也解释了为什么xrange叫做迭代生成器, 因为它返回一个迭代器, 而这个迭代器实现了Iterator接口.调用迭代器的方法一次, 其中的代码运行一次.例如, 如果你调用$range->rewind(), 那么xrange()里的代码就会运行到控制流第一次出现yield的地方. 而函数内传递给yield语句的返回值可以通过$range->current()获取.为了继续执行生成器中yield后的代码, 你就需要调用$range->next()方法. 这将再次启动生成器, 直到下一次yield语句出现. 因此,连续调用next()和current()方法, 你就能从生成器里获得所有的值, 直到再没有yield语句出现.对xrange()来说, 这种情形出现在$i超过$end时. 在这中情况下, 控制流将到达函数的终点,因此将不执行任何代码.一旦这种情况发生,vaild()方法将返回假, 这时迭代结束.协程协程的支持是在迭代生成器的基础上, 增加了可以回送数据给生成器的功能(调用者发送数据给被调用的生成器函数). 这就把生成器到调用者的单向通信转变为两者之间的双向通信.传递数据的功能是通过迭代器的send()方法实现的. 下面的logger()协程是这种通信如何运行的例子:<?phpfunction logger($fileName) {$fileHandle = fopen($fileName, 'a'); while (true) { fwrite($fileHandle, yield . "\n"); }}$logger = logger(__DIR__ . '/log');$logger->send('Foo');$logger->send('Bar')?>正如你能看到,这儿yield没有作为一个语句来使用, 而是用作一个表达式, 即它能被演化成一个值. 这个值就是调用者传递给send()方法的值. 在这个例子里, yield表达式将首先被"Foo"替代写入Log, 然后被"Bar"替代写入Log.上面的例子里演示了yield作为接受者, 接下来我们看如何同时进行接收和发送的例子:<?phpfunction gen() {$ret = (yield 'yield1'); var_dump($ret); $ret = (yield 'yield2'); var_dump($ret);}$gen = gen();var_dump($gen->current()); // string(6) "yield1"var_dump($gen->send('ret1')); // string(4) "ret1" (the first var_dump in gen) // string(6) "yield2" (the var_dump of the ->send() return value)var_dump($gen->send('ret2')); // string(4) "ret2" (again from within gen) // NULL (the return value of ->send())?>要很快的理解输出的精确顺序可能稍微有点困难, 但你确定要搞清楚为什按照这种方式输出. 以便后续继续阅读.另外, 我要特别指出的有两点:第一点,yield表达式两边的括号在PHP7以前不是可选的, 也就是说在PHP5.5和PHP5.6中圆括号是必须的.第二点,你可能已经注意到调用current()之前没有调用rewind().这是因为生成迭代对象的时候已经隐含地执行了rewind操作.多任务协作如果阅读了上面的logger()例子, 你也许会疑惑“为了双向通信我为什么要使用协程呢?我完全可以使用其他非协程方法实现同样的功能啊?", 是的, 你是对的, 但上面的例子只是为了演示了基本用法, 这个例子其实并没有真正的展示出使用协程的优点.正如上面介绍里提到的,协程是非常强大的概念,不过却应用的很稀少而且常常十分复杂.要给出一些简单而真实的例子很难.在这篇文章里,我决定去做的是使用协程实现多任务协作.我们要解决的问题是你想并发地运行多任务(或者“程序”).不过我们都知道CPU在一个时刻只能运行一个任务(不考虑多核的情况).因此处理器需要在不同的任务之间进行切换,而且总是让每个任务运行 “一小会儿”.多任务协作这个术语中的“协作”很好的说明了如何进行这种切换的:它要求当前正在运行的任务自动把控制传回给调度器,这样就可以运行其他任务了. 这与“抢占”多任务相反, 抢占多任务是这样的:调度器可以中断运行了一段时间的任务, 不管它喜欢还是不喜欢. 协作多任务在Windows的早期版本(windows95)和Mac OS中有使用, 不过它们后来都切换到使用抢先多任务了. 理由相当明确:如果你依靠程序自动交出控制的话, 那么一些恶意的程序将很容易占用整个CPU, 不与其他任务共享.现在你应当明白协程和任务调度之间的关系:yield指令提供了任务中断自身的一种方法, 然后把控制交回给任务调度器. 因此协程可以运行多个其他任务. 更进一步来说, yield还可以用来在任务和调度器之间进行通信.为了实现我们的多任务调度, 首先实现“任务” -- 一个用轻量级的包装的协程函数:<?phpclass Task {protected $taskId; protected $coroutine; protected $sendValue = null; 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变量, 需要考虑下面的代码片段:<?phpfunction 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"通过添加 beforeFirstYieldcondition 我们可以确定第一个yield的值能被正确返回.调度器现在不得不比多任务循环要做稍微多点了, 然后才运行多任务:<?phpclass 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()方法扫描任务队列, 运行任务.如果一个任务结束了, 那么它将从队列里删除, 否则它将在队列的末尾再次被调度.让我们看看下面具有两个简单(没有什么意义)任务的调度器:<?phpfunction task1() {for ($i = 1; $i <= 10; ++$i) { echo "This is task 1 iteration $i.\n"; yield; }}function task2() {for ($i = 1; $i <= 5; ++$i) { echo "This is task 2 iteration $i.\n"; yield; }}$scheduler = new Scheduler;$scheduler->newTask(task1());$scheduler->newTask(task2());$scheduler->run();两个任务都仅仅回显一条信息,然后使用yield把控制回传给调度器.输出结果如下:This is task 1 iteration 1.This is task 2 iteration 1.This is task 1 iteration 2.This is task 2 iteration 2.This is task 1 iteration 3.This is task 2 iteration 3.This is task 1 iteration 4.This is task 2 iteration 4.This is task 1 iteration 5.This is task 2 iteration 5.This is task 1 iteration 6.This is task 1 iteration 7.This is task 1 iteration 8.This is task 1 iteration 9.This is task 1 iteration 10.输出确实如我们所期望的:对前五个迭代来说,两个任务是交替运行的, 而在第二个任务结束后, 只有第一个任务继续运行.与调度器之间通信既然调度器已经运行了, 那么我们来看下一个问题:任务和调度器之间的通信.我们将使用进程用来和操作系统会话的同样的方式来通信:系统调用.我们需要系统调用的理由是操作系统与进程相比它处在不同的权限级别上. 因此为了执行特权级别的操作(如杀死另一个进程), 就不得不以某种方式把控制传回给内核, 这样内核就可以执行所说的操作了. 再说一遍, 这种行为在内部是通过使用中断指令来实现的. 过去使用的是通用的int指令, 如今使用的是更特殊并且更快速的syscall/sysenter指令.我们的任务调度系统将反映这种设计:不是简单地把调度器传递给任务(这样就允许它做它想做的任何事), 我们将通过给yield表达式传递信息来与系统调用通信. 这儿yield即是中断, 也是传递信息给调度器(和从调度器传递出信息)的方法.为了说明系统调用, 我们对可调用的系统调用做一个小小的封装:<?phpclass 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方法:<?phppublic 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外什么都没有做:<?phpfunction getTaskId() {return new SystemCall(function(Task $task, Scheduler $scheduler) { $task->setSendValue($task->getTaskId()); $scheduler->schedule($task); });}这个函数设置任务id为下一次发送的值, 并再次调度了这个任务 .由于使用了系统调用, 所以调度器不能自动调用任务, 我们需要手工调度任务(稍后你将明白为什么这么做). 要使用这个新的系统调用的话, 我们要重新编写以前的例子:<?phpfunction task($max) {$tid = (yield getTaskId()); // <-- here's the syscall! for ($i = 1; $i <= $max; ++$i) { echo "This is task $tid iteration $i.\n"; yield; }}$scheduler = new Scheduler;$scheduler->newTask(task(10));$scheduler->newTask(task(5));$scheduler->run();?>这段代码将给出与前一个例子相同的输出. 请注意系统调用如何同其他任何调用一样正常地运行, 只不过预先增加了yield.要创建新的任务, 然后再杀死它们的话, 需要两个以上的系统调用:<?phpfunction newTask(Generator $coroutine) {return new SystemCall( function(Task $task, Scheduler $scheduler) use ($coroutine) { $task->setSendValue($scheduler->newTask($coroutine)); $scheduler->schedule($task); } );}function killTask($tid) {return new SystemCall( function(Task $task, Scheduler $scheduler) use ($tid) { $task->setSendValue($scheduler->killTask($tid)); $scheduler->schedule($task); } );}killTask函数需要在调度器里增加一个方法:<?phppublic 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; } } return true;}用来测试新功能的微脚本:<?phpfunction childTask() {$tid = (yield getTaskId()); while (true) { echo "Child task $tid still alive!\n"; yield; }}function task() {$tid = (yield getTaskId()); $childTid = (yield newTask(childTask())); for ($i = 1; $i <= 6; ++$i) { echo "Parent task $tid iteration $i.\n"; yield; if ($i == 3) yield killTask($childTid); }}$scheduler = new Scheduler;$scheduler->newTask(task());$scheduler->run();?>这段代码将打印以下信息:Parent task 1 iteration 1.Child task 2 still alive!Parent task 1 iteration 2.Child task 2 still alive!Parent task 1 iteration 3.Child task 2 still alive!Parent task 1 iteration 4.Parent task 1 iteration 5.Parent task 1 iteration 6.经过三次迭代以后子任务将被杀死, 因此这就是"Child is still alive"消息结束的时候. 不过你要明白这还不是真正的父子关系. 因为在父任务结束后子任务仍然可以运行, 子任务甚至可以杀死父任务. 可以修改调度器使它具有更层级化的任务结构, 不过这个不是我们这个文章要继续讨论的范围了.现在你可以实现许多进程管理调用. 例如 wait(它一直等待到任务结束运行时), exec(它替代当前任务)和fork(它创建一个当前任务的克隆). fork非常酷,而 且你可以使用PHP的协程真正地实现它, 因为它们都支持克隆.让我们把这些留给有兴趣的读者吧,我们来看下一个议题.非阻塞IO很明显, 我们的任务管理系统的真正很酷的应用应该是web服务器. 它有一个任务是在套接字上侦听是否有新连接, 当有新连接要建立的时候, 它创建一个新任务来处理新连接.Web服务器最难的部分通常是像读数据这样的套接字操作是阻塞的. 例如PHP将等待到客户端完成发送为止. 对一个Web服务器来说, 这有点不太高效. 因为服务器在一个时间点上只能处理一个连接.解决方案是确保在真正对套接字读写之前该套接字已经“准备就绪”. 为了查找哪个套接字已经准备好读或者写了, 可以使用 流选择函数.首先,让我们添加两个新的 syscall, 它们将等待直到指定socket 准备好:<?phpfunction waitForRead($socket) {return new SystemCall( function(Task $task, Scheduler $scheduler) use ($socket) { $scheduler->waitForRead($socket, $task); } );}function waitForWrite($socket) {return new SystemCall( function(Task $task, Scheduler $scheduler) use ($socket) { $scheduler->waitForWrite($socket, $task); } );}这些 syscall 只是在调度器中代理其各自的方法:<?php// resourceID => [socket, tasks]protected $waitingForRead = [];protected $waitingForWrite = [];public function waitForRead($socket, Task $task) {if (isset($this->waitingForRead[(int) $socket])) { $this->waitingForRead[(int) $socket][1][] = $task; } else { $this->waitingForRead[(int) $socket] = [$socket, [$task]]; }}public function waitForWrite($socket, Task $task) {if (isset($this->waitingForWrite[(int) $socket])) { $this->waitingForWrite[(int) $socket][1][] = $task; } else { $this->waitingForWrite[(int) $socket] = [$socket, [$task]]; }}waitingForRead 及 waitingForWrite 属性是两个承载等待的socket 及等待它们的任务的数组. 有趣的部分在于下面的方法,它将检查 socket 是否可用, 并重新安排各自任务:<?phpprotected function ioPoll($timeout) {$rSocks = []; foreach ($this->waitingForRead as list($socket)) { $rSocks[] = $socket; } $wSocks = []; foreach ($this->waitingForWrite as list($socket)) { $wSocks[] = $socket; } $eSocks = []; // dummy if (!stream_select($rSocks, $wSocks, $eSocks, $timeout)) { return; } foreach ($rSocks as $socket) { list(, $tasks) = $this->waitingForRead[(int) $socket]; unset($this->waitingForRead[(int) $socket]); foreach ($tasks as $task) { $this->schedule($task); } } foreach ($wSocks as $socket) { list(, $tasks) = $this->waitingForWrite[(int) $socket]; unset($this->waitingForWrite[(int) $socket]); foreach ($tasks as $task) { $this->schedule($task); } }}stream_select 函数接受承载读取、写入以及待检查的socket的数组(我们无需考虑最后一类). 数组将按引用传递, 函数只会保留那些状态改变了的数组元素. 我们可以遍历这些数组, 并重新安排与之相关的任务.为了正常地执行上面的轮询动作, 我们将在调度器里增加一个特殊的任务:<?phpprotected function ioPollTask() {while (true) { if ($this->taskQueue->isEmpty()) { $this->ioPoll(null); } else { $this->ioPoll(0); } yield; }}?>需要在某个地方注册这个任务, 例如, 你可以在run()方法的开始增加$this->newTask($this->ioPollTask()). 然后就像其他任务一样每执行完整任务循环一次就执行轮询操作一次(这么做一定不是最好的方法), ioPollTask将使用0秒的超时来调用ioPoll, 也就是stream_select将立即返回(而不是等待).只有任务队列为空时,我们才使用null超时,这意味着它一直等到某个套接口准备就绪.如果我们没有这么做,那么轮询任务将一而再, 再而三的循环运行, 直到有新的连接建立. 这将导致100%的CPU利用率. 相反, 让操作系统做这种等待会更有效.现在编写服务器就相对容易了:<?phpfunction server($port) {echo "Starting server at port $port...\n"; $socket = @stream_socket_server("tcp://localhost:$port", $errNo, $errStr); if (!$socket) throw new Exception($errStr, $errNo); stream_set_blocking($socket, 0); while (true) { yield waitForRead($socket); $clientSocket = stream_socket_accept($socket, 0); yield newTask(handleClient($clientSocket)); }}function handleClient($socket) {yield waitForRead($socket); $data = fread($socket, 8192); $msg = "Received following request:\n\n$data"; $msgLength = strlen($msg); $response = <<<resHTTP/1.1 200 OK\rContent-Type: text/plain\rContent-Length: $msgLength\rConnection: close\r\r$msgRES;yield waitForWrite($socket); fwrite($socket, $response); fclose($socket);}$scheduler = new Scheduler;$scheduler->newTask(server(8000));$scheduler->run();这段代码实现了接收localhost:8000上的连接, 然后返回发送来的内容作为HTTP响应. 当然它还能处理真正的复杂HTTP请求, 上面的代码片段只是演示了一般性的概念.你可以使用类似于ab -n 10000 -c 100 localhost:8000/这样命令来测试服务器. 这条命令将向服务器发送10000个请求, 并且其中100个请求将同时到达. 使用这样的数目, 我得到了处于中间的10毫秒的响应时间. 不过还有一个问题:有少数几个请求真正处理的很慢(如5秒), 这就是为什么总吞吐量只有2000请求/秒(如果是10毫秒的响应时间的话, 总的吞吐量应该更像是10000请求/秒)协程堆栈如果你试图用我们的调度系统建立更大的系统的话, 你将很快遇到问题:我们习惯了把代码分解为更小的函数, 然后调用它们. 然而, 如果使用了协程的话, 就不能这么做了. 例如,看下面代码:<?phpfunction echoTimes($msg, $max) {for ($i = 1; $i <= $max; ++$i) { echo "$msg iteration $i\n"; yield; }}function task() {echoTimes('foo', 10); // print foo ten times echo "---\n"; echoTimes('bar', 5); // print bar five times yield; // force it to be a coroutine}$scheduler = new Scheduler;$scheduler->newTask(task());$scheduler->run();这段代码试图把重复循环“输出n次“的代码嵌入到一个独立的协程里,然后从主任务里调用它. 然而它无法运行. 正如在这篇文章的开始所提到的, 调用生成器(或者协程)将没有真正地做任何事情, 它仅仅返回一个对象.这 也出现在上面的例子里:echoTimes调用除了放回一个(无用的)协程对象外不做任何事情.为了仍然允许这么做,我们需要在这个裸协程上写一个小小的封装.我们将调用它:“协程堆栈”. 因为它将管理嵌套的协程调用堆栈. 这将是通过生成协程来调用子协程成为可能:$retval = (yield someCoroutine($foo, $bar));使用yield,子协程也能再次返回值:yield retval("I'm a return value!");retval函数除了返回一个值的封装外没有做任何其他事情.这个封装将表示它是一个返回值.<?phpclass CoroutineReturnValue {protected $value; public function __construct($value) { $this->value = $value; } public function getValue() { return $this->value; }}function retval($value) {return new CoroutineReturnValue($value);}为了把协程转变为协程堆栈(它支持子调用),我们将不得不编写另外一个函数(很明显,它是另一个协程):<?phpfunction stackedCoroutine(Generator $gen) {$stack = new SplStack; for (;;) { $value = $gen->current(); if ($value instanceof Generator) { $stack->push($gen); $gen = $value; continue; } $isReturnValue = $value instanceof CoroutineReturnValue; if (!$gen->valid() || $isReturnValue) { if ($stack->isEmpty()) { return; } $gen = $stack->pop(); $gen->send($isReturnValue ? $value->getValue() : NULL); continue; } $gen->send(yield $gen->key() => $value); }}这个函数在调用者和当前正在运行的子协程之间扮演着简单代理的角色.在$gen->send(yield $gen->key()=>$value);这行完成了代理功能.另外它检查返回值是否是生成器,万一是生成器的话,它将开始运行这个生成器,并把前一个协程压入堆栈里.一旦它获得了CoroutineReturnValue的话,它将再次请求堆栈弹出,然后继续执行前一个协程.为了使协程堆栈在任务里可用,任务构造器里的$this-coroutine =$coroutine;这行需要替代为$this->coroutine = StackedCoroutine($coroutine);.现在我们可以稍微改进上面web服务器例子:把wait+read(和wait+write和warit+accept)这样的动作分组为函数.为了分组相关的 功能,我将使用下面类:<?phpclass CoSocket {protected $socket; public function __construct($socket) { $this->socket = $socket; } public function accept() { yield waitForRead($this->socket); yield retval(new CoSocket(stream_socket_accept($this->socket, 0))); } public function read($size) { yield waitForRead($this->socket); yield retval(fread($this->socket, $size)); } public function write($string) { yield waitForWrite($this->socket); fwrite($this->socket, $string); } public function close() { @fclose($this->socket); }}现在服务器可以编写的稍微简洁点了:<?phpfunction server($port) {echo "Starting server at port $port...\n"; $socket = @stream_socket_server("tcp://localhost:$port", $errNo, $errStr); if (!$socket) throw new Exception($errStr, $errNo); stream_set_blocking($socket, 0); $socket = new CoSocket($socket); while (true) { yield newTask( handleClient(yield $socket->accept()) ); }}function handleClient($socket) {$data = (yield $socket->read(8192)); $msg = "Received following request:\n\n$data"; $msgLength = strlen($msg); $response = <<<resHTTP/1.1 200 OK\rContent-Type: text/plain\rContent-Length: $msgLength\rConnection: close\r\r$msgRES;yield $socket->write($response); yield $socket->close();}错误处理作为一个优秀的程序员, 相信你已经察觉到上面的例子缺少错误处理. 几乎所有的 socket 都是易出错的. 我没有这样做的原因一方面固然是因为错误处理的乏味(特别是 socket), 另一方面也在于它很容易使代码体积膨胀.不过, 我仍然想讲下常见的协程错误处理:协程允许使用 throw() 方法在其内部抛出一个错误.throw() 方法接受一个 Exception, 并将其抛出到协程的当前悬挂点, 看看下面代码:<?phpfunction gen() {echo "Foo\n"; try { yield; } catch (Exception $e) { echo "Exception: {$e->getMessage()}\n"; } echo "Bar\n";}$gen = gen();$gen->rewind(); // echos "Foo"$gen->throw(new Exception('Test')); // echos "Exception: Test" // and "Bar"这非常好, 有没有? 因为我们现在可以使用系统调用以及子协程调用异常抛出了.不过我们要对系统调用Scheduler::run() 方法做一些小调整:<?phpif ($retval instanceof SystemCall) {try { $retval($task, $this); } catch (Exception $e) { $task->setException($e); $this->schedule($task); } continue;}Task 类也要添加 throw 调用处理:<?phpclass Task {// ... protected $exception = null; public function setException($exception) { $this->exception = $exception; } public function run() { if ($this->beforeFirstYield) { $this->beforeFirstYield = false; return $this->coroutine->current(); } elseif ($this->exception) { $retval = $this->coroutine->throw($this->exception); $this->exception = null; return $retval; } else { $retval = $this->coroutine->send($this->sendValue); $this->sendValue = null; return $retval; } } // ...}现在, 我们已经可以在系统调用中使用异常抛出了!例如,要调用 killTask,让我们在传递 ID 不可用时抛出一个异常:<?phpfunction killTask($tid) {return new SystemCall( function(Task $task, Scheduler $scheduler) use ($tid) { if ($scheduler->killTask($tid)) { $scheduler->schedule($task); } else { throw new InvalidArgumentException('Invalid task ID!'); } } );}试试看:<?phpfunction task() {try { yield killTask(500); } catch (Exception $e) { echo 'Tried to kill task 500 but failed: ', $e->getMessage(), "\n"; }}这些代码现在尚不能正常运作,因为 stackedCoroutine 函数无法正确处理异常.要修复需要做些调整:<?phpfunction stackedCoroutine(Generator $gen) {$stack = new SplStack; $exception = null; for (;;) { try { if ($exception) { $gen->throw($exception); $exception = null; continue; } $value = $gen->current(); if ($value instanceof Generator) { $stack->push($gen); $gen = $value; continue; } $isReturnValue = $value instanceof CoroutineReturnValue; if (!$gen->valid() || $isReturnValue) { if ($stack->isEmpty()) { return; } $gen = $stack->pop(); $gen->send($isReturnValue ? $value->getValue() : NULL); continue; } try { $sendValue = (yield $gen->key() => $value); } catch (Exception $e) { $gen->throw($e); continue; } $gen->send($sendValue); } catch (Exception $e) { if ($stack->isEmpty()) { throw $e; } $gen = $stack->pop(); $exception = $e; } }}结束语在这篇文章里,我使用多任务协作构建了一个任务调度器, 其中包括执行“系统调用”, 做非阻塞操作和处理错误. 所有这些里真正很酷的事情是任务的结果代码看起来完全同步, 甚至任务正在执行大量的异步操作的时候也是这样.如果你打算从套接口读取数据的话, 你将不需要传递某个回调函数或者注册一个事件侦听器. 相反, 你只要书写yield $socket->read(). 这儿大部分都是你常常也要编写的,只 在它的前面增加yield.当我第一次听到协程的时候, 我发现这个概念完全令人折服, 正是因为这个激励我在PHP中实现了它. 同时我发现协程真正非常的令人惊叹:在令人敬畏的代码和一大堆乱代码之间只有一线之隔, 我认为协程恰好处在这条线上, 不多不少. 不过, 要说使用上面所述的方法书写异步代码是否真的有益, 这个就见仁见智了.但, 不管咋样, 我认为这是一个有趣的话题, 而且我希望你也能找到它的乐趣. 欢迎评论:)
2023年08月07日
20 阅读
0 评论
0 点赞
2023-08-07
php deamon 后台服务(守护进程)
php http server守护进程(daemon)
2023年08月07日
10 阅读
0 评论
0 点赞
2023-08-07
PHP Socket 深度探索
PHP Socket 深度探索Socket(套接字)一直是网络层的底层核心内容,也是 TCP/IP 以及 UDP 底层协议的实现通道。随着互联网信息时代的爆炸式发展,当代服务器的性能问题面临越来越大的挑战,著名的 C10K 问题(http://www.kegel.com/c10k.html)也随之出现。幸亏通过大牛们的不懈努力,区别于传统的 select/poll 的 epoll/kqueue 方式出现了,目前 linux2.6 以上的内核都普遍支持,这是 Socket 领域一项巨大的进步,不仅解决了 C10K 问题,也渐渐成为了当代互联网的底层核心技术。libevent 库就是其中一个比较出彩的项目(现在非常多的开源项目都有用到,包括 Memcached),感兴趣的朋友可以研究一下。由于网络上系统介绍这个部分的文章并不多,而涉及 PHP 的就更少了,所以石头君在这里希望通过《Socket深度探究4PHP》这个系列给对这个领域感兴趣的读者们一定的帮助,也希望大家能和我一起对这个问题进行更深入的探讨。首先,解释一下目前 Socket 领域比较易于混淆的概念有:阻塞/非阻塞、同步/异步、多路复用等。1、阻塞/非阻塞:这两个概念是针对 IO 过程中进程的状态来说的,阻塞 IO 是指调用结果返回之前,当前线程会被挂起;相反,非阻塞指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。2、同步/异步:这两个概念是针对调用如果返回结果来说的,所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回;相反,当一个异步过程调用发出后,调用者不能立刻得到结果,实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。3、多路复用(IO/Multiplexing):为了提高数据信息在网络通信线路中传输的效率,在一条物理通信线路上建立多条逻辑通信信道,同时传输若干路信号的技术就叫做多路复用技术。对于 Socket 来说,应该说能同时处理多个连接的模型都应该被称为多路复用,目前比较常用的有 select/poll/epoll/kqueue 这些 IO 模型(目前也有像 Apache 这种每个连接用单独的进程/线程来处理的 IO 模型,但是效率相对比较差,也很容易出问题,所以暂时不做介绍了)。在这些多路复用的模式中,异步阻塞/非阻塞模式的扩展性和性能最好。感觉概念很抽象对吧,“一切答案在于现场”,下面让我们从三种经典的 PHP Socket IO 模型实例来对以上的概念再做一次分析:1、使用 accept 阻塞的古老模型:属于同步阻塞 IO 模型,代码如下:socket_server.php<?php/**SocketServer ClassBy James.Huang <shagoo#gmail.com>**/set_time_limit(0);class SocketServer { private static $socket; function SocketServer($port) {global $errno, $errstr; if ($port < 1024) { die("Port must be a number which bigger than 1024/n"); } $socket = stream_socket_server("tcp://0.0.0.0:{$port}", $errno, $errstr); if (!$socket) die("$errstr ($errno)"); // stream_set_timeout($socket, -1); // 保证服务端 socket 不会超时,似乎没用:) while ($conn = stream_socket_accept($socket, -1)) { // 这样设置不超时才油用 static $id = 0; static $ct = 0; $ct_last = $ct; $ct_data = ''; $buffer = ''; $id++; // increase on each accept echo "Client $id come./n"; while (!preg_match('//r?/n/', $buffer)) { // 没有读到结束符,继续读// if (feof($conn)) break; // 防止 popen 和 fread 的 bug 导致的死循环 $buffer = fread($conn, 1024); echo 'R'; // 打印读的次数 $ct += strlen($buffer); $ct_data .= preg_replace('//r?/n/', '', $buffer); } $ct_size = ($ct - $ct_last) * 8; echo "[$id] " . __METHOD__ . " > " . $ct_data . "/n"; fwrite($conn, "Received $ct_size byte data./r/n"); fclose($conn); } fclose($socket);}}new SocketServer(2000);socket_client.php<?php/**Socket Test ClientBy James.Huang <shagoo#gmail.com>**/function debug ($msg){// echo $msg; error_log($msg, 3, '/tmp/socket.log');}if ($argv[1]) {$socket_client = stream_socket_client('tcp://0.0.0.0:2000', $errno, $errstr, 30);// stream_set_blocking($socket_client, 0);// stream_set_timeout($socket_client, 0, 100000);if (!$socket_client) {die("$errstr ($errno)");} else {$msg = trim($argv[1]); for ($i = 0; $i < 10; $i++) { $res = fwrite($socket_client, "$msg($i)"); usleep(100000); echo 'W'; // 打印写的次数// debug(fread($socket_client, 1024)); // 将产生死锁,因为 fread 在阻塞模式下未读到数据时将等待} fwrite($socket_client, "/r/n"); // 传输结束符 debug(fread($socket_client, 1024)); fclose($socket_client);}}else {// $phArr = array();// for ($i = 0; $i < 10; $i++) {// $phArr[$i] = popen("php ".__FILE__." '{$i}:test'", 'r');// }// foreach ($phArr as $ph) {// pclose($ph);// }for ($i = 0; $i < 10; $i++) {system("php ".__FILE__." '{$i}:test'");}}首先,解释一下以上的代码逻辑:客户端 socket_client.php 循环发送数据,最后发送结束符;服务端 socket_server.php 使用 accept 阻塞方式接收 socket 连接,然后循环接收数据,直到收到结束符,返回结果数据(接收到的字节数)。虽然逻辑很简单,但是其中有几种情况很值得分析一下:A> 默认情况下,运行 php socket_client.php test,客户端打出 10 个 W,服务端打出若干个 R 后面是接收到的数据,/tmp/socket.log 记录下服务端返回的接收结果数据。这种情况很容易理解,不再赘述。然后,使用 telnet 命令同时打开多个客户端,你会发现服务器一个时间只处理一个客户端,其他需要在后面“排队”;这就是阻塞 IO 的特点,这种模式的弱点很明显,效率极低。B> 只打开 socket_client.php 第 26 行的注释代码,再次运行 php socket_client.php test 客户端打出一个 W,服务端也打出一个 R,之后两个程序都卡住了。这是为什么呢,分析逻辑后你会发现,这是由于客户端在未发送结束符之前就向服务端要返回数据;而服务端由于未收到结束符,也在向客户端要结束符,造成死锁。而之所以只打出一个 W 和 R,是因为 fread 默认是阻塞的。要解决这个死锁,必须打开 socket_client.php 第 16 行的注释代码,给 socket 设置一个 0.1 秒的超时,再次运行你会发现隔 0.1 秒出现一个 W 和 R 之后正常结束,服务端返回的接收结果数据也正常记录了。可见 fread 缺省是阻塞的,我们在编程的时候要特别注意,如果没有设置超时,就很容易会出现死锁。C> 只打开 15 行注释,运行 php socket_client.php test,结果基本和情况 A 相同,唯一不同的是 /tmp/socket.log 没有记录下返回数据。这里可以看出客户端运行在阻塞和非阻塞模式的区别,当然在客户端不在乎接受结果的情况下,可以使用非阻塞模式来获得最大效率。D> 运行 php socket_client.php 是连续运行 10 次上面的逻辑,这个没什么问题;但是很奇怪的是如果你使用 35 - 41 行的代码,用 popen 同时开启 10 个进程来运行,就会造成服务器端的死循环,十分怪异!后来经调查发现只要是用 popen 打开的进程创建的连接会导致 fread 或者 socket_read 出错直接返回空字串,从而导致死循环,查阅 PHP 源代码后发现 PHP 的 popen 和 fread 函数已经完全不是 C 原生的了,里面都插入了大量的 php_stream_* 实现逻辑,初步估计是其中的某个 bug 导致的 Socket 连接中断所导致的,解决方法就是打开 socket_server.php 中 31 行的代码,如果连接中断则跳出循环,但是这样一来就会有很多数据丢失了,这个问题需要特别注意!2、使用 select/poll 的同步模型:属于同步非阻塞 IO 模型,代码如下:select_server.php<?php/**SelectSocketServer ClassBy James.Huang <shagoo#gmail.com>**/set_time_limit(0);class SelectSocketServer { private static $socket; private static $timeout = 60; private static $maxconns = 1024; private static $connections = array(); function SelectSocketServer($port) {global $errno, $errstr; if ($port < 1024) { die("Port must be a number which bigger than 1024/n"); } $socket = socket_create_listen($port); if (!$socket) die("Listen $port failed"); socket_set_nonblock($socket); // 非阻塞 while (true) { $readfds = array_merge(self::$connections, array($socket)); $writefds = array(); // 选择一个连接,获取读、写连接通道 if (socket_select($readfds, $writefds, $e = null, $t = self::$timeout)) { // 如果是当前服务端的监听连接 if (in_array($socket, $readfds)) { // 接受客户端连接 $newconn = socket_accept($socket); $i = (int) $newconn; $reject = ''; if (count(self::$connections) >= self::$maxconns) { $reject = "Server full, Try again later./n"; } // 将当前客户端连接放入 socket_select 选择 self::$connections[$i] = $newconn; // 输入的连接资源缓存容器 $writefds[$i] = $newconn; // 连接不正常 if ($reject) { socket_write($writefds[$i], $reject); unset($writefds[$i]); self::close($i); } else { echo "Client $i come./n"; } // remove the listening socket from the clients-with-data array $key = array_search($socket, $readfds); unset($readfds[$key]); } // 轮循读通道 foreach ($readfds as $rfd) { // 客户端连接 $i = (int) $rfd; // 从通道读取 $line = @socket_read($rfd, 2048, PHP_NORMAL_READ); if ($line === false) { // 读取不到内容,结束连接 echo "Connection closed on socket $i./n"; self::close($i); continue; } $tmp = substr($line, -1); if ($tmp != "/r" && $tmp != "/n") { // 等待更多数据 continue; } // 处理逻辑 $line = trim($line); if ($line == "quit") { echo "Client $i quit./n"; self::close($i); break; } if ($line) { echo "Client $i >>" . $line . "/n"; } } // 轮循写通道 foreach ($writefds as $wfd) { $i = (int) $wfd; $w = socket_write($wfd, "Welcome Client $i!/n"); } } }}function close ($i) {socket_shutdown(self::$connections[$i]); socket_close(self::$connections[$i]); unset(self::$connections[$i]);}}new SelectSocketServer(2000);select_client.php<?php/**SelectSocket Test ClientBy James.Huang <shagoo#gmail.com>**/function debug ($msg){// echo $msg; error_log($msg, 3, '/tmp/socket.log');}if ($argv[1]) {$socket_client = stream_socket_client('tcp://0.0.0.0:2000', $errno, $errstr, 30);// stream_set_timeout($socket_client, 0, 100000);if (!$socket_client) {die("$errstr ($errno)");} else {$msg = trim($argv[1]); for ($i = 0; $i < 10; $i++) { $res = fwrite($socket_client, "$msg($i)/n"); usleep(100000);// debug(fread($socket_client, 1024)); // 将产生死锁,因为 fread 在阻塞模式下未读到数据时将等待} fwrite($socket_client, "quit/n"); // add end token debug(fread($socket_client, 1024)); fclose($socket_client);}}else {$phArr = array(); for ($i = 0; $i < 10; $i++) {$phArr[$i] = popen("php ".__FILE__." '{$i}:test'", 'r');} foreach ($phArr as $ph) {pclose($ph);}// for ($i = 0; $i < 10; $i++) {// system("php ".__FILE__." '{$i}:test'");// }}以上代码的逻辑也很简单,select_server.php 实现了一个类似聊天室的功能,你可以使用 telnet 工具登录上去,和其他用户文字聊天,也可以键入“quit”命令离开;而 select_client.php 则模拟了一个登录用户连续发 10 条信息,然后退出。这里也分析两个问题:A> 这里如果我们执行 php select_client.php 程序将会同时打开 10 个连接,同时进行模拟登录用户操作;观察服务端打印的数据你会发现服务端确实是在同时处理这些连接,这就是多路复用实现的非阻塞 IO 模型,当然这个模型并没有真正的实现异步,因为最终服务端程序还是要去通道里面读取数据,得到结果后同步返回给客户端。如果这次你也使用 telnet 命令同时打开多个客户端,你会发现服务端可以同时处理这些连接,这就是非阻塞 IO,当然比古老的阻塞 IO 效率要高多了,但是这种模式还是有局限的,继续看下去你就会发现了~B> 我在 select_server.php 中设置了几个参数,大家可以调整试试:$timeout :表示的是 select 的超时时间,这个一般来说不要太短,否则会导致 CPU 负载过高。$maxconns :表示的是最大连接数,客户端超过这个数的话,服务器会拒绝接收。这里要提到的一点是,由于 select 是通过句柄来读写的,所以会受到系统默认参数 __FD_SETSIZE 的限制,一般默认值为 1024,修改的话需要重新编译内核;另外通过测试发现 select 模式的性能会随着连接数的增大而线性便差(详情见《Socket深度探究4PHP(二)》),这也就是 select 模式最大的问题所在,所以如果是超高并发服务器建议使用下一种模式。3、使用 epoll/kqueue 的异步模型:属于异步阻塞/非阻塞 IO 模型,代码如下:epoll_server.php<?php/**EpollSocketServer Class (use libevent)By James.Huang <shagoo#gmail.com>Defined constants:EV_TIMEOUT (integer)EV_READ (integer)EV_WRITE (integer)EV_SIGNAL (integer)EV_PERSIST (integer)EVLOOP_NONBLOCK (integer)EVLOOP_ONCE (integer)**/set_time_limit(0);class EpollSocketServer{ private static $socket; private static $connections; private static $buffers;function EpollSocketServer ($port) {global $errno, $errstr; if (!extension_loaded('libevent')) { die("Please install libevent extension firstly/n"); } if ($port < 1024) { die("Port must be a number which bigger than 1024/n"); } $socket_server = stream_socket_server("tcp://0.0.0.0:{$port}", $errno, $errstr); if (!$socket_server) die("$errstr ($errno)"); stream_set_blocking($socket_server, 0); // 非阻塞 $base = event_base_new(); $event = event_new(); event_set($event, $socket_server, EV_READ | EV_PERSIST, array(__CLASS__, 'ev_accept'), $base); event_base_set($event, $base); event_add($event); event_base_loop($base); self::$connections = array(); self::$buffers = array();}function ev_accept($socket, $flag, $base) {static $id = 0; $connection = stream_socket_accept($socket); stream_set_blocking($connection, 0); $id++; // increase on each accept $buffer = event_buffer_new($connection, array(__CLASS__, 'ev_read'), array(__CLASS__, 'ev_write'), array(__CLASS__, 'ev_error'), $id); event_buffer_base_set($buffer, $base); event_buffer_timeout_set($buffer, 30, 30); event_buffer_watermark_set($buffer, EV_READ, 0, 0xffffff); event_buffer_priority_set($buffer, 10); event_buffer_enable($buffer, EV_READ | EV_PERSIST); // we need to save both buffer and connection outside self::$connections[$id] = $connection; self::$buffers[$id] = $buffer;}function ev_error($buffer, $error, $id) {event_buffer_disable(self::$buffers[$id], EV_READ | EV_WRITE); event_buffer_free(self::$buffers[$id]); fclose(self::$connections[$id]); unset(self::$buffers[$id], self::$connections[$id]);}function ev_read($buffer, $id) {static $ct = 0; $ct_last = $ct; $ct_data = ''; while ($read = event_buffer_read($buffer, 1024)) { $ct += strlen($read); $ct_data .= $read; } $ct_size = ($ct - $ct_last) * 8; echo "[$id] " . __METHOD__ . " > " . $ct_data . "/n"; event_buffer_write($buffer, "Received $ct_size byte data./r/n");}function ev_write($buffer, $id) {echo "[$id] " . __METHOD__ . "/n";}}new EpollSocketServer(2000);epoll_client.php<?php/**EpollSocket Test ClientBy James.Huang <shagoo#gmail.com>**/function debug ($msg){// echo $msg; error_log($msg, 3, '/tmp/socket.log');}if ($argv[1]) { $socket_client = stream_socket_client('tcp://0.0.0.0:2000', $errno, $errstr, 30);// stream_set_blocking($socket_client, 0); if (!$socket_client) {die("$errstr ($errno)");} else {$msg = trim($argv[1]); for ($i = 0; $i < 10; $i++) { $res = fwrite($socket_client, "$msg($i)"); usleep(100000); debug(fread($socket_client, 1024)); } fclose($socket_client);}}else {$phArr = array(); for ($i = 0; $i < 10; $i++) {$phArr[$i] = popen("php ".__FILE__." '{$i}:test'", 'r');} foreach ($phArr as $ph) {pclose($ph);}// for ($i = 0; $i < 10; $i++) {// system("php ".__FILE__." '{$i}:test'");// }}先说一下,以上的例子是基于 PHP 的 libevent 扩展实现的,需要运行的话要先安装此扩展,参考:http://pecl.php.net/package/libevent。这个例子做的事情和前面介绍的第一个模型一样,epoll_server.php 实现的服务端也是接受客户端数据,然后返回结果(接收到的字节数)。但是,当你运行 php epoll_client.php 的时候你会发现服务端打印出来的结果和 accept 阻塞模型就大不一样了,当然运行效率也有极大的提升,这是为什么呢?接下来就介绍一下 epoll/kqueue 模型:在介绍 select 模式的时候我们提到了这种模式的局限,而 epoll 就是为了解决 poll 的这两个缺陷而生的。首先,epoll 模式基本没有限制(参考 cat /proc/sys/fs/file-max 默认就达到 300K,很令人兴奋吧,其实这也就是所谓基于 epoll 的 Erlang 服务端可以同时处理这么多并发连接的根本原因,不过现在 PHP 理论上也可以做到了,呵呵);另外,epoll 模式的性能也不会像 select 模式那样随着连接数的增大而变差,测试发现性能还是很稳定的(下篇会有详细介绍)。epoll 工作有两种模式 LT(level triggered) 和 ET(edge-triggered),前者是缺省模式,同时支持阻塞和非阻塞 IO 模式,虽然性能比后者差点,但是比较稳定,一般来说在实际运用中,我们都是用这种模式(ET 模式和 WinSock 都是纯异步非阻塞模型)。而另外一点要说的是 libevent 是在编译阶段选择系统的 I/O demultiplex 机制的,不支持在运行阶段根据配置再次选择,所以我们在这里也就不细讨论 libevent 的实现的细节了,如果朋友有兴趣进一步了解的话,请参考:http://monkey.org/~provos/libevent/。到这里,第一部分的内容结束了,相信大家已经了解了 Socket 编程的几个重点概念和一些实战技巧,在下一篇《Socket深度探究4PHP(二)》我将会对 select/poll/epoll/kqueue 几种模式做一下深入的介绍和对比,另外也会涉及到两种重要的 I/O 多路复用模式:Reactor 和 Proactor 模式。To be continued ...上一篇《Socket深度探究4PHP(一)》中,大家应该对 poll/select/epoll/kqueue 这几个 IO 模型有了一定的了解,为了让大家更深入的理解 Socket 的技术内幕,在这个篇幅,我会对这几种模式做一个比较详细的分析和对比;另外,大家可能也同说过 AIO 的概念,这里也会做一个简单的介绍;最后我们会对两种主流异步模式 Reactor 和 Proactor 模式进行对比和讨论。首先,然我们逐个介绍一下 2.6 内核(2.6.21.1)中的 poll/select/epoll/kqueue 这几个 IO 模型。POLL先说说 poll,poll 和 select 为大部分 Unix/Linux 程序员所熟悉,这俩个东西原理类似,性能上也不存在明显差异,但 select 对所监控的文件描述符数量有限制,所以这里选用 poll 做说明。poll 是一个系统调用,其内核入口函数为 sys_poll,sys_poll 几乎不做任何处理直接调用 do_sys_poll,do_sys_poll 的执行过程可以分为三个部分:1、将用户传入的 pollfd 数组拷贝到内核空间,因为拷贝操作和数组长度相关,时间上这是一个 O(n) 操作,这一步的代码在 do_sys_poll 中包括从函数开始到调用 do_poll 前的部分。2、查询每个文件描述符对应设备的状态,如果该设备尚未就绪,则在该设备的等待队列中加入一项并继续查询下一设备的状态。查询完所有设备后如果没有一个设备就绪,这时则需要挂起当前进程等待,直到设备就绪或者超时,挂起操作是通过调用 schedule_timeout 执行的。设备就绪后进程被通知继续运行,这时再次遍历所有设备,以查找就绪设备。这一步因为两次遍历所有设备,时间复杂度也是 O(n),这里面不包括等待时间。相关代码在 do_poll 函数中。3、将获得的数据传送到用户空间并执行释放内存和剥离等待队列等善后工作,向用户空间拷贝数据与剥离等待队列等操作的的时间复杂度同样是 O(n),具体代码包括 do_sys_poll 函数中调用 do_poll 后到结束的部分。但是,即便通过 select() 或者 poll() 函数复用事件通知具有突出的优点,不过其他具有类似功能的函数实现也可以达到同样的性能。然而,这些实现在跨平台方面没有实现标准化。你必须在使用这些特定函数实现同丧失可移植性之间进行权衡。我们现在就讨论一下两个替代方法:Solaris 系统下的 /dev/poll 和 FreeBSD 系统下的 kqueue:1、Solaris 系统下的 /dev/poll:在Solaris 7系统上,Sun引入了/dev/poll设备。在使用 /dev/poll的时候,你首先要打开/dev/poll作为一个普通文件。然后构造pollfd结构,方式同普通的poll()函数调用一样。这些 pollfd结构随后写入到打开的 /dev/poll 文件描述符。在打开句柄的生存周期内, /dev/poll会根据pollfd结构返回事件(注意,pollfd结构内的事件字段中的特定POLLREMOVE将从/dev/poll的列表中删除对应的fd)。通过调用特定的ioctl (DP_POLL) 和dvpoll,程序就可以从/dev/poll获得需要的信息。在使用dvpoll结构的情况下,发生的事件就可以被检测到了。2、FreeBSD 系统下的 kqueue:在FreeBSD 4.1中推出。FreeBSD的kqueue API设计为比其他对应函数提供更为广泛的事件通知能力。kqueue API提供了一套通用过滤器,可以模仿poll()语法(EVFILT_READ和EVFILT_WRITE)。不过,它还实现了文件系统变化(EVFILT_VNODE)、进程状态变更(EVFILT_PROC)和信号交付(EVFILT_SIGNAL)的有关通知。EPOLL接下来分析 epoll,与 poll/select 不同,epoll 不再是一个单独的系统调用,而是由 epoll_create/epoll_ctl/epoll_wait 三个系统调用组成,后面将会看到这样做的好处。先来看 sys_epoll_create(epoll_create对应的内核函数),这个函数主要是做一些准备工作,比如创建数据结构,初始化数据并最终返回一个文件描述符(表示新创建的虚拟 epoll 文件),这个操作可以认为是一个固定时间的操作。epoll 是做为一个虚拟文件系统来实现的,这样做至少有以下两个好处:1、可以在内核里维护一些信息,这些信息在多次 epoll_wait 间是保持的,比如所有受监控的文件描述符。2、epoll 本身也可以被 poll/epoll。具体 epoll 的虚拟文件系统的实现和性能分析无关,不再赘述。在 sys_epoll_create 中还能看到一个细节,就是 epoll_create 的参数 size 在现阶段是没有意义的,只要大于零就行。接着是 sys_epoll_ctl(epoll_ctl对应的内核函数),需要明确的是每次调用 sys_epoll_ctl 只处理一个文件描述符,这里主要描述当 op 为 EPOLL_CTL_ADD 时的执行过程,sys_epoll_ctl 做一些安全性检查后进入 ep_insert,ep_insert 里将 ep_poll_callback 做为回掉函数加入设备的等待队列(假定这时设备尚未就绪),由于每次 poll_ctl 只操作一个文件描述符,因此也可以认为这是一个 O(1) 操作。ep_poll_callback 函数很关键,它在所等待的设备就绪后被系统回掉,执行两个操作:1、将就绪设备加入就绪队列,这一步避免了像 poll 那样在设备就绪后再次轮询所有设备找就绪者,降低了时间复杂度,由 O(n) 到 O(1)。2、唤醒虚拟的 epoll 文件。最后是 sys_epoll_wait,这里实际执行操作的是 ep_poll 函数。该函数等待将进程自身插入虚拟 epoll 文件的等待队列,直到被唤醒(见上面 ep_poll_callback 函数描述),最后执行 ep_events_transfer 将结果拷贝到用户空间。由于只拷贝就绪设备信息,所以这里的拷贝是一个 O(1) 操作。还有一个让人关心的问题就是 epoll 对 EPOLLET 的处理,即边沿触发的处理,粗略看代码就是把一部分水平触发模式下内核做的工作交给用户来处理,直觉上不会对性能有太大影响,感兴趣的朋友欢迎讨论。POLL/EPOLL 对比:表面上 poll 的过程可以看作是由一次 epoll_create,若干次 epoll_ctl,一次 epoll_wait,一次 close 等系统调用构成,实际上 epoll 将 poll 分成若干部分实现的原因正是因为服务器软件中使用 poll 的特点(比如Web服务器):1、需要同时 poll 大量文件描述符;2、每次 poll 完成后就绪的文件描述符只占所有被 poll 的描述符的很少一部分。3、前后多次 poll 调用对文件描述符数组(ufds)的修改只是很小;传统的 poll 函数相当于每次调用都重起炉灶,从用户空间完整读入 ufds,完成后再次完全拷贝到用户空间,另外每次 poll 都需要对所有设备做至少做一次加入和删除等待队列操作,这些都是低效的原因。epoll 将以上情况都细化考虑,不需要每次都完整读入输出 ufds,只需使用 epoll_ctl 调整其中一小部分,不需要每次 epoll_wait 都执行一次加入删除等待队列操作,另外改进后的机制使的不必在某个设备就绪后搜索整个设备数组进行查找,这些都能提高效率。另外最明显的一点,从用户的使用来说,使用 epoll 不必每次都轮询所有返回结果已找出其中的就绪部分,O(n) 变 O(1),对性能也提高不少。此外这里还发现一点,是不是将 epoll_ctl 改成一次可以处理多个 fd(像 semctl 那样)会提高些许性能呢?特别是在假设系统调用比较耗时的基础上。不过关于系统调用的耗时问题还会在以后分析。POLL/EPOLL 测试数据对比:测试的环境:我写了三段代码来分别模拟服务器,活动的客户端,僵死的客户端,服务器运行于一个自编译的标准 2.6.11 内核系统上,硬件为 PIII933,两个客户端各自运行在另外的 PC 上,这两台PC比服务器的硬件性能要好,主要是保证能轻易让服务器满载,三台机器间使用一个100M交换机连接。服务器接受并poll所有连接,如果有request到达则回复一个response,然后继续poll。活动的客户端(Active Client)模拟若干并发的活动连接,这些连接不间断的发送请求接受回复。僵死的客户端(zombie)模拟一些只连接但不发送请求的客户端,其目的只是占用服务器的poll描述符资源。测试过程:保持10个并发活动连接,不断的调整僵并发连接数,记录在不同比例下使用 poll 与 epoll 的性能差别。僵死并发连接数根据比例分别是:0,10,20,40,80,160,320,640,1280,2560,5120,10240。下图中横轴表示僵死并发连接与活动并发连接之比,纵轴表示完成 40000 次请求回复所花费的时间,以秒为单位。红色线条表示 poll 数据,绿色表示 epoll 数据。可以看出,poll 在所监控的文件描述符数量增加时,其耗时呈线性增长,而 epoll 则维持了一个平稳的状态,几乎不受描述符个数影响。但是要注意的是在监控的所有客户端都是活动时,poll 的效率会略高于 epoll(主要在原点附近,即僵死并发连接为0时,图上不易看出来),究竟 epoll 实现比 poll 复杂,监控少量描述符并非它的长处。epoll 的优点综述1、支持一个进程打开大数目的socket描述符(FD):select 最不能忍受的是一个进程所打开的 FD 是有一定限制的,由 FD_SETSIZE 设置,在 Linux 中,这个值是 1024。对于那些需要支持的上万连接数目的网络服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache 方案),不过虽然 linux 上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll 则没有这个限制,它所支持的 FD 上限是最大可以打开文件的数目,这个数字一般远大于 1024,举个例子,在 1GB 内存的机器上大约是 10 万左右,具体数目可以 cat /proc/sys/fs/file-max 察看,一般来说这个数目和系统内存关系很大。2、IO 效率不随 FD 数目增加而线性下降:传统的 select/poll 另一个致命弱点就是当你拥有一个很大的 socket 集合,不过由于网络延时,任一时间只有部分的 socket 是"活跃"的,但是 select/poll 每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是 epoll 不存在这个问题,它只会对"活跃"的 socket 进行操作---这是因为在内核实现中 epoll 是根据每个 fd 上面的 callback 函数实现的。那么,只有"活跃"的 socket 才会主动的去调用 callback 函数,其他 idle 状态 socket 则不会,在这点上,epoll 实现了一个"伪"AIO,因为这时候推动力在 os 内核。在一些 benchmark 中,如果所有的 socket 基本上都是活跃的 --- 比如一个高速LAN环境,epoll 并不比 select/poll 有什么效率,相反,如果过多使用 epoll_ctl,效率相比还有稍微的下降。但是一旦使用 idle connections 模拟 WAN 环境,epoll 的效率就远在 select/poll 之上了。3、使用 mmap 加速内核与用户空间的消息传递:这点实际上涉及到 epoll 的具体实现了。无论是 select,poll 还是 epoll 都需要内核把 FD 消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll 是通过内核于用户空间 mmap 同一块内存实现的。而如果你想我一样从 2.5 内核就关注 epoll 的话,一定不会忘记手工 mmap 这一步的。4、内核微调:这一点其实不算 epoll 的优点了,而是整个 linux 平台的优点。也许你可以怀疑 linux 平台,但是你无法回避 linux 平台赋予你微调内核的能力。比如,内核 TCP/IP 协议栈使用内存池管理 sk_buff 结构,那么可以在运行时期动态调整这个内存 pool(skb_head_pool) 的大小 --- 通过 echo XXXX > /proc/sys/net/core/hot_list_length 完成。再比如 listen 函数的第 2 个参数(TCP 完成 3 次握手的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的 NAPI 网卡驱动架构。AIO 和 Epollepoll 和 aio(这里的aio是指linux 2.6内核后提供的aio api)的区别:1、aio 是异步非阻塞的。其实是aio是用线程池实现了异步IO。2、epoll 在这方面的定义上有点复杂,首先 epoll 的 fd 集里面每一个 fd 都是非阻塞的,但是 epoll(包括 select/poll)在调用时阻塞等待 fd 可用,然后 epoll 只是一个异步通知机制,只是在 fd 可用时通知你,并没有做任何 IO 操作,所以不是传统的异步。在这方面,Windows 无疑是前行者,当然 Boost C++ 库已经实现了 linux 下 aio 的机制,有兴趣的朋友可以参考:http://stlchina.huhoo.net/twiki/bin/view.pl/Main/WebHomeReactor 和 Proactor一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可将来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)。开发人员预先注册需要处理的事件及其事件处理器(或回调函数);事件分离器负责将请求事件传递给事件处理器。两个与事件分离器有关的模式是Reactor和Proactor。Reactor模式采用同步IO,而Proactor采用异步IO。在Reactor中,事件分离器负责等待文件描述符或socket为读写操作准备就绪,然后将就绪事件传递给对应的处理器,最后由处理器负责完成实际的读写工作。而在Proactor模式中,处理器--或者兼任处理器的事件分离器,只负责发起异步读写操作。IO操作本身由操作系统来完成。传递给操作系统的参数需要包括用户定义的数据缓冲区地址和数据大小,操作系统才能从中得到写出操作所需数据,或写入从socket读到的数据。事件分离器捕获IO操作完成事件,然后将事件传递给对应处理器。比如,在windows上,处理器发起一个异步IO操作,再由事件分离器等待IOCompletion事件。典型的异步模式实现,都建立在操作系统支持异步API的基础之上,我们将这种实现称为“系统级”异步或“真”异步,因为应用程序完全依赖操作系统执行真正的IO工作。举个例子,将有助于理解Reactor与Proactor二者的差异,以读操作为例(类操作类似)。在Reactor中实现读:注册读就绪事件和相应的事件处理器事件分离器等待事件事件到来,激活分离器,分离器调用事件对应的处理器。事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。与如下Proactor(真异步)中的读过程比较:处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。事件分离器等待操作完成事件在分离器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操作完成。事件分离器呼唤处理器。事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分离器。对于不提供异步 IO API 的操作系统来说,这种办法可以隐藏 Socket API 的交互细节,从而对外暴露一个完整的异步接口。借此,我们就可以进一步构建完全可移植的,平台无关的,有通用对外接口的解决方案。上述方案已经由Terabit P/L公司实现为 TProactor (ACE compatible Proactor) :http://www.terabit.com.au/solutions.php。正是因为 linux 对 aio 支持的不完整,所以 ACE_Proactor 框架在 linux 上的表现很差,大部分在 windows 上执行正常的代码,在 linux 则运行异常,甚至不能编译通过。这个问题一直困扰着很大多数 ACE 的用户,现在好了,有一个 TProactor 帮助解决了在 Linux 不完整支持 AIO 的条件下,正常使用(至少是看起来正常)ACE_Proactor。TProactor 有两个版本:C++ 和 Java 的。C++ 版本采用 ACE 跨平台底层类开发,为所有平台提供了通用统一的主动式异步接口。Boost.Asio 库,也是采取了类似的这种方案来实现统一的 IO 异步接口。以下是一张 TProactor 架构设计图,有兴趣的朋友可以看看:到这里,第二部分的内容结束了,相信大家对 Socket 的底层技术原理有了一个更深层次的理解,在下一篇《Socket深度探究4PHP(三)》我将会深入 PHP 源代码,探究一下 PHP 在 Socket 这部分的一些技术内幕,然后介绍一下目前在这个领域比较活跃的项目(node.js)。To be continued ...看过前两篇文章《Socket深度探究4PHP(一)》和《Socket深度探究4PHP(二)》,大家应该对目前 Socket 技术的底层有了一定的了解。本文我们会对 PHP-5.3.6 的源码中的Socket 模块进行一定的分析,然后再简单介绍一下目前比较热门的一些相关技术,比如 Node.js 等。自 PHP4 之后,越来越多的模块都被作为扩展提取出来(可单独编译),都在 PHP 源码的 ext 目录下面,因此我们我需要先进入 ext/sockets/ 目录,做过 PHP 扩展的同学应该都很熟悉下面的一些文件了,这次我们主要分析的是 php_sockets.h 和 sockets.c 这两个 C 源码文件。ext/sockets/php_sockets.h这个头文件很简单,我们主要看一下下面列出的几个重点:32 行:ifdef PHP_WIN32include <winsock.h>elseif HAVE_SYS_SOCKET_Hinclude <sys/socket.h>endifendif以上就是 PHP 对于不同环境 Socket 底层调用的定义了,我们可以看到不管是 Unix 还是 Windows 环境,PHP均调用的是系统标准的 BSD Socket 库。然后我们看下面这个重要的结构体定义:82 行:typedef struct {PHP_SOCKET bsd_socket;int type;int error;int blocking;} php_socket;这个就是 php socket 的存储结构了,此结构体在以下的代码阅读中将会大量出现,里面的几个字段很容易理解:bsd_socket 就是标准的 socket 类型,type 表示 socket 类型(PF_UNIX/AF_UNIX),error 是错误代码,blocking 则表示是否阻塞。ext/sockets/sockets.c这个文件比较长,为了直接切入重点,我们会按照《Socket 深度探索 4 PHP (一) 》中 select_server.php 部分代码来按顺序分析一下在最经典的 select 模式中我们用到的主要方法:socket_create_listen859 行:PHP_FUNCTION(socket_create_listen)这个函数很简单,初始化 php_sock 并获取 socket 需要监听的端口,然后传入下面的 php_open_listen_sock 函数进行加工,最后调用 ZEND_REGISTER_RESOURCE 宏返回 php_sock。347行:static int php_open_listen_sock(php_socket **php_sock, int port, int backlog TSRMLS_DC)此函数基本上就是 socket 的标准初始化过程:socket(...) -> bind(...) -> listen(...)(详见 368 行至 391 行)。sock->bsd_socket = socket(PF_INET, SOCK_STREAM, 0);sock->blocking = 1;...sock->type = PF_INET;...if (bind(sock->bsd_socket, (struct sockaddr *)&la, sizeof(la)) != 0) {...}if (listen(sock->bsd_socket, backlog) != 0) {...}socket_set_nonblock906 行:PHP_FUNCTION(socket_set_nonblock)这个函数也很简单,从 ZEND_FETCH_RESOURCE 取出 runtime 中的 php_sock 然后调用 php_set_sock_blocking 函数来设置 sockfd 的阻塞或者非阻塞(此函数可以参考 main/network.c 第 1069 行,我们可以看到 PHP 是使用 fcntl 函数来设置的)。socket_select785 行:PHP_FUNCTION(socket_select)也是标准的 select 函数调用,过程如下:FD_ZERO(...) -> php_sock_array_to_fd_set(...) -> select(...) -> php_sock_array_from_fd_set(...),可能比较特殊的就是 php_sock_array_from_fd_set() 和 php_sock_array_from_fd_set() 两个函数,这是由于我们要先把 PHP 的 fd 数组转换成原生 fd 集合,才能调用原生的 select 函数,而最后系统还把 fd 集合重新转回到PHP 的 fd 数组(具体代码参考 799 行至 851 行)。socket_accept881 行:PHP_FUNCTION(socket_accept)此函数基本上也就是 socket 原生 accept 函数的包装,具体代码可参考 397 行:php_accept_connect 函数中的逻辑,最后调用 ZEND_REGISTER_RESOURCE 宏返回 new_sock,若失败程序会清理使用的 out_socket 资源。socket_write986 行:PHP_FUNCTION(socket_write)按照以上的思路看这个函数也非常简单,详见 986 行,唯一值得注意的是对于不同操作系统调用的函数有点不同,代码(见 1004 行)如下:ifndef PHP_WIN32retval = write(php_sock->bsd_socket, str, MIN(length, str_len));elseretval = send(php_sock->bsd_socket, str, min(length, str_len), 0);endifsocket_read1021 行:PHP_FUNCTION(socket_read)此函数是用于接受 socket 的数据,调用的原生函数是 recv(),不过这里需要注意的是 PHP 为我们提供两种获取方式:1、PHP_NORMAL_READ按行读取,具体代码见 419 行:php_read 函数的逻辑,我们注意到此函数在非阻塞模式下会立即返回,否则将会读取直至遇到 \n 或者 \r 字符。2、PHP_BINARY_READ代码见 1045 行:retval = recv(php_sock->bsd_socket, tmpbuf, length, 0); 相当原生和“环保”。最后,如果返回值为 -1 则会进行一些错误记录和系统清理工作。socket_close970 行:PHP_FUNCTION(socket_close)清理 socket 运行时所用的资源。socket_shutdown1968 行:PHP_FUNCTION(socket_shutdown)调用原生 shutdown 函数来关闭 socket。分析下来,PHP 的 socket 模块中绝大部分的代码还是使用的是系统标准的原生 socket 库,其中唯一有可能造成性能隐患的就是 select 中 PHP 的 fd 数组与原生 fd 集合转换,至于其他的一些简单的数据拷贝基本对效率不会有什么影响。总的来说,PHP 的 socket 模块应该效率还是比较高的,但是在使用的时候还是需要注意到一些资源的及时释放,因为毕竟是 Daemon 程序,需要不断运行的,而且 PHP 的数据结构是很占内存(是原生 C 的 4 倍左右)的。node.js最后,我们看看现在很流行的 Node.js(http://nodejs.org/),它采用了 JavaScript 的语言引擎,语法非常的简洁,对闭包的完美支持让它特别适合做异步 IO 的代码编写,下面是一个最简单的 HTTP Server,只用仅仅六行代码:var http = require('http');http.createServer(function (req, res) {res.writeHead(200, {'Content-Type': 'text/plain'});res.end('Hello World\n');}).listen(8000, "127.0.0.1");console.log('Server running at http://127.0.0.1:8000/');运行起来感受一下,有没有惊艳的感觉啊?事实上用它来写一些简单的服务确实很不错,有兴趣的朋友可以多研究研究(中文社区:http://cnodejs.org/),它有 8000 行 C++ 代码,2000 行 javascript 代码,使用 Google 的 V8 引擎(和 Mongodb 一样),相当的很小巧精悍。下面是我在使用过程总结出中几个要点,大家可以参考:1、使用 V8 引擎(和 Mongodb 一样),内置 JSON,代码简洁,使用方便。2、使用单线程非阻塞 I/O 中的 select 方式,比较稳定(但是对于超高并发有点力不从心)。3、一些第三方应用接口不是很稳定,比如 Mongodb 的接口,并发 200 出现卡死现象,Mysql 接口也比 fast-cgi 差很多。4、注意使用 try{...}catch{...} 来捕获错误;使用 process.on('uncaughtException', function(err){...}); 来处理未捕获的错误,否则出错会导致整个服务退出。当然,Node.js 还在不断的更新发展中,虽然目前我在公司的服务架构中还不敢使用它,我还是很希望它能够迅速成长起来,这样子我们开发服务中间件的时候,就会多出一个很棒的选项啦~
2023年08月07日
14 阅读
0 评论
0 点赞
2023-08-07
PHP语言规范
PHP语言规范PHP书写规范通用原则:1、语义化看到名字,就知道意思。2、通用前缀is表示是否、get表示读、set表示写。is后面优先跟形容词,而不是名词,比如是否多语言文字,应使用is_multilingual,而不是is_multilanguage。3、单数与复数参考js的函数命名规则:getElementById、getElementsByTagName、getElementsByName。例如:取我的多个好友的名字,应使用getFriendsName,而不是getFriendNames或者getFriendName取一个用户,是getUser取多个用户,是getUsers4、冗余后缀尽量不使用data、list、info后缀。比如,js的命名就很注意,使用getElementsByTagName而不是getElementsInfoByTagName。应该使用getFriends或者getFriendsUserId,而不是getFriendsList;应该使用getUser,而不使用getUserInfo或者getUserData。不过有时候很难避免,比如有2个函数,分别是取用户基本信息,和取用户详细信息。取用户基本信息:昵称、头像URI,函数名getUserBasic还是getUserBasicInfo?函数名以形容词结尾感觉不合适,待讨论。取用户详细信息:昵称、头像URI、签名、生日,函数名getUser没问题。5、含义模糊的类名、文件名、目录名每 当使用common、util、functions、class、object、basic作为文件名时要慎重,由于这些词太通用,发展下去里面东西可 能越来越多,变成垃圾箱。要给这些起一个准确的名字,比如要做字符串处理的类,可以叫StringLib.php,放在lib目录里。6、lib、plugin与addon的区别有些类、函数算做lib、plugin还是addon。待讨论。类名:大写字母开头,驼峰命名。一般使用名词,比如配置解析类ConfigParser,而不是ParseConfig。与Java、C++一致。例如:class UserModel类的文件名:与类名相同。这与php autoload有关,为了autoload,类名总要很长,待讨论。与Java一致。例如:class UserModel的文件名为UserModel.php非类文件名:全小写,下划线分隔,不得使用空格。比如get_user.php。目录名:全小写,下划线分隔,不得使用空格。比如model、www。函数名:小写字母开头,驼峰命名,例如:function addBlog()。与Java、C++一致。函数表示功能,即动作,所以动词优先,例如使用editBlog,而不用blogEdit。PHP内置函数由于历史原因,有多种风格,do_something,something_do,dosomething,比较新的函数用了doSomething,才与目前主流语言保持一致。比如:paser_str、json_encode、substr、fetchAll。历史原因可能无法改变,但我们能保证新的代码是严谨的,不要让自己成为历史原因。类中的函数:两个函数中间空一行。如果有时间的话,各个函数按英文字母排序,免得太混乱。例如:复制代码class BlogModel { public function addBlog(){ } public function updateBlog() { } } 复制代码文件注释:注释紧跟<?php下一行。注明作者。@version暂不需要写,因为svn提供了版本管理。格式按照PHPdoc的要求:http://manual.phpdoc.org/HTMLframesConverter/default/phpDocumentor/tutorial_tags.author.pkg.html复制代码<?php /**blog的各种业务:添加、更新@author sink*/class BlogModel { } ?> 复制代码API注释:一定要写输入参数,和输出格式。写清楚正确时输出什么,错误时输出什么。否则别人无法使用。函数注释:一定要写输出格式。写清楚正确时输出什么,错误时输出什么。如果输入参数比较复杂,包含数组,看参数无法一目了然,则要写输入参数的注释。文档注释与函数之间不能有空行。如果函数内部步骤比较复杂,需要写“行内注释”。例如:复制代码/**更新blog@param int $id blog_id@param array $data array( "content" => "", //内容 "tags" => "", //标签 "update_time" => "", //更新时间 )@return bool*/public function updateBlog($id,$data) {step1 //第一步:asdf step2 //第二步:qwer } 复制代码URI:根据rfc1034国际标准的规定,域名中禁止出现下划线“_”,域名不区分大小写。比如http://dl_dir.qq.com/是错误域名。http://example.com与http://EXAMPLE.COM相同。所以优先在URI中使用全小写,GET的name小写,但是GET的值除外。比如http://www.google.com/?hl=zh-CNhttp://www.google.com/?hl=zh-cnURI中非参数的专有名词的缩写是否使用小写,有争议无定论。比如http://fedoraproject.org/zh_CN/http://zh.wikipedia.org/zh-cn/http://code.google.com/intl/zh-CN/http://www.microsoft.com/en-us/语言文字代码是专有名词,ISO规定必须是减号,且建议地区使用大写。fedora的用法很奇怪,使用了自己制造的zh_CN,而不是zh-CN。而且不建议在URI中使用下划线。wiki用了小写,google用了大写,微软用了小写。优先在URI中使用减号“-”,而不是下划线,GET的name除外。比如http://example.com/1-2-2http://example.com/?user_id=123如果希望用户手动输入URI,则不要区分大小写,且优先使用小写,因为用户输入更方便。实际情况是:用户一般是手动输入域名,而不手动输入URI,因为URI很长。在这种情况下,URI小写是否有意义,如果使用 http://example.com/?userId=123,变量名就可以使用驼峰$userId = $_GET['userId'],就能够和Java、C++保持一致,这样数据库也要驼峰命名。待讨论。变量:全小写,下划线分隔,例如:$user_id。与Java、C++不一致。待讨论。类的成员变量、函数的形参、类实例化成一个对象,都遵守变量的命名规则。原因:URI、数据库有小写惯例,从$_GET、$_POST中获得参数入库,所以用小写。PHP内置变量$_GET、$_POST使用下划线开头,全大写。自定义的变量无论多么重要,都不要使用下划线开头,以免将来与内置变量冲突。比如:不要使用$_PUT、$_DELETE。常量:全大写,下划线分隔。例如:const MEMCACHE_TTL = 600;PHP短标签:使用,不使用短标签。因为与xml冲突,且不利于部署。类大括号换行:可以采用大括号单独占一行,也可以大括号与别的放在一行,有争议无定论,待讨论。class UserModel { } 支持换行者:http://www.php.net/manual/zh/language.oop5.basic.phphttp://pear.php.net/manual/en/standards.classdef.php函数大括号换行:有争议无定论,待讨论。function getUser() { } 支持换行者:http://www.php.net/manual/zh/language.oop5.basic.phphttp://pear.php.net/manual/en/standards.funcdef.phpif大括号换行:有争议无定论,待讨论。例如:if(!emptyempty($name)) { } 或者if(!emptyempty($name)){ } 支持换行者:http://www.possibility.com/Cpp/CppCodingStandard.html#brace支持同行者:http://www.php.net/manual/zh/language.oop5.basic.phphttp://pear.php.net/manual/en/standards.control.phpswitch大括号换行:复制代码switch (...) {case 1: ... break; default: } 复制代码支持换行者:http://www.possibility.com/Cpp/CppCodingStandard.html#switch数组小括号换行:有争议无定论,待讨论。复制代码$user = array("id" => "123", "name" => "user1", "email" => "a@example.com", ) 复制代码支持同行者:http://pear.php.net/manual/en/standards.arrays.php数组内部换行:2维及以上数组的数组内部换行。如复制代码$user = array('id' => '123', 'name' => 'user1', 'email' => 'a@example.com', ); 复制代码1维数组内部不换行?待讨论。如$users_id = array('23','12','24');数组最后的逗号:数组每一行最后要有逗号,这样方便以后添加。不过前端JSON最后不能有逗号,否则有的浏览器不支持,待讨论。比如复制代码$user = array('id' => '123', 'name' => 'user1', //正确 ); $user = array('id' => '123', 'name' => 'user1' //错误 ); 复制代码单引号与双引号:优先使用单引号,当需要转义时使用双引号。这与JSON不同,JSON全是双引号,待讨论。比如:echo 'name is:' . $name . '.' . "\n"; $user = array('id' => '123', ); 条件判断的大括号:必须有大括号,即使只有一行。正确:if(!emptyempty($name)) {doSomething(); } 错误:if(!emptyempty($name))doSomething(); 回车换行:使用换行LF(\n,0a,Unix风格)。不使用CR+LF(Windows风格)。参考:http://zh.wikipedia.org/zh-cn/%E6%8F%9B%E8%A1%8Ceclipse——》workspace——》New text file line delimiter——》Other:Unix编码:使用UTF-8 no BOM。不得使用Windows记事本进行保存,因为记事本是UTF-8 BOM CR+LF。eclipse——》workspace——》Text file encoding——》Other:UTF-8缩进:使用4个空格进行缩进,也可以采用tab进行缩进。有争议无定论,待讨论。支持4个空格者:http://www.oracle.com/technetwork/java/codeconventions-136091.html#262支持2个空格者:http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Spaces_vs._Tabs支持3、4或8个空格者:http://www.possibility.com/Cpp/CppCodingStandard.html#indent要保证缩进正确,如果使用4个空格,一定不要出现5个空格或者11个空格。eclipse——》General——》Editor——》Text Editors——》show whitespace charactersvim ~/.vimrcset expandtabset softtabstop=4set shiftwidth=4HTTP协议缓存:文章使用Last Modified表示最后修改时间,不禁止缓存。header('Last Modified:Sat, 30 Oct 2010 13:21:21 GMT');需要用户登录的页面,禁止缓存。header('Cache-Control:max-age=0'); header('Cache-Control:private');HTTP协议编码与mime:web输出一定要声明编码与mime。charset与分号之间要有一个空格。小写utf-8还是大写UTF-8,尚未找到文档,待调研。比如header('Content-Type:application/json; charset=UTF-8'); header('Content-Type:application/xml; charset=UTF-8'); header('Content-Type:application/xhtml+xml; charset=UTF-8'); header('Content-Type:text/plain; charset=UTF-8'); header('Content-Type:text/html; charset=UTF-8');专有名词大小写:在类、函数、文件名、目录名等各种地方,不特殊对待专有名词,不采用全大写。原因:专有名词难以界定,比如HTML、CSS、CRUD。而且全大写导致与驼峰冲突,比如页面助手类,全大写是HTMLHelper,不如HtmlHelper。支持不特殊处理:HTML是专有名词,但mime中就使用Content-Type:text/html,而不是text/HTML。例子:采用UserDb.php,而不是UserDB.php。
2023年08月07日
9 阅读
0 评论
0 点赞
2023-08-07
php 导出Excel方法
php 导出Excel方法<?php/**PHPExcel*Copyright (C) 2006 - 2014 PHPExcel*This library is free software; you can redistribute it and/ormodify it under the terms of the GNU Lesser General PublicLicense as published by the Free Software Foundation; eitherversion 2.1 of the License, or (at your option) any later version.*This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty ofMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNULesser General Public License for more details.*You should have received a copy of the GNU Lesser General PublicLicense along with this library; if not, write to the Free SoftwareFoundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA*@category PHPExcel@package PHPExcel@copyright Copyright (c) 2006 - 2014 PHPExcel (http://www.codeplex.com/PHPExcel)@license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt LGPL@version 1.8.0, 2014-03-02*//* Error reporting /error_reporting(E_ALL);ini_set('display_errors', TRUE);ini_set('display_startup_errors', TRUE);define('EOL',(PHP_SAPI == 'cli') ? PHP_EOL : '');date_default_timezone_set('Asia/Shanghai');/* Include PHPExcel /require_once(dirname(__FILE__)."/../PHPExcel/PHPExcel.php");require_once(dirname(__FILE__)."/config.php");//require_once (dirname(__FILE__) . "/../include/common.inc.php");$diyid = isset($diyid) && is_numeric($diyid) ? $diyid : 0;$chk=$_GET['chk'];require_once ITVINC.'/diyform.cls.php';$diy = new diyform($diyid);/* if($dsql->IsTable($diy->table)){$sql = "SELECT * FROM $diy->table";$dsql->Execute('me',$sql);// while($arr = $dsql->GetArray('me',MYSQL_BOTH))//{// echo "id = {$arr[0]} ,name = {$arr[1]}";//}}*//*include_once ITVINC.'/datalistcp.class.php';if (isset($chk)) {$query = "SELECT * FROM {$diy->table} where ifcheck=$chk ORDER BY id DESC";}else {$query = "SELECT * FROM {$diy->table} ORDER BY id DESC";}$datalist = new DataListCP();$datalist->pageSize = 10;$datalist->SetParameter('diyid', $diyid);$datalist->SetSource($query);$fieldlist = $diy->getFieldList();*/// Create new PHPExcel object//echo date('H:i:s') , " Create new PHPExcel object" , EOL;$objPHPExcel = new PHPExcel();// Set document properties设置文档属性//echo date('H:i:s') , " Set document properties" , EOL;$objPHPExcel->getProperties()->setCreator("itv media inc")->setLastModifiedBy("itv media inc")->setTitle("Office 2007 XLSX Test Document")->setSubject("Office 2007 XLSX Test Document")->setDescription("Test document for Office 2007 XLSX, generated using PHP classes.")->setKeywords("office 2007 openxml php")->setCategory("Test result file");// Create a first sheet, representing sales data//写入单元格数据//echo date('H:i:s') , " Add some data" , EOL;$objPHPExcel->setActiveSheetIndex(0);if ($diyid==2||$diyid==4) {$objPHPExcel->getActiveSheet()->setCellValue('A1', 'id')->setCellValue('B1', '分组名称')->setCellValue('C1', '申请人姓名')->setCellValue('D1', '身份证号')->setCellValue('E1', '联系方式')->setCellValue('F1', '是否愿意担任小组联络人')->setCellValue('G1', '申请说明')->setCellValue('H1', '状态')->setCellValue('I1', 'time');}else {$objPHPExcel->getActiveSheet()->setCellValue('A1', 'id')->setCellValue('B1', '分组名称')->setCellValue('C1', '单位名称')->setCellValue('D1', '参加人数')->setCellValue('E1', '联系人')->setCellValue('F1', '联系电话')->setCellValue('G1', '申请说明')->setCellValue('H1', '状态')->setCellValue('I1', 'time');}if($dsql->IsTable($diy->table)){// $sql = "SELECT * FROM $diy->table";if (isset($chk) && $chk!=='') {$sql = "SELECT * FROM $diy->table where ifcheck='$chk'";}else {$sql = "SELECT * FROM $diy->table";}$dsql->Execute('me',$sql);$item=1;while($arr = $dsql->GetArray('me',MYSQL_BOTH)){$item++;//$arr[8]=date("Y-m-d H:i:s",$arr[8]);if($arr[1]=="1") {$arr[1]="已验证";}else {$arr[1]="未验证";}$objPHPExcel->getActiveSheet()->setCellValue("A$item", "$arr[0]")->setCellValue("B$item", "$diy->name")->setCellValue("C$item", "$arr[2]")->setCellValue("D$item", " $arr[3]")->setCellValue("E$item", " $arr[4]")->setCellValue("F$item", " $arr[5]")->setCellValue("G$item", "$arr[6]")->setCellValue("H$item", "$arr[1]")->setCellValue("I$item", date('Y-m-d H:i:s',$arr[7]));}}// Set column widths设置单元格宽度//echo date('H:i:s') , " Set column widths" , EOL;$objPHPExcel->getActiveSheet()->getColumnDimension('A')->setWidth(10);$objPHPExcel->getActiveSheet()->getColumnDimension('B')->setWidth(17);$objPHPExcel->getActiveSheet()->getColumnDimension('C')->setWidth(12);$objPHPExcel->getActiveSheet()->getColumnDimension('D')->setWidth(22);$objPHPExcel->getActiveSheet()->getColumnDimension('E')->setWidth(12);$objPHPExcel->getActiveSheet()->getColumnDimension('F')->setWidth(25);$objPHPExcel->getActiveSheet()->getColumnDimension('G')->setWidth(62);$objPHPExcel->getActiveSheet()->getColumnDimension('H')->setWidth(12);$objPHPExcel->getActiveSheet()->getColumnDimension('I')->setWidth(15);// Add conditional formatting//echo date('H:i:s') , " Add conditional formatting" , EOL;$objConditional1 = new PHPExcel_Style_Conditional();$objConditional1->setConditionType(PHPExcel_Style_Conditional::CONDITION_CELLIS)->setOperatorType(PHPExcel_Style_Conditional::OPERATOR_BETWEEN)->addCondition('200')->addCondition('400');$objConditional1->getStyle()->getFont()->getColor()->setARGB(PHPExcel_Style_Color::COLOR_YELLOW);$objConditional1->getStyle()->getFont()->setBold(true);$objConditional1->getStyle()->getNumberFormat()->setFormatCode(PHPExcel_Style_NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE);$objConditional2 = new PHPExcel_Style_Conditional();$objConditional2->setConditionType(PHPExcel_Style_Conditional::CONDITION_CELLIS)->setOperatorType(PHPExcel_Style_Conditional::OPERATOR_LESSTHAN)->addCondition('0');$objConditional2->getStyle()->getFont()->getColor()->setARGB(PHPExcel_Style_Color::COLOR_RED);$objConditional2->getStyle()->getFont()->setItalic(true);$objConditional2->getStyle()->getNumberFormat()->setFormatCode(PHPExcel_Style_NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE);$objConditional3 = new PHPExcel_Style_Conditional();$objConditional3->setConditionType(PHPExcel_Style_Conditional::CONDITION_CELLIS)->setOperatorType(PHPExcel_Style_Conditional::OPERATOR_GREATERTHANOREQUAL)->addCondition('0');$objConditional3->getStyle()->getFont()->getColor()->setARGB(PHPExcel_Style_Color::COLOR_GREEN);$objConditional3->getStyle()->getFont()->setItalic(true);$objConditional3->getStyle()->getNumberFormat()->setFormatCode(PHPExcel_Style_NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE);//$conditionalStyles = $objPHPExcel->getActiveSheet()->getStyle('B2')->getConditionalStyles(); 后来去array_push($conditionalStyles, $objConditional1);array_push($conditionalStyles, $objConditional2);array_push($conditionalStyles, $objConditional3);//$objPHPExcel->getActiveSheet()->getStyle('B2')->setConditionalStyles($conditionalStyles); 后来去// duplicate the conditional styles across a range of cells复制单元格格式//echo date('H:i:s') , " Dulicate the conditional formatting across a range of cells" , EOL;/*$objPHPExcel->getActiveSheet()->duplicateConditionalStyle($objPHPExcel->getActiveSheet()->getStyle('B2')->getConditionalStyles(), 后来去'B3:B7');*/// Set fonts设置字体//echo date('H:i:s'), " Set fonts" , EOL;$objPHPExcel->getActiveSheet()->getStyle('A1:I1')->getFont()->setBold(true);//$objPHPExcel->getActiveSheet()->getStyle('B1')->getFont()->setBold(true);//$objPHPExcel->getActiveSheet()->getStyle('A7:B7')->getFont()->setBold(true);//$objPHPExcel->getActiveSheet()->getStyle('B7')->getFont()->setBold(true);//set color设置背景色$objPHPExcel->getActiveSheet()->getStyle( 'A1:I1')->getFill()->setFillType(PHPExcel_Style_Fill::FILL_SOLID);$objPHPExcel->getActiveSheet()->getStyle( 'A1:I1')->getFill()->getStartColor()->setARGB('FFD2D2D2');// Set header and footer. When no different headers for odd/even are used, odd header is assumed.//echo date('H:i:s') , " Set header/footer" , EOL;$objPHPExcel->getActiveSheet()->getHeaderFooter()->setOddHeader('&L&BPersonal cash register&RPrinted on &D');$objPHPExcel->getActiveSheet()->getHeaderFooter()->setOddFooter('&L&B' . $objPHPExcel->getProperties()->getTitle() . '&RPage &P of &N');// Set page orientation and size设计页面方向和大小//echo date('H:i:s') , " Set page orientation and size" , EOL;$objPHPExcel->getActiveSheet()->getPageSetup()->setOrientation(PHPExcel_Worksheet_PageSetup::ORIENTATION_PORTRAIT);$objPHPExcel->getActiveSheet()->getPageSetup()->setPaperSize(PHPExcel_Worksheet_PageSetup::PAPERSIZE_A4);// Rename worksheet//echo date('H:i:s') , " Rename worksheet" , EOL;$objPHPExcel->getActiveSheet()->setTitle('工作表名字');// Set active sheet index to the first sheet, so Excel opens this as the first sheet$objPHPExcel->setActiveSheetIndex(0);// 保存文件// Save Excel 2007 file//echo date('H:i:s') , " Write to Excel2007 format" , EOL;$cttime=$diy->name.date('YmdHis').'.php';$cttimed=$diy->name.date('YmdHis');$callStartTime = microtime(true);$objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');$objWriter->save(str_replace('.php', '.xlsx', $cttime));$callEndTime = microtime(true);$callTime = $callEndTime - $callStartTime;//echo date('H:i:s') , " File written to " , str_replace('.php', '.xlsx', pathinfo(__FILE__, PATHINFO_BASENAME)) , EOL;//echo 'Call time to write Workbook was ' , sprintf('%.4f',$callTime) , " seconds" , EOL;// Save Excel5 file//echo date('H:i:s') , " Write to Excel5 format" , EOL;$callStartTime = microtime(true);$objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel5');$objWriter->save(str_replace('.php', '.xls', $cttime));$callEndTime = microtime(true);$callTime = $callEndTime - $callStartTime;//直接输出到浏览器$objWriter = new PHPExcel_Writer_Excel5($objPHPExcel);header("Pragma: public");header("Expires: 0");header("Cache-Control:must-revalidate, post-check=0, pre-check=0");header("Content-Type:application/force-download");header("Content-Type:application/vnd.ms-execl");header("Content-Type:application/octet-stream");header("Content-Type:application/download");;header("Content-Disposition:attachment;filename=$cttimed.xls");header("Content-Transfer-Encoding:binary");$objWriter->save('php://output');//echo date('H:i:s') , " File written to " , str_replace('.php', '.xls', pathinfo(__FILE__, PATHINFO_BASENAME)) , EOL;//echo 'Call time to write Workbook was ' , sprintf('%.4f',$callTime) , " seconds" , EOL;// Echo memory usage//echo date('H:i:s') , ' Current memory usage: ' , (memory_get_usage(true) / 1024 / 1024) , " MB" , EOL;// Echo memory peak usage//echo date('H:i:s') , " Peak memory usage: " , (memory_get_peak_usage(true) / 1024 / 1024) , " MB" , EOL;// Echo done//echo date('H:i:s') , " Done writing file" , EOL;//echo 'File has been created in ' , getcwd() , EOL;
2023年08月07日
11 阅读
0 评论
0 点赞
1
...
91
92
93
...
112