1.1.10 深入浅出 Laravel Echo 的 private channel
看完 public channel
的流程,我们该来说说怎么跑通 private channel
了。
本文结合之前使用的 JWT
来做身份认证。
但这个流程,我们要先从前端说起。
socker.io
我们先写一个 demo:
window.Echo.private('App.User.3')
.listen('RssCreatedEvent', (e) => {
that.names.push(e.name)
});
先创建 private channel
:
/**
* Get a private channel instance by name.
*
* @param {string} name
* @return {SocketIoChannel}
*/
privateChannel(name: string): SocketIoPrivateChannel {
if (!this.channels['private-' + name]) {
this.channels['private-' + name] = new SocketIoPrivateChannel(
this.socket,
'private-' + name,
this.options
);
}
return this.channels['private-' + name];
}
它与 public channel
的区别在于为 private channel
的 channel
名前头增加 private-
。
接着我们需要为每次请求添加认证信息 headers
:
window.Echo = new Echo({
broadcaster: 'socket.io',
host: window.location.hostname + ':6001',
auth:
{
headers:
{
'authorization': 'Bearer ' + store.getters.token
}
}
});
这里,我们用 store.getters.token
存储着 jwt
登录后下发的认证 token
。
好了,只要创新页面,就会先往 Laravel-echo-server
发送一个 subscribe
事件:
/**
* Subscribe to a Socket.io channel.
*
* @return {object}
*/
subscribe(): any {
this.socket.emit('subscribe', {
channel: this.name,
auth: this.options.auth || {}
});
}
我们来看看 Laravel-echo-server
怎么接收到这个事件,并把 auth
,也就是 jwt token
发到后台的?在研究怎么发之前,我们还是先把 Laravel 的 private channel Event
建好。
RssCreatedEvent
我们创建 Laravel PrivateChannel
:
// RssCreatedEvent
<?php
namespace App\Events;
use App\User;
use Carbon\Carbon;
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 RssCreatedEvent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct()
{
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
// 14. 创建频道
info('broadcastOn');
return new PrivateChannel('App.User.3');
}
/**
* 指定广播数据。
*
* @return array
*/
public function broadcastWith()
{
// 返回当前时间
return ['name' => 'private_channel_'.Carbon::now()->toDateTimeString()];
}
}
// routes/console.php
Artisan::command('echo', function () {
event(new RssCreatedEvent());
})->describe('echo demo');
与 jwt 结合
修改 BroadcastServiceprovider
的认证路由为 api:
// 修改前
// Broadcast::routes();
// 修改后
Broadcast::routes(["middleware" => "auth:api"]);
当然,我们的认证方式也已经改成 JWT 方式了:
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
...
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
最后,别忘了把 BroadcastServiceprovider
加入 app.config
中。
注:更多有关
JWT
欢迎查看之前的文章
Laravel-echo-server
有了前端和后台的各自 private channel
,那必然需要用 Laravel-echo-server
来衔接。
先说回怎么接收前端发过来的 subscribe
事件和 token
。
首先看 echo-server
初始化:
init(io: any): Promise<any> {
return new Promise((resolve, reject) => {
this.channel = new Channel(io, this.options);
this.redisSub = new RedisSubscriber(this.options);
this.httpSub = new HttpSubscriber(this.server.express, this.options);
this.httpApi = new HttpApi(io, this.channel, this.server.express, this.options.apiOriginAllow);
this.httpApi.init();
this.onConnect();
this.listen().then(() => resolve(), err => Log.error(err));
});
}
其中,this.onConnect()
:
onConnect(): void {
this.server.io.on('connection', socket => {
this.onSubscribe(socket);
this.onUnsubscribe(socket);
this.onDisconnecting(socket);
this.onClientEvent(socket);
});
}
主要注册了四个事件,第一个就是我们需要关注的:
onSubscribe(socket: any): void {
socket.on('subscribe', data => {
this.channel.join(socket, data);
});
}
这就和前端呼应上了,接着看 join
函数:
join(socket, data): void {
if (data.channel) {
if (this.isPrivate(data.channel)) {
this.joinPrivate(socket, data);
} else {
socket.join(data.channel);
this.onJoin(socket, data.channel);
}
}
}
看 isPrivate()
函数:
/**
* Channels and patters for private channels.
*
* @type {array}
*/
protected _privateChannels: string[] = ['private-*', 'presence-*'];
/**
* Check if the incoming socket connection is a private channel.
*
* @param {string} channel
* @return {boolean}
*/
isPrivate(channel: string): boolean {
let isPrivate = false;
this._privateChannels.forEach(privateChannel => {
let regex = new RegExp(privateChannel.replace('\*', '.*'));
if (regex.test(channel)) isPrivate = true;
});
return isPrivate;
}
这也是印证了,为什么 private channel
要以 private-
开头了。接着看代码:
/**
* Join private channel, emit data to presence channels.
*
* @param {object} socket
* @param {object} data
* @return {void}
*/
joinPrivate(socket: any, data: any): void {
this.private.authenticate(socket, data).then(res => {
socket.join(data.channel);
if (this.isPresence(data.channel)) {
var member = res.channel_data;
try {
member = JSON.parse(res.channel_data);
} catch (e) { }
this.presence.join(socket, data.channel, member);
}
this.onJoin(socket, data.channel);
}, error => {
if (this.options.devMode) {
Log.error(error.reason);
}
this.io.sockets.to(socket.id)
.emit('subscription_error', data.channel, error.status);
});
}
就因为是 private channel
,所以需要走认证流程:
/**
* Send authentication request to application server.
*
* @param {any} socket
* @param {any} data
* @return {Promise<any>}
*/
authenticate(socket: any, data: any): Promise<any> {
let options = {
url: this.authHost(socket) + this.options.authEndpoint,
form: { channel_name: data.channel },
headers: (data.auth && data.auth.headers) ? data.auth.headers : {},
rejectUnauthorized: false
};
return this.serverRequest(socket, options);
}
/**
* Send a request to the server.
*
* @param {any} socket
* @param {any} options
* @return {Promise<any>}
*/
protected serverRequest(socket: any, options: any): Promise<any> {
return new Promise<any>((resolve, reject) => {
options.headers = this.prepareHeaders(socket, options);
let body;
this.request.post(options, (error, response, body, next) => {
if (error) {
if (this.options.devMode) {
Log.error(`[${new Date().toLocaleTimeString()}] - Error authenticating ${socket.id} for ${options.form.channel_name}`);
Log.error(error);
}
reject({ reason: 'Error sending authentication request.', status: 0 });
} else if (response.statusCode !== 200) {
if (this.options.devMode) {
Log.warning(`[${new Date().toLocaleTimeString()}] - ${socket.id} could not be authenticated to ${options.form.channel_name}`);
Log.error(response.body);
}
reject({ reason: 'Client can not be authenticated, got HTTP status ' + response.statusCode, status: response.statusCode });
} else {
if (this.options.devMode) {
Log.info(`[${new Date().toLocaleTimeString()}] - ${socket.id} authenticated for: ${options.form.channel_name}`);
}
try {
body = JSON.parse(response.body);
} catch (e) {
body = response.body
}
resolve(body);
}
});
});
}
到此,相信你就看的出来了,会把前端发过来的 auth.headers
加入发往后台的请求中。
测试
好了,我们测试下,先刷新页面,加入 private channel
中,
然后在后台,发一个事件出来,看前端是不是可以接收
总结
到此,基本就解释了怎么建立 private channel
,然后利用 jwt
认证身份,最后将 Event
内容下发出去。
接下来我们就可以看看怎么建 chat room
,然更多客户端加入进来聊天。
未完待续