美文网首页
laravel5.6 Broadcasting实践

laravel5.6 Broadcasting实践

作者: 某不科学的XX | 来源:发表于2018-08-24 18:30 被阅读0次

    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

    相关文章

      网友评论

          本文标题:laravel5.6 Broadcasting实践

          本文链接:https://www.haomeiwen.com/subject/sfdpiftx.html