PHP 长连接的猜测和验证

2019-03-25  本文已影响0人  马六甲的笔记

一、什么是长连接,长连接的意义


以上便是本文要了解的问题

二、支持长连接的常见pecl扩展


1. PDO

https://www.php.net/manual/zh/pdo.construct.php
https://www.php.net/manual/zh/pdo.connections.php

2.memcache

https://www.php.net/manual/zh/class.memcache.php
https://www.php.net/manual/zh/memcache.pconnect.php

3.memcached

https://www.php.net/manual/zh/book.memcached.php
https://www.php.net/manual/zh/memcached.construct.php

4.reids

https://pecl.php.net/package/redis
https://github.com/phpredis/phpredis/#connection

5.kafka

https://pecl.php.net/package/rdkafka
https://github.com/arnaud-lb/php-rdkafka
https://github.com/arnaud-lb/php-rdkafka/issues/42
写本文时还未支持,但看issue应该是快了

6.omq

https://www.php.net/manual/zh/zmqcontext.construct.php

以上是各种服务的 client 端,可以看到基本都支持长连接,或未来也要支持;
想必以后有什么其他客户端扩展的话,应该也是这种思路

三、php socket 连接


https://www.php.net/manual/zh/book.sockets.php
https://www.php.net/manual/zh/book.stream.php
https://www.php.net/manual/zh/function.fsockopen.php
https://www.php.net/manual/zh/function.pfsockopen.php

对以上几个做个简单说明

  1. sockets 库是默认是关闭的,编译php时需要--enable-sockets 才能打开,更为底层,需要自己封装各种协议,目前看来并不支持持久连接

  2. stream 库从 php4.3 之后是在内核中了,所以无需担心是否可用的问题了,相比 sockets,是更高一层的实现,封装了一些 常见协议 ,支持 持久连接, 通过 flags 参数设置

  3. fsockopen / pfsockopen 是更高层级的封装,也是直接在php内核中的,无需额外配置;两者只有一个差别,就是 pfsockopen 打开的是持久连接

  4. 所以选择起来,一般就不使用 sockets 了,毕竟不一定支持,且太底层了,要自己去实现传输器;剩下两个呢,推荐 stream,更加灵活

四、验证长连接


php 可以以 cgi/fastcgi/cli 方式运行,所以这里就针对这三种模式来验证长连接的表现,若对 PHP 运行模式疑问,可以看看这篇 PHP运行方式

没有精力去验证各 pecl 扩展,这里仅用 pfsockopen 来做验证,因为本地环境安装了 php swoole 扩展,所以直接使用 swoole 创建一个服务端来测试

1、创建一个 tcp 服务端

<?php
//tcp.php
$serv = new Swoole\Server('0.0.0.0', 9501, SWOOLE_BASE, SWOOLE_SOCK_TCP);
$serv->set([
   'worker_num' => 1,
]);
$serv->on('start', function () {
    echo "server start\n";
});
$serv->on('Connect', function(swoole_server $server, int $fd, int $reactorId) {
    echo "connect:$fd\n";
});
$serv->on('Receive', function (Swoole\Server $serv, $fd, $reactor_id, $data) {
    echo "receive[$fd]: $data\n";
    $serv->send($fd, "Server: $data\n");
});
$serv->on('Close', function(swoole_server $server, int $fd, int $reactorId) {
    echo "close:$fd\n";
});
$serv->start();

运行服务端

$ php tcp.php

2、创建一个测试函数

//client.php
<?php
function connect($callback, $try = 0)
{
    if ($try > 3) {
        $callback('connect error');
    } else {
        // 可测试 fsockopen  或 pfsockopen
        $fp = pfsockopen("tcp://127.0.0.1", 9501, $errno, $errstr);
        if (!$fp) {
            $callback("ERROR: $errno - $errstr<br />\n");
        } else {
            if (!fwrite($fp, "message")) {
                // 对于长连接,若 tcp 服务端重启了, $fp 不会自动重连, 这里判断一下
                fclose($fp);
                connect($callback, ++$try);
            } else {
                $callback(fread($fp, 1024));
                // 不去手动关闭
                // fsockopen 打开的连接会自动关闭
                // pfsockopen 打开的连接不会自动关闭,
                // 若手工关闭, 那就是自愿放弃长连接的持久特性了
                //fclose($fp);
            }
        }
    }
}

