الكورoutines
تم تطوير webman بناءً على workerman، لذا يمكن لـ webman استخدام ميزات الكورoutines الخاصة بـ workerman.
يدعم الكورoutines ثلاثة محركات هي Swoole
و Swow
و Fiber
.
شروط مسبقة
- PHP >= 8.1
- Workerman >= 5.1.0 (
composer require workerman/workerman ~v5.1
) - webman-framework >= 2.1 (
composer require workerman/webman-framework ~v2.1
) - تم تثبيت توسيع swoole أو swow، أو تم تثبيت
composer require revolt/event-loop
(Fiber) - الكورoutines مغلقة بشكل افتراضي، و تحتاج إلى إعداد
eventLoop
لتمكينها بشكل منفصل.
طريقة التمكين
يدعم webman تمكين محركات مختلفة لعمليات مختلفة، لذلك يمكنك تكوين محرك الكورoutines في config/process.php
من خلال eventLoop
:
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '', // الافتراضي فارغ يختار تلقائيًا Select أو Event، بدون تفعيل الكورoutines
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
'my-coroutine' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8686',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
// يجب تعيين الكورoutines إلى Workerman\Events\Swoole::class أو Workerman\Events\Swow::class أو Workerman\Events\Fiber::class
'eventLoop' => Workerman\Events\Swoole::class,
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
]
// ... تكوينات أخرى مهملة ...
];
تنبيه
يمكن لـ webman تعيينeventLoop
مختلف لعمليات مختلفة، مما يعني أنه يمكنك اختيارياً تمكين الكورoutines لعمليات معينة.
على سبيل المثال، الخدمة على المنفذ 8787 في التكوين أعلاه لم يتم تمكين الكورoutines، بينما الخدمة على المنفذ 8686 تم تمكين الكورoutines لها، مما يتيح نشر مختلط بين الكورoutines وغير الكورoutines من خلال إعادة توجيه Nginx.
مثال على الكورoutines
<?php
namespace app\controller;
use support\Response;
use Workerman\Coroutine;
use Workerman\Timer;
class IndexController
{
public function index(): Response
{
Coroutine::create(function(){
Timer::sleep(1.5);
echo "hello coroutine\n";
});
return response('hello webman');
}
}
عندما يكون eventLoop
هو Swoole
أو Swow
أو Fiber
، يقوم webman بإنشاء كورoutine لكل طلب للتنفيذ، ويمكنه إنشاء كورoutines جديدة أثناء معالجة الطلب لتنفيذ رمز الأعمال.
قيود الكورoutines
- عند استخدام Swoole أو Swow كمحرك، سيتحول الكورoutine تلقائيًا عند مواجهة عمليات الإدخال/الإخراج المحجوزة، مما يتيح تنفيذ كود متزامن بشكل غير متزامن.
- عند استخدام محرك Fiber، عندما يتم مواجهة إدخال/إخراج محجوز، لن يحدث تبديل في الكورoutines، ويدخل العملية حالة الحظر.
- عند استخدام الكورoutines، لا يمكن أن يقوم عدة كورoutines بالعمليات على نفس المورد في وقت واحد، مثل اتصال قاعدة البيانات أو عمليات الملفات، حيث قد تؤدي إلى تنافس على الموارد. الطريقة الصحيحة هي استخدام مجموعة الاتصال أو الأقفال لحماية الموارد.
- عند استخدام الكورoutines، لا يمكن تخزين بيانات الحالة المتعلقة بالطلبات في متغيرات عالمية أو متغيرات ثابتة، حيث قد يؤدي ذلك إلى تلوث البيانات العالمية. الطريقة الصحيحة هي استخدام سياق الكورoutines
context
لتخزينها واسترجاعها.
ملاحظات أخرى
يقوم Swow بشكل تلقائي بتوصيل وظائف PHP المحجوزة في المستوى الأدنى، ولكن نظرًا لأن هذا التوصيل يؤثر على بعض سلوكيات PHP الافتراضية، فقد تحدث أخطاء عند عدم استخدام Swow ولكن تم تثبيته.
لذا يُوصى بــ:
- إذا كان مشروعك لا يستخدم Swow، فلا تقم بتثبيت امتداد Swow.
- إذا كان مشروعك يستخدم Swow، يرجى تعيين
eventLoop
إلىWorkerman\Events\Swow::class
.
سياق الكورoutines
في بيئة الكورoutines، يُمنع تخزين المعلومات الحالة المتعلقة بالطلبات في متغيرات عالمية أو متغيرات ثابتة، حيث قد يتسبب ذلك في تلوث المتغيرات العالمية، على سبيل المثال:
<?php
namespace app\controller;
use support\Request;
use Workerman\Timer;
class TestController
{
protected static $name = '';
public function index(Request $request)
{
static::$name = $request->get('name');
Timer::sleep(5);
return static::$name;
}
}
ملاحظة
في بيئة الكورoutines، لا يُمنع استخدام المتغيرات العالمية أو المتغيرات الثابتة، بل يُمنع استخدام المتغيرات العالمية أو المتغيرات الثابتة لتخزين بيانات الحالة المتعلقة بالطلبات.
على سبيل المثال، يوصى بتخزين بيانات الكائنات التي تحتاج إلى مشاركة عالمية، مثل التكوينات العالمية، واتصالات قاعدة البيانات، وبعض الأولويات كمتغيرات عالمية أو ثابتة.
عند تعيين عدد العمليات إلى 1، وعندما نقوم بإرسال طلبين متتابعين
http://127.0.0.1:8787/test?name=lilei
http://127.0.0.1:8787/test?name=hanmeimei
نتوقع أن النتائج العائدة من الطلبين هما lilei
و hanmeimei
، لكن في الواقع العائد هو hanmeimei
كلاهما.
هذا لأن الطلب الثاني قد تغلب على المتغير الثابت $name
، حيث يصبح الثابت $name
عند انتهاء نوم الطلب الأول هو hanmeimei
.
الطريقة الصحيحة هي استخدام السياق لتخزين بيانات الحالة للطلبات
<?php
namespace app\controller;
use support\Request;
use support\Context;
use Workerman\Timer;
class TestController
{
public function index(Request $request)
{
Context::set('name', $request->get('name'));
Timer::sleep(5);
return Context::get('name');
}
}
يستخدم support\Context
لتخزين بيانات سياق الكورoutines، وعندما تنتهي الكورoutine، يتم حذف بيانات السياق المعنية تلقائيًا.
في بيئة الكورoutines، نظرًا لأن كل طلب هو كورoutine منفصل، فستتم إزالة بيانات السياق تلقائيًا عند انتهاء الطلب.
في بيئة غير الكورoutines، سيتم إزالة بيانات السياق تلقائيًا عند انتهاء الطلب.
المتغيرات المحلية لن تسبب تلوث البيانات
<?php
namespace app\controller;
use support\Request;
use support\Context;
use Workerman\Timer;
class TestController
{
public function index(Request $request)
{
$name = $request->get('name');
Timer::sleep(5);
return $name;
}
}
لأن $name
هو متغير محلي، لا يمكن أن تصل الكورoutines إلى المتغير المحلي، لذا فإن استخدام المتغيرات المحلية آمن للكورoutines.
الأقفال Locker
في بعض الأحيان، قد لا تأخذ بعض المكونات أو الأعمال في الاعتبار بيئة الكورoutines، مما قد يؤدي إلى تنافس على الموارد أو مشكلات الذرة، في تلك الحالات، يمكن استخدام Workerman\Locker
للقفل لتحقيق معالجة تعتمد على الترتيب، ومنع مشكلات التزامن.
<?php
namespace app\controller;
use Redis;
use support\Response;
use Workerman\Coroutine\Locker;
class IndexController
{
public function index(): Response
{
static $redis;
if (!$redis) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
}
// إذا لم يتم قفل، سيؤدي ذلك إلى خطأ مشابه "Socket#10 has already been bound to another coroutine#10" تحت Swoole
// قد يسبب Swow في حدوث core dump
// تحت Fiber، بسبب توسيع Redis أنه IO محجوز، فلن تكون هناك مشاكل
Locker::lock('redis');
$time = $redis->time();
Locker::unlock('redis');
return json($time);
}
}
التنفيذ المتوازي Parallel
عندما نحتاج إلى تنفيذ عدة مهام بشكل متوازي والحصول على النتائج، يمكن استخدام Workerman\Parallel
لتحقيق ذلك.
<?php
namespace app\controller;
use support\Response;
use Workerman\Coroutine\Parallel;
class IndexController
{
public function index(): Response
{
$parallel = new Parallel();
for ($i=1; $i<5; $i++) {
$parallel->add(function () use ($i) {
// قم بشيء ما
return $i;
});
}
$results = $parallel->wait();
return json($results); // الاستجابة: [1,2,3,4]
}
}
Pool مجموعة الاتصالات
استخدام كورoutines متعددة لمشاركة نفس الاتصال قد يؤدي إلى فوضى في البيانات، لذا يلزم استخدام مجموعة الاتصالات لإدارة قواعد البيانات، Redis، وغيرها من موارد الاتصالات.
قدم webman بالفعل مكونات مثل webman/database و webman/redis و webman/cache و webman/think-orm و webman/think-cache، جميعها متكاملة مع مجموعة الاتصالات، وتدعم الاستخدام في بيئات الكورoutines وغير الكورoutines.
إذا كنت تريد تعديل مكون لا يحتوي على مجموعة اتصالات، يمكنك استخدام Workerman\Pool
لتحقيق ذلك، كما هو موضح في الكود أدناه.
مكون قاعدة البيانات
<?php
namespace app;
use Workerman\Coroutine\Context;
use Workerman\Coroutine;
use Workerman\Coroutine\Pool;
class Db
{
private static ?Pool $pool = null;
public static function __callStatic($name, $arguments)
{
if (self::$pool === null) {
self::initializePool();
}
// احصل على الاتصال من سياق الكورoutines، لضمان استخدام نفس الاتصال عند تشغيل الكورoutine نفسه
$pdo = Context::get('pdo');
if (!$pdo) {
// احصل على الاتصال من مجموعة الاتصالات
$pdo = self::$pool->get();
Context::set('pdo', $pdo);
// عند انتهاء الكورoutine، قم بإرجاع الاتصال تلقائيًا
Coroutine::defer(function () use ($pdo) {
self::$pool->put($pdo);
});
}
return call_user_func_array([$pdo, $name], $arguments);
}
private static function initializePool(): void
{
// إنشاء مجموعة اتصالات، العدد الأقصى للاتصالات هو 10
self::$pool = new Pool(10);
// تعيين منشئ الاتصالات (لتبسيط الأمر، تم حذف قراءة ملف الإعدادات)
self::$pool->setConnectionCreator(function () {
return new \PDO('mysql:host=127.0.0.1;dbname=your_database', 'your_username', 'your_password');
});
// تعيين مُغلق الاتصالات
self::$pool->setConnectionCloser(function ($pdo) {
$pdo = null;
});
// تعيين فاحص القلب
self::$pool->setHeartbeatChecker(function ($pdo) {
$pdo->query('SELECT 1');
});
}
}
الاستخدام
<?php
namespace app\controller;
use support\Response;
use app\Db;
class IndexController
{
public function index(): Response
{
$value = Db::query('SELECT NOW() as now')->fetchAll();
return json($value); // [{"now":"2025-02-06 23:41:03","0":"2025-02-06 23:41:03"}]
}
}
المزيد من معلومات الكورoutines والمكونات ذات الصلة
للمزيد، راجع وثيقة الكورoutines لـ workerman
نشر مختلط بين الكورoutines وغير الكورoutines
يدعم webman نشر مختلط بين الكورoutines وغير الكورoutines، مثل معالجة الأعمال العادية بدون كورoutines، ومعالجة الأعمال البطيئة بإدخال/إخراج محجوز من خلال إعادة توجيه Nginx إلى خدمات مختلفة.
على سبيل المثال، إعداد config/process.php
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '', // الافتراضي فارغ يختار تلقائيًا Select أو Event، بدون تفعيل الكورoutines
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
'my-coroutine' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8686',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
// يجب تعيين الكورoutines إلى Workerman\Events\Swoole::class أو Workerman\Events\Swow::class أو Workerman\Events\Fiber::class
'eventLoop' => Workerman\Events\Swoole::class,
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
// ... تكوينات أخرى مهملة ...
];
ثم من خلال تكوين Nginx الذي يقوم بإعادة توجيه الطلبات إلى الخدمات المختلفة
upstream webman {
server 127.0.0.1:8787;
keepalive 10240;
}
# إضافة upstream جديد على 8686
upstream task {
server 127.0.0.1:8686;
keepalive 10240;
}
server {
server_name webman.com;
listen 80;
access_log off;
root /path/webman/public;
# الطلبات التي تبدأ بـ /tast تتجه إلى المنفذ 8686، يرجى تغيير /tast حسب الحاجة
location /tast {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://task;
}
# الطلبات الأخرى تذهب إلى المنفذ 8787
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
if (!-f $request_filename){
proxy_pass http://webman;
}
}
}