Laravel 廣播系統工做原理

這是一篇譯文,譯文首發於 Laravel 廣播系統工做原理,轉載請註明出處。

今天,讓咱們深刻研究下 Laravel 的廣播系統。廣播系統的目的是用於實現當服務端完成某種特定功能後向客戶端推送消息的功能。本文咱們將學習如何使用第三方 Pusher 工具向客戶端推送消息的功能。php

若是您遇到在 Laravel 中須要實現當服務器處理完成某項工做後向客戶端發送消息這類的功能,那麼您須要使用到 Laravel 的廣播系統。css

好比在一個支持用戶互相發送消息的即時通訊應用,當用戶 A 給用戶 B 發送一條消息時,系統須要實時的將消息推送給用戶 B,而且信息以彈出框或提示消息框形式展示給用戶 B。html

這種使用場景能夠完美詮釋 Laravel 廣播系統的工做原理。另外,本教程將使用 Laravel 廣播系統實現這樣一個即時通訊應用。前端

或許您會對服務器是如何將消息及時的推送給客戶端的技術原理感興趣,這是由於在服務端實現這類功能時使用了套接字編程技術。在開始實現即時通訊系統前,先讓咱們瞭解下套接字編程的大體流程:node

  • 首先,服務器須要支持 WebSocket 協議,而且容許客戶端創建 WebSocket 鏈接;
  • 您能夠實現本身的 WebSocket 服務,或者使用第三方服務如 Pusher,後文會用到 Pusher 庫;
  • 客戶端建立一個服務器的 Web Socket 鏈接,鏈接成功後客戶端會獲取惟一標識符;
  • 一旦客戶端鏈接成功,表示該客戶端訂閱了指定頻道,將接收這個頻道的消息;
  • 最後,客戶端還會註冊其所訂閱的頻道的監聽事件;
  • 當服務端完成指定功能後,咱們以指定頻道名稱和事件名稱的信息通知到 WebSocket 服務器;
  • 最終,WebSocket 服務器將這個指定事件已廣播的形式推送到全部註冊這個頻道監聽的客戶端。

以上所涉及的內容看似不少,但經過本文學習您將掌握箇中的訣竅。laravel

接下來,讓咱們打開 Laravel 默認廣播系統配置文件 config/broadcasting.php 看看裏面的配置選項:git

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Default Broadcaster 默認廣播驅動
    |--------------------------------------------------------------------------
    |
    | This option controls the default broadcaster that will be used by the
    | framework when an event needs to be broadcast. You may set this to
    | any of the connections defined in the "connections" array below.
    |
    | 該配置選項用於配置項目須要提供廣播服務時的默認驅動器。配置鏈接器可使任意
    | 在 "connections" 節點配置的驅動名稱。
    |
    | Supported: "pusher", "redis", "log", "null"
    | 
    | 支持:"pusher", "redis", "log", "null"
    |
    */

    'default' => env('BROADCAST_DRIVER', 'null'),

    /*
    |--------------------------------------------------------------------------
    | Broadcast Connections
    |--------------------------------------------------------------------------
    |
    | Here you may define all of the broadcast connections that will be used
    | to broadcast events to other systems or over websockets. Samples of
    | each available type of connection are provided inside this array.
    |
    */

    'connections' => [

        'pusher' => [
            'driver' => 'pusher',
            'key' => env('PUSHER_APP_KEY'),
            'secret' => env('PUSHER_APP_SECRET'),
            'app_id' => env('PUSHER_APP_ID'),
            'options' => [
                'cluster' => env('PUSHER_APP_CLUSTER'),
                'encrypted' => true,
            ],
        ],

        'redis' => [
            'driver' => 'redis',
            'connection' => 'default',
        ],

        'log' => [
            'driver' => 'log',
        ],

        'null' => [
            'driver' => 'null',
        ],

    ],

];

默認狀況下 Laravel 框架提供諸多開箱即用的廣播驅動器程序。github

