Swoole实战代码笔记

Swoole入门到实战代码笔记

一部分是我去年在慕课网买的丝袜老师的视频教程,学完之后根据自己的理解和在其他地方学到的东西整理出来的笔记.

一部分是之前项目中用到和学到的东西.

swoole基于docker时要用到的初始化环境

更换163源

apt-get -y clean\
    && echo "deb http://mirrors.163.com/ubuntu/ bionic main restricted universe multiverse" > /etc/apt/sources.list \
    && echo "deb http://mirrors.163.com/ubuntu/ bionic-security main restricted universe multiverse" >> /etc/apt/sources.list \
    && echo "deb http://mirrors.163.com/ubuntu/ bionic-updates main restricted universe multiverse" >> /etc/apt/sources.list \
    && echo "deb http://mirrors.163.com/ubuntu/ bionic-proposed main restricted universe multiverse" >> /etc/apt/sources.list \
    && echo "deb http://mirrors.163.com/ubuntu/ bionic-backports main restricted universe multiverse" >> /etc/apt/sources.list \
    && echo "deb-src http://mirrors.163.com/ubuntu/ bionic main restricted universe multiverse" >> /etc/apt/sources.list \
    && echo "deb-src http://mirrors.163.com/ubuntu/ bionic-security main restricted universe multiverse" >> /etc/apt/sources.list \
    && echo "deb-src http://mirrors.163.com/ubuntu/ bionic-updates main restricted universe multiverse" >> /etc/apt/sources.list \
    && echo "deb-src http://mirrors.163.com/ubuntu/ bionic-proposed main restricted universe multiverse" >> /etc/apt/sources.list \
    && echo "deb-src http://mirrors.163.com/ubuntu/ bionic-backports main restricted universe multiverse" >> /etc/apt/sources.list \
&& apt-get -y update \
&& apt-get -y install sudo git vim curl wget net-tools iputils-ping

安装PHP7.2

cd ~/ \
&& apt install php7.2 php7.2-dev php7.2-redis -y \
&& apt install mysql-server -y \
&& apt install redis-server -y \
&& wget https://github.com/redis/hiredis/archive/v0.13.3.tar.gz \
&& tar -zxf v0.13.3.tar.gz \
&& cd hiredis-0.13.3/ \
&& make -j \
&& make install \
&& ldconfig \
&& cd ../ \
&& wget https://pecl.php.net/get/swoole-4.2.1.tgz \
&& tar -zxf swoole-4.2.1.tgz \
&& cd swoole-4.2.1 \
&& phpize \
&& ./configure --with-php-config=php-config --enable-async-redis \
&& make clean \
&& make -j \
&& make install \
&& echo "extension=swoole.so" > /etc/php/7.2/mods-available/swoole.ini \
&& ln -s /etc/php/7.2/mods-available/swoole.ini /etc/php/7.2/cli/conf.d/

swoole开发注意事项

注意

如果将swoole应用于框架中时,框架中有这些东西, 需要注意.
还有要注意的是,exit(),die()之类的也需要特别注意.
遇到这些之后,swoole也会马上被杀掉.然后重启进程.
例如我们在方法中需要返回给前端一些返回值
Response::json('100','参数错误');
如果在后面有exit()或者在Response中有exit();会导致进程杀死,返回值无法正常返回给前端.
这时候我们可以用return Response::json('100','参数错误');而Response中使用echo直接输出内容即可.

不要在代码中执行sleep以及其他睡眠函数,这样会导致整个进程阻塞
exit/die是危险的,会导致worker进程退出
可通过register_shutdown_function来捕获致命错误,在进程异常退出时做一些请求工作
PHP代码中如果有异常抛出,必须在回调函数中进行try/catch捕获异常,否则会导致工作进程退出
swoole不支持set_exception_handler,必须使用try/catch方式处理异常
Worker进程不得共用同一个Redis或MySQL等网络服务客户端,Redis/MySQL创建连接的相关代码可以放到onWorkerStart回调函数中。

swoole中的超全局变量不会被自动清除,需要手动清理

在官方文档https://wiki.swoole.com/wiki/page/324.html有介绍

局部变量

    在事件回调函数返回后,所有局部对象和变量会全部回收,不需要unset。如果变量是一个资源类型,那么对应的资源也会被PHP底层释放。
    
    function test()
    {
        $a = new Object;
        $b = fopen('/data/t.log', 'r+');
        $c = new swoole_client(SWOOLE_SYNC);
        $d = new swoole_client(SWOOLE_SYNC);
        global $e;
        $e['client'] = $d;
    }
    $a, $b, $c 都是局部变量,当此函数return时,这3个变量会立即释放,对应的内存会立即释放,打开的IO资源文件句柄会立即关闭。
    $d 也是局部变量,但是return前将它保存到了全局变量$e,所以不会释放。当执行unset($e['client'])时,并且没有任何其他PHP变量仍然在引用$d变量,那么$d 就会被释放。

全局变量

在PHP中,有3类全局变量。

使用global关键词声明的变量
使用static关键词声明的类静态变量、函数静态变量
PHP的超全局变量,包括$_GET、$_POST、$GLOBALS等
全局变量和对象,类静态变量,保存在swoole_server对象上的变量不会被释放。
需要程序员自行处理这些变量和对象的销毁工作。
据说define()定义的也不能被销毁,但是这个我没有去证实

class Test
{
    static $array = array();
    static $string = '';
}

function onReceive($serv, $fd, $reactorId, $data)
{
    Test::$array[] = $fd;
    Test::$string .= $data;
}
在事件回调函数中需要特别注意非局部变量的array类型值,某些操作如 TestClass::$array[] = "string" 可能会造成内存泄漏,严重时可能发生爆内存,必要时应当注意清理大数组。

在事件回调函数中,非局部变量的字符串进行拼接操作是必须小心内存泄漏,如 TestClass::$string .= $data,可能会有内存泄漏,严重时可能发生爆内存。

