用laravel-echo-server实现实时消息通知
一、实时消息
举个栗子,如聊天消息和点赞评论通知等,都是通过laravel提供的消息通知系统,将相关的notification对象存储到数据库中,然后提供一些api或者gql接口给前端,然后前端不停地轮询接口,获得更新数据。这样会有两个显而易见的问题:
- 消息没有更新的时候,前端值轮询接口导致了很多无效的接口查询,增加服务器压力,影响app加载速度。
- 消息有更新,但是轮询会有一定的时间间隔,就会导致消息更新不及时。
这个缺陷是由于HTTP协议的天然缺陷导致的:请求只能从客户端发起。因此使用HTTP协议通讯是无法让服务端无法及时通知客户端数据更新的,这里就涉及了一个新的协议。WebSocket。
WebSocket它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话。由此可见,相比于不停地轮询应用程序,WebSocket 是一种更加可靠和高效的选择。
而laravel的广播系统,就支持使用WebSocket来搭建自己的实时消息系统。
二、Laravel Echo Server
WebSocket 服务器的实现,常用的 Node 实现有以下三种。
而开源项目tlaverdure/laravel-echo-server,是一个与 Laravel 兼容的 Socket.IO 服务器。
GitHub:https://github.com/tlaverdure/laravel-echo-server
下图解释了laravel怎么通过socket.io来完成了一个消息通知。
三、搭建一个实时消息通知系统
1、设置广播驱动
Laravel的广播系统自带了4个广播驱动器,pusher,redis,log,null
这里我们使用redis作为驱动,先用下面的命令安装redis拓展支持。
composer require predis/predis "~1.0"
修改.env文件:BROADCAST_DRIVER=redis
如果是未曾使用广播系统的项目,需要先到config/app.php 配置文件的 providers 数组中取消对广播服务的提供者BroadcastServiceProvider的注释.
2、服务端,配置laravel-echo-server
安装方法
npm install -g laravel-echo-server # 这里是全局安装
初始化服务端
根据自己的需要填充配置参数。
$ laravel-echo-server init
? Do you want to run this server in development mode? Yes
? Which port would you like to serve from? 6001
? Which database would you like to use to store presence channel members? redis
? Enter the host of your Laravel authentication server. your_host_address
? Will you be serving on http or https? http
? Do you want to generate a client ID/Key for HTTP API? Yes
? Do you want to setup cross domain access to the API? No
appId: c953434932b06864
key: 551440289d2d41c81e87d55c1d0217e5
Configuration file saved. Run laravel-echo-server start to run server.
初始化完成之后,项目下就会生成一个 laravel-echo-sever.json的配置文件,里面的配置就是我们在初始化时设置的值。后续的服务都是基于这个配置文件启动的。
本项目用到的配置为:
{
"authHost": "http://xxx.xxx.cn",
"authEndpoint": "/api/broadcasting/auth",
"clients": [],
"database": "redis",
"databaseConfig": {
"redis": {
"port": "port",
"host": "host",
"db": "db",
"password": "password"
},
"sqlite": {
"databasePath": "/database/laravel-echo-server.sqlite"
}
},
"devMode": true,
"host": null,
"port": "6002",
"protocol": "http",
"socketio": {},
"secureOptions": 67108864,
"sslCertPath": "",
"sslKeyPath": "",
"sslCertChainPath": "",
"sslPassphrase": "",
"subscribers": {
"http": true,
"redis": true
},
"apiOriginAllow": {
"allowCors": false,
"allowOrigin": "",
"allowMethods": "",
"allowHeaders": ""
}
}
运行服务端
$ laravel-echo-server start
L A R A V E L E C H O S E R V E R
version 1.3.6
⚠ Starting server in DEV mode...
✔ Running at localhost on port 6001
✔ Channels are ready.
✔ Listening for http events...
✔ Listening for redis events...
Server ready!
检测下socket.io客户端url地址是否可访问
http://your_host_address:6001/socket.io/socket.io.js
可访问,并内容类似于以下则说明服务器可用
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(......
3、广播自己的通知
这里以系统通知Notice为例,Notice为系统通知实体,包含了通知内容content,通知对象to_user_id等。
业务场景是每当运营创建一条系统消息时,用户可以及时接受到相关的通知。这里就分为了个人通知和系统通知两部分。对应为公共频道广播和私人频道广播。
定义广播事件类NewNotice
<?php
namespace App\Events;
use App\Notice;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Support\Str;
class NewNotice implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets;
public $notice;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Notice $notice)
{
$this->notice = $notice;
}
/**
* 广播该事件到哪一个频道,channel代表一个公共频道
* 私人频道使用PrivateChannel
*/
public function broadcastOn()
{
return new Channel('notice');
}
//广播事件名
public function broadcastAs()
{
return 'system.notice';
}
//广播消息携带的内容数据
public function broadcastWith()
{
$notice = $this->notice;
return [
'title' => $notice->title,
'description' => Str::limit($notice->content, 20),
'content' => $notice->content,
'id' => $notice->id,
];
}
}
对事件进行广播
class NoticeObserver
{
/**
* Handle the notice "created" event.
*
* @param \App\Notice $notice
* @return void
*/
public function created(Notice $notice)
{
broadcast(new NewNotice($notice));
}
}
开启队列监听:
php artisan queue:work
以上这些就可以将我们的广播发布到事件发布到指定频道,这里是将systen.notice广播事件广播到了公共频道notice.
laravel-echo-server会打印相应的日志:
Channel: notice
Event: system.notice
广播到私人频道
发送给某个人的系统通知,是只有指定的接收方才能接收到的,那么就需要监听一个特定的私人频道,并进行用户的授权校验。
public function broadcastOn()
{
return new PrivateChannel('App.User.' . $this->user->id);
}
私人频道可以拼接用户id作为用户名,保证每个用户都能监听一个属于自己的私人频道,该用户的所有私人广播事件也都可以广播到这一个频道上。
- 频道授权
对于私有频道,用户只有被授权后才能监听。实现过程是用户向你的 Laravel 应用程序发起一个携带频道名称的 HTTP 请求,你的应用程序判断该用户是否能够监听该频道。在使用 Laravel Echo 时,上述 HTTP 请求会被自动发送;尽管如此,你仍然需要定义适当的路由来响应这些请求。
- 授权路由:
BroadcastServiceProvider 中,你会看到一个对 Broadcast::routes 方法的调用。该方法会注册 /broadcasting/auth 路由来处理授权请求(与配置文件中的authEndpoint对应):
- 授权回调
接下来,我们需要定义真正用于处理频道授权的逻辑。这是在 routes/channels.php 文件中完成。在该文件中,你可以用 Broadcast::channel 方法来注册频道授权回调函数:
Broadcast::channel('App.User.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
channel 方法接收两个参数:频道名称和一个回调函数,该回调通过返回 true 或 false 来表示用户是否被授权监听该频道。
u s e r , 是 当 前 被 认 证 的 用 户 , 任 何 额 外 的 通 配 符 参 数 作 为 后 续 参 数 。 这 里 的 user,是当前被认证的用户,任何额外的通配符参数作为后续参数。这里的 user,是当前被认证的用户,任何额外的通配符参数作为后续参数。这里的id,也就是我们在定义名称是传递的$this->user->id.
4、客户端监听
安装 Laravel Echo
Laravel Echo 是一个 JavaScript 库,它使得订阅频道和监听由 Laravel 广播的事件变得非常容易。你可以通过 NPM 包管理器来安装 Echo。在本例中,因为我们将使用 Pusher 广播器,请安装 pusher-js 包:
npm install --save laravel-echo pusher-js
一旦 Echo 被安装好,你就可以在你应用程序的 JavaScript 中创建一个全新的 Echo 实例。做这件事的一个理想地方是在 resources/assets/js/bootstrap.js 文件的底部,Laravel 框架自带了该文件:
import Echo from "laravel-echo"
window.Echo = new Echo({
broadcaster: 'socket.io',
host: 'ws://xxx.xxx.cn:6002',
});
ws是webSocket协议标识符,如果加密,则为wss,服务器网址就是 URL,对应于http和https。
对事件进行监听
一旦你安装好并实例化了 Echo,你就可以开始监听事件广播了。首先,使用 channel 方法来获取一个频道实例,然后调用 listen 方法来监听指定的事件:
Echo.channel('notice')
.listen('.system.notice', (e) => {
console.log(e);
});
-
要注意,如果您使用 broadcastAs 方法自定义广播名称,你需要在客户端使用订阅事件的时候为事件类加上 . 前缀
-
如果采用的是默认的类名作为广播事件名,那么要记得检查database配置中是否有添加默认的prefix
如果你想监听私有频道上的事件,请使用 private 方法。你可以通过链式调用 listen 方法来监听一个频道上的多个事件:
Echo.private('App.User.'+me.id)
.listen(...)
.listen(...)
.listen(...);
退出频道
如果想退出频道,你需要在你的 Echo 实例上调用 leave 方法:
Echo.leave('orders');
生产环境配置
本地我们是自己执行laravel-echo-server start来启动服务的,生产环境我们采用supervisor来自动管理laravel-echo-server服务。
[program:laravel-worker-yinxiangshipin-echo-server]
process_name=%(program_name)s_%(process_num)02d
directory=/xxx/.../yinxiangshipin.com
command=/xxx/.../.nvm/versions/node/v10.16.0/bin/node /xxx/.../.nvm/versions/node/v10.16.0/bin/laravel-echo-server start --dir=/xxx/.../yinxiangshipin.com
autostart=true
autorestart=true
user=root
numprocs=1
redirect_stderr=true
stdout_logfile=/xxx/.../yinxiangshipin.com/storage/logs/echo-server.log
supervisor相关配置可以查看相关文档,这里我们需要安装的东西为nvm,node,laravel-echo-server
Nvm is Node Version Manager github:nvm-sh
- 安装nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash\n
或者
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash
- 添加对应环境变量
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
然后使用nvm安装最新的node
nvm install node
或者安装指定版本
nvm install v10.16.0
然后安装 laravel-echo-server
npm install -g laravel-echo-server
安装完成后默认的目录为~/.nvm/versions/node/v10.15.2/bin/laravel-echo-server
验证安装成功,
执行laravel-echo-server.json中配置的command,能执行这两个命令那么就是全部安装成功了
如果提示没有node命令,那么就需要自己手动添加一个软链接
ln -s /xxx/…/.nvm/versions/node/v10.16.0/bin/node /usr/bin/node
完成上述操作之后,就可以restart laravel-echo-server队列,验证服务是否可以成功启动了。
supervisorctl restart laravel-worker-yinxiangshipin-echo-server:
如果启动失败,可以检查一下启动用户是否有权限
到这里,已经完成了一个简单的实时消息系统啦,如果还有问题,可以查看stdout_logfile配置的log路径,检查执行日志,排查问题。
如果对laravel 关于这部分的实现有兴趣,相应看看源码的话,可以参考一下这篇文章
看源代码,解析一次完整的 public channel 下发流程:https://learnku.com/articles/17327