本文將使用 Pusher 做爲廣播驅動器。但在調試階段,咱們能夠選擇使用 log 做爲廣播驅動。同時若是選用 log 驅動,也就表示客戶端將不會接收任何消息,而只是將須要廣播的消息寫入到 laravel.log 日誌文件內。web

在下一節,咱們將進一步講解如何實現一個即時通訊應用。redis

前期準備

Laravel 廣播系統支持 3 中不一樣頻道類型 - public(公共), private(私有) 和 presence(存在)。當系統須要向所用用戶推送信息時,可使用 「public(公共)」 類型的頻道。相反,若是僅須要將消息推送給指定的頻道,則須要使用 「 private(私有)」 類型的頻道。

咱們的示例項目將實現一個僅支持登陸用戶才能收到即時信息的消息系統,因此將使用 「 private(私有)」 類型的頻道。

開箱即用的認證服務

首先對於新建立的 Laravel 項目,咱們須要安裝 Laravel 提供的開箱即用的認證服務組件,默認認證服務功能包括:註冊、登陸等功能。若是您不知道如何使用默認認證服務,能夠查看 Laravel 的用戶認證系統 文檔快速入門。

服務端 Pusher SDK 安裝配置

這邊咱們將使用 Pusher 這個第三方服務做爲 WebSocket 服務器,因此還須要建立一個 賬號 並確保已獲取 API 證書。安裝配置遇到任何問題,請在評論區說明。

以後須要使用 Composer 包管理工具安裝 Pusher 的 PHP 版本 SDK,這樣才能在 Laravel 項目中使用 Pusher 發送廣播信息。

如今進入 Laravel 項目的根目錄,執行下面這條命令進行安裝:

composer require pusher/pusher-php-server "~3.0"

安裝完成後修改廣播配置文件,啓用 Pusher 驅動做爲廣播系統的驅動器。

<?php
 
return [
 
    /*
    |--------------------------------------------------------------------------
    | Default Broadcaster
    |--------------------------------------------------------------------------
    |
    | This option controls the default broadcaster that will be used by the
    | framework when an event needs to be broadcast. You may set this to
    | any of the connections defined in the "connections" array below.
    |
    | Supported: "pusher", "redis", "log", "null"
    |
    */
 
    'default' => env('BROADCAST_DRIVER', 'pusher'),
 
    /*
    |--------------------------------------------------------------------------
    | Broadcast Connections
    |--------------------------------------------------------------------------
    |
    | Here you may define all of the broadcast connections that will be used
    | to broadcast events to other systems or over websockets. Samples of
    | each available type of connection are provided inside this array.
    |
    */
 
    'connections' => [
 
        'pusher' => [
            'driver' => 'pusher',
            'key' => env('PUSHER_APP_KEY'),
            'secret' => env('PUSHER_APP_SECRET'),
            'app_id' => env('PUSHER_APP_ID'),
            'options' => [
                        'cluster' => 'ap2',
                        'encrypted' => true
            ],
        ],
 
        'redis' => [
            'driver' => 'redis',
            'connection' => 'default',
        ],
 
        'log' => [
            'driver' => 'log',
        ],
 
        'null' => [
            'driver' => 'null',
        ],
 
    ],
 
];

如你所見,咱們修改了默認驅動器。而且將 connections 節點的 Pusher 配置的 cluster 修改爲 ap2

同時還有須要從 .env 配置文件獲取的配置選項,因此咱們須要更新 .env 文件,加入以下配置信息:

BROADCAST_DRIVER=pusher
 
PUSHER_APP_ID={YOUR_APP_ID}
PUSHER_APP_KEY={YOUR_APP_KEY}
PUSHER_APP_SECRET={YOUR_APP_SECRET}

接下來,還須要對 Laravel 核心文件稍做修改才能使用最新的 Pusher SDK。不過,我並不提倡修改 Laravel 核心文件,這邊因爲演示方便因此我修改了其中的代碼。

