电竞比分网-中国电竞赛事及体育赛事平台

分享

【Laravel系列7.3】Session與響應(yīng)

 硬核項(xiàng)目經(jīng)理 2022-06-02 發(fā)布于湖南

Session與響應(yīng)

Session 這個(gè)東西還需要多說?學(xué) PHP 或者任何 Web 開發(fā)語言的入門課好不好!既然這么說,那么看來你是沒用過 Laravel 自帶的 Session 呀,Laravel 的 Session 可不是用得 PHP 默認(rèn)的那個(gè) Session 哦。今天我們就來一起看看 Laravel 自己實(shí)現(xiàn)的這個(gè) Session 是啥樣的。另外,請(qǐng)求流程我們?cè)谧钤绲臅r(shí)候就已經(jīng)講過了,但是響應(yīng)一直都沒怎么提過,咱們也一起來看一看。

Session

默認(rèn)情況下對(duì)于普通的 Session 來說,我們的 PHP 會(huì)生成一個(gè)臨時(shí)文件存儲(chǔ)到系統(tǒng)的 /tmp 目錄下,里面的內(nèi)容實(shí)際上是一個(gè)序列化的對(duì)象結(jié)構(gòu)。這個(gè)大家可以自己去試試,如果需要改成使用 memcached 或者 redis 的話,需要修改 php.ini 文件,還是比較麻煩的。Laravel 框架沒有使用默認(rèn)的 PHP 的 Session 來進(jìn)行存儲(chǔ),而是自己實(shí)現(xiàn)了一套。其實(shí)功能差不多,默認(rèn)同樣是存儲(chǔ)成文件,但如果需要修改存儲(chǔ)方式的話,框架就會(huì)方便很多,畢竟不用去動(dòng) php.ini 文件。

Route::get('session/test'function(){
    \Illuminate\Support\Facades\Session::put('a''aaaaaaa');
    echo request()->session()->get('a'); // aaaaaaa
    echo session()->get('a'); // aaaaaaa

    echo \Illuminate\Support\Facades\Session::getId(); // SZL5LXKfJTm9ZRotUZRxM59qXO4IcmdKollBMFW9
});

在這里我們展示了三種操作 Session 的方式,一個(gè)是通過門面的 Session 對(duì)象,另一個(gè)則是通過請(qǐng)求對(duì)象 Request 的 session() 方法,最后還可以通過 session() 這個(gè)全局輔助函數(shù)來操作。put() 添加數(shù)據(jù),get() 獲取數(shù)據(jù),getId() 獲得 Session ID ,都非常簡單。

這個(gè)時(shí)候,你可以去 storage/framework/sessions 目錄下面找到對(duì)應(yīng) Session ID 名稱的緩存文件,它的內(nèi)容是下面這個(gè)樣子的。

a:5:{s:6:"_token";s:40:"EZOEx7QwZNKbibc9hHOqJRo07Srd5RT9q5mYVmVC";s:1:"a";s:7:"aaaaaaa";s:6:"_flash";a:2:{s:3:"new";a:0:{}s:3:"old";a:1:{i:0;s:1:"b";}}s:9:"_previous";a:1:{s:3:"url";s:28:"http://laravel8/session/test";}s:1:"b";s:3:"bbb";}

后面的 _flash 是我們馬上要測試的另外一個(gè)功能保存的數(shù)據(jù),暫時(shí)可以忽略掉。是不是和 PHP 默認(rèn)的 Session 沒啥區(qū)別,都是保存了一個(gè)序列化對(duì)象。另外需要注意的是,和普通的 Session 一樣,我們?cè)谑褂闷胀ǖ?Session 前需要 session_start() 一下,在 Laravel 中,則是需要保證 app/Http/Kernel.php 中 StartSession 中間件沒有被注釋掉,當(dāng)然,默認(rèn)它是打開的。

閃存

閃存是什么東西?這可不是我們的 U盤 呀,而是一種一次性的 Session 機(jī)制。它的作用就是一個(gè)一次性的 Session 數(shù)據(jù),當(dāng)數(shù)據(jù)被放到緩存后,下一次或者別的請(qǐng)求只能從 Session 中取一次這個(gè)數(shù)據(jù),再次取數(shù)據(jù)就沒有了。注意,一定是別的請(qǐng)求,而不是當(dāng)前這個(gè)請(qǐng)求。

