首發於:我的部落格
前言
PHP從5.4開始,就提供了一個內建的web伺服器。
這個主要是用來做本地的開發用的。不能用於線上環境。現在我就介紹一下這個工具如何使用。
基礎應用
首先我們假定專案目錄是/home/baoguoxiao/www/php/demo
,外界可訪問的目錄是/home/baoguoxiao/www/php/demo/public
。然後訪問的埠是8000
,入口檔案是index.php
和index.html
。那麼我們可以執行如下命令:
cd /home/baoguoxiao/www/php/demo/public
php -S localhost:8000
然後這個時候就可以正常訪問了。
那麼現在有個問題,就是難道每次必須要進入public
資料夾才能啟動web伺服器嗎,其實我們可以指定根目錄的,那麼可以使用如下命令:
cd /home/baoguoxiao/www/php/demo
php -S localhost:8000 -t public/
那麼現在有一個問題就是說,如果我們使用了單入口,而且還是用了PATHINFO模式。那麼上面的可能就有問題了。
對此,我們可以使用如下方案:
cd /home/baoguoxiao/www/php/demo
php -S localhost:8000 router.php
router.php 檔案的程式碼
/**
* 對URL進行解析,並獲取請求的檔名
*/
$uri = urldecode(parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH));
/**
* 判斷是否存在該檔案,如果不存在,則直接繼續載入入口檔案
*/
if ($uri !== "/" && file_exists(__DIR__ . "$uri")) {
return false;
}
/**
* 載入入口檔案
*/
require_once "./index.php";
通過這個路由檔案,我們就可以支援目前常用的開發情況了。
框架參考
上面的方式是我們自己的實現,那麼我們也可以看看相關知名框架的實現方法。
比如 Laravel 和 Symfony。
Laravel
在Laravel中的安裝一節中介紹了一個命令可以使用PHP內建web伺服器實現外部訪問的命令。實現的命令是:
php artisan serve
我們可以看一下相關程式碼:
具體的檔案路徑為:vendor/laravel/framework/src/Illuminate/Foundation/Console/ServeCommand.php
/**
* 執行命令.
*
* @return int
*
* @throws \Exception
*/
public function handle()
{
// 切換路徑到 public 目錄
chdir(public_path());
// 在命令臺進行輸出相關內容
$this->line("<info>Laravel development server started:</info> <http://{$this->host()}:{$this->port()}>");
// 執行外部程式,並且 $status 為系統的返回狀態
passthru($this->serverCommand(), $status);
// $status 為0 表示執行正常, 為其他大於0的數字表示出現了錯誤,有可能是埠被搶佔了,這個時候就會接著判斷是否進行再次嘗試
if ($status && $this->canTryAnotherPort()) {
// 對繫結的埠號加1 預設是8000, 如果失敗則重試埠號為8001,再次失敗重試埠號為8002,以此類推。
$this->portOffset += 1;
// 再次呼叫此程式
return $this->handle();
}
// 返回狀態值
return $status;
}
/**
* 獲取完整的 server 命令.
*
* @return string
*/
protected function serverCommand()
{
return sprintf('%s -S %s:%s %s',
// 獲取PHP可執行命令的路徑
ProcessUtils::escapeArgument((new PhpExecutableFinder)->find(false)),
// 獲取需要繫結的host
$this->host(),
// 獲取需要繫結的埠
$this->port(),
// 對需要執行的引數進行轉義處理。這裡的 server 就是我們之前說的路由檔案,它在專案的根路徑下
ProcessUtils::escapeArgument(base_path('server.php'))
);
}
對上面的命令進行翻譯一下,實際上就是執行的
cd ./public
php -S 0.0.0.0:8000 ../server.php
note:
這裡我們可以看到一個區別就是之前我自己寫的程式碼,host 都是 localhost, 但是這裡寫的是 0.0.0.0。這兩個有什麼區別呢?
其實區別很簡單,比如我之前寫的 localhost 繫結的ip 是 127.0.0.1, 這個相當於一個迴環地址,那麼我們就只允許本機的IP進行訪問。而 0.0.0.0,則表示我們對ip不進行限制,所有的IP都可以進行訪問。
那我們接著再來看看專案根目錄下面的server.php
:
/**
* Laravel - A PHP Framework For Web Artisans
*
* @package Laravel
* @author Taylor Otwell <taylor@laravel.com>
*/
$uri = urldecode(
parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
);
// 這個檔案允許我們從內建 PHP web 伺服器中模擬 Apache 的 "mod_rewrite" 功能.
// 這提供了一種測試 Laravel 應用程式的便捷方法,
// 而無需在此安裝"真正的" web 伺服器軟體。
if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) {
return false;
}
require_once __DIR__.'/public/index.php';
發現跟我之前寫的路由檔案相同。沒錯,我就是從這裡抄過來的。
基本上 Larvel 的實現方法就是這樣了。
Symfony
如果你在使用 Symfony 框架話,發現Symfony有一個元件叫做web-server-bundle,這個元件的作用跟Laravel相同,也是不借助web伺服器,實現通過瀏覽器訪問應用程式。
基本的操作可以參考該頁面
我在這裡主要說一下Symfony是如何實現的.
在Symfony中有一段程式碼是這樣的:
public function start(WebServerConfig $config, $pidFile = null)
{
// 獲取預設的PID檔案位置
$pidFile = $pidFile ?: $this->getDefaultPidFile();
// 判斷是否在執行,如果執行則提示已經在監聽了
if ($this->isRunning($pidFile)) {
throw new \RuntimeException(sprintf('A process is already listening on http://%s.', $config->getAddress()));
}
// fork了一個子程式,如果成功,會有兩個程式進行同時執行下面的檔案,父程式,也就是當前執行的程式會返回子程式的PID,而子程式則返回的PID為0,
// 如果失敗,則子程式不會建立,並且父程式會返回的pid為-1。更多內容可檢視 https://www.php.net/manual/zh/function.pcntl-fork.php
$pid = pcntl_fork();
// 表示fork程式失敗
if ($pid < 0) {
throw new \RuntimeException('Unable to start the server process.');
}
// 進入這個判斷,表示執行的是父程式,表示不用繼續向下執行
if ($pid > 0) {
return self::STARTED;
}
// 從此往後是子程式執行,首先通過 posix_setsid 變為守護程式,意思是使其脫離終端的管理,自立門戶,誰也沒辦法管理這個程式,除了PID。
if (posix_setsid() < 0) {
throw new \RuntimeException('Unable to set the child process as session leader.');
}
// 建立命令,命令類似Laravel,不過這裡的路由檔案跟Laravel類似。也是處理載入規則,並載入入口檔案。具體的router.php 路徑為:
// vendor\symfony\web-server-bundle/Resources/router.php
// 下面是禁用輸出並且開始執行
$process = $this->createServerProcess($config);
$process->disableOutput();
$process->start();
// 判斷是否執行成功
if (!$process->isRunning()) {
throw new \RuntimeException('Unable to start the server process.');
}
// 寫入PID檔案
file_put_contents($pidFile, $config->getAddress());
// 檢測PID檔案,如果PID檔案刪除了,那麼程式就立即退出。
while ($process->isRunning()) {
if (!file_exists($pidFile)) {
$process->stop();
}
sleep(1);
}
// 返回停止的狀態
return self::STOPPED;
}
/**
* 啟動PHP內建web伺服器
* @return Process The process
*/
private function createServerProcess(WebServerConfig $config)
{
// 查詢PHP的可執行程式
$finder = new PhpExecutableFinder();
if (false === $binary = $finder->find(false)) {
throw new \RuntimeException('Unable to find the PHP binary.');
}
$xdebugArgs = ini_get('xdebug.profiler_enable_trigger') ? ['-dxdebug.profiler_enable_trigger=1'] : [];
// 例項化PHP要執行的命令 php_path -dvariables_order=EGPCS -S 127.0.0.1:8000 vendor\symfony\web-server-bundle/Resources/router.php
$process = new Process(array_merge([$binary], $finder->findArguments(), $xdebugArgs, ['-dvariables_order=EGPCS', '-S', $config->getAddress(), $config->getRouter()]));
// 設定工作目錄
$process->setWorkingDirectory($config->getDocumentRoot());
// 設定超時時間
$process->setTimeout(null);
// 設定環境變數
if (\in_array('APP_ENV', explode(',', getenv('SYMFONY_DOTENV_VARS')))) {
$process->setEnv(['APP_ENV' => false]);
$process->inheritEnvironmentVariables();
}
// 返回相關變數
return $process;
}
我在上面的程式碼中進行了註釋, 描述了Symfony是如何啟動的.
裡面有一個問題就是使用pcntl_fork
, 該擴充套件在Windows中是不受支援的. 所以 Symfony框架會提示使用php bin/console server:run
命令執行程式.
未來展望
其實還有一個方式, 就是 Workman 是通過自身的實現的web伺服器,它並沒有藉助php -S
命令。這一塊的程式碼我還沒有吃透,並且我覺得這個也可以單獨拎幾章出來講。希望以後有這個機會。
總結
通過我們學習 PHP 命令實現web伺服器訪問以及對 Laravel 和 Symfony 框架的分析, 讓我瞭解到在Windows的開發過程中,我們完全可以藉助該方式來擺脫對web伺服器的依賴.既能方便我們在Windows環境進行開發並且學習了PHP一個技巧.感覺挺好的.
大家如果對此有什麼疑問可以評論進行交流.
參考
- PHP: 內建Web Server - Manual
- Laravel
- How to Use PHP's built-in Web Server
- PHP: pcntl_fork - Manual
- PHP: posix_setsid - Manual
本作品採用《CC 協議》,轉載必須註明作者和本文連結