一.安装laravel

1.安装准备

安装前必须保证系统已经安装了下列软件

PHP >= 7.1.3 PHP OpenSSL 扩展 PHP PDO 扩展 PHP Mbstring 扩展 PHP Tokenizer 扩展 PHP XML 扩展 PHP Ctype 扩展 PHP JSON 扩展 PHP BCMath 扩展

2.composer安装

composer create-project –prefer-dist laravel/laravel laravel_swoole 5.8.*

3.laravel配置

1
2
3
#swoole配置
LARAVELS_LISTEN_PORT=9502
LARAVELS_DAEMONIZE=true

二.安装swoole

见swoole学习笔记

三.安装LaravelS

1.composer安装插件

1
2
3
composer require hhxsv5/laravel-s
php artisan laravels publish
php bin/laravels start

2.构建 HTTP nginx服务器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
gzip on;
gzip_min_length 1024;
gzip_comp_level 2;
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml application/x-httpd-php image/jpeg image/gif image/png font/ttf font/otf image/svg+xml;
gzip_vary on;
gzip_disable "msie6";
upstream laravels {
    # Connect IP:Porty
    server 127.0.0.1:9502 weight=5 max_fails=3 fail_timeout=30s;
    keepalive 16;
}

server {
    listen 80;
    root /data/website/laravel_swoole/public/;
    server_name lavarels.dy2.cn;
    index index.html index.htm index.php;
    
    # Nginx 处理静态资源,LaravelS 处理动态资源
    location / {
        try_files $uri @laravels;
    }

    location @laravels {
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Real-PORT $remote_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header Server-Protocol $server_protocol;
        proxy_set_header Server-Name $server_name;
        proxy_set_header Server-Addr $server_addr;
        proxy_set_header Server-Port $server_port;
        proxy_pass http://laravels;
    }
}

四.性能测试

1.PHP-FPM驱动

请求1000次,每次1000并发 结果平均响应时间为24574毫秒,每秒请求数14.51 详细结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
.\ab.exe -n1000 -c1000 http://dy2.cn/laravel_swoole/public/

Server Software:        nginx

Server Hostname:        dy2.cn

Server Port:            80

Document Path:          /laravel_swoole/public/

Document Length:        2360 bytes

Concurrency Level:      1000

Time taken for tests:   68.934 seconds

Complete requests:      1000

Failed requests:        206

   (Connect: 0, Receive: 0, Length: 206, Exceptions: 0)

Non-2xx responses:      206

Total transferred:      2682422 bytes

HTML transferred:       1908036 bytes