解决方法
同步阻塞并且请求响应式无状态的Server程序可以设置max_request,当Worker进程/Task进程结束运行时或达到任务上限后进程自动退出。该进程的所有变量/对象/资源均会被释放回收。
程序内在onClose或设置定时器及时使用unset清理变量,回收资源
异步客户端
Swoole提供的异步客户端与普通的PHP变量不同,异步客户端在发起connect时底层会增加一次引用计数,在连接close时会减少引用计数。

包括swoole_client、swoole_mysql、swoole_redis、swoole_http_client

function test()
{
    $client = new swoole_client(SWOOLE_TCP | SWOOLE_ASYNC);
    $client->on("connect", function($cli) {
        $cli->send("hello world\n");
    });
    $client->on("receive", function($cli, $data){
        echo "Received: ".$data."\n";
        $cli->close();
    });
    $client->on("error", function($cli){
        echo "Connect failed\n";
    });
    $client->on("close", function($cli){
        echo "Connection close\n";
    });
    $client->connect('127.0.0.1', 9501);
    return;
}
$client是局部变量,常规情况下return时会销毁。
但这个$client是异步客户端在执行connect时swoole引擎底层会增加一次引用计数,因此return时并不会销毁。
该客户端执行onReceive回调函数时进行了close或者服务器端主动关闭连接触发onClose,这时底层会减少引用计数,$client才会被销毁。

  1. 注意,swoole对超全局变量不会自动释放,但是我们这里有一个首先的置空数组,相当于一开始就释放了
  2. 但是像请求的URL什么的,就需要在框架中进行处理处理了,而且那个也不是超全局变量而是ThinkPHP一种单例的机制,
  3. 具体可以去看thinkphp的thinkphp/library/think/Request.php中的pathinfo()和path()
  4. 另一个超全局变量不会被自动回收的案例:
  5. 之前一直用这个链接增删参数做测试http://localhost:8810/index/index/nihao?a=a&b=b&c=c&d=d
  6. 后面我把链接换成http://localhost:8810/index/index/nihao?name=liuhao
  7. 时不时的命中那个进程 ,还会把上面的哪个请求给打印出来,
$http = new swoole_http_server('0.0.0.0', '8810');
$http->set([
    'enable_static_handler' => true,    //开启静态页面支持
    'document_root'         => '/workspace/imooc_swoole/thinkphp_5.1.0_rc/public/static',   //指定静态页面路径
    "worker_num"            => 5,
]);
//worker进程启动时的回调
$http->on('WorkerStart', function (swoole_server $server, $workId) {
    //加载框架中的文件
    // 定义应用目录
    define('APP_PATH', __DIR__ . '/../application/');
    // 加载基础文件, 这里不能使用start.php 要不然引导文件会被每个worker进程加载, 每次启动都会在命令行中处理5次首页的内容
    require __DIR__ . '/../thinkphp/base.php';
    // 加载框架引导文件
    //        require __DIR__ . '/../thinkphp/start.php';
});
//处理请求和响应
$http->on('request', function ($request, $response) {
  
    // 注意,swoole对超全局变量不会自动释放,但是我们这里有一个首先的置空数组,相当于一开始就释放了
    // 但是像请求的URL什么的,就需要在框架中进行处理处理了,而且那个也不是超全局变量而是ThinkPHP一种单例的机制,
    // 具体可以去看thinkphp的thinkphp/library/think/Request.php中的pathinfo()和path()
    // 另一个超全局变量不会被自动回收的案例:
    // 之前一直用这个链接增删参数做测试http://localhost:8810/index/index/nihao?a=a&b=b&c=c&d=d
    // 后面我把链接换成http://localhost:8810/index/index/nihao?name=liuhao
    // 时不时的命中那个进程 ,还会把上面的哪个请求给打印出来,
      $_SERVER = [];
    if (isset($request->server)) {
        foreach ($request->server as $k => $v) {
            $_SERVER[strtoupper($k)] = $v;
        }
    }
    if (isset($request->header)) {
        foreach ($request->server as $k => $v) {
            $_SERVER[strtoupper($k)] = $v;
        }
    }
    $_GET = [];
    if (isset($request->get)) {
        foreach ($request->get as $k => $v) {
            if($k == 's'){
                $_GET[$k] = $v;
            }else{
                $_GET[strtoupper($k)] = $v;
            }
        }
    }
    $_POST =[];
    if (isset($request->post)) {
        foreach ($request->post as $k => $v) {
            $_POST[strtoupper($k)] = $v;
        }
    }
    ob_start();
    try {
        think\Container::get('app', [APP_PATH])
                       ->run()
                       ->send();
    }catch (Exception $e){
        //todo

    }
    $result = ob_get_contents();
    ob_end_clean();
    $response->end($result);
    
    
    //关闭当前进程,自然清理了所有的超全局变量,但是得不偿失.
    //且swoole命令行会疯狂的报错.
    $http->close();
});
$http->start();

例如THinkPHP框架,第一次请求/index/index/nihao之后所有的请求都是这个,这个不是超全局变量的问题,是框架的设计机制而已 我们用ThinkPHP如Request()->get('name');获取到的一直是第一次请求的值,原因为thinkPHP的机制问题 解决方案,我们可以通过swoole的原始方式获取,或者通过我们赋值后的$_GET之类的来获取.或者将onWorkder回调中的THinkPHp的引用 放在onRqeust中,即每次请求的时候重新加载.注意在这里的时候要把require改为require_once才行

