swoole
初步
- 环境准备
和php扩展安装一样,会和扩展产生冲突,比如xdebug,phptrace,aop,molten
我这边本地用的是window来学习 直接使用了phpstudy进行安装了 - php --ri swoole 查看swoole的配置
- window启动服务 swoole-cli 文件名
swoole初体验
<?php
$http = new Swoole\Http\Server('0.0.0.0', 9501);
$http->on('Request', function ($request, $response) {
echo "接收到了请求";
$response->header('Content-Type', 'text/html; charset=utf-8');
$response->end('<h1>Hello Swoole. #' . rand(1000, 9999) . '</h1>');
});
echo "服务启动";
$http->start();
HTTP,TCP,UDP
- 超全局变量 R E Q U E S T , _REQUEST, REQUEST,_SERVER
- 全局变量会导致进程隔离问题,内存泄漏问题。
- 列子如下
$http = new Swoole\Http\Server('0.0.0.0', 9501);
$i = 1;
$http->set([
'worker_num'=>2,
]);
$http->on('Request', function ($request, $response) {
global $i;
$response->end($i++);
});
$http->start();
- 监听TCP用的是connect时间 测试tcp一般是使用telnet
//创建Server对象,监听 9501 端口
$server = new Swoole\Server('0.0.0.0', 9501);
//监听连接进入事件
$server->on('Connect', function ($server, $fd) {
echo "Client: Connect.\n";
});
//监听数据接收事件 $fd 客户端的一个id
$server->on('Receive', function ($server, $fd, $reactor_id, $data) {
$server->send($fd, "Server: {$data}");
});
//监听连接关闭事件
$server->on('Close', function ($server, $fd) {
echo "Client: Close.\n";
});
//启动服务器
$server->start();
- 监听udp udp是不可靠的速度比tcp快
$server = new Swoole\Server('0.0.0.0', 9501, SWOOLE_PROCESS, SWOOLE_SOCK_UDP);
//监听数据接收事件
$server->on('Packet', function ($server, $data, $clientInfo) {
var_dump($clientInfo);
$server->sendto($clientInfo['address'], $clientInfo['port'], "Server:{$data}");
});
//启动服务器
$server->start();
tcp,udp客户端
- tcp客户端
$client = new Swoole\Client(SWOOLE_SOCK_TCP);
if (!$client->connect('127.0.0.1', 9501, -1)) {
exit("connect failed. Error: {$client->errCode}\n");
}
$client->send("hello world\n");
echo $client->recv();
$client->close();
- udp客户端
$client = new Swoole\Client(SWOOLE_SOCK_UDP);
$client->sendto('127.0.0.1', 9501, "hello world\n");
echo $client->recv();
$lient->close();
其他的一些方法
- 第一个 isConnected() 用于返回客户端是否连接的布尔值。前提当然是要调用了 connect() 并成功建立连接之后才会返回 true
- getSocket() 用于返回一个 socket 扩展的句柄资源符,目前我们的系统环境中暂时没有安装 socket 扩展,所以这个函数还用不了。
- getsockname() 用于获取客户端的 socket 在本地的 host 和 port 端口。可以看到注释中我们程序自动在本地开了 47998 这个端口用于和服务端的 TCP 进行通信使用。
- 我们可以使用 getpeername() 获得对端 socket 的 IP 地址和端口。
WebSocket 服务
- 服务端
//创建WebSocket Server对象,监听0.0.0.0:9501端口
$ws = new Swoole\WebSocket\Server('0.0.0.0', 9501);
//监听WebSocket连接打开事件
$ws->on('Open', function ($ws, $request) {
while(1){
$time = date("Y-m-d H:i:s");
$ws->push($request->fd, "hello, welcome {$time}\n");
Swoole\Coroutine::sleep(10);
}
});
//监听WebSocket消息事件
$ws->on('Message', function ($ws, $frame) {
echo "Message: {$frame->data}\n";
$ws->push($frame->fd, "server: {$frame->data}");
});
//监听WebSocket连接关闭事件
$ws->on('Close', function ($ws, $fd) {
echo "client-{$fd} is closed\n";
});
$ws->start();
- 客户端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<input type="text" id="txt"/><br/>
<button onclick="send()">发送</button>
<p id="response">
</p>
<script type="text/javascript">
var wsServer = 'ws://192.168.56.133:9501';
var websocket = new WebSocket(wsServer);
websocket.onopen = function (evt) {
console.log("Connected to WebSocket server.");
};
websocket.onclose = function (evt) {
console.log("Disconnected");
};
websocket.onmessage = function (evt) {
console.log('Retrieved data from server: ' + evt.data);
document.getElementById("response").innerHTML = document.getElementById("response").innerHTML + 'Retrieved data from server: ' + evt.data + '<br/>';
};
websocket.onerror = function (evt, e) {
console.log('Error occured: ' + evt.data);
};
function send(){
websocket.send(document.getElementById('txt').value);
}
</script>
</body>
</html>
异步任务
$http = new Swoole\Http\Server('0.0.0.0', 9501);
$http->set([
'task_worker_num'=>4,
]);
$http->on('Request', function ($request, $response) use($http) {
echo "接收到了请求", PHP_EOL;
$response->header('Content-Type', 'text/html; charset=utf-8');
//监听服务
$http->task("发送邮件");
$http->task("发送广播");
$http->task("执行队列");
$response->end('<h1>Hello Swoole. #' . rand(1000, 9999) . '</h1>');
});
//处理异步任务(此回调函数在task进程中执行)
$http->on('Task', function ($serv, $task_id, $reactor_id, $data) {
echo "New AsyncTask[id={$task_id}]".PHP_EOL;
sleep(rand(1,5));
//返回任务执行的结果
$serv->finish("{$data} -> OK");
});
//处理异步任务的结果(此回调函数在worker进程中执行)
$http->on('Finish', function ($serv, $task_id, $data) {
//处理服务
echo "AsyncTask[{$task_id}] Finish: {$data}".PHP_EOL;
});
echo "服务启动", PHP_EOL;
$http->start();
- worker_num 进程数量
- task_worker_num 异步进程数量 也是会产生进程的
redis服务器
- 可以用来写redis
进程,线程,协程
进程
- 包工头
- 前面swoole中的task也是进程 不是线程
- 就是我们执行的程序。程序和程序之间没有共享内容,都是自己独立的内存空间。
- 调度切换由操作系统完成,用户无感知,切换内容非常多,开销大,效率低
- 需要通信的话一般是通过信号传递,或者外部工具。
线程
- 农民工
- 进程下面的小弟,同一个进程间的多个线程共享内存。
- 真正的并行执行,可以利用 CPU 的核数。
- 调度切换比进程小,但一样是操作系统完成,开销和效率中等。
协程
- 协程,从官方意义上来说,不是进程也不是线程,它更类似于一个不带返回值的函数调用。但是,如果通俗一点说,你把它想象成线程也问题不大,只不过这个线程是用户态的。什么意思呢?进程和线程是系统来控制的,包括切换调度,而协程是可以自己操作的,不需要操作系统参与,创建销毁的成本非常低。但同时,它又不像多线程可以充分利用 CPU 。在 Swoole 中,我们在协程中需要借助于多进程模型来利用到多核 CPU 的优势。
- 当然,并不是说协程就无敌了。协程只有在等待 IO 的过程中才能重复利用线程。一条线程在等待耗时的 IO 操作时会阻塞,其实操作系统这个时候认识的只有线程,当它阻塞了,这个线程里面的协程也是在阻塞态的。如果没有异步调用的支持,其实协程也发挥不了什么作用。异步调用又是个啥?就是 异步IO ,epoll 有了解过不?或者说,JavaScript 中的事件循环。
- 既然是循环,其实想必你也清楚了,我们来总结一下。协程只是说在一个线程中,通过自己控制自己,来让一个线程中可以执行多个协程程序,当前面的协程阻塞时,通过异步 IO 放在那里去执行,然后切换到其它的协程运行。在这个时候,操作系统并不需要切换或者创新新的线程,减少了线程的切换时间。注意,真正并行的只有线程,或者两个不相干的进程,而协程,并不是并行处理的,在线程中,它也是在 CPU 的时间分片机制下的切换执行。一个线程中的一个协程运行时,其它的协程是挂起状态的。它的切换成本非常低,而且是用户态可控的。协程和进程、线程完全不是一个维度的概念,就像上面说的,它就是个函数。
- 线程的小弟,但其实更像是一个函数。
- 在线程之上的串行运行,依赖于线程,而且依赖于异步 IO ,如果没有 异步IO 的支持,它和线程没什么区别。
- 更加轻量级,用户态切换,开销成本低,效率非常高。
Swoole 异步进程服务系统
Server两种运行模式
- 默认SWOOLE_BASE
- SWOOLE_PROCESS
SWOOLE_BASE模式
- 传统的异步非阻塞模式:是通过一个主线线程来处理所有的请求,然后对 I/O 操作进行异步线程处理,避免创建、销毁线程以及线程切换的消耗。当 I/O 任务完成后,通过观察者执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环。
- 当一个请求进来的时候,所有的 Worker 都会争抢这一个连接,并最终会有一个 Worker 进程成功直接和客户端建立连接,之后这个连接中所有的数据收发直接和这个 Worker 通讯,不再经过主进程的 Reactor 线程转发。
- 通过一个主线线程来处理所有的请求
---------php|----php
|----php
base模式下的进程
SWOOLE_PROCESS模式
- SWOOLE_PROCESS 的所有客户端都是和主进程建立的,内部实现比较复杂,用了大量的进程间通信、进程管理机制。适合业务逻辑非常复杂的场景,因为它可以方便地进行进程间的互相通信。
- 在 SWOOLE_PROCESS 中,所有的 Worker 不会去争抢连接,也不会让某一个连接与某个固定的 Worker 通讯,而是通过一个主进程进行连接。剩下的事件处理则交给不同的 Worker 去执行,当到达 Worker 之后,同样地也是使用回调方式进行处理了,后续内容基本就和 BASE 差不多了。也就是说,Worker 功能的不同是它和 SWOOLE_BASE 最大的差异,它实现了连接与数据请求的分离,不会因为某些连接数据量大某些量小导致 Worker 进程不均衡。
- 不会去争抢连接 而是会多一个进程去操作
-------php-----|----php
| |----php
|
|-php
两种模式的差异
- 如果客户端之间不需要交互,也就是我们普通的 HTTP 服务的话,SWOOLE_BASE 模式是一个很好的选择,但是它除了 send() 和 close() 方法之外,不支持跨进程执行。但其实,这两种模式在底层的处理上没什么太大的区别,都是走的异步IO机制。只是说它们的连接方式不同。SWOOLE_BASE 的每个 Worker 都可以看成是 SWOOLE_PROCESS 的 Reactor 线程和 Worker 进程两部分的组合。
- BASE 模式因为更简单,所以不容易出错,它也没有 IPC 开销,而 PROCESS 模式有 2 次 IPC (
我的理解是要进行线程的调度一次是调起master,另一个是调起worker,但是base就是worker直接走完就行了,不需要线程的切换看到后面好像发现理解错了?就只是通信而已? )开销,master 进程与 worker 进程需要 Unix Socket 进行通信。IPC 这东西就是同一台主机上两个进程间通信的简称。它一般有两种形式,一个是通过 Unix Socket 的方式,就是我们最常见的类似于 php-fcgi.sock 或者 mysql.sock 那种东西。另一种就是 sysvmsg 形式,这是 Linux 提供的一种消息队列,一般使用的比较少。
一些重要的线程和进程
Master 进程
- 它是一个多线程进程。用于管理线程,它会创建 Master 线程以及 Reactor 线程,并且还有心跳检测线程、UDP 收包线程等等。
Reactor 线程
- 这个线程我们不止一次提到了,它是在 Master 进程中创建的,负责客户端 TCP 连接、处理网络 IO 、处理协议、收发数据,它不执行任何 PHP 代码,用于将 TCP 客户端发来的数据缓冲、拼接、拆分成完整的一个请求数据包。我们在 Swoole 代码中操作不了线程,为什么呢?其实 PHP 本身就是不支持多线程的,Swoole 是一种多进程应用框架。在这里的线程是在底层用 C/C++ 封装好的。因此,也并没有为我们提供可以直接操作线程的接口。但我们已经学习过了,协程本身就是工作在线程之上的,而且,协程也已经是现在的主流方向了,所以在 Swoole 中,进程管理和协程,才是我们学习的重点。
Worker 进程
- Worker 是接受 Reactor 线程投递过来的请求数据包,并执行具体的 PHP 回调函数来进行数据处理的。在处理完成之后,将生成的响应数据发送回 Reactor 线程,再由 Reactor 发送给客户端。Worker 进程可以是异步非阻塞模式的,也可以是同步阻塞模式的,并且是以多进程方式运行的。
TaskWorker 进程
- 它是接受收 Worker 进程投递过来的任务,处理任务后将结果返回给 Worker 进程,这种模式是同步阻塞的模式,同样它也是以多进程的方式运行的。
Manager 进程
- 这个进程主要负责创建、回收 Worker/TaskWorkder 进程。其实就是一个进程管理器。
swoole服务运行流程
单进程管理Process
- 使用多进程能力
for ($i = 0; $i < 2; $i++) {
$process = new \Swoole\Process(function () {
$t = rand(10, 20);
echo 'Child Process #' . getmypid() . 'start and sleep ' . $t . 's', PHP_EOL;
sleep($t);
echo 'Child Process #' . getmypid() . ' exit', PHP_EOL;
});
$process->start();
}
//-------------回收之后这个进程就是被阻塞了无法做其他事情
for ($n = 2; $n--;) {
//等待两个程序回收之后再退出
$status = \Swoole\Process::wait(true);
echo "Recycled #{$status['pid']}, code={$status['code']}, signal={$status['signal']}" . PHP_EOL;
}
echo 'Parent #' . getmypid() . ' exit' . PHP_EOL;
//while(1) sleep(100);
|||
|||
||| 对上面写法的一个升级 就是让主进程可以去做其他的事情
|||
VVV
Swoole\Process::signal(SIGCHLD, function ($sig) {
//必须为false,非阻塞模式
while ($ret = Swoole\Process::wait(false)) {
echo "PID={$ret['pid']}\n";
}
});
echo 'Parent #' . getmypid() . ' exit' . PHP_EOL;
//while(1) sleep(100);
Swoole\Timer::tick(2000, function () {});
//----------------
- 僵尸进程 子进程退出后没有回收
- 证明进程间是相互没有联系的 对变量操作都是无法去共享的
$obj = new stdClass();
$obj->parent = 1;
var_dump($obj);
(new \Swoole\Process(function () use ($obj) {
$obj->child1 = 1;
var_dump($obj);
}))->start();
(new \Swoole\Process(function () use ($obj) {
$obj->child2 = 1;
var_dump($obj);
}))->start();
// [root@localhost source]# php 3.3单进程管理Process.php
// object(stdClass)#1 (1) {
// ["parent"]=>
// int(1)
// }
// object(stdClass)#1 (2) {
// ["parent"]=>
// int(1)
// ["child1"]=>
// int(1)
// }
// object(stdClass)#1 (2) {
// ["parent"]=>
// int(1)
// ["child2"]=>
// int(1)
// }
- 回调参数的方法
(new \Swoole\Process(function () {
var_dump(func_get_args());
}))->start();
// [root@localhost source]# php 3.3单进程管理Process.php
// array(1) {
// [0]=>
// object(Swoole\Process)#1 (6) {
// ["pipe"]=>
// int(4)
// ["msgQueueId"]=>
// NULL
// ["msgQueueKey"]=>
// NULL
// ["pid"]=>
// int(1956)
// ["id"]=>
// NULL
// ["callback":"Swoole\Process":private]=>
// object(Closure)#2 (0) {
// }
// }
// }
- 改名
(new \Swoole\Process(function (\Swoole\Process $process) {
$process->name('Child Test1');
sleep(10);
}))->start();
(new \Swoole\Process(function (\Swoole\Process $process) {
$process->name('Child Test2');
sleep(10);
}))->start();
swoole_set_process_name("Parent Test");
// [root@localhost ~]# ps -ef | grep Test
// root 1942 1413 0 21:45 pts/0 00:00:00 Parent Test
// root 1943 1942 0 21:45 pts/0 00:00:00 Child Test1
// root 1944 1942 0 21:45 pts/0 00:00:00 Child Test2
Process 对象的 name() 方法是 swoole_set_process_name() 这个全局函数的别名。所以我们在主进程使用的是 swoole_set_process_name() 演示的。如果是主进程,改名方法要在 start() 之后使用。如果是子进程,要在子进程的回调函数中使用。下面这样是无效的哦。
- 执行外部程序
// php -r "echo 1+1;"
(new \Swoole\Process(function (\Swoole\Process $process) {
echo $process->exec('/usr/local/php/bin/php', ['-r', 'echo 1+1;']);
// 2
}))->start();
- 守护进程
Swoole\Process::daemon();
守护进程的概念也不用我多说了吧,最明显的就是我们的进程如果不是守护进程,那么在命令行运行起来的时候界面会一直保持在程序运行的状态中。而开启了守护进程之后,进程就转移到后台运行了,就像 nohup 的作用一样。
- exit和kill
(new \Swoole\Process(function(\Swoole\Process $pro){
$pro->exit(9);
sleep(20);
}))->start();
// Array
// (
// [pid] => 2086
// [code] => 9
// [signal] => 0
// )
// PID=2086
$process = new \Swoole\Process(function(\Swoole\Process $pro){
sleep(20);
});
$process->start();
Swoole\Process::kill($process->pid);
// Array
// (
// [pid] => 2087
// [code] => 0
// [signal] => 15
// )
// PID=2087
Swoole\Process::signal(SIGCHLD, function ($sig) {
//必须为false,非阻塞模式
while ($ret = Swoole\Process::wait(false)) {
print_r($ret);
echo "PID={$ret['pid']}\n";
}
});
- 优先级
Swoole\Process::setAffinity([0,1]);
$process = new \Swoole\Process(function(\Swoole\Process $pro){
echo $pro->getPriority(PRIO_PROCESS), PHP_EOL;
});
$process->start();
$process->setPriority(PRIO_PROCESS, -10);
进程间的相互通信
- 通过管道
$workers = [];
for ($i = 1; $i < 3; $i++) {
$process = new Swoole\Process(function (Swoole\Process $worker) {
var_dump($worker);
$data = $worker->read();
echo "Child {$worker->pid}:来自 Master 的数据 - \"{$data}\"。", PHP_EOL;
$worker->write("发送给领导,来自 {$worker->pid} 的数据,通过管道 {$worker->pipe} !");
});
$process->start();
$workers[$process->pid] = $process;
}
foreach ($workers as $pid => $process) {
$process->write("{$pid},你好!");
$data = $process->read();
echo "Master:来自子进程 {$pid} 的数据 - \"{$data}\"", PHP_EOL;
}
// [root@localhost source]# php 3.4进程间通信.php
// Child 2076:来自 Master 的数据 - "2076,你好!"。
// Master:来自子进程 2076 的数据 - "发送给领导,来自 2076 的数据,通过管道 4 !"
// Child 2077:来自 Master 的数据 - "2077,你好!"。
// Master:来自子进程 2077 的数据 - "发送给领导,来自 2077 的数据,通过管道 6 !