应用php(非swoole)实现tcp/http服务器。

php内置的stream系列函数 和 socket扩大提供了对网络编程的反对。socket扩大须要在编译时通过配置--enable-sockets开启,而strem系列函数则齐全是php外围内置的函数。php社区中的workman框架底层就是基于stream函数来实现的。以下代码通过stream系列函数演示php如何实现简略的tcp/http等服务器

1:单过程阻塞模式

<?phpclass 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:多过程革新

<?phpclass 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形式)

<?phpclass 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...