美文网首页
PHP 长连接的猜测和验证

PHP 长连接的猜测和验证

作者: 马六甲的笔记 | 来源:发表于2019-03-25 11:38 被阅读0次

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


    • php 作为 server 对外提供服务, 每次处理新的请求都会重头运行一次代码
    • 在运行的代码中, php 可能会作为客户端从另外一个远程服务器获取数据(如:mysql,redis,memcached)
    • 在处理每一次请求的过程中, PHP都会经历连接远程服务器->获取数据->断开连接的过程
    • 可不可以只连接一次远程服务器, 此后处理请求的时候,直接用已连接的通道获取数据呢?
    • php作为server有多种运行方式, cgi/fastcgi/php-fpm/cli,不同运行方式有什么不同?

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

    二、支持长连接的常见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 多了一个自己写代码缓存连接通道、保持连接的功能。

    相关文章

      网友评论

          本文标题:PHP 长连接的猜测和验证

          本文链接:https://www.haomeiwen.com/subject/kfvmvqtx.html