一、什么是长连接,长连接的意义
- 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
对以上几个做个简单说明
-
sockets 库是默认是关闭的,编译php时需要--enable-sockets 才能打开,更为底层,需要自己封装各种协议,目前看来并不支持持久连接
-
stream 库从 php4.3 之后是在内核中了,所以无需担心是否可用的问题了,相比 sockets,是更高一层的实现,封装了一些 常见协议 ,支持 持久连接, 通过 flags 参数设置
-
fsockopen / pfsockopen 是更高层级的封装,也是直接在php内核中的,无需额外配置;两者只有一个差别,就是 pfsockopen 打开的是持久连接
-
所以选择起来,一般就不使用 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 多了一个自己写代码缓存连接通道、保持连接的功能。
网友评论