讓咱們打開 vendor/laravel/framework/src/Illuminate/Broadcasting/Broadcasters/PusherBroadcaster.php 文件。將 use Pusher; 替換爲 use PusherPusher;

以後打開 vendor/laravel/framework/src/Illuminate/Broadcasting/BroadcastManager.php 文件,在相似下面的代碼中作相同修改:

return new PusherBroadcaster(
  new \Pusher\Pusher($config['key'], $config['secret'],
  $config['app_id'], Arr::get($config, 'options', []))
);

最後,在 config/app.php 配置中開啓廣播服務提供者配置:

App\Providers\BroadcastServiceProvider::class,

這樣 Pusher 庫的安裝工做就完成了。下一節,咱們將講解客戶端類庫的安裝。

客戶端 Pusher 和 Laravel Echo 類庫的安裝配置

在廣播系統中,客戶端接口負責鏈接 WebSocket 服務器、訂閱指定頻道和監聽事件等功能。

幸運的是 Laravel 已經給咱們提供了一個叫 Laravel Echo 的插件,它實現一個複雜的 JavaScript 客戶端程,。而且這個插件內置支持 Pusher 的服務器鏈接。

能夠經過 NPM 包管理器安裝 Laravel Echo 模塊。若是您尚未安裝 Node.js 及 NPM 包管理程序,仍是要先安裝 Node.js 才行。

這裏我認爲您已經安裝好了 Node.js,因此安裝 Laravel Echo 擴展的命令以下:

npm install laravel-echo

安裝完成後咱們直接將 node_modules/laravel-echo/dist/echo.js 文件複製到 public/echo.js 就好了。

僅適用一個 echo.js 文件有點殺雞用了牛刀的感受,因此您還能夠到 Github 直接下載 echo.js 文件。

至此,咱們就完成了客戶端組件的安裝。

服務端文件設置

回想一下前文提到的內容:首先咱們須要實現一個容許用戶互相發送消息的應用;另外,應用會經過廣播系統向已登陸系統而且有收到消息的用戶推送消息。

這一節咱們將編寫服務端代碼實現廣播系統相關功能。

建立 message 遷移文件

首先,咱們須要建立一個 Message 模型用於存儲用戶發送的消息,執行以下命令建立一個遷移文件:

php make:model Message --migration

但在執行 migrate 命令前,咱們須要在遷移文件中加入表字段 tofrommessage

<?php
 
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
 
class CreateMessagesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('messages', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('from', false, true);
            $table->integer('to', false, true);
            $table->text('message');
            $table->timestamps();
        });
    }
 
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('messages');
    }
}

而後運行 migrate 命令運行數據庫遷移文件:

php artisan migrate

當須要在 Laravel 執行事件時,咱們首先須要作的是建立一個事件類,Laravel 將基於不一樣的事件類型執行不一樣的操做。

若是事件爲一個普通事件,Laravel 會調用對應的監聽類。若是事件類型爲廣播事件,Laravel 會使用 config/broadcasting.php 配置的驅動器將事件推送到 WebSocket 服務器。

本文使用的是 Pusher 服務,因此 Laravel 將事件推送到 Pusher 服務器。

先使用下面的 artisan 命令建立一個事件類:

php artisan make:event NewMessageNotification

這個命令會建立 app/Events/NewMessageNotification.php 文件,讓咱們修改文件內的代碼:

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use App\Message;

class NewMessageNotification implements ShouldBroadcastNow
{
    use SerializesModels;

    public $message;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Message $message)
    {
        $this->message = $message;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('user.'.$this->message->to);
    }
}

須要重點指出的是 NewMessageNotification 類實現了 ShouldBroadcastNow 接口,因此當咱們觸發一個事件時,Laravel 就可以當即知道有事件須要廣播給其餘用戶了。

實際上,咱們還能夠去實現 ShouldBroadcast 接口,這個接口會將事件加入到消息隊列中。而後由隊列的 Worker 進程依據入隊順序依次執行。因爲咱們項目須要當即將消息推送給用戶,因此咱們實現 ShouldBroadcastNow 接口更爲合適。

