Swoole笔记

Swoole是一个PHP扩展,提供了PHP语言的异步多线程服务器,异步TCP/UDP网络客户端,异步MySQL,异步Redis,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件读写,异步DNS查询。 Swoole内置了Http/WebSocket服务器端/客户端、Http2.0服务器端。

源码安装

wget -O swoole.zip https://github.com/swoole/swoole-src/archive/v1.9.11.zip
unzip swoole.zip
cd swoole
phpize
./configure
make && make install

pecl安装

yum install -y gcc gcc-c++ make cmake bison autoconf
pecl install swoole
pecl install redis

在php.ini文件到中添加:

[swoole]
extension = /usr/lib64/php/modules/swoole.so

或者添加:extension=swoole.so

查看当前swoole的版本:

php --ri swoole

Swoole的进程模型

Swoole目前总共有三种运行模式,默认为多进程模式(SWOOLE_PROCESS)。

# Base模式(SWOOLE_BASE)
传统的异步非阻塞Server,reactor和worker是同一个角色。TCP连接是在worker进程中维持的。
如果客户端连接之间不需要交互,可以使用BASE模式。如Memcache、Http服务器等。

# 线程模式
多线程Worker模式,Reactor线程来处理网络事件轮询,读取数据。得到的请求交给Worker线程去处理。

缺点:一个线程发生内存错误,整个进程会全部结束。
由于PHP的ZendVM在多线程模式存在内存错误,多线程模式在v1.6.0版本后已关闭。

# 进程模式
与多线程Worker模式不同的是,线程换成了进程。Reactor线程来处理网络事件轮询,读取数据。得到的请求交给Worker进程去处理。适合业务逻辑非常复杂的场景。如WebSocket服务器等。
$serv = new swoole_server(string $host, int $port, int $mode = SWOOLE_PROCESS, int $sock_type = SWOOLE_SOCK_TCP);

实例分析

我们来使用实例进行分析:

<?php
$server = new \swoole_server("127.0.0.1",9501);//默认是多进程模式、TCP类型
$server->on('connect', function ($serv, $fd){ });
$server->on('receive', function ($serv, $fd, $from_id, $data){ });
$server->on('close', function ($serv, $fd){ });
$server -> start();

继续在Shell中输入以下命令:

php swoole_tcp_server.php
pstree -ap|grep swoole_tcp_server
  |   |       `-php,2454 swoole_tcp_server.php
  |   |           |-php,2456 swoole_tcp_server.php
  |   |           |   `-php,2458 swoole_tcp_server.php

从系统的输出中,我们可以很容看出server其实有3个进程,进程的pid分别是2454、2456、2458,其中2454是2456的父进程,而2456又是2458的父进程。

所以,其实我们虽然看起来只是启动了一个Server,其实最后产生的是三个进程。

这三个进程中,所有进程的根进程(2454),就是所谓的Master进程;而2456进程,则是Manager进程;最后的2458进程,是Worker进程。

基于此,我们简单梳理一下,当执行的start方法之后,发生了什么:

  • 守护进程模式下,当前进程fork出Master进程,然后退出,Master进程触发OnMasterStart事件。
  • Master进程启动成功之后,fork出Manager进程,并触发OnManagerStart事件。
  • Manager进程启动成功时候,fork出Worker进程,并触发OnWorkerStart事件。
非守护进程模式下,则当前进程直接作为Master进程工作。

所以,一个最基础的Swoole Server,至少需要有3个进程,分别是Master进程、Manager进程和Worker进程。

事实上,一个多进程模式下的Swoole Server中,有且只有一个Master进程;有且只有一个Manager进程;却可以有n个Worker进程。

进程模型

Master进程是一个多线程进程,其中有一组非常重要的线程,叫做Reactor线程(组),每当一个客户端连接上服务器的时候,都会由Master进程从已有的Reactor线程中,根据一定规则挑选一个,专门负责向这个客户端提供维持链接、处理网络IO与收发数据等服务。分包拆包等功能也是在这里完成。

Manager进程,某种意义上可以看做一个代理层,它本身并不直接处理业务,其主要工作是将Master进程中收到的数据转交给Worker进程,或者将Worker进程中希望发给客户端的数据转交给Master进程进行发送。

Manager进程还负责监控Worker进程,如果Worker进程因为某些意外挂了,Manager进程会重新拉起新的Worker进程,有点像Supervisor的工作。而这个特性,也是最终实现热重载的核心机制。

