本篇文章給大家分享的是有關PHP中怎么搭建一個HTTP服務,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
<?php
use Swoole\Http\Request;
use Swoole\Http\Response;
$process = new Swoole\Process(function (Swoole\Process $process) {
$server = new Swoole\Http\Server('127.0.0.1', 9501, SWOOLE_BASE);
$server->set([
'log_file' => '/dev/null',
'log_level' => SWOOLE_LOG_INFO,
'worker_num' => swoole_cpu_num() * 2,
// 'hook_flags' => SWOOLE_HOOK_ALL,
]);
$server->on('workerStart', function () use ($process, $server) {
$process->write('1');
});
$server->on('request', function (Request $request, Response $response) use ($server) {
try {
$redis = new Redis;
$redis->connect('127.0.0.1', 6379);
$greeter = $redis->get('greeter');
if (!$greeter) {
throw new RedisException('get data failed');
}
$response->end("<h2>{$greeter}</h2>");
} catch (\Throwable $th) {
$response->status(500);
$response->end();
}
});
$server->start();
});
if ($process->start()) {
register_shutdown_function(function () use ($process) {
$process::kill($process->pid);
$process::wait();
});
$process->read(1);
System('ab -c 256 -n 10000 -k http://127.0.0.1:9501/ 2>&1');
}首先,我們創建了一個Swoole\Process對象,這個對象會開啟一個子進程,在子進程中,我創建了一個HTTP Server,這個服務器是BASE模式的。除了BASE模式之外,還有一種PROCESS模式。在PROCESS模式下,套接字連接是在Master進程維持的,Master進程和Worker進程會多一層IPC通信的開銷,但是,當Worker進程奔潰的時候,因為連接是在Master進程維持的,所以連接不會被斷開。所以,Process模式適用于維護大量長連接的場景。
BASE模式是在每個工作進程維持自己的連接,所以性能會比Master更好。并且,在HTTP Server下,BASE模式會更加的適用。
這里,我們將worker_num,也就是進程的數量設置為當前機器CPU核數的兩倍。但是,在實際的項目中,我們需要不斷的壓測,來調整這個參數。
在workerStart的時候,也就是工作進程啟動的時候,我們讓子進程向管道中隨意寫入一個數據給父進程,父進程此時會讀到一點數據,讀到數據后,父進程才開始壓測。
此時,壓測的請求會進入onRequest回調。在這個回調中,我們創建了一個Redis客戶端,這個客戶端會連接Redis服務器,并請求一條數據。得到數據后,我們調用end方法來響應壓測的請求。當錯誤時,我們返回一個錯誤碼為500的響應。
在開始壓測前,我們需要安裝Redis擴展:
pecl install redis
然后php.ini配置中開啟redis擴展即可。
我們還需要在Redis服務器里面插入一條數據:
127.0.0.1:6379> SET greeter swoole OK 127.0.0.1:6379> GET greeter "swoole" 127.0.0.1:6379>
OK,我們現在進行壓測:
~/codeDir/phpCode/swoole/server # php server.php Concurrency Level: 256 Time taken for tests: 2.293 seconds Complete requests: 10000 Failed requests: 0 Keep-Alive requests: 10000 Total transferred: 1680000 bytes HTML transferred: 150000 bytes Requests per second: 4361.00 [#/sec] (mean) Time per request: 58.702 [ms] (mean) Time per request: 0.229 [ms] (mean, across all concurrent requests) Transfer rate: 715.48 [Kbytes/sec] received
我們發現,現在的QPS比較低,只有4361.00。
因為,我們目前使用的Redis擴展是PHP官方的同步阻塞客戶端,沒有利用到協程(或者說異步的特性)。當進程去連接Redis服務器的時候,可能會阻塞整個進程,導致進程無法處理其他的連接,這樣,這個HTTP Server處理請求的速度就不可能快。但是,這個壓測結果會比FPM下好,因為Swoole是常駐進程的。
現在,我們來開啟Swoole提供的RuntimeHook機制,也就是在運行時動態的將PHP同步阻塞的方法全部替換為異步非阻塞的協程調度的方法。我們只需要在server->set配置中加入一行即可:
'hook_flags' => SWOOLE_HOOK_ALL
此時,我們再來運行這個腳本:
Concurrency Level: 256 Time taken for tests: 1.643 seconds Complete requests: 10000 Failed requests: 0 Keep-Alive requests: 10000 Total transferred: 1680000 bytes HTML transferred: 150000 bytes Requests per second: 6086.22 [#/sec] (mean) Time per request: 42.062 [ms] (mean) Time per request: 0.164 [ms] (mean, across all concurrent requests) Transfer rate: 998.52 [Kbytes/sec] received
我們發現,此時的QPS還是有一定的提升的。(這里,視頻中壓測的時候,會夯住請求,導致QPS非常的低,但是我實際測試的時候沒有發生這個情況,估計是和Redis服務器本身對連接個數的的配置有關)
但是,為了避免請求數量過多,導致創建連接個數過多的問題,我們可以使用一個Redis連接池來解決。(同步阻塞是沒有Redis連接過多的問題的,因為一旦worker進程阻塞住了,那么后面的請求就不會繼續執行了,也就不會創建新的Redis連接了。因此,在同步阻塞的模式下,Redis的連接數量最大是worker進程的個數)
現在,我們來實現一下Redis連接池:
class RedisQueue
{
protected $pool;
public function __construct()
{
$this->pool = new SplQueue;
}
public function get(): Redis
{
if ($this->pool->isEmpty()) {
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
return $redis;
}
return $this->pool->dequeue();
}
public function put(Redis $redis)
{
$this->pool->enqueue($redis);
}
public function close()
{
$this->pool = null;
}
}這里通過spl的隊列實現的連接池。如果連接池中沒有連接的時候,我們就新建一個連接,并且把創建的這個連接返回;如果連接池里面有連接,那么我們獲取隊列中前面的一個連接。當我們用完連接的時候,就可以調用put方法歸還連接。這樣,我們就可以在一定程度上復用Redis的連接,緩解Redis服務器的壓力,以及頻繁創建Redis連接的開銷也會降低。
我們現在使用這個連接池隊列:
<?php
use Swoole\Http\Request;
use Swoole\Http\Response;
$process = new Swoole\Process(function (Swoole\Process $process) {
$server = new Swoole\Http\Server('127.0.0.1', 9501, SWOOLE_BASE);
$server->set([
'log_file' => '/dev/null',
'log_level' => SWOOLE_LOG_INFO,
'worker_num' => swoole_cpu_num() * 2,
'hook_flags' => SWOOLE_HOOK_ALL,
]);
$server->on('workerStart', function () use ($process, $server) {
$server->pool = new RedisQueue;
$process->write('1');
});
$server->on('request', function (Request $request, Response $response) use ($server) {
try {
$redis = $server->pool->get();
// $redis = new Redis;
// $redis->connect('127.0.0.1', 6379);
$greeter = $redis->get('greeter');
if (!$greeter) {
throw new RedisException('get data failed');
}
$server->pool->put($redis);
$response->end("<h2>{$greeter}</h2>");
} catch (\Throwable $th) {
$response->status(500);
$response->end();
}
});
$server->start();
});
if ($process->start()) {
register_shutdown_function(function () use ($process) {
$process::kill($process->pid);
$process::wait();
});
$process->read(1);
System('ab -c 256 -n 10000 -k http://127.0.0.1:9501/ 2>&1');
}
class RedisQueue
{
protected $pool;
public function __construct()
{
$this->pool = new SplQueue;
}
public function get(): Redis
{
if ($this->pool->isEmpty()) {
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
return $redis;
}
return $this->pool->dequeue();
}
public function put(Redis $redis)
{
$this->pool->enqueue($redis);
}
public function close()
{
$this->pool = null;
}
}我們在worker進程初始化的時候,創建了這個RedisQueue。然后在onRequest的階段,從這個RedisQueue里面獲取一個Redis連接。
現在,我們來進行壓測:
Concurrency Level: 256 Time taken for tests: 1.188 seconds Complete requests: 10000 Failed requests: 0 Keep-Alive requests: 10000 Total transferred: 1680000 bytes HTML transferred: 150000 bytes Requests per second: 8416.18 [#/sec] (mean) Time per request: 30.418 [ms] (mean) Time per request: 0.119 [ms] (mean, across all concurrent requests) Transfer rate: 1380.78 [Kbytes/sec] received
QPS提升到了8416.18。
但是,通過splqueue實現的連接池是有缺陷的,因為這個隊列是可以無限長的。這樣,當并發量特別大的時候,還是會有可能創建非常多的連接,因為連接池里面可能始終都是空的。
這個時候,我們可以使用Channel來實現連接池。代碼如下:
class RedisPool
{
protected $pool;
public function __construct(int $size = 100)
{
$this->pool = new \Swoole\Coroutine\Channel($size);
for ($i = 0; $i < $size; $i++)
{
while (true) {
try {
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$this->put($redis);
break;
} catch (\Throwable $th) {
usleep(1 * 1000);
continue;
}
}
}
}
public function get(): \Redis
{
return $this->pool->pop();
}
public function put(\Redis $redis)
{
$this->pool->push($redis);
}
public function close()
{
$this->pool->close();
$this->pool = null;
}
}可以看到,我們在這個構造方法中,將這個Channel的size設置為這個傳入的參數。并且,創建size個連接。這些連接會在初始化連接池的時候就被創建,處于就就緒狀態。這個有好處也有壞處,壞處就是在每個進程初始化的時候,就會占用一些連接,但是此時的進程并不會接收連接。好處就是提前創建好了Redis連接,這樣服務器響應的延遲就會降低。
雖然,其他地方的代碼其實和RedisQueue的實現一樣。但是,底層是和RedisQueue大有不同的。因為當Channel里面沒有Redis連接的時候,會讓當前的協程掛起,讓其他的協程繼續被執行。等有協程把Redis連接還回到連接池里面的時候,這個被掛起的協程才會繼續執行。這就是協程協作的原理。
現在,我們修改服務器的代碼:
<?php
use Swoole\Http\Request;
use Swoole\Http\Response;
$process = new Swoole\Process(function (Swoole\Process $process) {
$server = new Swoole\Http\Server('127.0.0.1', 9501, SWOOLE_BASE);
$server->set([
'log_file' => '/dev/null',
'log_level' => SWOOLE_LOG_INFO,
'worker_num' => swoole_cpu_num() * 2,
'hook_flags' => SWOOLE_HOOK_ALL,
]);
$server->on('workerStart', function () use ($process, $server) {
$server->pool = new RedisPool(64);
$process->write('1');
});
$server->on('request', function (Request $request, Response $response) use ($server) {
try {
$redis = $server->pool->get();
// $redis = new Redis;
// $redis->connect('127.0.0.1', 6379);
$greeter = $redis->get('greeter');
if (!$greeter) {
throw new RedisException('get data failed');
}
$server->pool->put($redis);
$response->end("<h2>{$greeter}</h2>");
} catch (\Throwable $th) {
$response->status(500);
$response->end();
}
});
$server->start();
});
if ($process->start()) {
register_shutdown_function(function () use ($process) {
$process::kill($process->pid);
$process::wait();
});
$process->read(1);
System('ab -c 256 -n 10000 -k http://127.0.0.1:9501/ 2>&1');
}
class RedisPool
{
protected $pool;
public function __construct(int $size = 100)
{
$this->pool = new \Swoole\Coroutine\Channel($size);
for ($i = 0; $i < $size; $i++)
{
while (true) {
try {
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$this->put($redis);
break;
} catch (\Throwable $th) {
usleep(1 * 1000);
continue;
}
}
}
}
public function get(): \Redis
{
return $this->pool->pop();
}
public function put(\Redis $redis)
{
$this->pool->push($redis);
}
public function close()
{
$this->pool->close();
$this->pool = null;
}
}只需要修改workerStart里面的部分即可,其他地方不需要做修改。這樣,每個進程最多只能創建64個Redis連接。
我們繼續壓測:
Concurrency Level: 256 Time taken for tests: 0.817 seconds Complete requests: 10000 Failed requests: 0 Keep-Alive requests: 10000 Total transferred: 1680000 bytes HTML transferred: 150000 bytes Requests per second: 12234.30 [#/sec] (mean) Time per request: 20.925 [ms] (mean) Time per request: 0.082 [ms] (mean, across all concurrent requests) Transfer rate: 2007.19 [Kbytes/sec] received
以上就是PHP中怎么搭建一個HTTP服務,小編相信有部分知識點可能是我們日常工作會見到或用到的。希望你能通過這篇文章學到更多知識。更多詳情敬請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。