laravel5.6 Broadcasting实践

WebSocket的出现替代了原有的轮询与http长链接,双向通信的特点解决了服务器端实时推送的不少问题,所以这东西好像在各种H5游戏平台用的比较多。

简单的聊天室

如果只是想简单的尝试WebSocket,那ratchetphp/Ratchet包或许是个不错的选择。官网中快速开始的两个聊天室例子就很简单,教程:http://socketo.me/docs/hello-world
第一个例子中启用脚本后就可以使用telnet命令进行简单的测试。而在第二个例子中在启用脚本后,可以在浏览器中打开console直接进行测试,但部分网站可能做了XSS防御设置了CSP,所以建议在空白页面进行测试。在浏览器中发送的数据可以在控制台的Network中找到传递的数据(可以使用WS选项对WebSocket进行过滤)。

laravel中的广播

laravel中广播部分属于进阶系列的子条目,是在5.4版本后添加的新功能,建议最后再尝试因为其中牵扯到了不少事件与队列方面的内容。
广播的大致流程为:

  1. laravel发送一个广播事件
  2. 事件加入到队列中
  3. 发布事件到redis
  4. node.js收听事件
  5. laravel-echo-server广播事件
  6. 浏览器接收事件广播

可以参考:https://blog.csdn.net/nsrainbow/article/details/80428769
不过文中图片上说的第一步事件不一定要发送到redis,也可以发送到数据库或本地文件

实践

laravel官方文档中说有Pusher,Redis,Socket.IO三种驱动,其中Pusher是由第三方提供的服务https://pusher.com/channels(官方文档中主要介绍的就是这个驱动),Redis是使用 pub/sub 功能进行广播,Socket.IO是通过连接Redis后使用node.js再进行广播(其实这个应该不能说叫驱动,因为他是依赖redis的,而且.env的配置项中也不能填写socket.io)。除此之外还有log与null驱动,分别用来调试与关闭广播。

初始环境配置

  • 服务器端PHP
    由于我使用的是redis作为驱动,所以需要安装predis/predis包,并修改.env配置。由于默认情况下laravel关闭了广播服务,所以需要在app.php配置文件中取消注释。
composer require predis/predis
// .env配置
BROADCAST_DRIVER=redis
// app.php
App\Providers\BroadcastServiceProvider::class
  • 服务器端node.js
    然后再安装laravel-echo-server服务端,并初始化配置生成laravel-echo-server.json配置文件。
// 安装
npm install -g laravel-echo-server
// 初始化配置,一般使用默认配置即可
laravel-echo-server init

laravel-echo-server.json配置修改为开发模式,方便之后调试

{
    "authHost": "http://localhost:8082",
    "authEndpoint": "/broadcasting/auth",
    "clients": [],
    "database": "redis",
    "databaseConfig": {
        "redis": {},
        "sqlite": {
            "databasePath": "/database/laravel-echo-server.sqlite"
        }
    },
    "devMode": true,
    "host": null,
    "port": "6001",
    "protocol": "http",
    "socketio": {},
    "sslCertPath": "",
    "sslKeyPath": "",
    "sslCertChainPath": "",
    "sslPassphrase": "",
    "apiOriginAllow": {
        "allowCors": false,
        "allowOrigin": "",
        "allowMethods": "",
        "allowHeaders": ""
    }
}
  • 用户浏览器端js
    修改bootstrap.js文件,安装laravel-echo扩展,由于Socket.IO 服务器会自动通过一个标准的 URL 来暴露客户端 JavaScript 库,所以可以直接在页面中引入socket.io.js,当然页面还要添加csrf令牌(为了方便页面使用模版中的welcome.blade.php)
// bootstrap.js
import Echo from 'laravel-echo'
window.Echo = new Echo({
    broadcaster: 'socket.io',
    host: window.location.hostname + ':6001'
});
// 安装laravel-echo扩展
npm install laravel-echo
// 在页面中引入socket.io.js
<script src="//{{ Request::getHost() }}:6001/socket.io/socket.io.js"></script>
// 添加csrf令牌
<meta name="csrf-token" content="{{ csrf_token() }}">

编译js文件,并在页面中引入编译好的app.js,app.js应该位于socket.io.js之后否则前端会引用出错。

npm install
npm run dev
// 引入app.js
<script src="//{{ Request::getHost() }}:6001/socket.io/socket.io.js"></script>
<script src="{{ asset('/js/app.js') }}"></script>

创建广播事件

由于广播是建立在事件基础上的,所以需要新建一个广播事件。

php artisan make:event PublicBroadcastEvent

修改PublicBroadcastEvent.php

<?php

namespace App\Events;

use App\Article;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class PublicBroadcastEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $article;

    /**
     * 事件被推送的队列名称.
     *
     * @var string
     */
    public $broadcastQueue = 'myBroadcast';

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Article $article)
    {
        $this->article = $article;
    }

    /**
     * 广播频道
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new Channel('publicChannel');
    }

    /**
     * 广播内容
     *
     * @return string
     */    public function broadcastWith(){
        return [
            'id' => 'xxxx',
            'article' => $this->article,
        ];
    }

    /**
     * 广播的事件名称.如果未定义则默认为事件名称即 App\Events\PublicBroadcastEvent
     *
     * @return string
     */
    public function broadcastAs()
    {
        return 'publicArticle';
    }


    /**
     * 判定事件是否广播
     *
     * @return bool
     */
    public function broadcastWhen()
    {
        return $this->article->id < 100;
    }


}

