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值,初始化<pre>
不能识别此Latex公式: _workers和</pre>
_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" 的索引,能够节省内存占用
$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);
//同样适用fopen
var_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函数。 <pre>
不能识别此Latex公式: connection表示为连接对象.用于操作客户端连接,发送数据 关闭连接等。 </pre>
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::<pre>不能识别此Latex公式: _workers和static::</pre>
_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 后台进程企图从控制终端写
捕获信号小例子
<?php
#为了pcntl能够截获信号
//declare(ticks = 1);
class SignalManage
{
/**
* 截获信号处理
*
* @param $signal
*/
public static function signalHandler($signal)
{
echo $signal.'退出了'.PHP_EOL;
}
}
//注册信号量
pcntl_signal(SIGINT, ['SignalManage', 'signalHandler'],false);
$pid = pcntl_fork();
if ($pid) {
pcntl_wait($status, WUNTRACED);
echo "pcntl_wait return\n";
} else {
sleep(1000);
}
//向当前进程发送SIGUSR1信号
//posix_kill(posix_getpid(), SIGUSR1);
pcntl_signal_dispatch();
output:
^C
pcntl_wait return
2退出了
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 0
I'm the Parent 1
I'm the Parent 2
success
success
success
Child 0 completed
Child 1 completed
Child 2 completed
上面的代码实现了并行的运行。