الفكرة باختصار: بدل أن يسأل تطبيقك نقطة النهاية res.php كل خمس ثوانٍ عن نتيجة لم تجهز بعد، يفتح المتصفح اتصال SSE واحداً مع خادمك، وتستدعي CaptchaAI رابط الـ pingback الخاص بك فور انتهاء الحل، فيدفع خادمك الرمز إلى الواجهة في اللحظة نفسها. طلبات أقل، وزمن وصول أقصر، وكود أبسط من إدارة دورة استطلاع مستقلة لكل مهمة.
الأحداث المرسلة من الخادم — Server-Sent Events — قناة أحادية الاتجاه فوق HTTP عادي: الخادم يكتب والمتصفح يسمع، وهذا بالضبط شكل نتيجة CAPTCHA. في ما يلي المسار كاملاً: خادم Flask يبثّ رد النداء، عميل متصفح يعرض النتيجة، نسخة على Node.js مع Express، ثم التشغيل خلف موازن التحميل.
متى يستحق SSE العناء ومتى لا يستحقه
الفحص الدوري ليس خطأً بحد ذاته، لكنه يصبح مكلفاً مع ارتفاع الأعداد. مهمة تُحل خلال عشرين ثانية تستهلك أربعة طلبات استطلاع، ثلاثة منها ترجع «لم تجهز بعد». اضرب ذلك في مئة مهمة متزامنة، وأضف تأخيراً يبلغ طول الفاصل الزمني كاملاً بين جاهزية الرمز ووصوله إلى المستخدم.
انتقل إلى SSE عندما تنطبق واحدة من هذه الحالات:
- لديك واجهة ويب أو لوحة تحكم داخلية تعرض حالة كل مهمة أمام مستخدم بشري.
- تعمل على عشرات المهام المتزامنة، وفارق الثواني بين الجاهزية والعرض مهم لتجربة الاستخدام.
- تريد تقليل الطلبات الصادرة من الواجهة دون بناء بنية رسائل كاملة.
وابقَ على الفحص الدوري إذا كان العميل سكربت أتمتة بلا واجهة، أو كان الحجم أصغر من أن يبرر نقطة نهاية بث.
كيف تنتقل النتيجة من CaptchaAI إلى المتصفح
[Client] ← SSE stream ← [Your Server] ← Callback ← [CaptchaAI]
↓ ↑
Submit task → [CaptchaAI] ──┘ (pingback URL points to your server)
- يفتح العميل اتصالاً مستمراً مع نقطة نهاية SSE على خادمك
- يرسل العميل مهمة CAPTCHA إلى CaptchaAI مع معامل
pingbackيشير إلى خادمك - تحلّ CaptchaAI المهمة وترسل النتيجة إلى نقطة نهاية رد النداء لديك
- يدفع خادمك النتيجة عبر دفق SSE إلى العميل صاحب الجلسة
طريقة الإرسال لم تتغير: نفس نقطة النهاية in.php ونفس المعاملات ونفس مفتاح الـ API، وكل الإضافة معامل pingback وعنوان يستقبل الرد. إن كنت تبدأ من الصفر فابدأ من دليل البدء السريع مع CaptchaAI.
الخطوة 1: خادم Flask يجمع البث ورد النداء
الخادم يحتفظ بقائمة انتظار لكل عميل متصل: نقطة نهاية /events تفتح الدفق وتنتظر، و/callback تضع النتيجة الواردة في قائمة العميل المناسب فيكتبها المولّد فوراً في الاستجابة المفتوحة.
import os
import queue
import threading
import requests
from flask import Flask, Response, request, jsonify
app = Flask(__name__)
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
# Per-client event queues: client_id -> Queue
client_queues = {}
queues_lock = threading.Lock()
@app.route("/events/<client_id>")
def sse_stream(client_id):
"""SSE endpoint — clients connect here for real-time results."""
q = queue.Queue()
with queues_lock:
client_queues[client_id] = q
def generate():
try:
while True:
# Block until a result arrives (timeout for keepalive)
try:
data = q.get(timeout=30)
yield f"event: captcha-solved\ndata: {data}\n\n"
except queue.Empty:
# Send keepalive comment to prevent connection timeout
yield ": keepalive\n\n"
finally:
with queues_lock:
client_queues.pop(client_id, None)
return Response(
generate(),
mimetype="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no" # Disable nginx buffering
}
)
@app.route("/submit", methods=["POST"])
def submit_captcha():
"""Submit a CAPTCHA task with callback to this server."""
data = request.json
client_id = data["client_id"]
sitekey = data["sitekey"]
pageurl = data["pageurl"]
callback_url = f"{request.host_url}callback?client_id={client_id}"
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"pingback": callback_url,
"json": 1
})
result = resp.json()
if result.get("status") == 1:
return jsonify({"task_id": result["request"]})
return jsonify({"error": result.get("request")}), 400
@app.route("/callback")
def captcha_callback():
"""Receive CaptchaAI callback and push to SSE stream."""
client_id = request.args.get("client_id")
task_id = request.args.get("id")
solution = request.args.get("code")
import json
message = json.dumps({
"task_id": task_id,
"solution": solution
})
with queues_lock:
q = client_queues.get(client_id)
if q:
q.put(message)
return "OK", 200
if __name__ == "__main__":
app.run(port=5000, threaded=True)
ثلاث نقاط تستحق الانتباه: مهلة q.get عند 30 ثانية تكتب تعليق : keepalive يمنع الخوادم الوسيطة من إغلاق الاتصال الصامت؛ والترويسة X-Accel-Buffering: no تعطّل التخزين المؤقت في nginx وبدونها تصل الأحداث مجمّعة ومتأخرة؛ وحذف قائمة الانتظار داخل finally يمنع تراكم قوائم يتيمة لعملاء أغلقوا الصفحة.
الخطوة 2: عميل المتصفح الذي يستقبل الحدث
كائن EventSource مدمج في كل متصفح حديث، ويتكفّل بإعادة الاتصال تلقائياً إذا انقطع الدفق.
<!DOCTYPE html>
<html>
<body>
<button onclick="submitCaptcha()">Solve CAPTCHA</button>
<div id="results"></div>
<script>
const clientId = crypto.randomUUID();
const resultsDiv = document.getElementById("results");
// Connect SSE stream
const eventSource = new EventSource(`/events/${clientId}`);
eventSource.addEventListener("captcha-solved", (event) => {
const data = JSON.parse(event.data);
resultsDiv.innerHTML += `<p>Task ${data.task_id}: ${data.solution.substring(0, 30)}...</p>`;
});
eventSource.onerror = () => {
console.log("SSE connection lost, reconnecting...");
};
async function submitCaptcha() {
const response = await fetch("/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_id: clientId,
sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl: "https://example.com"
})
});
const result = await response.json();
resultsDiv.innerHTML += `<p>Submitted: ${result.task_id}</p>`;
}
</script>
</body>
</html>
المعرّف clientId هو الرابط بين الجلسة ورد النداء: ولّده مرة واحدة عند فتح الصفحة، واحفظه في sessionStorage لينجو من إعادة التحميل. وحقن الرمز داخل النموذج المستهدف مشروح في دليل حل reCAPTCHA v2 عبر الـ API.
الخطوة 3: النسخة نفسها على Node.js مع Express
المنطق متطابق، لكن Express يخزّن كائن الاستجابة نفسه بدل قائمة الانتظار ويكتب فيه عند وصول رد النداء.
const express = require("express");
const axios = require("axios");
const app = express();
app.use(express.json());
const API_KEY = process.env.CAPTCHAAI_API_KEY;
const BASE_URL = process.env.BASE_URL || "http://localhost:3000";
// Per-client SSE connections: clientId -> Response object
const clients = new Map();
// SSE endpoint
app.get("/events/:clientId", (req, res) => {
const clientId = req.params.clientId;
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"X-Accel-Buffering": "no",
});
clients.set(clientId, res);
// Keepalive every 30 seconds
const keepalive = setInterval(() => {
res.write(": keepalive\n\n");
}, 30000);
req.on("close", () => {
clearInterval(keepalive);
clients.delete(clientId);
});
});
// Submit CAPTCHA
app.post("/submit", async (req, res) => {
const { client_id, sitekey, pageurl } = req.body;
const callbackUrl = `${BASE_URL}/callback?client_id=${client_id}`;
try {
const resp = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: {
key: API_KEY,
method: "userrecaptcha",
googlekey: sitekey,
pageurl: pageurl,
pingback: callbackUrl,
json: 1,
},
});
if (resp.data.status === 1) {
return res.json({ task_id: resp.data.request });
}
res.status(400).json({ error: resp.data.request });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// CaptchaAI callback → push to SSE
app.get("/callback", (req, res) => {
const clientId = req.query.client_id;
const taskId = req.query.id;
const solution = req.query.code;
const clientRes = clients.get(clientId);
if (clientRes) {
const data = JSON.stringify({ task_id: taskId, solution: solution });
clientRes.write(`event: captcha-solved\ndata: ${data}\n\n`);
}
res.sendStatus(200);
});
app.listen(3000, () => console.log("SSE server running on :3000"));
يجب أن يكون BASE_URL عنواناً عاماً يصل إليه خادم CaptchaAI؛ فـlocalhost لن يستقبل شيئاً. أثناء التطوير استخدم نفقاً يعرّض المنفذ، أو خادماً اختبارياً بعنوان ثابت.
SSE مقابل WebSocket مقابل الفحص الدوري
| المعيار | SSE | WebSocket | الفحص الدوري |
|---|---|---|---|
| اتجاه البيانات | من الخادم إلى العميل | ثنائي الاتجاه | من العميل إلى الخادم |
| البروتوكول | HTTP/1.1 فما فوق | WS/WSS | HTTP |
| إعادة الاتصال | مدمجة في المتصفح | يدوية | لا تنطبق |
| دعم المتصفحات | كل المتصفحات الحديثة | كل المتصفحات الحديثة | الجميع |
| كلفة التنفيذ | منخفضة | متوسطة | منخفضة |
| الطلبات المهدرة | لا شيء | لا شيء | كثيرة |
| ملاءمته لنتائج CAPTCHA | الخيار الأنسب | مبالغة تقنية | يعمل لكنه مسرف |
نتيجة CAPTCHA تسير في اتجاه واحد، فلا داعي للكلفة التشغيلية لـ WebSocket — إلا إذا كانت لوحتك تتبادل أوامر مع الخادم في الاتجاهين.
التوسّع خلف موازن التحميل باستخدام Redis
اتصال SSE ذو حالة: يبقى مربوطاً بالخادم الذي فتحه. فمع ثلاث نسخ خلف موازن تحميل قد يصل رد النداء إلى النسخة الثانية بينما يجلس العميل على الأولى، فتضيع النتيجة بصمت. الحل المعتاد ناقل رسائل عبر Redis Pub/Sub.
# Callback handler publishes to Redis
import redis
r = redis.Redis()
r.publish(f"captcha:{client_id}", json.dumps(message))
# SSE handler subscribes to Redis
pubsub = r.pubsub()
pubsub.subscribe(f"captcha:{client_id}")
for msg in pubsub.listen():
if msg["type"] == "message":
yield f"data: {msg['data'].decode()}\n\n"
وانتبه لحد الاتصالات: ستة اتصالات SSE لكل نطاق على HTTP/1.1. فعّل HTTP/2 لرفع الحد، أو مرّر نتائج كل المهام عبر دفق واحد لكل عميل.
سيناريو تشغيلي من السوق العربي
تخيّل فريق QA في متجر إلكتروني بالرياض يختبر مسار إتمام الشراء قبل موسم تخفيضات: ثلاثون جلسة اختبار متزامنة، ولوحة داخلية تعرض حالة كل جلسة على شاشة في غرفة الفريق. مع الفحص الدوري كانت اللوحة تتأخر ثوانيَ عن الواقع بلا فائدة. بعد ربط الـ pingback بدفق SSE صارت كل جلسة تنتقل من «قيد المعالجة» إلى «جاهزة» لحظة وصول الرمز.
الجانب الآخر هو حجم الاشتراك: التسعير في CaptchaAI قائم على عدد الـ threads المتزامنة لا على عدد عمليات الحل. ثلاثون جلسة متزامنة تفوق سعة خطة BASIC — 15 دولاراً شهرياً و5 threads — وتجد راحتها في STANDARD بـ 30 دولاراً و15 thread أو فيما فوقها. حدد الرقم من أعلى تزامن حقيقي لديك لا من إجمالي المهام اليومية.
أخطاء شائعة وكيف تعالجها
| العَرَض | السبب المرجّح | المعالجة |
|---|---|---|
| ينقطع الدفق كل 30 ثانية تقريباً | مهلة الخادم الوسيط تغلق الاتصال الصامت | أرسل تعليق : keepalive دورياً وارفع مهلة الوسيط |
| النتيجة لا تصل رغم نجاح المهمة | رد النداء وصل إلى نسخة خادم غير التي تحمل الاتصال | أدخل Redis Pub/Sub بين معالج رد النداء ومعالج الدفق |
| الأحداث تصل مجمّعة ومتأخرة | تخزين مؤقت في nginx أو في طبقة CDN | عطّل التخزين المؤقت لهذا المسار عبر X-Accel-Buffering: no |
| المتصفح يعيد الاتصال بلا توقف | تنسيق الحدث ناقص | تأكد من إنهاء كل حدث بسطرين فارغين ومن صحة بنية data: |
| رد النداء لا يصل أصلاً | العنوان في pingback غير قابل للوصول من الخارج |
استخدم نطاقاً عاماً على HTTPS وراجع قواعد الجدار الناري |
أسئلة شائعة
هل يتغير شكل الطلب إلى in.php عند اعتماد SSE؟
لا. تُرسل المهمة بالطريقة والمعاملات نفسها، وكل ما يضاف معامل pingback يحمل عنوان نقطة النهاية المستقبِلة، ويبقى الرد الأول حاملاً معرّف المهمة.
ماذا يحدث لو أغلق المستخدم الصفحة قبل وصول النتيجة؟
سيصل رد النداء ولن يجد اتصالاً مفتوحاً. احفظ النتيجة في مخزن قصير العمر مفهرس بمعرّف العميل وابعثها فور إعادة الاتصال، وإلا ضاعت نتيجة دفعت ثمن حلها.
كم مهمة متزامنة أحتاج فعلاً؟
كل مهمة قيد التنفيذ تشغل thread واحداً حتى تنتهي، ثم يتحرر للمهمة التالية. اقسم ذروة التزامن لديك على هذا الأساس: خمس مهام متوازية تكفيها BASIC بـ 15 دولاراً، وخمس عشرة مهمة تحتاج STANDARD بـ 30 دولاراً. أما عدد عمليات الحل داخل الخطة فغير محدود.
هل يلغي SSE الحاجة إلى الفحص الدوري نهائياً؟
الأفضل إبقاؤه كخطة بديلة: إذا لم يصل رد النداء خلال مهلة تحددها — دقيقة مثلاً — فاستعلم عن res.php مرة واحدة لتلك المهمة. هذا يحميك من انقطاع شبكي دون العودة إلى الاستطلاع المستمر.
الخطوات التالية
- البدء السريع مع CaptchaAI: حلّ أول كابتشا في 5 دقائق
- كيفية حلّ reCAPTCHA v2 عبر الـ API: دليل خطوة بخطوة
- التعامل مع Cloudflare Turnstile عبر الـ API
- حل GeeTest v3 خطوة بخطوة