新建广播路由,与普通路由相似,为避免路由文件过于臃肿可以新建类似控制器的channel,其中channel中的join方法与路由中的匿名函数功能相同都是控制访问,返回true或false

// channel.php
Broadcast::channel('publicChannel', function ($user, $article) {
    return true;
});
// 或者
// 新建类似控制器的channel
php artisan make:channel PublicChannel
// 然后修改广播路由
Broadcast::channel('publicChannel', \App\Broadcasting\PublicChannel::class);

修改页面模版中的js,收听广播

<script>
Echo.channel(`publicChannel`) // 广播频道名称
         .listen('.publicArticle', (e) => { // 消息名称。由于自定义了消息名称所以需要在名称前加.或者加\\
                console.log(e); // 收到消息进行的操作,参数 e 为所携带的数据
         });
</script>

尝试收听广播

为了方便测试,在web路由中添加了/testPublic,用来广播事件

// web路由
Route::get('/testPublic', 'HomeController@public');
// 控制器
<?php

namespace App\Http\Controllers;

use App\Article;
use App\Events\PublicBroadcastEvent;
use Illuminate\Http\Request;

class HomeController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        return view('home');
    }

    public function public()
    {
        echo "Public \n";
        $article = Article::where([
            ['id', 1]
        ])->first();

        // 广播事件
        // 也可以写 event(new PublicBroadcastEvent($article));但broadcast()多了一个toOthers()方法更加方便
        broadcast(new PublicBroadcastEvent($article));
    }
}

在命令行中启用队列监听事件并开启laravel-echo-server服务

// 两条命令最好在不同控制台下执行,方便查看调试信息
php artisan queue:work --queue=myBroadcast
laravel-echo-server start

正常情况下打开浏览器访问有监听js的welcome页面后,在浏览器控制台的网络中可以找到websocket的收发数据信息,但暂时没有收听到广播事件。而laravel-echo-server的控制台下会有websocket加入到频道的信息

L A R A V E L  E C H O  S E R V E R

version 1.3.8

⚠ Starting server in DEV mode...

✔  Running at localhost on port 6001
✔  Channels are ready.
✔  Listening for http events...
✔  Listening for redis events...

Server ready!
[2:02:19 PM] - ewdT-ug1Ng2Zj4-mAAAD joined channel: publicChannel

当我们访问127.0.0.1:8082/testPublic尝试发送广播事件时,队列中会显示事件状态

[2018-08-24 06:27:01][2M4zn2e0gr2LfQMBeU2XfX8B8m4S6oHZ] Processing: App\Events\PublicBroadcastEvent
[2018-08-24 06:27:01][2M4zn2e0gr2LfQMBeU2XfX8B8m4S6oHZ] Processed:  App\Events\PublicBroadcastEvent

laravel-echo-server中会显示事件的频道与事件

Channel: publicChannel
Event: publicArticle

浏览器中的控制台网络中会显示websocket发送过来的数据,大致如下

42["publicArticle","publicChannel",{"id":"xxxx","article":{"id":1,"user_id":11,"content":"+TTTTT++TTTTT++TTTTT++TTTTT++TTTTT+9999","created_at":null,"updated_at":"2018-08-23 07:07:18"},"socket":null}]

当客户端要正常或非正常退出频道时,laravel-echo-server都会有记录

// 正常调用Echo.leave('publicChannel');退出频道
[3:29:23 PM] - 6PsU23vDCmTD3RpTAAAH left channel: publicChannel (unsubscribed)
// 非正常退出,如直接关闭浏览器
[3:25:38 PM] - 0-xOdLjQDc3qPW26AAAG left channel: publicChannel (transport error)

私有频道

私有频道与公共频道类似,但必须用户登陆后才能收听。laravel通过已经定义好的/broadcasting/auth路由来验证用户是否登陆,如果尝试直接访问会抛出AccessDeniedHttpException异常。
同样,新建广播事件,添加广播路由,添加测试路由发布广播事件

php artisan make:event PrivateBroadcastEvent

<?php

namespace App\Events;

use App\Article;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class PrivateBroadcastEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $article;

    public $broadcastQueue = 'myBroadcast';

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Article $article)
    {
        $this->article = $article;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        // 私有频道
        return new PrivateChannel('privateChannel');
    }

    public function broadcastWith(){
        return [
            'id' => 'wwwww',
            'article' => $this->article,
        ];
    }

    /**
     * 事件的广播名称.
     *
     * @return string
     */
    public function broadcastAs()
    {
        return 'privateArticle';
    }
}

Broadcast::channel('privateChannel', function ($user) {
    return true;
});

前端js增加收听私有频道配置

