Tutorial

Server-Sent Events untuk Notifikasi Solve CAPTCHA Real-Time

Polling res.php setiap 5 detik berfungsi, tetapi menyia-nyiakan request dan menambah latensi. Peristiwa Terkirim Server (SSE) memungkinkan server Anda mengirimkan solusi CAPTCHA ke klien yang terhubung begitu klien tiba – tanpa request yang sia-sia, pengiriman dalam hitungan sepersekian detik.

Bagaimana SSE Sesuai dengan Alur Kerja CAPTCHA

[Client] ← SSE stream ← [Your Server] ← Callback ← [CaptchaAI]
   ↓                          ↑
   Submit task → [CaptchaAI] ──┘ (pingback URL points to your server)
  1. Klien terhubung ke endpoint SSE Anda (koneksi HTTP persisten)
  2. Klien mengirimkan tugas CAPTCHA ke CaptchaAI dengan pingback menunjuk ke server Anda
  3. CaptchaAI memecahkan dan mengirimkan hasilnya ke endpoint callback Anda
  4. Server Anda mengirimkan hasilnya melalui aliran SSE ke klien

Implementasi Penuh – Python (Flask)

pelayan

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)

Klien Peramban

<!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>

Implementasi Penuh – JavaScript (Ekspres)

pelayan

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"));

SSE vs WebSocket vs Polling

Fitur SSE Soket Web pemungutan suara
Arah Server -> Klien Dua arah Klien -> Server
Protokol HTTP/1.1+ WS/WSS HTTP
Sambungkan kembali secara otomatis Bawaan petunjuk N/A
Dukungan peramban Semuanya modern Semuanya modern Semua
Kompleksitas Rendah Sedang Rendah
Request yang sia-sia Tidak ada Tidak ada Banyak
Terbaik untuk hasil CAPTCHA Iya Berlebihan Bekerja tapi boros

SSE ideal untuk hasil CAPTCHA karena data hanya mengalir dari server ke klien.

Pertimbangan Produksi

Penskalaan dengan Beberapa Instans Server

Koneksi SSE bersifat stateful — jika server Anda memiliki beberapa instance di belakang penyeimbang beban, callback mungkin mengenai instance yang berbeda dari instance yang menyimpan koneksi SSE klien.

Solusi: Gunakan Redis Pub/Sub sebagai bus pesan:

# 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"

Batas Koneksi

Browser membatasi koneksi SSE hingga 6 per domain (HTTP/1.1). Gunakan HTTP/2 untuk batas yang lebih tinggi, atau gandakan beberapa hasil tugas melalui satu koneksi SSE per klien.

Pemecahan Masalah

Masalah Penyebab Solusi
Koneksi SSE terputus setiap 30 detik Timeout penyeimbang Proxy/load Kirim komentar tetap hidup; menambah timeout proxy
Hasil tidak sampai Callback mengenai contoh server yang berbeda Tambahkan Redis Pub/Sub antara callback dan penangan SSE
Browser menunjukkan kesalahan di konsol Header CORS hilang Tambahkan header Access-Control-Allow-Origin ke endpoint SSE
Beberapa koneksi ulang Server mengirimkan SSE yang salah format Pastikan \n\n menghentikan setiap acara; memvalidasi format data

Pertanyaan Umum

Apakah SSE bekerja di belakang Cloudflare?

Ya, tapi Cloudflare mungkin melakukan buffer terhadap respons. Nonaktifkan buffering respons dengan header X-Accel-Buffering: no, atau gunakan mode streaming Cloudflare.

Berapa banyak koneksi SSE bersamaan yang dapat ditangani oleh satu server?

Node.js menangani 10.000+ koneksi SSE secara bersamaan dengan mudah karena masing-masing koneksi merupakan koneksi HTTP ringan yang tetap aktif. Python dengan threading lebih terbatas – gunakan kerangka async (FastAPI dengan asyncio) untuk konkurensi tinggi.

Haruskah saya menggunakan SSE untuk klien non-browser?

Untuk alat CLI atau layanan backend, penanganan callback langsung atau pendekatan berbasis antrean lebih sederhana. SSE paling berguna saat mendorong hasil ke dashboard berbasis browser atau aplikasi web.

Langkah Selanjutnya

Streaming solusi CAPTCHA secara real-time —dapatkan kunci API CaptchaAI Andadan sambungkan SSE ke saluran callback Anda.

Panduan terkait:

Komentar dinonaktifkan untuk artikel ini.