PHP 长连接的猜测和验证

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


  • 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 服务端

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
 3) {
        $callback('connect error');
    } else {
        // 可测试 fsockopen  或 pfsockopen
        $fp = pfsockopen("tcp://127.0.0.1", 9501, $errno, $errstr);
        if (!$fp) {
            $callback("ERROR: $errno - $errstr
\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 测试文件

4、swoole cli 测试文件

set([
    'worker_num' => 1,
]);
$http->on('request', function ($request, $response) {
    connect(function ($str) use ($response) {
        $response->end($str);
    });
});
$http->start();

5、workerman cli 测试文件

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

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 长连接的猜测和验证)