3、cgi / fastcgi 测试文件

<?php
require __DIR__.'/client.php';
connect(function ($str) {
    echo $str;
});

4、swoole cli 测试文件

<?php
require __DIR__.'/client.php';
$http = new Swoole\Http\Server("127.0.0.1", 8888);
$http->set([
    'worker_num' => 1,
]);
$http->on('request', function ($request, $response) {
    connect(function ($str) use ($response) {
        $response->end($str);
    });
});
$http->start();

5、workerman cli 测试文件

<?php
require __DIR__.'/../library/workerman/Autoloader.php';
require __DIR__.'/client.php';
use Workerman\Worker;

$http_worker = new Worker("http://0.0.0.0:6666");
$http_worker->count = 1;
$http_worker->onMessage = function($connection, $data) {
    connect(function ($str) use ($connection) {
        $connection->send($str);
    });
};
Worker::runAll();

五、测试结果


1、cgi模式

//没环境,暂未测试,想必是无法使用长连接的,等测试了再补充

2、fastcgi 模式

符合预期
使用 fsockopen : tcp 客户端会在每次处理完自动关闭
使用 pfsockopen:tcp 客户端处理后不会关闭,下次会复用通道

3、cli 模式

swoole / workerman 的表现与 fastcgi 模式下一致,fsockopen自动关闭,pfsockopen持久连接;这就需要思考两个问题了

第一个问题:fsockopen 并没有手工去关闭,php 是守护进程运行的,为什么处理完,通道会关闭呢,猜测是因为 swoole 、workerman 中的处理请求的闭包函数在每次运行完之后,都会清理内存,释放变量,试一下把 连接通道 放到闭包之外进行测试。

swoole

<?php
class Client
{
    protected $fp;

    public function getFp()
    {
        if (!$this->fp) {
            $this->fp = fsockopen("tcp://127.0.0.1", 9501, $errno, $errstr);
        }
        return $this->fp;
    }
}
$client = new Client();
$http = new Swoole\Http\Server("127.0.0.1", 8888);
$http->set([
    'worker_num' => 1,
]);
$http->on('request', function ($request, $response) use ($client) {
    $fp = $client->getFp();
    fwrite($fp, "message");
    $response->end(fread($fp, 1024));
});
$http->start();

workerman

require __DIR__.'/../library/workerman/Autoloader.php';
require __DIR__.'/client.php';
use Workerman\Worker;

class Client
{
    protected $fp;

    public function getFp()
    {
        if (!$this->fp) {
            $this->fp = fsockopen("tcp://127.0.0.1", 9501, $errno, $errstr);
        }
        return $this->fp;
    }
}
$client = new Client();
$http_worker = new Worker("http://0.0.0.0:6666");
$http_worker->count = 1;
$http_worker->onMessage = function($connection, $data) use ($client) {
    $fp = $client->getFp();
    fwrite($fp, "message");
    $connection->send(fread($fp, 1024));
};
Worker::runAll();

再次测试,就会发现,fsockopen 打开的通道,在处理完请求之后也不会关闭,这就比较符合直觉了。

第二个问题:闭包函数内使用 pfsockopen 打开的连接为什么没有被释放呢?

看一下 php 的源码,这里这里,这就好理解了,释放的仅仅是变量,而通道被 php 内部的内存管理缓存起来了,cli 也好,php-fpm 也罢,都还是运行在 php 内核之上的,所以二者都符合 php 的处理机制:释放变量,保持连接。只是 cli 多了一个自己写代码缓存连接通道、保持连接的功能。

上一篇 下一篇

猜你喜欢

热点阅读