Route::get('session/test'function(){
    \Illuminate\Support\Facades\Session::flash('b''bbb');

    echo request()->session()->get('b');
    echo request()->session()->get('b');
});
Route::get('session/test2'function(){
    echo request()->session()->get('b');
});

使用 flash() 方法就可以保存一條閃存數(shù)據(jù),它在數(shù)據(jù)中的格式就是我們上面看到的 Session 內(nèi)容中的 _flash 字段所保存的內(nèi)容,可以看到在這個(gè)對(duì)象中還包含了 _previous 字段,其中就有我們保存數(shù)據(jù)時(shí)的鏈接地址。

在保存數(shù)據(jù)時(shí)的地址訪問時(shí),可以一直訪問,但當(dāng)你使用別的鏈接訪問數(shù)據(jù)時(shí),就只能訪問一次了,不信你可以試試多訪問幾次 session/test2 這條鏈接。這時(shí),再查看保存的 Session 數(shù)據(jù),我們會(huì)發(fā)現(xiàn) b 的數(shù)據(jù)內(nèi)容已經(jīng)沒有了。

a:4:{s:6:"_token";s:40:"5I8GP7LNzygNcYeTKer8kINqdxMGY1ArTqAXd13n";s:1:"a";s:7:"aaaaaaa";s:6:"_flash";a:2:{s:3:"new";a:0:{}s:3:"old";a:0:{}}s:9:"_previous";a:1:{s:3:"url";s:29:"http://laravel8/session/test2";}}

這個(gè)就是閃存的作用。如果你想要將這個(gè)數(shù)據(jù)保存到其它的請(qǐng)求,而不是在 test2 中使用的話,可以使用 reflash() ?;蛘吣阆胍獙⑦@個(gè)數(shù)據(jù)轉(zhuǎn)換成正常的 Session 數(shù)據(jù),那么可以使用 keep() 方法。這兩個(gè)方法大家可以自己測試一下,官方文檔上都有代碼演示。

切換成 redis

默認(rèn)情況下,我們的 Session 走的是文件存儲(chǔ),這個(gè)上面我們已經(jīng)看到了,而且也很方便地能夠找到生成的 Session 文件。對(duì)于正式的開發(fā)環(huán)境來說,稍微上一點(diǎn)規(guī)模的項(xiàng)目多少都會(huì)需要進(jìn)行多臺(tái)服務(wù)器的分布式布局,這個(gè)時(shí)候,如果 Session 還是以文件形式分布在不同的服務(wù)器,就會(huì)出現(xiàn)很尷尬的局面,那就是用戶的請(qǐng)求可能并不一定每次都會(huì)落在同一臺(tái)服務(wù)器。于是,使用外部的 公共硬盤 或者使用 Redis 或者 Memcached 之類的緩存框架來進(jìn)行 Session 的保存就是非常常見的做法了。相對(duì)于 公共硬盤 來說,肯定是緩存服務(wù)效率更好,而且也更便于維護(hù)。

Laravel 中使用 Redis 或 Memcached 來進(jìn)行 Session 保存非常簡單,只需要修改 .env 配置文件就可以了,這里我們就以 Redis 為例。

SESSION_DRIVER=redis
SESSION_CONNECTION=default

直接修改 SESSION_DRIVER 驅(qū)動(dòng)為 redis 即可,下面的 SESSION_CONNECTION 則是指定要使用的連接,也就是我們?cè)?config/db.php 中配置的連接。

通過設(shè)置之后,我們?cè)俅卧L問測試頁面,然后直接在 redis 中就可以看到一個(gè) laravel_database_laravel_cache:SZL5LXKfJTm9ZRotUZRxM59qXO4IcmdKollBMFW9 鍵的緩存數(shù)據(jù),里面的內(nèi)容就是我們的 Session 信息。這個(gè) key 使用的依然是 Laravel 生成的那個(gè) Session ID 。

阻塞

默認(rèn)情況下,Laravel 是允許使用同一 Session 的請(qǐng)求并發(fā)執(zhí)行的。但是一小部分應(yīng)用程序中可能會(huì)丟失 Session ,比如兩個(gè)請(qǐng)求同時(shí)到達(dá),其中一個(gè)設(shè)置另一個(gè)讀取,這時(shí)候,讀取的請(qǐng)求可能就是無法讀取到內(nèi)容的,或者兩個(gè)請(qǐng)求同時(shí)寫入同一個(gè) Session 。其實(shí)這就是一個(gè)并發(fā)的問題,一般情況下,我們?cè)?Swoole 或者 Java 中會(huì)加鎖來實(shí)現(xiàn),而 Laravel 框架則是提供了一個(gè)阻塞的能力。

