1.1.10 深入浅出 Laravel Echo 的 private channel

优质
小牛编辑
134浏览
2023-12-01

看完 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 channelchannel 名前头增加 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 欢迎查看之前的文章

  1. 《学习 Lumen 用户认证 (一)》
  2. 学习 Lumen 用户认证 (二) —— 使用 jwt-auth 插件

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,然更多客户端加入进来聊天。

未完待续