還有就是咱們須要顯示用戶接收的消息信息,因此咱們將 Message 模型做爲構造函數的參數,這樣消息信息就會同事件一塊兒傳入到指定頻道。

接下來還在 NewMessageNotification 類中建立了一個 broadcastOn 方法,在該方法中定義了廣播事件的頻道名稱,由於只有登陸的用戶才能接收消息,因此這裏建立了 PrivateChannel 實例做爲一個私有頻道。

定義頻道名稱格式相似於 user.{USER_ID} ,其中包含了指向接收信息的用戶 ID,用戶ID 從 $this->message->to 中獲取。

對於客戶端程序須要先進行用戶身份校驗,而後才能驚醒鏈接 WebSocket 服務器處理;這樣才能保證私有頻道的消息僅會廣播給登陸用戶。一樣在客戶端也僅容許登陸用戶纔可以訂閱 user.{USER_ID} 私有頻道。

若是您在客戶端程序使用了 Laravel Echo 組件處理訂閱服務。那在客戶端代碼中僅需設置頻道路由便可,而無需關心用戶認證處理細節。

打開 routes/channels.php 文件,而後定義一個廣播路由:

<?php
 
/*
|--------------------------------------------------------------------------
| Broadcast Channels
|--------------------------------------------------------------------------
|
| Here you may register all of the event broadcasting channels that your
| application supports. The given channel authorization callbacks are
| used to check if an authenticated user can listen to the channel.
|
*/

Broadcast::channel('App.User.{id}', function ($user, $id) {
    return (int) $user->id === (int) $id;
});

Broadcast::channel('user.{toUserId}', function ($user, $toUserId) {
    return $user->id == $toUserId;
});

以上,咱們設置了名爲 user.{toUserId} 路由,Broadcast::channel 方法的第二個參數接收一個閉包,Laravel 會將登陸用戶信息自動注入到閉包的第一個參數,第二個參數會從渠道中解析並獲取。

當客戶端嘗試訂閱 user.{USER_ID} 這個私有頻道時 Laravel Echo 組件會使用 XMLHttpRequest 以異步請求方式進行用戶身份校驗處理。

到這裏即時通訊全部編碼工做就完成了。

建立測試用例

首先,建立一個控制器 app/Http/Controllers/MessageController.php

<?php
namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Message;
use App\Events\NewMessageNotification;
use Illuminate\Support\Facades\Auth;

class MessageController extends Controller
{
    public function __construct() {
        $this->middleware('auth');
    }

    public function index()
    {
        $user_id = Auth::user()->id;
        $data = array('user_id' => $user_id);

        return view('broadcast', $data);
    }

    public function send()
    {
        // ...

        // 建立消息
        $message = new Message;
        $message->setAttribute('from', 1);
        $message->setAttribute('to', 2);
        $message->setAttribute('message', 'Demo message from user 1 to user 2');
        $message->save();

        // 將 NewMessageNotification 加入到事件
        event(new NewMessageNotification($message));

        // ...
    }
}

接下來建立 index 路由所需的 broadcast 視圖文件 resources/views/broadcast.blade.php