Worker进程其实就是处理各种业务工作的进程,Manager将数据包转交给Worker进程,然后Worker进程进行具体的处理,并根据实际情况将结果反馈给客户端。

我们可以总结出来上面简单的Server,当客户端连接的时候这个过程中,三种进程之间是怎么协作的:

  1. Client主动Connect的时候,Client实际上是与Master进程中的某个Reactor线程发生了连接。
  2. 当TCP的三次握手成功了以后,由这个Reactor线程将连接成功的消息告诉Manager进程,再由Manager进程转交给Worker进程。
  3. 在这个Worker进程中触发了OnConnect的方法。
  4. 当Client向Server发送了一个数据包的时候,首先收到数据包的是Reactor线程,同时Reactor线程会完成组包,再将组好的包交给Manager进程,由Manager进程转交给Worker。
  5. 此时Worker进程触发OnReceive事件。
  6. 如果在Worker进程中做了什么处理,然后再用Send方法将数据发回给客户端时,数据则会沿着这个路径逆流而上。

现在,我们基于上面的例子修改代码,来看看一个简单的多进程Swoole Server的几个基本配置:

<?php
$server->set(array(
    'demonize' => false,//是否后台运行
    'reactor_num' => 2,
    'worker_num' => 4
));
$server -> start();

reactor_num:表示Master进程中,Reactor线程总共开多少个,注意,这个可不是越多越好,因为计算机的CPU是有限的,所以一般设置为与CPU核心数量相同,或者两倍即可。

worker_num:表示启动多少个Worker进程,同样,Worker进程数量不是越多越好,仍然设置为与CPU核心数量相同,或者两倍即可。

我们可以在Shell里运行,使用pstree查看进程模型结构:

php swoole_tcp_server.php
pstree -ap|grep swoole_tcp
  |   |       `-php,2505 swoole_tcp_server.php
  |   |           |-php,2507 swoole_tcp_server.php
  |   |           |   |-php,2510 swoole_tcp_server.php
  |   |           |   |-php,2511 swoole_tcp_server.php
  |   |           |   |-php,2512 swoole_tcp_server.php
  |   |           |   `-php,2513 swoole_tcp_server.php

回调函数

Swoole作为Server时,回调函数有很多。但可以简单分个类:
1) 进程启动时执行的:onStart、onManagerStart、onWorkerStart;onWorkerStop、onManagerStop、onShutdown;onWorkerError
2) 客户端交互时触发的:onReceive/onRequest/onPacket/onMessage、onOpen/onConnect、onClose
3) Task:onTask、onFinish
4) Timer:onTimer

事件执行顺序:

  • 所有事件回调均在$server->start后发生
  • 服务器关闭程序终止时最后一次事件是onShutdown
  • 服务器启动成功后,onStart/onManagerStart/onWorkerStart会在不同的进程内并发执行。
  • onReceive/onConnect/onClose/onTimer在worker进程(包括task进程)中各自触发
  • worker/task进程启动/结束时会分别调用onWorkerStart/onWorkerStop
  • onTask事件仅在task进程中发生
  • onFinish事件仅在worker进程中发生
  • onStart/onManagerStart/onWorkerStart 3个事件的执行顺序是不确定的
  • UDP协议下只有onReceive事件,没有onConnect/onClose事件
  • 如果未设置onPacket回调函数,收到UDP数据包默认会回调onReceive函数
  • onOpen事件回调是可选的:当WebSocket客户端与服务器建立连接并完成握手后会回调此函数

实际使用的时候不是所有回调都可以使用的,例如UDP服务器没有onConnect/onClose;例如接收数据,在WebSocket里使用onReceive,在HttpServer使用onRequest,在UDPServer使用onPacket。

示例:

<?php
$server = new \swoole_server("127.0.0.1",8088);

$server->set(array(
    'daemonize' => false,
    'reactor_num' => 2,
    'worker_num' => 4
));

$server->on('connect', function ($serv, $fd){ 
    echo "client connect. fd is {$fd}\n";
});

$server->on('receive', function ($serv, $fd, $from_id, $data){
    echo "client connect. fd is {$fd}\n";
});

$server->on('close', function ($serv, $fd){
    echo "client close. fd is {$fd}\n";
});

// 以下回调发生在Master进程
$server->on("start", function (\swoole_server $server){
    echo "On master start.\n";
});
$server->on('shutdown', function (\swoole_server $server){
    echo "On master shutdown.\n";
});