Echo.private(`privateChannel`) // 广播频道名称
         .listen('\\privateArticle', (e) => { // 消息名称。由于自定义了消息名称所以需要在名称前加.或者加\\
                 console.log(e); // 收到消息进行的操作,参数 e 为所携带的数据
         });

私有频道可以通过使用wishper方法只通过laravel-echo-server而不用通过laravel进行通讯

// 收听端
Echo.private('privateChannel')
    .listenForWhisper('typing', (e) => {
        console.log(e);
    });
// 发送端
Echo.private('privateChannel')
    .whisper('typing', {
        name: '11111'
    });

正常情况下重复上述尝试收听广播的操作即可接收到广播信息。如果没有接收到,则应该先检查队列中广播事件是否正常,如果不正常可以尝试重启队列,重新监听。然后再检查laravel-echo-server中是否报错,如果显示403错误则是因为用户没有登陆导致的认证失败,需要用户重新登陆,如果是404错误则是laravel-echo-server.json中"authHost"与"authEndpoint"项配置出错,导致laravel-echo-server不能访问用户认证路由。

存在频道

存在广播是建立在私有广播基础之上的,并且提供了额外功能:获知谁订阅了频道。
同样,新建广播事件,添加广播路由,添加测试路由发布广播事件

php artisan make:event PresenceBroadcastEvent

<?php

namespace App\Events;

use App\Article;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class PresenceBroadcastEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $article;

    public $broadcastQueue = 'myBroadcast';

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Article $article)
    {
        $this->article = $article;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        // 存在频道
        return new PresenceChannel('presenceChannel');
    }

    public function broadcastWith(){
        return [
            'id' => 'sssss',
            'article' => $this->article,
        ];
    }

    public function broadcastAs()
    {
        return 'presenceArticle';
    }
}

Broadcast::channel('presenceChannel', function ($user) {
    return [$user->id,$user->name];
});

// 由于存在频道需要知道谁订阅来频道,所以在认证通过后一般返回一些用户信息,认证失败则返回null
Broadcast::channel('presenceChannel', function ($user) {
    return $user->id < 3 ? [$user->id, $user->name] : null;
});

前端js增加收听存在频道配置

Echo.join(`presenceChannel`)
    .here((users) => {
        console.log(users);

    })
    .joining((user) => {
        console.log(user);
    })
    // .leaving((user) => {
    //     console.log(user);
    // })
    .listen('\\presenceArticle', (e) => {
        console.log(e); // 收到消息进行的操作,参数 e 为所携带的数据

    });

正常情况下会返回用户信息,而且当其他用户加入时也会发送该信息

// 用户一登陆
42["presence:subscribed","presence-presenceChannel",[{"user_id":1,"user_info":[1,"xx"],"socketId":"d87GDV2MsyKLqg2HAAAE"}]]
// 用户二登陆
42["presence:subscribed","presence-presenceChannel",[{"user_id":2,"user_info":[2,"123"],"socketId":"6bgbJS-1hhgZSKaAAAAG"},{"user_id":1,"user_info":[1,"xx"],"socketId":"d87GDV2MsyKLqg2HAAAE"}]]

toOthers

laravel广播事件还提供来toOthers方法,用来避免向当前的页面给自己发送广播。
我们可以修改任意一个频道添加toOthers()选项。

// 在web路由中添加/toOthers,并修改控制器
Route::get('/toOthers', 'HomeController@toOthers');

public function toOthers()
    {
        echo "toOthers \n";
        $article = Article::where([
            ['id', 1]
        ])->first();

        broadcast(new PresenceBroadcastEvent($article))->toOthers();
        broadcast(new PrivateBroadcastEvent($article))->toOthers();
        broadcast(new PresenceBroadcastEvent($article))->toOthers();
    }

在存在websocket链接的页面,通过浏览器控制台访问该链接即可,此时当前页面收不到该广播,而其他页面可以收听。如果浏览器直接访问该链接则所有页面都可以收到广播,这是因为没有websocket的链接

axios.get('/toOthers');

其他页面应该可以收到三条信息。

END

参考文章

官方文档:http://laravelacademy.org/post/8945.html
实践教程(存在一些误导):http://laravelacademy.org/post/8559.html
HTTP与WebSocket:
https://www.jianshu.com/p/0e5b946880b4
https://www.jianshu.com/p/f666da1b1835
https://www.jianshu.com/p/99610d84ab2a
socket与WebSocket:
https://www.jianshu.com/p/59b5594ffbb0
PHP的socket相关库:
https://github.com/ratchetphp/Ratchet
https://github.com/pusher/pusher-http-php
node.js的相关库
https://socket.io
https://github.com/laravel/echo
https://github.com/tlaverdure/laravel-echo-server

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 原文链接 必备品 文档:Documentation API:API Reference 视频:Laracasts ...
    layjoy阅读 8,516评论 0 121
  • 在项目中用到socket.io在WEB端做消息推送,遂花了点时间看了socket.io实现,做个简单分析,如有错漏...
    __七把刀__阅读 29,586评论 20 54
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 在看书的时候,作者提到要计划下一本书,提名叫做《领导力与社会变革的语言技术》,它会探讨和阐明苏格拉底,马克思,林肯...
    罗蕾_938f阅读 447评论 3 1