原因就是thinkphp/library/Request.php中的path()将$this->path;存了起来.
所以每次都是第一次请求的URL
解决方式就是向上面说的那样调用$http->close();//不建议, 会出问题
将path()调优.删掉外面的if (is_null($this->path)) {判断

例如THinKPHP框架中不能使用正常的pathinfo访问/index/index/nihao只能,通过s=/index/index/nihao的方式访问

//原因就是thinkphp/library/Request.php中的pathinfo();第一行添加这个判断
//要不然每次swoole启动都被后面判断为cli而不是正常的http请求
      if(isset($_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] != '/') {
            return ltrim($_SERVER['PATH_INFO'], '/');
        }

swoole的多进程管理.md

P.S

这里的课程是基于
Swoole 2.1
但是我学习的时候也在用4.0也没问题
  1. 利用swoole_process()添加一个子进程
  2. swoole_process::wait()让父进程等待回收子进程
swoole_process::__construct(callable $function, $redirect_stdin_stdout = false, $pipe_type = 2);

$function,子进程创建成功后要执行的函数,底层会自动将函数保存到对象的callback属性上。如果希望更改执行的函数,可赋值新的函数到对象的callback属性
$redirect_stdin_stdout,重定向子进程的标准输入和输出。启用此选项后,在子进程内输出内容将不是打印屏幕,而是写入到主进程管道。读取键盘输入将变为从管道中读取数据。默认为阻塞读取。
$pipe_type,管道类型,启用$redirect_stdin_stdout后,此选项将忽略用户参数,强制为1。如果子进程内没有进程间通信,可以设置为 0

0:不创建管道
1:创建SOCK_STREAM类型管道
2:创建SOCK_DGRAM类型管道
启用$redirect_stdin_stdout 后,此选项将忽略用户参数,强制为1
swoole_process ( 或 Swoole\Process) 对象在销毁时会自动关闭管道,子进程内如果监听了管道会收到CLOSE事件
使用swoole_process作为监控父进程,创建管理子process时,父类必须注册信号SIGCHLD对退出的进程执行wait,否则子process一旦被kill会引起父process exit


官方案例 在子进程中创建 Server

例 1: 可以在 swoole_process 创建的子进程中使用 swoole_server, 但为了安全必须在$process->start 创建进程后,调用 $worker->exec() 执行。 代码如下:

<?php
$process = new swoole_process('callback_function', true);

$pid = $process->start();

function callback_function(swoole_process $worker)
{
    $worker->exec('/usr/local/bin/php', array(__DIR__.'/swoole_server.php'));
}

swoole_process::wait();

例 2:使用匿名函数作为进程逻辑,并实现了一个简单的父子进程通讯

<?php
$process = new swoole_process(function (swoole_process $process) {
    $process->write('Hello');
}, true);

$process->start();
usleep(100);

echo $process->read(); // 输出 Hello

IO 线程池问题 由于Swoole的异步文件IO使用了线程池,在使用了这些API之后再创建Process可能会出现非常复杂的带线程fork问题。因此请勿在使用异步文件IO函数后创建Process。

2.1.4/1.10.4或更高版本已经禁止了这种行为,底层检测到已创建线程池再执行new Process会抛出致命错误

简单的代码案例

#!/usr/bin/php
<?php
/**
 * @Author: liuhao
 * @Date  :   2018/9/23
 * @Time  :   15:11
 * @File  :   process.php
 * @Desc  : 进程管理
 */
//一般认为这里是父进程区间
//这里设置为true的时候, 在子进程内输出内容将不是打印屏幕,而是写入到主进程管道。
$process = new swoole_process(function (swoole_process $process) {
    //一般认为这里是子进程区间
    //todo
    echo '这里是子进程的区间' . PHP_EOL;
    //使用指定的命令, 调用外部的脚本文件, 命令必须用完整的路径
    $process->exec('/usr/bin/php', [__DIR__ . '/../ws_server.php']);
}, false);
$pid     = $process->start();
//打印子进程的PID
var_dump($pid);
//让父进程等待回收子进程
swoole_process::wait();

swoole多进程的应用场景

    1. 同时执行多个URL
    1. 原始方案 同步顺序执行和多进程执行
    1. 在使用time ./curl.php运行时,使用原始方式的时候需要12S,使用swoole多进程需要2S
    1. 这里需要注意的是,要读取管道中的数据时swoole_process必须为true,读取要用子进程创建时的new swoole_process的返回值调用->read()读取
#!/usr/bin/php
<?php
$urls = [
    'http://www.baidu.com',
    'http://www.sina.com.cn',
    'http://www.qq.com',
    'http://baidu.com?search=nihao',
    'http://baidu.com?search=tahao',
    'http://baidu.com?search=wohao',
];
//传统方式执行
//foreach ($urls as $url) {
//    echo getCurl($url).PHP_EOL;
//}
//swoole多进程方式执行
$works = [];
for ($i = 0; $i < count($urls); $i++) {
    //子进程
    $process = new swoole_process(function (swoole_process $process) use ($urls, $i) {
        //每个进程执行一个curl
        $content = getCurl($urls[$i]);
        //输出的内容会放在管道中, 因为这里的swoole_process参数是true
        //        echo $content . PHP_EOL;
        //这里也可以使用这种形式来将内容写入管道
        $process->write($content . PHP_EOL);

    }, true);
    $pid     = $process->start();
    //将子进程的对方赋值到一个数组中
    $works[$pid] = $process;
}
//通过赋值后的进程数组,来逐个获取进程内输出到管道中的内容
foreach ($works as $work) {
    echo $work->read();
}
//模拟获取
function getCurl($url)
{
    //file_get_contents($url);
    sleep(2);

    return $url . '执行成功' . PHP_EOL;
}

swoole的各种异步IO

P.S

这里的课程是基于
Swoole 2.1
但是我学习的时候也在用4.0也没问题

异步,毫秒级定时器

用这个东西的时候要注意一点, 如果放在onMessage中的之类的时候每个worker都会调用一次

  1. swoole_timer_tick 每隔一段时间执行一次
  2. swoole_timer_after 一段时间后执行一次
  3. swoole_timer_clear 使用定时器ID来删除定时器
int swoole_timer_tick(int $msec, callable $callback);

设置一个间隔时钟定时器,与after定时器不同的是tick定时器会持续触发,直到调用swoole_timer_clear清除。
$msec 指定时间,单位为毫秒。如1000表示1秒,最大不得超过 86400000
$callback_function 时间到期后所执行的函数,必须是可以调用的
可以使用匿名函数的use语法传递参数到回调函数中
定时器仅在当前进程空间内有效
定时器是纯异步实现的,不能与阻塞IO的函数一起使用,否则定时器的执行时间会发生错乱

int swoole_timer_after(int $after_time_ms, mixed $callback_function);

执行成功返回定时器ID,若取消定时器,可调用 swoole_timer_clear
$after_time_ms 指定时间,单位为毫秒,最大不得超过 86400000
$callback_function 时间到期后所执行的函数,必须是可以调用的。
可以使用匿名函数的use语法传递参数到回调函数中
  
bool swoole_timer_clear(int $timer_id)
  
$timer_id,定时器ID,调用swoole_timer_tick、swoole_timer_after后会返回一个整数的ID
swoole_timer_clear不能用于清除其他进程的定时器,只作用于当前进程


//每两秒执行一次输出操作
swoole_timer_tick(2000,function($timerId){
  echo "2秒: timerId{$timerId}\n";
});

swoole_timer_after(2000,function(){
    echo "2秒: after\n";
});

异步,文件IO

  1. swoole_async_readfile();
  2. 还有一种写法,Swoole\Async::readFile();
  3. 该方法会将文件内容全部复制到内存,所以不能用于大文件的读取
  4. 如果要读取超大文件,请使用swoole_async_read函数来分段读取
  5. 该方法最大可读取4M的文件
//函数风格
swoole_async_readfile(string $filename, mixed $callback);
//命名空间风格
Swoole\Async::readFile(string $filename, mixed $callback);

swoole_async_readfile(__DIR__,"./nihao.txt", function($fileName, $fileContent){
  echo "fileName: {$fileName}" . PHP_EOL;
  echo "fileContent: {$fileContent }".PHP_EOL;
});
//所谓异步, 这时候就会发现, 先输出start再输出的文件名和文件内容
echo 'start';

  1. swoole_async_read();
  2. 与swoole_async_readfile不同,它是分段读取,可以用于读取超大文件。
bool swoole_async_read(string $filename, mixed $callback, int $size = 8192, int $offset = 0);
//在读完后会自动回调$callback函数,回调函数接受2个参数:
bool callback(string $filename, string $content);
$filename,文件名称
$content,读取到的分段内容,如果内容为空,表明文件已读完
$callback函数,可以通过return true/false,来控制是否继续读下一段内容。

  1. swoole_async_writefile();
  2. 异步写文件,最大可写入4M
  3. 超过4M的需要使用swoole_async_write()
Swoole\Async::writeFile(string $filename, string $fileContent, callable $callback = null, int $flags = 0)
  
参数1=为文件的名称,必须有可写权限,文件不存在会自动创建。打开文件失败会立即返回false
参数2为要写入到文件的内容,最大可写入4M
参数3为写入成功后的回调函数,可选
参数4为写入的选项,可以使用FILE_APPEND表示追加到文件末尾
如果文件已存在,底层会覆盖旧的文件内容
  
$content = date("Y-m-d H:i:s",time()).PHP_EOL;
swoole_async_writefile(__DIR__."nihao.txt",$content, function($fileName){
    echo "写入成功".PHP_EOL;
},FILE_APPEND);
  1. swoole_async_write
  2. 异步写文件,与swoole_async_writefile不同,该方法是分段写的
bool swoole_async_write(string $filename, string $content, int $offset = -1, mixed $callback = NULL);

异步写文件,与swoole_async_writefile不同,swoole_async_write是分段写的。
不需要一次性将要写的内容放到内存里,所以只占用少量内存。
swoole_async_write通过传入的offset参数来确定写入的位置。

当offset为-1时表示追加写入到文件的末尾
Linux原生异步IO不支持追加模式,并且$content的长度和$offset必须为512的整数倍。如果传入错误的字符串长度或者$offset写入会失败,并且错误码为EINVAL

异步MySQL

  1. 所有的SQL操作,不论增删改都用swoole_mysql->query()
  2. 其他方式都比较直观, 不在这里累述, 到时候可以去看官方手册
function swoole_mysql->query($sql, callable $callback);

function onSQLReady(swoole_mysql $link, mixed $result);
执行失败,$result为false,读取$link对象的error属性获得错误信息,errno属性获得错误码
执行成功,SQL为非查询语句,$result为true,读取$link对象的affected_rows属性获得影响的行数,insert_id属性获得Insert操作的自增ID
执行成功,SQL为查询语句,$result为结果数组

我的简单案例

#!/usr/bin/php
<?php

/**
 * Class  AsyncMysql
 *异步MySQL
 *
 * @author  liuhao <lh@btctrade.com>
 * @date    2018/9/23
 * @time    00:12
 */
class asyncMysql
{

    public $db = null;
    public $dbConfig = null;

    public function __construct()
    {
        $this->db       = new Swoole\Mysql();
        $this->dbConfig = [
            'host'     => '127.0.0.1',
            'port'     => '3306',
            'user'     => 'root',
            'password' => 'sss',
            'database' => 'swoole',
            'charset'  => 'utf8',
            'timeout'  => '2',
        ];
    }

    /**
     * Method  update
     *
     * @desc    执行update操作
     * @author  liuhao <lh@btctrade.com>
     * @date    2018/9/23
     * @time    01:02
     * @return  void
     */
    public function update()
    {
        //todo 修改的封装
    }

    /**
     * Method  query
     *
     * @desc    这里为了省事,不做拆分, 增删改查都放在一起了
     * @author  liuhao <lh@btctrade.com>
     * @date    2018/9/23
     * @time    01:02
     *
     * @param $sql
     *
     * @throws \Swoole\Mysql\Exception
     * @return  bool
     */
    public function query($sql)
    {
        //尝试连接MySQL
        $this->db->connect($this->dbConfig, function ($db, $connectResult) use ($sql) {
            echo "MySQL connecting" . PHP_EOL;
            if ($connectResult === false) {
                //连接失败,打印错误信息和错误号
                var_dump($connectResult);
                var_dump($this->db->connect_error);
                var_dump($this->db->connect_errno);

                return false;
            }
            //开始操作MySQL
            $db->query($sql, function ($db, $queryResult) {

                if ($queryResult === false) {
                    //todo 执行错误的封装
                    //SQL执行失败,打印错误信息
                    var_dump("执行失败,输出错误信息");
                    var_dump($db->error, $db->errno);

                } elseif ($queryResult === true) {
                    //todo 修改和插入的封装
                    // SQL执行成功, 打印受影响的行数
                    var_dump("执行成功,返回受影响行数, 打印插入ID");
                    var_dump($db->affected_rows);
                    var_dump($db->insert_id);

                } else {
                    //todo 查询的封装
                    //打印SQL执行结果
                    var_dump("打印执行结果");
                    var_dump($queryResult);
                }
                //执行完毕,关闭连接
                $db->close();
            });

        });

        return true;
    }
}

$asyncMysql = new AsyncMysql();
$result     = $asyncMysql->query("select * from test");
//$result = $asyncMysql->query("update test set `username`='liuhao' where id=1");
// 运行发现,先运行下面两行,后运行的上面的查询, 而执行查询的时候,先返回的true 然后是"MySQL connecting"
var_dump($result);
echo 'start' . PHP_EOL;

简单表结构


create database swoole default character set utf8 collate utf8_general_ci;

--
-- 表的结构 `test`
--

CREATE TABLE `test` (
  `id` int(10) UNSIGNED NOT NULL,
  `username` varchar(100) NOT NULL,
  `create_time` int(10) UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- 转存表中的数据 `test`
--

INSERT INTO `test` (`id`, `username`, `create_time`) VALUES
(1, 'singwa', 1520763235),
(2, 'singwa2', 1520763235),
(3, 'mooc', 1520763235);

--
-- Indexes for dumped tables
--

--
-- Indexes for table `test`
--
ALTER TABLE `test`
  ADD PRIMARY KEY (`id`),
  ADD UNIQUE KEY `id` (`id`),
  ADD KEY `username` (`username`);

--
-- 在导出的表使用AUTO_INCREMENT
--

--
-- 使用表AUTO_INCREMENT `test`
--
ALTER TABLE `test`
  MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

异步Redis

  • 4.2.x 中 redis-client 即是 异步redis客户端开启, 并非无法开启, 以实际使用为准

必须 安装hiredis库

# hiredis下载地址:https://github.com/redis/hiredis/releases

make -j
sudo make install
sudo ldconfig

必须 编译swoole

# swoole下载地址 https://pecl.php.net/get/swoole-4.2.1.tgz
/usr/bin/phpize7.2
./configure --with-php-config=/usr/bin/php-config7.2 --enable-async-redis
make clean
make -j
sudo make install

代码案例

  1. 在观看官方文档时,发现官方就体统了get和set两个方法
  2. 但是官方也提供了__call的魔术方法,在使用swoole的Redis时.
  3. 其他命令swoole未封装的,但是Redis有的,也可直接去用.也没关系
#!/usr/bin/php
<?php
$redisClient = new swoole_redis();
$redisClient->connect('127.0.0.1', '6379', function (swoole_redis $redisClient, $connectResult) {

    //打印开始连接, 同时打印, 连接后的返回值
    var_dump("开始连接Redis");
    var_dump($connectResult);
    //切换Redis库,将库指定到2号库
      $redisClient->select('2', function (swoole_redis $swooleRedis, $selectResult) {
        // 打印切换库,同时输出Redis切换库的返回值
        var_dump("切换Redis库");
        var_dump($selectResult);
        //关闭连接
        $swooleRedis->close();
    });

    //写入数据
    $redisClient->set('nihao', date('Y-m-d H:i:s', time()), function (swoole_redis $swooleRedis, $setResult) {
        // 打印写入,同时打印写入后的返回值
        var_dump("写入Redis数据");
        var_dump($setResult);
        //关闭连接
        $swooleRedis->close();
    });
    //获取数据
    $redisClient->get('nihao', function (swoole_redis $swooleRedis, $getResult) {
        //打印读取到的数据
        var_dump("读取数据");
        var_dump($getResult);
        $swooleRedis->close();

    });
    //使用redis有但是swoole未定义的方法来获取参数,因为swoole有__call魔术方法
    $redisClient->keys("*", function (swoole_redis $swooleRedis, $keysResult) {
        //打印获取到的数据
        var_dump("打印所有的key");
        var_dump($keysResult);
        $swooleRedis->close();
    });

});
//你会发现,这里的输出反而先运行
echo "start " . PHP_EOL;

Swoole的协程

P.S

这里的课程是基于
Swoole 2.1
但是我学习的时候也在用4.0也没问题

直接使用swoole的协程

  • 直接使用会报错,因为要求协程只能在各种回到方法中使用
PHP Fatal error:  Swoole\Coroutine\Redis::connect(): must be called in the coroutine. in /workspace/swoole/imooc/coroutine/redis.php on line 11

  • 直接使用的代码案例
#!/usr/bin/php
<?php
/**
 * @Author: liuhao
 * @Date  :   2018/9/24
 * @Time  :   01:14
 * @File  :   redis.php
 * @Desc  : ...
 */
$redis = new Swoole\Coroutine\Redis();
$redis->connect('127.0.0.1', '6379');
$valude = $redis->get('nihao');

在回调函数用正常使用swoole的协程的案例

    1. 明显优势,普通的写法时,同时用MySQL和Redis的时候,响应时间是,程序处理时间+Redis处理时间+MySQL处理时间的和
    1. 使用swoole的协程Redis和MySQL后,响应时间为:程序处理时间+(MySQL处理时间,Redis处理时间)两者运行时间最大的那个
    1. 开发者可以无感知的用同步的代码编写方式达到异步IO的效果和性能,避免了传统异步回调所带来的离散的代码逻辑和陷入多层回调中导致代码无法维护。

#!/usr/bin/php
<?php
/**
 * @Author: liuhao
 * @Date  :   2018/9/24
 * @Time  :   01:14
 * @File  :   redis.php
 * @Desc  : ...
 */
//将Redis协程写在http的onRequest回调中,可以正常使用
$http = new swoole_http_server('0.0.0.0', 8811);
//处理接收到的http请求, 这里的$request是用户提交的数据, $response是调用给用户响应的方法
$http->on('request', function ($request, $response) {
    //使用swoole的协程Redis读取数据,这里是可以正常使用的
    $redis = new Swoole\Coroutine\Redis();
    $redis->connect('127.0.0.1', '6379');
    $value = $redis->get('nihao');
    //swoole协程MySQL读取数据
    $mysql = new \Swoole\Coroutine\Mysql();
    $mysql->connect([
        'host'     => '127.0.0.1',
        'port'     => 3306,
        'user'     => 'root',
        'password' => 'sss',
        'database' => 'mysql',
    ]);
    $mysqlResult = $mysql->query('show tables');
    $mysqlResult = json_encode($mysqlResult);
    //设置响应头
    $response->header('Content-Type', 'text/html;charset=utf-8');
    //返回响应内容
    $response->end("通过swoole协程的Redis获取到的数据: {$value}; mysql数据: {$mysqlResult}");
});
$http->start();

swoole简单各种server服务

P.S

这里的课程是基于
Swoole 2.1
但是我学习的时候也在用4.0也没问题

server 事件中可以使用一下四种回调

  • 4种PHP回调函数风格

匿名函数

$server->on('Request', function ($req, $resp) {
    echo "hello world";
});

类静态方法

class A
{
    static function test($req, $resp)
    {
        echo "hello world";
    }
}
$server->on('Request', 'A::Test');
$server->on('Request', array('A', 'Test'));

函数

function my_onRequest($req, $resp)
{
    echo "hello world";
}
$server->on('Request', 'my_onRequest');

对象方法


class A
{
    function test($req, $resp)
    {
        echo "hello world";
    }
}

$object = new A();
$server->on('Request', array($object, 'test'));

官方tcp/ip服务器

//创建Server对象,监听 127.0.0.1:9501端口,以默认的多进程,tcp服务启动
$serv = new swoole_server("127.0.0.1", 9501,SWOOLE_PROCESS,SWOOLE_SOCK_TCP ); 

//设置swoole_server运行时的各项参数
$serv->set(array(
    'worker_num' => 4,    //worker 进程数, CPU的1-4倍
    'max_request' => 10000,
    //其他配置,如: 开启日志,之类的都可以在这里设置,具体查看手册
));

/**
$fd 客户端的唯一标识
$reactor_id  线程ID,来自哪个Reactor线程
*/
//监听连接进入事件
$serv->on('connect', function ($serv, $fd, $reactor_id) {  
    echo "Client: {$reactor_id}-{$fd}Connect.\n";
});

//监听数据接收事件
$serv->on('receive', function ($serv, $fd, $from_id, $data) {
    $serv->send($fd, "Server:  {$reactor_id}-{$fd".$data);
});

//监听连接关闭事件
$serv->on('close', function ($serv, $fd) {
    echo "Client: Close.\n";
});

//启动服务器
$serv->start(); 

简易TCP/IP客户端

<?php
  $client = new swoole_client(SWOOLE_SOCK_TCP);
if($client->connect("127.0.0.1",9501))
{
  echo "连接失败";
  exit;
}

//PHP cli常量 提醒用户输入内容, 同时获取用户输入内容
fwrite(STDOUT, "请输入发送内容");
$msg = trim(fgets(STDIN));

//发送消息给tcp/ip服务器
$client->send($msg);

//接收来自服务器的数据
$result = $client->recv();
echo $result;$$

UDP服务器和客户端不在累述, 基本上就是和TCP的一样,就是new 的时候的参数不一样

简单的http服务器

http服务继承自tcp/ip服务,基本上tcp/ip的方法这个都可以用.

<?php
    $http = swoole_http_server("o.0.0.0",8080);
    $http->set([
    'enable_static_handler'=>true,    // 支持静态页面展示
    'document_root'=>'/www',    // 网站根目录,如果访问的静态页面存在, 则不会走下面的request事件回调
    //其他配置如开启http2,开启hppts,日志之类的都是在后面设置,具体查看手册,http2有需要依赖nghttp2库需要高版本openssl必须支持TLS1.2、ALPN、NPN
    'ssl_cert_file' => $ssl_dir . '/ssl.crt',
    'ssl_key_file' => $ssl_dir . '/ssl.key',
    'open_http2_protocol' => true,
    ]);

    //Http请求对象,保存了Http客户端请求的相关信息,包括GET、POST、COOKIE、Header等。
    //在收到一个完整的Http请求后,会回调此函数。回调函数共有2个参数:
    //$request,Http请求信息对象,包含了header/get/post/cookie等相关信息
    //$response,Http响应对象,支持cookie/header/status等Http操作
    //在onRequest回调函数返回时底层会销毁$request和$response对象,如果未执行$response->end()操作,底层会自动执行一次$response->end("")
    //简单来说就是,用$request回调来获取请求, $response来设置响应
    $http->on('request',function($request,$response){
        
        $content = [
          'date: '   => date('Y-m-d H:i:s',time());
          'get: '    =>$request->get,
          'post: '   =>$request->post,
          'header: ' =>$request->header,
        ];
      //这里调用异步IO使用追加模式写入文件, 每次内容不得大于4M
        swoole_async_writefile(__DIR__ ."access.log",json_encode($content).PHP_EOL,function($fileName){
          //todo
        }FILE_APPEND);
    
          //打印各种请求参数
          var_dum($request->get);
          var_dum($request->post);
          var_dump($request->get);
          var_dump($request->post);
          var_dump($request->cookie);
          var_dump($request->files);
          var_dump($request->header);
          var_dump($request->server);//这个里面还会有很多东西, 比如request_uri

          //设置cookie参数
          $response->cookie("name","liuhao",time()+10000);
          //设置响应头信息
          $response->header("Content-Type", "text/html; charset=utf-8");
          // 向客户端输出信息
          $response->end("<h1>This is swoole http server</h1>".json_encode($request->get));

    );

    $http->start();

nginx+swoole配置

server {
    root /data/wwwroot/;
    server_name local.swoole.com;

    location / {
        if (!-e $request_filename) {
             proxy_pass http://127.0.0.1:9501;
             proxy_http_version 1.1;
             proxy_set_header Connection "keep-alive";
        }
    }
}

简单的websocket服务器

  1. websocker 服务继承自Http服务, 很多http服务在这里都是可以用的.
  2. 包括上线的http服务器中的set 的enable_static_handler document_root
  3. websocket的push推送只支持2M以下的数据
<?php

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

//这里也可以使用http服务的一些配置,用来做页面的渲染也是可以的
//因为websocket服务是基于Http服务做得
//注意,要通过浏览器来访问才能得到页面.
//页面一旦被这里命中,后面的代码就不会运行了,
//但是注意, 最好不要这么用.各是各的最好
$http->set([
    'enable_static_handler'=>true,    // 支持静态页面展示
    'document_root'=>'/www',    // 网站根目录,如果访问的静态页面存在, 则不会走下面的request事件回调
]);

//打开终端
$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";
  //这里的fd就是客户端标识,是一个ID号
    $server->push($frame->fd, "this is server");
});

//关闭连接时,触发的回调函数
$server->on('close', function ($ser, $fd) {
    echo "client {$fd} closed\n";
});

$server->start();

简单的websocket服务器的面向对象方式的写法

#!/usr/bin/php
<?php

/**
 * Class  Ws
 * webSocket代码优化
 *
 * @author  liuhao <lh@btctrade.com>
 * @date    2018/9/22
 * @time    19:06
 */
class Ws
{

    const   HOST = '0.0.0.0';
    const   PORT = '8810';

    private $ws = null;

    public function __construct()
    {
        //激活swoole的句柄
        $this->ws = new swoole_websocket_server(self::HOST, self::PORT);
        //激活连接回调
        $this->ws->on('open', [$this, 'onOpen']);
        //激活接收消息的回调
        $this->ws->on('message', [$this, 'onMessage']);
        //激活连接断开的回调
        $this->ws->on('close', [$this, 'onClose']);
        //打开服务
        $this->ws->start();
    }

    public function onOpen($ws, $request)
    {
        //打印客户端连接ID
        var_dump($request->fd);
    }

    public function onMessage($ws, $frame)
    {
        //接收消息后输出客户端提交的信息, 同时推送消息给客户端
        echo "客户端提交的内容: {$frame->data}\n";
        $ws->push($frame->fd, "服务器推送内容: " . date('Y-m-d H:i:s', time()) . "\n客户端提交内容: {$frame->data}\n");
    }

    public function onClose($ws, $fd)
    {
        echo "clientId{$fd}\n";
    }
}

$ws = new Ws();

task任务的使用

  1. 必须onTask回调函数(处理task任务)
  2. 必须onFinish回调函数(处理完毕后到这里接收)
  3. 必须set设置task_worker_num
  4. 当task任务过多的时候可以向task传入多个变量,比如: $method,$data;而$method作为指定要调用的函数名,$data为指定函数传入的参数.
#!/usr/bin/php
<?php

/**
 * Class  Ws
 * webSocket代码优化
 *
 * @author  liuhao <lh@btctrade.com>
 * @date    2018/9/22
 * @time    19:06
 */
class Ws
{

    const   HOST = '0.0.0.0';
    const   PORT = '8810';

    private $ws = null;

    public function __construct()
    {
        //激活swoole的句柄
        $this->ws = new swoole_websocket_server(self::HOST, self::PORT);
        $this->ws->set([
            'worker_num'      => 2,
            'task_worker_num' => 2,
        ]);
        
        //因为websocket是继承自httpServer而来的,所以这里也可以使用http的配置和功能

        
        //激活连接回调
        $this->ws->on('open', [$this, 'onOpen']);
        //激活接收消息的回调
        $this->ws->on('message', [$this, 'onMessage']);
        //task任务处理的回调
        $this->ws->on('task', [$this, 'onTask']);
        //task任务处理完毕的回调
        $this->ws->on('finish', [$this, 'onFinish']);
        //激活连接断开的回调
        $this->ws->on('close', [$this, 'onClose']);
        //打开服务
        $this->ws->start();
    }

    public function onOpen($ws, $request)
    {
        //打印客户端连接ID
        var_dump($request->fd);
        if ($request->fd ==1) {
          //启动异步毫秒级定时器,没2秒打印一次
          swoole_timer_tick(2000,function($timerId){
              echo "2秒: timerId{$timerId}\n";
            });
        }
    }

    public function onMessage($ws, $frame)
    {
        //10S 会发送的消息
        $data = [
            'task' => 1,
            'fd'   => $frame->fd,
        ];
        $ws->task($data);
        //接收消息后输出客户端提交的信息, 同时推送消息给客户端
        swoole_timer_after(2000,function(){
          echo "2秒: after\n";
           $ws->push($frame->fd, "这是连接2秒后服务器推送内容: " . date('Y-m-d H:i:s', time()) . "\n客户端提交内容: {$frame->data}\n");
        });
        echo "客户端提交的内容: {$frame->data}\n";
        $ws->push($frame->fd, "服务器推送内容: " . date('Y-m-d H:i:s', time()) . "\n客户端提交内容: {$frame->data}\n");
    }

    //单个task任务时的解决方案
    public function onTask($serv, $taskId, $workId, $data)
    {
        //这里是异步运行的, 运行可知,
        //这里的var_dump在连接后直接输出,而3秒后才输出onFinish中的内容
        var_dump($data);
        //耗时3S
        sleep(3);

        return "task任务结束"; //这里return 的数据在finish中可以接收的到
        
    }
    
    //当task任务过多时的解决方案,task只接受一个自定义参数,即第四个参数$data,这个顺序是定死的
    public function onTask($serv, $taskId, $workId, $data)
    {
       //可以在library新建一个Task.php里面作为所有task任务的封装,比如Task.php里面有 发邮件的sendEmail 发短信的SendSms
       //如果Task.php有命名空间记得带上
       //这里也可以使用call_user_func()来调用
       //但是要注意,一点, 比如在swoole的http服务中,用户去请求一个发送邮件的接口.在接口中调用$http->task()这时候swoole已经被new过了
       //肯定是调用不到的,这时候我们可以在onRequest()中,即http请求接收中,将httpServer对象放到$_POST['http_server']中(最好不用放到$_GET中)
       //随后在接口中调用使用$_POST['http_server']->task()即可,在手动调用task()时,第一个参数是指定的参数,第二个参数是指定调用的task的ID.这都是定死的.
       //怕出错的话,可以在这里调用的时候try catch一下即可
       $taskClass = new Task();
       $method = $data['method'];
       $taskResult =  $taskClass->$method($data['data']); //或者这样写 $taskResult = call_user_func([$taskClass, $data['method']], $data['data']);
       if(!$taskResult){
           return fasle;
       }
       
       return trues;
    }

    public function onFinish($serv, $taskId, $data)
    {
        echo "taskID: {$taskId}\n";
        echo "finish接收到的数据: {$data}";  //这个是task完成会return 上来的数据,部署传给task的数据
    }

    public function onClose($ws, $fd)
    {
        echo "clientId{$fd}\n";
    }
}

$ws = new Ws();

基于swoole的毫秒级定时器的简单的进程监控

<?php

/**
 * @Author: liuhao
 * @Date  :   2018/11/22
 * @Time  :   19:19
 * @File  :   php.php
 * @Desc  : ...
 */
class  Server
{

    const POST = 8811;

    /**
     * Method  port
     *
     * @desc    端口监控
     * @author  liuhao <lh@btctrade.com>
     * @date    2018/11/22
     * @time    19:26
     * @return  void
     */
    public function port()
    {
        $shell  = 'netstat -anp 2>/dev/null | grep ' . self::POST . ' | grep LISTEN | wc -l';
        $result = shell_exec($shell);
        if ($result == 1) {
            echo '进程存活中: ' . date('Y-m-d H:i:s', time()) . PHP_EOL;
        }
        echo '进程不存在: ' . date('Y-m-d H:i:s', time()) . PHP_EOL;
    }
}

//2秒一次进程的端口监控
swoole_timer_tick('2000', function ($timerId) {
    (new Server())->port();
    echo 'time_start' . PHP_EOL;
});

swoole平滑重启

sigterm 信号源主要是用于停止服务器用的的,这里不说
sigusr1 用于重启worker进程, 这里我们只说这个
sigusr2 用于重启task进程, 要重启task进程只要把下面的shell改成-USR2即可

简单来说不用swoole代码直接操作也是可以的:
#重启所有worker进程
kill -USR1 主进程PID

#仅重启task进程
kill -USR2 主进程PID

相应的swoole在reload的时候也有一个回调,可以到官网去看

第一步swoole代码

  • 进程启动时在回调中添加
//worker进程启动时进行的回调
$this->ws->on("start", [$this, 'onStart']);
        

//启动时的回调函数        
public function onStart($server)
{
    //设置worker进程别名,用于平滑重启 该方式不支持Mac
    swoole_set_process_name('live_master');
}

第二步shell 脚本

echo "loading"
pid=`pidof live_master`
echo $pid
kill -USER1 $pid
echo "loading success"

swoole日志配置

日志等级控制

$serv->set([
    'log_level' => SWOOLE_LOG_TRACE,
    'trace_flags' => SWOOLE_TRACE_ALL,
]);

日志等级

可以通过设置log_level控制日志等级。底层支持6种错误日志等级:

    SWOOLE_LOG_DEBUG:调试日志,仅作为内核开发调试使用
    SWOOLE_LOG_TRACE:跟踪日志,可用于跟踪系统问题,调试日志是经过精心设置的,会携带关键性信息
    SWOOLE_LOG_INFO:普通信息,仅作为信息展示
    SWOOLE_LOG_NOTICE:提示信息,系统可能存在某些行为,如重启、关闭
    SWOOLE_LOG_WARNING:警告信息,系统可能存在某些问题
    SWOOLE_LOG_ERROR:错误信息,系统发生了某些关键性的错误,需要即时解决

其中SWOOLE_LOG_DEBUG和SWOOLE_LOG_TRACE两种日志,必须在编译swoole扩展时使用--enable-swoole-debug或--enable-trace-log后才可以使用。正常版本中即使设置了log_level = SWOOLE_LOG_TRACE也是无法打印此类日志的。

跟踪标签

线上运行的服务,随时都有大量请求在处理,底层抛出的日志数量非常巨大。可使用trace_flags设置跟踪日志的标签,仅打印部分跟踪日志。trace_flags支持使用|或操作符设置多个跟踪项。

$serv->set([
    'log_level' => SWOOLE_LOG_TRACE,
    'trace_flags' => SWOOLE_TRACE_SERVER | SWOOLE_TRACE_HTTP2,
]);

底层支持以下跟踪项,可使用SWOOLE_TRACE_ALL表示跟踪所有项目:


    SWOOLE_TRACE_SERVER
    SWOOLE_TRACE_CLIENT
    SWOOLE_TRACE_BUFFER
    SWOOLE_TRACE_CONN
    SWOOLE_TRACE_EVENT
    SWOOLE_TRACE_WORKER
    SWOOLE_TRACE_REACTOR
    SWOOLE_TRACE_PHP
    SWOOLE_TRACE_HTTP2
    SWOOLE_TRACE_EOF_PROTOCOL
    SWOOLE_TRACE_LENGTH_PROTOCOL
    SWOOLE_TRACE_CLOSE
    SWOOLE_TRACE_HTTP_CLIENT
    SWOOLE_TRACE_COROUTINE
    SWOOLE_TRACE_REDIS_CLIENT
    SWOOLE_TRACE_MYSQL_CLIENT
    SWOOLE_TRACE_AIO
    SWOOLE_TRACE_ALL

swoole要用的docker启动命令

MAC

docker run -it -p 8810:8810 -p 8811:8811 -p 8812:8812 -p 8813:8813 -p 8814:8814 -p 3306:3306 -p 6379:6379  -v /Users/liuhao/workspace/myProject/:/workspace ubuntu bash

win10

docker run -d -i -t -v d:\develop\www\:/www -p 8810:8810 -p 8811:8811 -p 8812:8812 -p 8813:8813 -p 8814:8814 -p 3306:3306 -p 6379:6379 151f90098842 bash

转载于:https://my.oschina.net/chinaliuhan/blog/3063760

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值