workerman官方描述
Workerman是一款纯PHP开发的开源高性能异步PHP socket框架。支持高并发,超高稳定性,被广泛的用于手机app、移动通讯,微信小程序,手游服务端、网络游戏、PHP聊天室、硬件通讯、智能家居、车联网、物联网等领域的开发。 支持TCP长连接,支持Websocket、HTTP等协议,支持自定义协议。拥有异步Mysql、异步Redis、异步Http、MQTT物联网客户端、异步消息队列等众多高性能组件。
正如标题,我们把范围缩小.来看下启动wokerman时候源码涉及到的知识点:
如何启动一个服务
require_once "Autoloader.php";$http_worker = new \Workerman\Worker("http://0.0.0.0:2347");$http_worker->count = 2;$http_worker->onMessage = function ($connection, $data) { $connection->send('hello baby');};$http_worker->runAll();复制代码
上面是一个最简单的一个例子,Wokerman类初始化时候传递了协议类型和服务地址【http类型】, 然后设置了进程数量为2,绑定了事件回调处理【onMessage】,最后核心的一步是启动这个服务
下面一步一步看下内部的实现:
调用:
$http_worker = new \Workerman\Worker("http://0.0.0.0:2347");复制代码
- 初始化
// Save all worker instances.$this->workerId = spl_object_hash($this);static::$_workers[$this->workerId] = $this;static::$_pidMap[$this->workerId] = array();复制代码
spl_object_hash 将对象生成一个hash值,初始化$_workers和$_pidMap数组
- 设置自动加载目录
// Get autoload root path. $backtrace = debug_backtrace(); $this->_autoloadRootPath = dirname($backtrace[0]['file']);复制代码
注意debug_backtrace 产生一条 PHP 的回溯跟踪,此处只是获取执行脚本的目录,如果兼容5.3.6以下版本,建议debug_backtrace(false)或更高版本使用debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS),忽略 "args" 的索引,能够节省内存占用
- 创建资源流上下文 stream_context_create 这个是php流操作里面的一个函数,具体作用如下
$opts = array( 'http' => array( 'method' => "GET", 'header' => "Accept-language: en\r\n" . "Cookie: foo=bar\r\n", 'timeout' => 1, ));$context = stream_context_create($opts);$text = file_get_contents('https://facebook.com',false,$context);//同样适用fopenvar_dump($text); //false复制代码
上面的是个简单的stream_context_create的示例, http协议设置了请求方法 、header头、超时时间,file_get_contents请求的是一个墙外的地址,第三个参数是需要一个资源对象,这里过1s钟后如果请求不到将会返回false.
流是一个很大的话题,可以做很多有意思的事情,这里不再展开,对此感兴趣可以参考http://www.php.net/manual/zh/ref.stream.php
下面继续回到程序主逻辑初始化后:
1.设置进程数[准确的讲应该成为worker进程数]
$http_worker->count = 2; //此处后期再讲复制代码
2.绑定事件
$http_worker->onMessage = function ($connection, $data) { $connection->send('hello baby');};复制代码
上述表示当接收到一个消息的时候,触发onMessage绑定的function函数。 $connection表示为连接对象.用于操作客户端连接,发送数据 关闭连接等。 $connection->send() 发送数据给客户端。 $data 表示接收的数据。
3.启动服务
$http_worker->runAll();复制代码
此处为该启动流程分析的核心,下面我们一起看下这里面执行了哪些操作.
protected static function checkSapiEnv(){ // Only for cli. if (php_sapi_name() != "cli") { exit("only run in command line mode \n"); } if (DIRECTORY_SEPARATOR === '\\') { self::$_OS = OS_TYPE_WINDOWS; }}复制代码
php_sapi_name函数获取运行模式。 DIRECTORY_SEPARATOR 根据系统分隔符判断是否是windows操作系统
foreach (static::$_workers as $worker_id => $worker) { $new_id_map = array(); $worker->count = $worker->count <= 0 ? 1 : $worker->count; for($key = 0; $key < $worker->count; $key++) { $new_id_map[$key] = isset(static::$_idMap[$worker_id][$key]) ? static::$_idMap[$worker_id][$key] : 0; } static::$_idMap[$worker_id] = $new_id_map;}复制代码
注意我们从开始设置的$worker->count参数,此处绑定woker_id => 一组count数量的进程,此处我们只有一个。
我们再来看看如何注册信号量的
protected static function installSignal(){ if (static::$_OS !== OS_TYPE_LINUX) { return; } // stop pcntl_signal(SIGINT, array('\Workerman\Worker', 'signalHandler'), false); // graceful stop pcntl_signal(SIGTERM, array('\Workerman\Worker', 'signalHandler'), false); // reload pcntl_signal(SIGUSR1, array('\Workerman\Worker', 'signalHandler'), false); // graceful reload pcntl_signal(SIGQUIT, array('\Workerman\Worker', 'signalHandler'), false); // status pcntl_signal(SIGUSR2, array('\Workerman\Worker', 'signalHandler'), false); // connection status pcntl_signal(SIGIO, array('\Workerman\Worker', 'signalHandler'), false); // ignore pcntl_signal(SIGPIPE, SIG_IGN, false);}复制代码
首先信号量只能运行在linux环境下。 核心函数 pcntl_signal,安装一个信号处理器
bool pcntl_signal ( int $signo , callback $handler [, bool $restart_syscalls = true ] )复制代码
保存pid
static::saveMasterPid();复制代码
绘制命令端界面
tatic::displayUI();复制代码
根据上面存储的static::$_workers和static::$_pidMap 循环fork进程
while (count(static::$_pidMap[$worker->workerId]) < $worker->count) { static::forkOneWorkerForLinux($worker);}复制代码
核心函数 pcntl_fork()
在当前进程当前位置产生分支(子进程)。译注:fork是创建了一个子进程,父进程和子进程 都从fork的位置开始向下继续执行,不同的是父进程执行过程中,得到的fork返回值为子进程号,而子进程得到的是0.
调用listen 根据调用选择引入Protocols下协议文件
-------写的我好想去死,我先休息下再继续写---------
2018-09-16 天气晴
$pid = pcntl_fork();//父进程和子进程都会执行下面代码if ($pid == -1) { //错误处理:创建子进程失败时返回-1. die('could not fork');} else if ($pid) { //父进程会得到子进程号,所以这里是父进程执行的逻辑 pcntl_wait($status); //等待子进程中断,防止子进程成为僵尸进程。} else { //子进程得到的$pid为0, 所以这里是子进程执行的逻辑。}复制代码
一部分信号表:
SIGHUP 1 A 终端挂起或者控制进程终止 SIGINT 2 A 键盘中断(如break键被按下) SIGQUIT 3 C 键盘的退出键被按下 SIGILL 4 C 非法指令 SIGABRT 6 C 由abort(3)发出的退出指令 SIGFPE 8 C 浮点异常 SIGKILL 9 AEF Kill信号 SIGSEGV 11 C 无效的内存引用 SIGPIPE 13 A 管道破裂: 写一个没有读端口的管道 SIGALRM 14 A 由alarm(2)发出的信号 SIGTERM 15 A 终止信号 SIGUSR1 30,10,16 A 用户自定义信号1 SIGUSR2 31,12,17 A 用户自定义信号2 SIGCHLD 20,17,18 B 子进程结束信号 SIGCONT 19,18,25 进程继续(曾被停止的进程) SIGSTOP 17,19,23 DEF 终止进程 SIGTSTP 18,20,24 D 控制终端(tty)上按下停止键 SIGTTIN 21,21,26 D 后台进程企图从控制终端读 SIGTTOU 22,22,27 D 后台进程企图从控制终端写 复制代码
捕获信号小例子
output:
^Cpcntl_wait return2退出了2退出了复制代码
declare(ticks = 1)效率低下,每一行都检查信号发生,建议使用pcntl_signal_dispatch 捕获
多进程执行
// 3个子进程处理任务for ($i = 0; $i < 3; $i++){ $pid = pcntl_fork(); if ($pid == -1) { die("could not fork"); } elseif ($pid) { echo "I'm the Parent $i\n"; } else {// 子进程处理 sleep(5); echo "success\n"; exit($i);// 一定要注意退出子进程,否则pcntl_fork() 会被子进程再fork,带来处理上的影响。 }}// 等待子进程执行结束while (pcntl_waitpid(0, $status) != -1) { $status = pcntl_wexitstatus($status); echo "Child $status completed\n";}复制代码
output:
I'm the Parent 0I'm the Parent 1I'm the Parent 2successsuccesssuccessChild 0 completedChild 1 completedChild 2 completed复制代码
上面的代码实现了并行的运行。