使用php(非swoole)實現tcp/http服務器。
php內置的stream系列函數 和 socket擴展提供了對網絡編程的支持。socket擴展需要在編譯時通過配置--enable-sockets開啓,而strem系列函數則完全是php核心內置的函數。php社區中的workman框架底層就是基於stream函數來實現的。以下代碼通過stream系列函數演示php如何實現簡單的tcp/http等服務器
1:單進程阻塞模式
<?php
class Server
{
/**
* @var false|resource
*/
private $socket;
/**
* 是否http服務器 默認tcp服務器
* @var bool
*/
private $isHttp = false;
public function __construct(string $address)
{
$this->socket = @stream_socket_server($address, $errNo, $errMessage);
if (!is_resource($this->socket)) {
throw new InvalidArgumentException("參數異常:" . $errMessage);
}
}
public function isHttp(bool $bool): self
{
$this->isHttp = $bool;
return $this;
}
/**
* 啓動服務器
*/
public function run()
{
while (true) {
$conn = @stream_socket_accept($this->socket);
if ($conn) {
if ($this->isHttp) {
//http server
$data = "Hello World";
$response = "HTTP/1.1 200 OK\r\n";
$response .= "Content-Type: text/html;charset=UTF-8\r\n";
$response .= "Server: MyServer1\r\n";
$response .= "Content-length: " . strlen($data) . "\r\n\r\n";
$response .= $data;
fwrite($conn, $response);
fclose($conn);
} else {
// tcp server
while ($message = fread($conn, 1024)) {
// 主動退出
if (trim($message) == 'quit') {
echo "close\n";
fclose($conn);
break;
}
echo 'I have received that : ' . $message;
fwrite($conn, "OK\n");
}
}
}
}
}
}
$server = new Server("0.0.0.0:2345");
// 啓動 tcp 服務器
$server->run();
// 啓動 http服務器
//$server->isHttp(true)->run();
http服務瀏覽器訪問http://127.0.0.1:2345/看可以看到輸出hello world
tcp服器通過telnet進行測試
由於連接和read都是阻塞的,此時一次只能處理一個請求,只能噹噹前請求處理結束後才能處理下一個請求,不具備併發能力。
2:多進程改造
<?php
class Server
{
/**
* @var false|resource
*/
private $socket;
/**
* 是否http服務器 默認tcp服務器
* @var bool
*/
private $isHttp = false;
public function __construct(string $address)
{
$this->socket = @stream_socket_server($address, $errNo, $errMessage);
if (!is_resource($this->socket)) {
throw new InvalidArgumentException("參數異常:" . $errMessage);
}
}
public function isHttp(bool $bool): self
{
$this->isHttp = $bool;
return $this;
}
/**
* 啓動服務器
*/
public function run()
{
while (true) {
$conn = @stream_socket_accept($this->socket);
if ($conn) {
if ($this->isHttp) {
if (pcntl_fork() == 0) {
//http server
$data = "Hello World";
$response = "HTTP/1.1 200 OK\r\n";
$response .= "Content-Type: text/html;charset=UTF-8\r\n";
$response .= "Server: MyServer1\r\n";
$response .= "Content-length: " . strlen($data) . "\r\n\r\n";
$response .= $data;
fwrite($conn, $response);
fclose($conn);
exit;
}
} else {
// tcp server
if (pcntl_fork() == 0) {
while ($message = fread($conn, 1024)) {
echo 'I have received that : ' . $message;
fwrite($conn, "OK\n");
}
exit;
}
}
}
}
}
}
$server = new Server("0.0.0.0:2345");
// 啓動 tcp 服務器
$server->run();
// 啓動 http服務器
//$server->isHttp(true)->run();
此時,由於通過子進程處理原本阻塞的read函數,所以主進程可以accept新的請求,提高併發能力。
由於創建進程/線程會帶來很多新的問題,比如系統開銷增大等,所以,這種方法基本不會在生產環境中使用。
3:IO多路複用(select方式)
<?php
class Server
{
/**
* @var false|resource
*/
private $socket;
/**
* 是否http服務器 默認tcp服務器
* @var bool
*/
private $isHttp = false;
/**
* @var []resource
*/
private $socketList = [];
public function __construct(string $address)
{
$this->socket = @stream_socket_server($address, $errNo, $errMessage);
if (!is_resource($this->socket)) {
throw new InvalidArgumentException("參數異常:" . $errMessage);
}
// stream_set_blocking 設置非阻塞io
stream_set_blocking($this->socket, false);
// 添加當前監聽的socket
$this->socketList[(int)$this->socket] = $this->socket;
}
public function isHttp(bool $bool): self
{
$this->isHttp = $bool;
return $this;
}
/**
* 啓動服務器
*/
public function run()
{
while (true) {
$read = $this->socketList;
$write = $except = [];
//阻塞監聽 設置null阻塞監聽
$count = @stream_select($read, $write, $except, null);
if ($count) {
foreach ($read as $k => $v) {
if ($v == $this->socket) {
// connect success
// accept Connection
@$conn = stream_socket_accept($this->socket, null, $remote_address);
echo "connect fd:" . $k . PHP_EOL;
$this->socketList[(int)$conn] = $conn;
} else {
//receive
if ($this->isHttp) {
$this->receiveHttp($v);
} else {
$this->receiveTcp($v);
}
}
}
}
}
}
protected function receiveHttp($stream)
{
$buffer = fread($stream, 1024);
if (empty($buffer) && (feof($stream) || !is_resource($stream))) {
fclose($stream);
unset($this->socketList[(int)$stream]);
echo "退出成功" . PHP_EOL;
return;
}
//http server
$data = "Hello World";
$response = "HTTP/1.1 200 OK\r\n";
$response .= "Content-Type: text/html;charset=UTF-8\r\n";
$response .= "Server: MyServer1\r\n";
$response .= "Content-length: " . strlen($data) . "\r\n\r\n";
$response .= $data;
fwrite($stream, $response);
}
/**
* 處理tcp請求
*/
private function receiveTcp($stream)
{
$buffer = fread($stream, 1024);
if (feof($stream) || !is_resource($stream)) {
unset($this->socketList[(int)$stream]);
fclose($stream);
echo "退出成功" . PHP_EOL;
return;
}
echo 'onReceive ' . $buffer . " ->" . $stream . PHP_EOL;
$message = 'I have received that : ' . $buffer;
fwrite($stream, "{$message}");
}
}
$server = new Server("0.0.0.0:2345");
// 啓動 tcp 服務器
//$server->run();
// 啓動 http服務器
$server->isHttp(true)->run();
通過IO多路複用是目前解決高併發(C10k)問題的主要思路。
通過stream_select函數調用操作系統提供的select方法,將接收到的文件描述符號傳遞給操作系統底層,由操作系統遍歷並且返回需要處理的文件描述符號,從而不需要在應用層面一直阻塞,提高併發能力。
4:IO多路費用(poll epoll)
由於select方法存在種種問題,比如性能差,數量限制等。應對小規模併發沒有問題,但是大規模併發就顯得捉襟見肘,這個時候就需要poll,epoll(推薦)方法實現多路複用。
具體到php裏面需要安裝event或者libevent擴展,詳細代碼可以參考workman源碼,這裏不再展示。
實例代碼
- https://github.com/tim1116/ph...
參考:
- https://www.php.net/manual/zh... (php支持的Socket Transports)
- https://www.php.net/manual/zh...
- https://www.php.net/manual/zh...