Route::get('session/test'function(){
    \Illuminate\Support\Facades\Session::flash('b''bbb');

    echo request()->session()->get('b');
    echo request()->session()->get('b');
    sleep(10);
})->block($lockSeconds = 10, $waitSeconds = 10);

Route::get('session/test2'function(){
    echo request()->session()->get('b');
})->block($lockSeconds = 10, $waitSeconds = 10);

在這段代碼中,我們?cè)O(shè)置了一個(gè)閃存數(shù)據(jù),同一個(gè)請(qǐng)求中,閃存可以無限次訪問。然后我們讓代碼停頓 10秒 用于測試。接下來就是使用了一個(gè) block() 方法來進(jìn)行阻塞,它有兩個(gè)參數(shù),一個(gè)是 lockSeconds 表示加鎖時(shí)間,另一個(gè) waitSeconds 表示等待時(shí)間。

加鎖時(shí)間也就是阻塞時(shí)間,如果請(qǐng)求的執(zhí)行時(shí)間長,則在阻塞時(shí)間內(nèi)會(huì)鎖住請(qǐng)求,另一個(gè)請(qǐng)求的等待時(shí)間則是有鎖情況下會(huì)一直等待會(huì)話鎖的完成,如果超過了設(shè)置的 10秒 則會(huì)返回 LockTimeoutException 異常。大家可以先運(yùn)行行一個(gè)請(qǐng)求,在等待的時(shí)候再運(yùn)行第二個(gè)請(qǐng)求,當(dāng) sleep() 結(jié)束后,兩個(gè)請(qǐng)求的結(jié)果才會(huì)返回。

Session 實(shí)現(xiàn)

相信大家對(duì)于如何找到源碼實(shí)現(xiàn)內(nèi)容已經(jīng)非常熟悉了,那么我也就不多說了,直接去找到 vendor/laravel/framework/src/Illuminate/Session/SessionManager.php 就可以了。在這個(gè)類中,我們可以看到許多的 Session 驅(qū)動(dòng),依然還是以 Redis 的來看一看。

protected function createRedisDriver()
{
    $handler = $this->createCacheHandler('redis');

    $handler->getCache()->getStore()->setConnection(
        $this->config->get('session.connection')
    );

    return $this->buildSession($handler);
}
protected function buildSession($handler)
{
    return $this->config->get('session.encrypt')
            ? $this->buildEncryptedSession($handler)
            : new Store($this->config->get('session.cookie'), $handler);
}
protected function createCacheHandler($driver)
{
    $store = $this->config->get('session.store') ?: $driver;

    return new CacheBasedSessionHandler(
        clone $this->container->make('cache')->store($store),
        $this->config->get('session.lifetime')
    );
}

在 bulidSession 中,我們獲得的是一個(gè) Store 對(duì)象,傳遞進(jìn)去的 handler 是在上面的 createRedisDriver() 中通過 createCacheHandler() 方法中定義的,其實(shí)在這個(gè)方法中,就是通過 服務(wù)容器 獲得了一個(gè) Cache 對(duì)象。當(dāng)你使用 SESSION 門面或者 session() 輔助函數(shù)調(diào)用 Session 的操作函數(shù)時(shí),其實(shí)是在 SessionManager 繼承的 Manager 對(duì)象中,它實(shí)現(xiàn)了 __call 方法的調(diào)用,實(shí)際上最后調(diào)用的都是 Store 對(duì)象。

public function __call($method, $parameters)
{
    return $this->driver()->$method(...$parameters);
}

接下來,在 vendor/laravel/framework/src/Illuminate/Session/Store.php 類中,我們就可以看到各種 Session 方法,在這里比較有意思的是,它是以這個(gè)對(duì)象進(jìn)行保存的,也就是說,在執(zhí)行 put()、get() 之類的方法時(shí),其實(shí)操作的是 Store 中的數(shù)組

