共计 4825 个字符,预计需要花费 13 分钟才能阅读完成。
应用 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…