// 以下回调发生在Manager进程
$server->on('ManagerStart', function (\swoole_server $server){
    echo "On manager start.\n";
});
$server->on('ManagerStop', function (\swoole_server $server){
    echo "On manager stop.\n";
});

// 以下回调也发生在Worker进程
$server->on('WorkerStart', function (\swoole_server $server, $worker_id){
    echo "Worker start\n";
});
$server->on('WorkerStop', function(\swoole_server $server, $worker_id){
    echo "Worker stop\n";
});
$server->on('WorkerError', function(\swoole_server $server, $worker_id, $worker_pid, $exit_code){
    echo "Worker error\n";
});

$server -> start();

编程须知

  • 不要在代码中执行sleep以及其他睡眠函数,这样会导致整个进程阻塞
  • exit/die是危险的,会导致worker进程退出
  • 可通过register_shutdown_function来捕获致命错误,在进程异常退出时做一些请求工作,具体参看/wiki/page/305.html
  • PHP代码中如果有异常抛出,必须在回调函数中进行try/catch捕获异常,否则会导致工作进程退出
  • Worker进程不得共用同一个RedisMySQL等网络服务客户端,Redis/MySQL创建连接的相关代码可以放到onWorkerStart回调函数中。原因是如果共用1个连接,那么返回的结果无法保证被哪个进程处理。持有连接的进程理论上都可以对这个连接进行读写,这样数据就发生错乱了。具体参考/wiki/page/325.html
  • 不能使用类的属性保存客户端连接信息,因为一个worker进程可以处理多个客户端连接,导致类属性数据错乱。常量则是可以的。

重新打开日志

在1.8.11及之后版本支持重新打开日志:向Server主进程发送SIGRTMIN信号。假设主进程id是3427,那么我们可以:

kill -34 3427

注:SIGRTMIN信号的id是34。通过kill -l查看。

那么如何利用这个特征实现每天自动写入新的日志文件里面呢?

假设日志文件是/log/swoole.log,我们可以在每天0点运行shell命令:

mv /log/swoole.log /log/$(date -d '-1 day' +%y-%m-%d).log
kill -34 $(ps aux|grep swoole_task|grep swoole_task_matser|grep -v grep|awk '{print $2}') # 找到主进程,需要提前命名

我们也可以把master进程的PID写入到文件

$server->set(array(
    'pid_file' => __DIR__.'/server.pid',
));

在Server关闭时自动删除PID文件。此选项在1.9.5或更高版本可用。

信号管理

Swoole支持的信号:

SIGKILL -9 pid 强制杀掉进程
SIGUSR1 -10 master_pid 重启所有worker进程
SIGUSR2 -12 master_pid 重启所有task_worker进程
SIGRTMIN -34 master_pid 重新打开日志(版本1.8.11+)

master_pid代表主进程pid。示例(假设主进程名称是swoole_server,pid是3427):

# 杀掉进程swoole_server
kill -9 $(ps aux|grep swoole_server|grep -v grep|awk '{print $2}')

# 重启swoole_server的worker进程
kill -10 $(ps aux|grep swoole_server|grep -v grep|awk '{print $2}')

# 重新打开日志
kill -34 3427

Task

我们可以在worker进程中投递一个异步任务到task_worker池中。此函数是非阻塞的,执行完毕会立即返回。worker进程可以继续处理新的请求。

通常会把耗时的任务交给task_worker来处理。

我们可以通过如下代码判断是Worker进程还是TaskWorker进程:

function onWorkerStart($serv, $worker_id) {
    if ($worker_id >= $serv->setting['worker_num']) {  //超过worker_num,表示这是一个task进程
        echo '这是task进程';
    }
}

或者:
function onWorkerStart($serv, $worker_id) {
    if (!$serv -> taskworker()){
        echo '这是worker进程';
    }
}

看一个示例:

<?php
$server = new \swoole_server("127.0.0.1",8088);

$server->set(array(
    'daemonize' => false,
    'reactor_num' => 2,
    'worker_num' => 1,
    'task_worker_num' => 1,
));

$server->on('start', function ($serv){ 
    swoole_set_process_name("swoole_task_matser"); //主进程命名
});

$server->on('connect', function ($serv, $fd){ 
    echo "client connect. fd is {$fd}\n";
});