Requests per second:    14.51 [#/sec] (mean)

Time per request:       68934.064 [ms] (mean)

Time per request:       68.934 [ms] (mean, across all concurrent requests)

Transfer rate:          38.00 [Kbytes/sec] received

Connection Times (ms)

              min  mean[+/-sd] median   max

Connect:        0   16 211.7      1    3002

Processing: 10722 28594 14160.8  24573   66227

Waiting:    10721 28591 14161.6  24572   66227

Total:      10723 28610 14148.3  24574   66230

Percentage of the requests served within a certain time (ms)

  50%  24574

  66%  29607

  75%  36742

  80%  44639

  90%  49959

  95%  53639

  98%  64683

  99%  66110

 100%  66230 (longest request)

2. Swoole 驱动

请求1000次,每次1000并发 结果平均响应时间为1741毫秒,每秒请求数253.92 详细结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
.\ab.exe -n1000 -c1000 http://laravels.dy2.cn/

Server Software:        nginx
Server Hostname:        laravels.dy2.cn
Server Port:            80

Document Path:          /
Document Length:        2360 bytes

Concurrency Level:      1000
Time taken for tests:   3.938 seconds
Complete requests:      1000
Failed requests:        1
   (Connect: 0, Receive: 0, Length: 1, Exceptions: 0)
Non-2xx responses:      1
Total transferred:      3263810 bytes
HTML transferred:       2357826 bytes
Requests per second:    253.92 [#/sec] (mean)
Time per request:       3938.276 [ms] (mean)
Time per request:       3.938 [ms] (mean, across all concurrent requests)
Transfer rate:          809.32 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   1.1      1      11
Processing:    95 1842 656.4   1739    3065
Waiting:       87 1840 657.7   1738    3064
Total:         95 1843 656.3   1741    3065

Percentage of the requests served within a certain time (ms)
  50%   1741
  66%   2201
  75%   2489
  80%   2577
  90%   2800
  95%   2881
  98%   2962
  99%   2965
 100%   3065 (longest request)

###3. 测试总结

swoole驱动是传统php-fpm驱动的17.5倍,优势明显

五.Laravel中swoole应用

1. 在 Laravel 中集成 Swoole 实现 WebSocket 服务器

创建 WebSocketService 类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php

namespace App\Services;

use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
use Illuminate\Support\Facades\Log;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;

class WebSocketService implements WebSocketHandlerInterface
{
    public function __construct()
    {

    }
    
    // 连接建立时触发
    public function onOpen(Server $server, Request $request)
    {
        // 在触发 WebSocket 连接建立事件之前,Laravel 应用初始化的生命周期已经结束,你可以在这里获取 Laravel 请求和会话数据
        // 调用 push 方法向客户端推送数据,fd 是客户端连接标识字段
        Log::info('WebSocket 连接建立');
        $server->push($request->fd, 'Welcome to WebSocket Server built on LaravelS');
    }
    
    // 收到消息时触发
    public function onMessage(Server $server, Frame $frame)
    {
        // 调用 push 方法向客户端推送数据
        $server->push($frame->fd, 'This is a message sent from WebSocket Server at ' . date('Y-m-d H:i:s'));
    }
    
    // 关闭连接时触发
    public function onClose(Server $server, $fd, $reactorId)
    {
        Log::info('WebSocket 连接关闭');
    }
}

修改配置文件

config/laravels.php 启用 WebSocket 通信并将刚刚创建的服务器类配置到对应的配置项

‘websocket’ => [ ‘enable’ => true, ‘handler’ => \App\Services\WebSocketService::class, ],

在 swoole 配置项中配置 WebSocket 长连接的强制关闭逻辑 // 心跳检测,WebSocket 长连接的强制关闭逻辑,每隔 60s 检测一次所有连接,如果某个连接在 600s 内都没有发送任何数据,则关闭该连接 ‘heartbeat_idle_time’ => 600, ‘heartbeat_check_interval’ => 60,

配置 Nginx 支持 WebSocket

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}
upstream laravels_websocket {
    # Connect IP:Port
    server 127.0.0.1:9502 weight=5 max_fails=3 fail_timeout=30s;
    keepalive 16;
}
server {
    listen 80;
    root /data/website/laravel_swoole/public/;
    server_name laravelsk.dy2.cn;

    error_log /data/webserver/nginx/logs/laravels_websocket_error.log;
    access_log /data/webserver/nginx/logs/laravels_websocket_access.log;
    
    index index.php index.html index.htm;
    
    # Nginx handles the static resources(recommend enabling gzip), LaravelS handles the dynamic resource.
    location / {
        try_files $uri @laravels;
    }
    
    # Response 404 directly when request the PHP file, to avoid exposing public/*.php
    #location ~* \.php$ {
    #    return 404;
    #}
    
    # Http and WebSocket are concomitant, Nginx identifies them by "location"
    # !!! The location of WebSocket is "/ws"
    # Javascript: var ws = new WebSocket("ws://todo-s.test/ws");
    # 处理 WebSocket 通信
    location =/ws {
        # proxy_connect_timeout 60s;
        # proxy_send_timeout 60s;
        # proxy_read_timeout: Nginx will close the connection if the proxied server does not send data to Nginx in 60 seconds; At the same time, this close behavior is also affected by heartbeat setting of Swoole.
        # proxy_read_timeout 60s;
        proxy_http_version 1.1;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Real-PORT $remote_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header Server-Protocol $server_protocol;
        proxy_set_header Server-Name $server_name;
        proxy_set_header Server-Addr $server_addr;
        proxy_set_header Server-Port $server_port;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_pass http://laravels_websocket;
    }
    
    location @laravels {
        # 如果60秒内被代理的服务器没有响应数据给Nginx,那么Nginx会关闭当前连接
        # proxy_connect_timeout 60s;
        # proxy_send_timeout 60s;
        # proxy_read_timeout 60s;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Real-PORT $remote_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header Server-Protocol $server_protocol;
        proxy_set_header Server-Name $server_name;
        proxy_set_header Server-Addr $server_addr;
        proxy_set_header Server-Port $server_port;
        proxy_pass http://laravels_websocket;
    }
}

创建views/websocket/client.blade.php视图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Chat Client</title>
</head>
<body>
    <h1>websocket客户端</h1>
<script>
    window.onload = function () {
        var nick = prompt("Enter your nickname");
        var input = document.getElementById("input");
        input.focus();

        // 初始化客户端套接字并建立连接
        var socket = new WebSocket("ws://laravelsk.dy2.cn/ws");
    
        // 连接建立时触发
        socket.onopen = function (event) {
            console.log("Connection open ...");
        }
    
        // 接收到服务端推送时执行
        socket.onmessage = function (event) {
            var msg = event.data;
            var node = document.createTextNode(msg);
            var div = document.createElement("div");
            div.appendChild(node);
            document.body.insertBefore(div, input);
            input.scrollIntoView();
        };
    
        // 连接关闭时触发
        socket.onclose = function (event) {
            console.log("Connection closed ...");
        }
    
        input.onchange = function () {
            var msg = nick + ": " + input.value;
            // 将输入框变更信息通过 send 方法发送到服务器
            socket.send(msg);
            input.value = "";
        };
    }
</script>
<input id="input" style="width: 100%;">
</body>
</html>

添加路由

Route::get('/websocket_client', function () { return view(‘websocket.client’); });

访问:http://laravelsk.dy2.cn/websocket_client

storage/logs下查看最新日志 [2019-05-22 13:55:02] local.INFO: WebSocket 连接建立
[2019-05-22 13:55:10] local.INFO: WebSocket 连接关闭

2. 在 Laravel 中集成 Swoole 实现 毫秒级定时器

创建Jobs\Timer\IndexCronJob执行文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2019/9/10 0010
 * Time: 10:13
 */

namespace App\Jobs\Timer;


use Hhxsv5\LaravelS\Swoole\Timer\CronJob;
use Illuminate\Support\Facades\Log;

class IndexCronJob extends CronJob
{
    protected $i = 0;

    // 该方法可类比为 Swoole 定时器中的回调方法
    public function run()
    {
        Log::info(__METHOD__, [$this->timerId,'start', $this->i, microtime(true)]);
        $this->i++;
        Log::info(__METHOD__, [$this->timerId,'end', $this->i, microtime(true)]);
    
        if ($this->i == 3) { // 总共运行3次
            Log::info(__METHOD__, [$this->timerId,'stop', $this->i, microtime(true)]);
            $this->stop(); // 清除定时器
        }
    }
    
    // 每隔 1000ms 执行一次任务
    public function interval()
    {
        return 1000;   // 定时器间隔,单位为 ms
    }
    
    // 是否在设置之后立即触发 run 方法执行
    public function isImmediate()
    {
        return false;
    }
}

修改config/laravels.php 配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
'timer'  => [
    'enable' => true,
    'jobs'   => [
        // Enable LaravelScheduleJob to run `php artisan schedule:run` every 1 minute, replace Linux Crontab
        // \Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class,
        // Two ways to configure parameters:
        // [\App\Jobs\XxxCronJob::class, [1000, true]], // Pass in parameters when registering
        \App\Jobs\Timer\TestCronJob::class, // Override the corresponding method to return the configuration
    ],
    'max_wait_time' => 5, // Max waiting time of reloading
],

storage/logs 下的最新日志里就可以看到上述定时任务的输出了

1
2
3
4
5
6
7
8

[2019-05-29 22:37:20] local.INFO: App\Jobs\Timer\TestCronJob::run ["start",0,1559140640.694025] 
[2019-05-29 22:37:20] local.INFO: App\Jobs\Timer\TestCronJob::run ["end",1,1559140640.752043] 
[2019-05-29 22:37:21] local.INFO: App\Jobs\Timer\TestCronJob::run ["start",1,1559140641.752905] 
[2019-05-29 22:37:21] local.INFO: App\Jobs\Timer\TestCronJob::run ["end",2,1559140641.754724] 
[2019-05-29 22:37:22] local.INFO: App\Jobs\Timer\TestCronJob::run ["start",2,1559140642.694884] 
[2019-05-29 22:37:22] local.INFO: App\Jobs\Timer\TestCronJob::run ["end",3,1559140642.696726] 
[2019-05-29 22:37:22] local.INFO: App\Jobs\Timer\TestCronJob::run ["stop",3,1559140642.698137] 

3. 在 Laravel 中集成 Swoole 实现 异步任务队列

创建 Jobs/IndexTasK类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
namespace App\Jobs;

use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Support\Facades\Log;

class IndexTask extends Task
{
    // 待处理任务数据
    private $data;

    // 任务处理结果
    private $result;
    
    public function __construct($data)
    {
        $this->data = $data;
    }
    
    // 任务投递调用 task 回调时触发,等同于 Swoole 中的 onTask 逻辑
    public function handle()
    {
        Log::info(__CLASS__ . ': 开始处理任务', [$this->data]);
        //  todo 耗时任务具体处理逻辑在这里编写
        sleep(3); // 模拟任务需要3秒才能执行完毕
        $this->result = 'The result of ' . $this->data . ' is balabalabala';
    }
    
    // 任务完成调用 finish 回调时触发,等同于 Swoole 中的 onFinish 逻辑
    public function finish()
    {
        Log::info(__CLASS__ . ': 任务处理完成', [$this->result]);
        // 可以在这里触发后续要执行的任务,或者执行其他善后逻辑
    }
}

####创建http访问路由

//分组路由 Route::group([ ‘namespace’ => ‘Task’ ,‘prefix’ => ‘task’], function ($router) { $router->get('/test', ‘TestController@test’); });

创建异步处理控制器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
namespace App\Http\Controllers\Task;


use App\Http\Controllers\Controller;

class TestController extends Controller
{
    public function test(){
        $task = new \App\Jobs\IndexTask('测试异步任务');
        $success = \Hhxsv5\LaravelS\Swoole\Task\Task::deliver($task);  // 异步投递任务,触发调用任务类的 handle 方法
        var_dump($success);
    }
}

修改配置文件

‘swoole’ => [ … ‘task_worker_num’ => function_exists(‘swoole_cpu_num’) ? swoole_cpu_num() * 2 : 8, … ]

测试异步任务执行

php bin/laravels restart 访问 http://laravelsk.dy2.cn 返回 bool(true) storage/logs 目录下查看最新的日志信息 [2019-09-10 03:33:49] local.INFO: App\Jobs\IndexTask: 开始处理任务 [“测试异步任务”] [2019-09-10 03:33:52] local.INFO: App\Jobs\IndexTask: 任务处理完成 [“The result of 测试异步任务 is balabalabala”]

4.在 Laravel 中实现数据库和 Redis 的持久连接

修改配置文件,设置persistent // 数据库持久连接配置 ‘mysql’ => [ ‘driver’ => ‘mysql’, ‘host’ => env(‘DB_HOST’, ‘127.0.0.1’), ‘port’ => env(‘DB_PORT’, ‘3306’), ‘database’ => env(‘DB_DATABASE’, ‘forge’), ‘username’ => env(‘DB_USERNAME’, ‘forge’), ‘password’ => env(‘DB_PASSWORD’, ‘'), ‘unix_socket’ => env(‘DB_SOCKET’, ‘'), ‘charset’ => ‘utf8mb4’, ‘collation’ => ‘utf8mb4_unicode_ci’, ‘prefix’ => ‘’, ‘prefix_indexes’ => true, ‘strict’ => true, ‘engine’ => null, ‘options’ => extension_loaded(‘pdo_mysql’) ? array_filter([ PDO::MYSQL_ATTR_SSL_CA => env(‘MYSQL_ATTR_SSL_CA’), PDO::ATTR_PERSISTENT => true ]) : [], ],

// Redis 持久连接配置

‘redis’ => [ … ‘default’ => [ ‘host’ => env(‘REDIS_HOST’, ‘127.0.0.1’), ‘password’ => env(‘REDIS_PASSWORD’, null), ‘port’ => env(‘REDIS_PORT’, 6379), ‘database’ => env(‘REDIS_DB’, 0), ‘persistent’ => true ], … ]

修改WebSocketService,添加table处理

class WebSocketService implements WebSocketHandlerInterface { public function __construct() {

}

// 连接建立时触发
public function onOpen(Server $server, Request $request)
{
    // 在触发 WebSocket 连接建立事件之前,Laravel 应用初始化的生命周期已经结束,你可以在这里获取 Laravel 请求和会话数据
    // 调用 push 方法向客户端推送数据,fd 是客户端连接标识字段
    Log::info('WebSocket 连接建立');

    app('swoole')->wsTable->set('fd:' . $request->fd, ['value' => $request->fd]);
    $server->push($request->fd, 'Welcome to WebSocket Server built on LaravelS');
}

// 收到消息时触发
public function onMessage(Server $server, Frame $frame)
{
    // 调用 push 方法向客户端推送数据
    $server->push($frame->fd, 'This is a message sent from WebSocket Server at ' . date('Y-m-d H:i:s'));

    foreach (app('swoole')->wsTable as $key => $row) {
        if (strpos($key, 'fd:') === 0 && $server->exist($row['value'])) {
            Log::info('Receive message from client: ' . $row['value']);
            // 调用 push 方法向客户端推送数据
            $server->push($frame->fd, 'This is a message sent from WebSocket Server at ' . date('Y-m-d H:i:s'));
        }
    }
}

// 关闭连接时触发
public function onClose(Server $server, $fd, $reactorId)
{
    Log::info('WebSocket 连接关闭');
}

}

访问http://laravelsk.dy2.cn/websocket_client tail -f storage/logs/laravel-2019-09-10.log [2019-09-10 07:55:50] local.INFO: WebSocket 连接建立
[2019-09-10 07:55:53] local.INFO: Receive message from client: 2
[2019-09-10 07:56:08] local.INFO: WebSocket 连接关闭