public function get($key, $default = null)
{
    return Arr::get($this->attributes, $key, $default);
}
public function put($key, $value = null)
{
    if (! is_array($key)) {
        $key = [$key => $value];
    }

    foreach ($key as $arrayKey => $arrayValue) {
        Arr::set($this->attributes, $arrayKey, $arrayValue);
    }
}

那么 Session 是在什么時(shí)候保存的呢?這個(gè)就要看 startSession 這個(gè)中間件了。

// vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php
protected function handleStatefulRequest(Request $request, $session, Closure $next)
{
    // If a session driver has been configured, we will need to start the session here
    // so that the data is ready for an application. Note that the Laravel sessions
    // do not make use of PHP "native" sessions in any way since they are crappy.
    $request->setLaravelSession(
        $this->startSession($request, $session)
    );

    $this->collectGarbage($session);

    $response = $next($request);

    $this->storeCurrentUrl($request, $session);

    $this->addCookieToResponse($response, $session);

    // Again, if the session has been configured we will need to close out the session
    // so that the attributes may be persisted to some storage medium. We will also
    // add the session identifier cookie to the application response headers now.
    $this->saveSession($request);

    return $response;
}

在 startSession 中間件中,handle() 最后會(huì)調(diào)用 handleStatefulRequest() 這個(gè)方法,可以看出,這個(gè)方法是一個(gè)后置中間件,在請(qǐng)求操作結(jié)束后,調(diào)用了 saveSession() 方法,它實(shí)際上調(diào)用的是 Manager 中的 save() 方法。關(guān)于這一塊,大家可以自己嘗試一下,讓一個(gè)請(qǐng)求暫停然后看 Session 文件里數(shù)據(jù)有沒有變化,然后暫停完成之后再看一下就明白了。當(dāng)然,我們也可以手動(dòng)調(diào)用 save() 方法實(shí)時(shí)保存。

響應(yīng)

對(duì)于請(qǐng)求流程,大家已經(jīng)非常熟悉了,也了解過在控制器或者路由中,想要返回響應(yīng)的內(nèi)容,直接 retrun 就可以了,不過對(duì)于具體的響應(yīng)操作我們還是沒有進(jìn)行過深入的學(xué)習(xí)。今天就一起來學(xué)習(xí)一下響應(yīng)的具體內(nèi)容。

添加響應(yīng)頭及 Cookie

如果要返回響應(yīng)內(nèi)容,直接 return 數(shù)據(jù)就可以了,但如果想為響應(yīng)增加頭和 Cookie 信息的話,最簡單的就是使用 response() 輔助函數(shù)。

Route::get('response/test1'function(){
    return response('Hello test1'200)
        ->header('Content-type''application/json')
        ->withHeaders([
            'A'=>'A info',
            'B'=>'B info'
        ])
        ->cookie('oppo''o', );
});

header() 方法可以指定單個(gè)響應(yīng)頭,而 withHeaders() 則可以以數(shù)組的方式設(shè)置多個(gè)響應(yīng)頭。cookie() 方法則是設(shè)置 Cookie 的方法,它的參數(shù)和我們普通的 Cookie 操作函數(shù)的參數(shù)是一致的,后面的可選參數(shù)中也可以設(shè)置過期時(shí)間、HttpOnly 等內(nèi)容。

重定向與文件下載

對(duì)于重定向來說,我們可以看成是跳轉(zhuǎn)至某個(gè)頁面,可以直接寫路由、使用路由別名,也可以直接跳轉(zhuǎn)到某個(gè)控制器方法。

Route::get('response/test2'function(){
//    return redirect('response/test1');
//    return redirect('response/test1',301);
//    return redirect()->route('rt3');
    return redirect()->action([\App\Http\Controllers\TestController::class, 'test2'], ['id'=>1]);

});

Route::name('rt3')->get('response/test3'function(){
    echo 111;
});

上面的測試代碼中,第二行注釋起來的測試代碼我們還可以指定重定向的狀態(tài)碼。默認(rèn)情況下走的是 302 跳轉(zhuǎn),在這里我們可以設(shè)置成 301 跳轉(zhuǎn)。關(guān)于 302 和 301 的區(qū)別我就不再多說了,一個(gè)是臨時(shí)重定向,一個(gè)是永久重定向,如果有不明白的小伙伴可以去查詢一下相關(guān)的資料。不過更推薦的是好好學(xué)習(xí)一下 HTTP 相關(guān)的知識(shí)。

