继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

深入浅出 Laravel Echo (3)

叶梅树
关注TA
已关注
手记 25
粉丝 1.7万
获赞 214

看完 public channel 的流程,我们该来说说怎么跑通 private channel 了。

本文结合之前使用的 JWT 来做身份认证。

但这个流程,我们要先从前端说起。

我们先写一个 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 中。

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

未完待续

打开App,阅读手记
1人推荐
发表评论
随时随地看视频慕课网APP