<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
 
    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">
 
    <title>Test</title>
 
    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
    <div id="app">
        <nav class="navbar navbar-default navbar-static-top">
            <div class="container">
                <div class="navbar-header">
 
                    <!-- Collapsed Hamburger -->
                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#app-navbar-collapse">
                        <span class="sr-only">Toggle Navigation</span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
 
                    <!-- Branding Image -->
                    <a class="navbar-brand123" href="{{ url('/') }}">
                        Test
                    </a>
                </div>
 
                <div class="collapse navbar-collapse" id="app-navbar-collapse">
                    <!-- Left Side Of Navbar -->
                    <ul class="nav navbar-nav">
                        &nbsp;
                    </ul>
 
                    <!-- Right Side Of Navbar -->
                    <ul class="nav navbar-nav navbar-right">
                        <!-- Authentication Links -->
                        @if (Auth::guest())
                            <li><a href="{{ route('login') }}">Login</a></li>
                            <li><a href="{{ route('register') }}">Register</a></li>
                        @else
                            <li class="dropdown">
                                <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
                                    {{ Auth::user()->name }} <span class="caret"></span>
                                </a>
 
                                <ul class="dropdown-menu" role="menu">
                                    <li>
                                        <a href="{{ route('logout') }}"
                                            onclick="event.preventDefault();
                                                     document.getElementById('logout-form').submit();">
                                            Logout
                                        </a>
 
                                        <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                                            {{ csrf_field() }}
                                        </form>
                                    </li>
                                </ul>
                            </li>
                        @endif
                    </ul>
                </div>
            </div>
        </nav>
 
        <div class="content">
                <div class="m-b-md">
                    New notification will be alerted realtime!
                </div>
        </div>
    </div>
 
    <!-- receive notifications -->
    <script src="{{ asset('js/echo.js') }}"></script>
 
    <script src="https://js.pusher.com/4.1/pusher.min.js"></script>
         
        <script>
          Pusher.logToConsole = true;
         
          window.Echo = new Echo({
            broadcaster: 'pusher',
            key: 'c91c1b7e8c6ece46053b',
            cluster: 'ap2',
            encrypted: true,
            logToConsole: true
          });
         
          Echo.private('user.{{ $user_id }}')
          .listen('NewMessageNotification', (e) => {
              alert(e.message.message);
          });
        </script>
    <!-- receive notifications -->
</body>
</html>

以後,打開 routes/web.php 路由配置文件定義 HTTP 路由:

Route::get('message/index', 'MessageController@index');
Route::get('message/send', 'MessageController@send');

因爲 MessageController 構造函數中使用了 auth 中間件,因此確保了僅有登陸用戶才能訪問以上路由。

接下來,讓咱們分析下 broadcast 視圖文件的核心代碼:

<!-- receive notifications -->
<script src="{{ asset('js/echo.js') }}"></script>

<script src="https://js.pusher.com/4.1/pusher.min.js"></script>

<script>
    Pusher.logToConsole = true;

    window.Echo = new Echo({
        broadcaster: 'pusher',
        key: 'c91c1b7e8c6ece46053b',
        cluster: 'ap2',
        encrypted: true,
        logToConsole: true
    });

    Echo.private('user.{{ $user_id }}')
    .listen('NewMessageNotification', (e) => {
        alert(e.message.message);
    });
</script>
<!-- receive notifications -->

視圖文件裏首先,引入了 echo.jspusher.min.js這兩個必要的模塊,這樣咱們纔可以使用 Laravel Echo 去鏈接 Pusher 的服務器。

接着,建立 Laravel Echo 實例。

以後,經過 Echo 實例的 private 方法訂閱 user.{USER_ID} 這個私有頻道。以前咱們說過只有登陸用戶才能訂閱私有頻道,因此 Echo 實例會使用 XHR 異步校驗用戶。而後,Laravel 會嘗試查找 user.{USER_ID} 路由,並匹配到已在 routes/channels.php 文件中定義的廣播路由。

一切順利的話,咱們的項目此時即完成了 Pusher 服務器鏈接,以後就會監聽 user.{USER_ID} 頻道。這樣客戶端才能夠正常接收指定頻道的全部消息。

完成客戶端接收 WebSocket 服務器消息接收編碼工做後,在服務端須要經過 Message::send 方法發送一個廣播消息。

發送的代碼以下:

public function send()
    {
        // ...

        // 建立消息
        $message = new Message;
        $message->setAttribute('from', 1);
        $message->setAttribute('to', 2);
        $message->setAttribute('message', 'Demo message from user 1 to user 2');
        $message->save();

        // 將 NewMessageNotification 加入到事件
        event(new NewMessageNotification($message));

        // ...
    }

