WebSocket的出现替代了原有的轮询与http长链接,双向通信的特点解决了服务器端实时推送的不少问题,所以这东西好像在各种H5游戏平台用的比较多。
简单的聊天室
如果只是想简单的尝试WebSocket,那ratchetphp/Ratchet包或许是个不错的选择。官网中快速开始的两个聊天室例子就很简单,教程:http://socketo.me/docs/hello-world
第一个例子中启用脚本后就可以使用telnet命令进行简单的测试。而在第二个例子中在启用脚本后,可以在浏览器中打开console直接进行测试,但部分网站可能做了XSS防御设置了CSP,所以建议在空白页面进行测试。在浏览器中发送的数据可以在控制台的Network中找到传递的数据(可以使用WS选项对WebSocket进行过滤)。
laravel中的广播
laravel中广播部分属于进阶系列的子条目,是在5.4版本后添加的新功能,建议最后再尝试因为其中牵扯到了不少事件与队列方面的内容。
广播的大致流程为:
- laravel发送一个广播事件
- 事件加入到队列中
- 发布事件到redis
- node.js收听事件
- laravel-echo-server广播事件
- 浏览器接收事件广播
可以参考: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:
//www.greatytc.com/p/0e5b946880b4
//www.greatytc.com/p/f666da1b1835
//www.greatytc.com/p/99610d84ab2a
socket与WebSocket:
//www.greatytc.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