分享下之前在使用swoole开发项目时遇到的swoole协程问题。

1645514093

swoole 异常信息

 P Fatal error: Uncaught Swoole\Error: Socket#37 has already been bound xxx

PHP Fatal error:  Uncaught Swoole\Error: Socket#37 has already been bound to another coroutine#19895, reading of the same socket in coroutine#19896 at the same time is not allowed in /www/wwwroot/default/vendor/xxx.php:34
Stack trace:
xxxxxxxxxx

问题分析

 从日志看像是同一个Socket链接不能同时存在于不同的协程里面,也就是不能夸协程使用mysql,redis同一条连接对象。

问题处理

 知道问题就好处理了可以使用swoole channel 实现一个连接池如下:

Channel

private static ?Channel $_dynamic_pool         = null; // 动态连接池
private static array    $_static_pool          = []; // 静态连接池
private static int      $_dynamic_pool_min_num = 50; // 动态连接池最大数量
private string          $_static_pool_name; // 静态连接池名称
    
public static function redisUtils($name = "")
{
        if ($name) {
            self::$_static_pool[$name] = self::$_static_pool[$name] ?? new self();
            $redisUtils                = self::$_static_pool[$name];
        } else {
            if (self::$_dynamic_pool === null) {
                self::$_dynamic_pool = new Channel(self::$_dynamic_pool_min_num);
            }

            if (self::$_dynamic_pool->isEmpty()) {
                for ($i = 1; $i <= self::$_dynamic_pool_min_num; $i++) {
                    if (!self::$_dynamic_pool->isFull()) {
                        self::$_dynamic_pool->push(new self());
                    }
                }
            }

            if (!self::$_dynamic_pool->isEmpty()) {
                $redisUtils = self::$_dynamic_pool->pop();
            } else {
                $redisUtils = new self();
            }
        }
        $redisUtils->_static_pool_name = $name;
        return $redisUtils;
}

// 静态连接池使用
xxx::redisUtils("name")->get("xxxx");
xxx::redisUtils("name")->set("xxxx");

// 动态连接池使用
xxx::redisUtils->get("xxxx");
go(function(){
    xxx::redisUtils->set("xxxx");
})

xxx::redisUtils->get("xxxx");
go(function(){
    xxx::redisUtils->set("xxxx");
})

也可以使用swoole自带的相关mysql,redis连接池处理如下:

RedisPool

<?php
declare(strict_types=1);

use Swoole\Coroutine;
use Swoole\Database\RedisConfig;
use Swoole\Database\RedisPool;
use Swoole\Runtime;

const N = 1024;

Runtime::enableCoroutine();
$s = microtime(true);
Coroutine\run(function () {
    $pool = new RedisPool((new RedisConfig)
        ->withHost('127.0.0.1')
        ->withPort(6379)
        ->withAuth('')
        ->withDbIndex(0)
        ->withTimeout(1)
    );
    for ($n = N; $n--;) {
        Coroutine::create(function () use ($pool) {
            $redis = $pool->get();
            $result = $redis->set('foo', 'bar');
            if (!$result) {
                throw new RuntimeException('Set failed');
            }
            $result = $redis->get('foo');
            if ($result !== 'bar') {
                throw new RuntimeException('Get failed');
            }
            $pool->put($redis);
        });
    }
});
$s = microtime(true) - $s;
echo 'Use ' . $s . 's for ' . (N * 2) . ' queries' . PHP_EOL;

MysqliPool

<?php
declare(strict_types=1);

use Swoole\Coroutine;
use Swoole\Database\MysqliConfig;
use Swoole\Database\MysqliPool;
use Swoole\Runtime;

const N = 1024;

Runtime::enableCoroutine();
$s = microtime(true);
Coroutine\run(function () {
    $pool = new MysqliPool((new MysqliConfig)
        ->withHost('127.0.0.1')
        ->withPort(3306)
        // ->withUnixSocket('/tmp/mysql.sock')
        ->withDbName('test')
        ->withCharset('utf8mb4')
        ->withUsername('root')
        ->withPassword('root')
    );
    for ($n = N; $n--;) {
        Coroutine::create(function () use ($pool) {
            $mysqli = $pool->get();
            $statement = $mysqli->prepare('SELECT ? + ?');
            if (!$statement) {
                throw new RuntimeException('Prepare failed');
            }
            $a = mt_rand(1, 100);
            $b = mt_rand(1, 100);
            if (!$statement->bind_param('dd', $a, $b)) {
                throw new RuntimeException('Bind param failed');
            }
            if (!$statement->execute()) {
                throw new RuntimeException('Execute failed');
            }
            if (!$statement->bind_result($result)) {
                throw new RuntimeException('Bind result failed');
            }
            if (!$statement->fetch()) {
                throw new RuntimeException('Fetch failed');
            }
            if ($a + $b !== (int)$result) {
                throw new RuntimeException('Bad result');
            }
            while ($statement->fetch()) {
                continue;
            }
            $pool->put($mysqli);
        });
    }
});
$s = microtime(true) - $s;
echo 'Use ' . $s . 's for ' . N . ' queries' . PHP_EOL;

总结

 到此 PHP Fatal error: Uncaught Swoole\Error: Socket#37 has already been bound to another coroutine#19895, reading of the same socket in coroutine#19896 at the same time is not allowed in /www/wwwroot/default/vendor/xxx.php:34 类似的相关异常已经完美解决了。

 有更多不同的处理方式或者疑问可以评论回复。。。