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