$server->on('receive', function ($serv, $fd, $from_id, $data){
    
    echo sprintf("onReceive. fd: %d , data: %s\n", $fd, json_encode($data) );
    
    $serv->task(json_encode([
        'fd' => $fd,
        'task_name' => 'send_email',
        'email_content' => $data,
        'email' => 'admin@qq.com'
    ]));
});

$server->on('close', function ($serv, $fd){
    echo "client close. fd is {$fd}\n";
});

$server->on('task', function (swoole_server $serv, $task_id, $from_id,  $data){
    echo $data;
    
    $data = json_decode($data, true);
    $serv->send($data['fd'], "send eamil to {$data['email']}, content is : {$data['email_content']}\n");
    
    //echo 'task finished';
    //return 'task finished';
    $serv->finish('task finished');
});

$server->on('finish', function (swoole_server $serv, $task_id, $data){
    echo 'onFinish:' .$data;
});

$server -> start();

这里新建了一个tcp服务器,参数里设置worker_num进程为1,task_worker_num为1。

配置了task_worker_num参数后将会启用task功能,swoole_server务必要注册onTask/onFinish2个事件回调函数。如果没有注册,服务器程序将无法启动。

onTask回调接收4个参数,分别是serv对象、任务ID、自于哪个worker进程、任务的内容。注意的是,$data必须是字符串。我们可以在worker进程里使用`swoole_server->task
($data)`进行任务投递。

onFinish回调用于将处理结果告知worker进程,此回调必须有,但是否被调用由OnTask决定。在OnTask里使用return或者finish()可以将处理结果发生到onFinish回调,否则onFinish回调是不会被调用的。也就是说:finish()是可选的。如果worker进程不关心任务执行的结果,不需要调用此函数。onFinish回调里的$data同样必须是字符串。

我们新起一个窗口,使用telnet发送消息到服务端进行测试:
client端:

telnet 127.0.0.1 8088
Trying 127.0.0.1...
Connected to 127.0.0.1.

hhh
send eamil to admin@qq.com, content is : hhh

server端:

client connect. fd is 1
onReceive. fd: 1 , data: "hhh\r\n"
{"fd":1,"task_name":"send_email","email_content":"hhh\r\n","email":"admin@qq.com"}
onFinish:task finished

onFinish回调里不使用return或者finish(),我们将看不到server端最后一行输出。

此时服务器进程模型:

pstree -ap | grep swoole
  |   |       `-php,3190 swoole_task.php
  |   |           |-php,3192 swoole_task.php
  |   |           |   |-php,3194 swoole_task.php
  |   |           |   `-php,3195 swoole_task.php

看到两个worker进程,其中一个是worker进程,另外一个是task_worker进程。

定时器

Swoole提供强大的异步毫秒定时器,基于timerfd+epoll实现。主要方法:
1、swoole_timer_tick:周期性定时器,类似于JavaScript里的setInterval() 
2、swoole_timer_after:一次性定时器。
3、swoole_timer_clear:清除定时器。

# 周期性定时器
int swoole_timer_tick(int $ms, callable $callback, mixed $user_param);

# 一次性定时器
swoole_timer_after(int $after_time_ms, mixed $callback_function, mixed $user_param);

# 清除定时器
bool swoole_timer_clear(int $timer_id)

# 定时器回调函数
function callbackFunction(int $timer_id, mixed $params = null);

注意:

  • $ms 最大不得超过 86400000。
  • manager进程中不能添加定时器。
  • 建议在WorkerStart回调里写定时器。

定时器示例:

$server->on('WorkerStart', function (\swoole_server $server, $worker_id){
    if ($server->worker_id == 0){//防止重复
        //每隔2000ms触发一次
        swoole_timer_tick(2000, function ($timer_id) {
            echo "tick-2000ms\n";
        });

        //3000ms后执行此函数
        swoole_timer_after(3000, function () {
            echo "after 3000ms.\n";
        });
    }
});

WebSocket

使用Swoole可以很简单的搭建异步非阻塞多进程的WebSocket服务器。

WebSocket服务器

<?php
$server = new swoole_websocket_server("0.0.0.0", 9501);

$server->set(array(
    'daemonize' => false,
    'worker_num' => 2,
));

$server->on('Start', function (swoole_websocket_server $server) {
    echo "Server Start... \n";
    swoole_set_process_name("swoole_websocket_server");
});

$server->on('ManagerStart', function (swoole_websocket_server $server) {
    echo "ManagerStart\n";
});