Route::get('response/test4'function(){
    return response()->download(\Illuminate\Support\Facades\Storage::path('public/8cb3c505713a1e861169aa227ee1c37c.jpg'));
});

文件下載有一個(gè)非常簡單的函數(shù)就是直接使用 download() 函數(shù),里面指定文件路徑就可以了。同時(shí)還有別的方式可以實(shí)現(xiàn)文件的下載,文檔中寫得很詳細(xì)了,這里就不多說了。

響應(yīng)流程

對(duì)于響應(yīng)來說,通過查閱 response() 方法的實(shí)現(xiàn)就可以發(fā)現(xiàn)它返回的是一個(gè) vendor/laravel/framework/src/Illuminate/Http/Response.php 對(duì)象,而這個(gè)對(duì)象又是繼承自 Symfony 的 vendor/symfony/http-foundation/Response.php 。就和請(qǐng)求一樣,它的底層實(shí)現(xiàn)依然是 Symfony 框架中的響應(yīng)實(shí)現(xiàn)。

首先到 public/index.php 入口文件中,我們會(huì)發(fā)現(xiàn)這樣的一段代碼。

$response = tap($kernel->handle(
    $request = Request::capture()
))->send();

在這里,Kernel 的 Handle() 方法實(shí)際上返回的就是一個(gè) Response 對(duì)象。

public function handle($request)
{
    try {
        $request->enableHttpMethodParameterOverride();

        $response = $this->sendRequestThroughRouter($request);
    } catch (Throwable $e) {
        $this->reportException($e);

        $response = $this->renderException($request, $e);
    }

    $this->app['events']->dispatch(
        new RequestHandled($request, $response)
    );

    return $response;
}

sendRequestThroughRouter() 方法在之前的 中間件 以及 服務(wù)容器 和 管道 相關(guān)的文章中都接觸過,他就是我們請(qǐng)求處理的核心流程,在請(qǐng)求的最后,就會(huì)返回響應(yīng)結(jié)果。在 index.php 中,Kernel 執(zhí)行完 handle() 之后,會(huì)再調(diào)用一個(gè) send() 方法。這個(gè)方法存在于 vendor/symfony/http-foundation/Response.php 中。

public function send()
{
    $this->sendHeaders();
    $this->sendContent();

    if (\function_exists('fastcgi_finish_request')) {
        fastcgi_finish_request();
    } elseif (!\in_array(\PHP_SAPI, ['cli''phpdbg'], true)) {
        static::closeOutputBuffers(0true);
    }

    return $this;
}

接下來我們?cè)龠M(jìn)入到 sendHeaders() 和 sendContent() 中。

public function sendHeaders()
{
    // headers have already been sent by the developer
    if (headers_sent()) {
        return $this;
    }

    // headers
    foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) {
        $replace = 0 === strcasecmp($name, 'Content-Type');
        foreach ($values as $value) {
            header($name.': '.$value, $replace, $this->statusCode);
        }
    }

    // cookies
    foreach ($this->headers->getCookies() as $cookie) {
        header('Set-Cookie: '.$cookie, false$this->statusCode);
    }

    // status
    header(sprintf('HTTP/%s %s %s'$this->version, $this->statusCode, $this->statusText), true$this->statusCode);

    return $this;
}

public function sendContent()
{
    echo $this->content;

    return $this;
}

嗯,還需要繼續(xù)解釋嗎?相信大家已經(jīng)明白最后的輸出就在這里完成了吧!

總結(jié)

今天學(xué)習(xí)了兩塊內(nèi)容,不過其實(shí)都和請(qǐng)求響應(yīng)有關(guān),Session 是非常常用的功能,響應(yīng)也是所有請(qǐng)求必不可少的。Session 之所以框架要重寫一套而不用原生的,也是為了靈活起見,我們不需要去 php.ini 配置文件修改 Session 相關(guān)的功能。而響應(yīng)則走的依然是 Symfony 的底層框架功能。就像 Laravel 的口號(hào)一樣,讓實(shí)現(xiàn)的代碼更優(yōu)雅,從而對(duì)這些功能又重新進(jìn)行更適合自己的封裝。

參考文檔:

https:///docs/laravel/8.x/session/9373

https:///docs/laravel/8.x/responses/9370

    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多