這段代碼先是模擬了登陸用戶發送消息的操做。

而後經過 event 輔助函數將 NewMessageNotification 事件類實例加入廣播頻道。因爲 NewMessageNotificationShouldBroadcastNow 類的實例,Laravel 會從 config/broadcasting.php 配置文件中讀取廣播配置數據,而後將 NewMessageNotification 事件分發到配置文件所配置的 WebSocket 服務器的 user.{USER_ID} 頻道。

對於本文示例會將消息廣播到 Pusher 服務器的 user.{USER_ID} 頻道里。若是訂閱者的 ID 是 1,事件所處的廣播頻道則爲 user.1

以前咱們已經在前端代碼中完成頻道的訂閱和監聽處理,這裏當用戶收到消息時會在頁面彈出一個消息框提示給用戶。

如今如何對以上功能進行測試呢?

在瀏覽器訪問地址 http://your-laravel-site-doma... 。若是您未登陸系統,請先進行登陸處理,登陸後就能夠看到廣播頁面信息了。

雖然如今的 Web 頁面看起來什麼也沒有作,可是 Laravel 已經在後臺進行了一系列處理。經過 Pusher 組件的 Pusher.logToConsole 咱們能夠開啓 Pusher 的調試功能。下面是登陸後的調試信息內容:

Pusher : State changed : initialized -> connecting
 
Pusher : Connecting : {"transport":"ws","url":"wss://ws-ap2.pusher.com:443/app/c91c1b7e8c6ece46053b?protocol=7&client=js&version=4.1.0&flash=false"}
 
Pusher : Connecting : {"transport":"xhr_streaming","url":"https://sockjs-ap2.pusher.com:443/pusher/app/c91c1b7e8c6ece46053b?protocol=7&client=js&version=4.1.0"}
 
Pusher : State changed : connecting -> connected with new socket ID 1386.68660
 
Pusher : Event sent : {"event":"pusher:subscribe","data":{"auth":"c91c1b7e8c6ece46053b:cd8b924580e2cbbd2977fd4ef0d41f1846eb358e9b7c327d89ff6bdc2de9082d","channel":"private-user.2"}}
 
Pusher : Event recd : {"event":"pusher_internal:subscription_succeeded","data":{},"channel":"private-user.2"}
 
Pusher : No callbacks on private-user.2 for pusher:subscription_succeeded

能夠看到咱們完成了 WebSocket 服務器鏈接和私有頻道監聽。固然您看到的頻道名稱獲取和個人不同,但內容大體相同。接下來不要關閉這個 Web 頁面,而後去訪問 send 方法發送消息。

新開一個頁面窗口在瀏覽器訪問 http://your-laravel-site-doma... 頁面,順利的話會在 http://your-laravel-site-doma... 頁面收到一個提示消息。

同時在 index 的控制檯您還將看到到以下調試信息:

Pusher : Event recd : {"event":"App\\Events\\NewMessageNotification","data":{"message":{"id":57,"from":1,"to":2,"message":"Demo message from user 1 to user 2","created_at":"2018-01-13 07:10:10","updated_at":"2018-01-13 07:10:10"}},"channel":"private-user.2"}

如你所見,調試信息告訴咱們咱們接收來自 Pusher 服務器的 private-user.2 頻道的 AppEventsNewMessageNotification 消息。

固然,咱們還能夠經過 Pusher 管理後臺的儀表盤看到這個消息內容,它在 Debug Console 標籤頁,咱們能夠看到以下日誌信息。

調試日誌

這就是今天的所有內容,但願能給你們帶來幫助。

結論

今天,咱們研究了 Laravel 的 廣播 這個較少使用的特性。廣播可讓咱們使用 Web Sockets 發送實時消息。此外咱們還使用廣播功能實現了一個簡單的實時消息推送項目。本文內容較多,須要一些時間消化,有任何問題能夠隨時聯繫我。

原文:How Laravel Broadcasting Works

相關文章
相關標籤/搜索