$server->on('WorkerStart', function (swoole_websocket_server $server, $worker_id) {
    echo "WorkerStart \n";
    if ($server->worker_id == 0){
        swoole_timer_tick(10000,function($id) use ($server) {
            echo "test timer\n";
        });
    }
});

$server->on('Open', function (swoole_websocket_server $server, $request) {
    echo "server: handshake success with fd{$request->fd}\n";
});

$server->on('Message', function (swoole_websocket_server $server, $frame) {
    echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
    $server->push($frame->fd, "this is server");
});

$server->on('Close', function ($ser, $fd) {
    echo "client {$fd} closed\n";
});

$server->start();

shell里直接运行php swoole_ws_server.php启动即可。如果设置了后台运行,可以使用下列命令强杀进程:

kill -9 $(ps aux|grep swoole|grep -v grep|awk '{print $2}')

或者重新启动worker进程:

kill -10 $(ps aux|grep swoole_websocket_server|grep -v grep|awk '{print $2}')

输出:

[2017-06-01 22:06:21 $2479.0]    NOTICE    Server is reloading now.
WorkerStart 
WorkerStart 

注意:

  • onMessage回调函数为必选,当服务器收到来自客户端的数据帧时会回调此函数。
/**
* @param $server
* @param $frame 包含了客户端发来的数据帧信息;使用$frame->fd获取fd;$frame->data获取数据内容
*/
function onMessage(swoole_server $server, swoole_websocket_frame $frame)
  • 使用$server->push()向客户端发送消息。长度最大不得超过2M。发送成功返回true,发送失败返回false。
function swoole_websocket_server->push(int $fd, string $data, int $opcode = 1, bool $finish = true);

WebSocket客户端

最简单的是使用JS编写:

socket = new WebSocket('ws://192.168.1.107:9501/'); 
socket.onopen = function(evt) { 
    // 发送一个初始化消息
    socket.send('I am the client and I\'m listening!'); 
}; 

// 监听消息
socket.onmessage = function(event) { 
    console.log('Client received a message', event); 
}; 

// 监听Socket的关闭
socket.onclose = function(event) { 
    console.log('Client notified socket has closed',event); 
}; 

socket.onerror = function(evt) { 
    console.log('Client onerror',event); 
}; 

Swoole里没有直接提供swoole_websocket客户端,不过通过引入WebSocketClient.php文件可以实现:

<?php

require_once __DIR__ . '/WebSocketClient.php';

$client = new WebSocketClient('192.168.1.107', 9501);

if (!$client->connect())
{
    echo "connect failed \n";
    return false;
}

$send_data = "I am client.\n";
if (!$client->send($send_data))
{
    echo $send_data. " send failed \n";
    return false;
}

echo "send succ \n";
return true;

上面代码实现的是一个同步的swoole_websocket客户端。发送完消息会自动关闭,可以用来与php-fpm应用协作:将耗时任务使用客户端发送到swoole_websocket_server。

如何创建一个聊天室

实际项目里,我们可以将用户uid和fd进行双向绑定(暂不考虑多台服务器分布式部署情况),例如使用Redis保存:在onMessage进行用户信息验证后:

$this->redis->set($fd, $uid);
$this->redis->set($uid, $fd);

后续需要指定给某人发消息,只需要根据uid/fd发送即可。在onClose事件里进行解绑操作。群发的话只需要遍历一遍$server->connections即可。

示例(该项目只实现群发):
moell-peng/webim: PHP + Swoole 实现的简单聊天室
https://github.com/moell-peng...

HttpServer

swoole内置Http服务器的支持。swoole版的http server相对于php-fpm,最大优势在于高性能:代码一次载入内存,后续无需再解释执行。缺点是调试没有nginx+php-fpm方便。

使用swoole,通过几行代码即可写出一个异步非阻塞多进程的Http服务器:

<?php
$serv = new swoole_http_server("0.0.0.0", 9502);

$serv->on('Start', function() {
    echo 'Start';
});

$serv->on('Request', function($request, $response) {
    var_dump($request->get);
    var_dump($request->post);
    var_dump($request->cookie);
    var_dump($request->files);
    var_dump($request->header);
    var_dump($request->server);

    $response->cookie("User", "Swoole");
    $response->header("X-Server", "Swoole");
    $response->end("<h1>Hello Swoole!</h1>");
});

$serv->start();

shell里使用php swoole_http_server.php运行server。浏览器打开http://192.168.1.107:9502/即可看到输出。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值