สรุปสั้นๆ: Polling คือการตรวจสอบการอัปเดตเป็นระยะ (เรียบง่ายแต่ไม่มีประสิทธิภาพ) Webhooks คือการส่งการอัปเดตแบบเรียลไทม์ (มีประสิทธิภาพแต่ซับซ้อน) ใช้ Polling สำหรับการตรวจสอบที่ไม่บ่อยนัก และ Webhooks สำหรับการอัปเดตแบบเรียลไทม์ Modern PetstoreAPI รองรับทั้งสองรูปแบบพร้อมการส่ง Webhook ที่เชื่อถือได้
ทำความเข้าใจความแตกต่าง
Polling: Client ถาม "มีการอัปเดตหรือไม่?" ซ้ำๆ Webhooks: Server บอกว่า "มีการอัปเดต!" เมื่อมีบางสิ่งเกิดขึ้น
คำอุปมา:
- Polling = ตรวจสอบตู้จดหมายของคุณทุกชั่วโมง
- Webhooks = บุรุษไปรษณีย์กดกริ่งเมื่อมีจดหมายมาส่ง
Polling: ทำงานอย่างไร
Client ทำการร้องขอเป็นระยะเพื่อตรวจสอบการเปลี่ยนแปลง
// Poll ทุก 30 วินาที
setInterval(async () => {
const response = await fetch('https://petstoreapi.com/api/v1/orders/123');
const order = await response.json();
if (order.status === 'completed') {
console.log('Order completed!', order);
clearInterval(pollInterval);
}
}, 30000);
รูปแบบการ Polling:
Simple polling (การ Polling แบบง่าย):
GET /api/v1/orders/123
# Returns current order state (ส่งคืนสถานะคำสั่งซื้อปัจจุบัน)
Conditional polling (การ Polling แบบมีเงื่อนไข (ETag)):
GET /api/v1/orders/123
If-None-Match: "abc123"
# Returns 304 Not Modified if unchanged (ส่งคืน 304 Not Modified หากไม่มีการเปลี่ยนแปลง)
# Returns 200 with new data if changed (ส่งคืน 200 พร้อมข้อมูลใหม่หากมีการเปลี่ยนแปลง)
Since-based polling (การ Polling ตามเวลาที่กำหนด):
GET /api/v1/orders/123/events?since=1710331200
# Returns events since timestamp (ส่งคืนเหตุการณ์ตั้งแต่เวลาที่กำหนด)
Webhooks: ทำงานอย่างไร
Server ส่ง HTTP POST ไปยัง Endpoint ของคุณเมื่อมีเหตุการณ์เกิดขึ้น
ขั้นตอนการตั้งค่า:
// 1. Register webhook endpoint (ลงทะเบียน webhook endpoint)
POST /api/v1/webhooks
{
"url": "https://myapp.com/webhooks/petstore",
"events": ["order.created", "order.completed"],
"secret": "whsec_abc123"
}
// 2. Server sends webhook when event occurs (Server ส่ง webhook เมื่อมีเหตุการณ์เกิดขึ้น)
POST https://myapp.com/webhooks/petstore
{
"id": "evt_123",
"type": "order.completed",
"created": 1710331200,
"data": {
"orderId": "123",
"status": "completed",
"completedAt": "2024-01-01T12:00:00Z"
}
}
// 3. Verify and process webhook (ตรวจสอบและประมวลผล webhook)
// Respond with 200 OK (ตอบกลับด้วย 200 OK)
ควรใช้ Polling เมื่อใด
เหมาะสำหรับ:
- การตรวจสอบที่ไม่บ่อยนัก (ชั่วโมงละครั้ง)
- ทรัพยากรจำนวนน้อย
- การใช้งานที่เรียบง่าย
- เมื่อคุณเป็นผู้ควบคุม Client
- การทดสอบและดีบัก
ตัวอย่าง:
- การตรวจสอบสถานะรายงานประจำวัน
- การซิงค์ผู้ติดต่อทุกสองสามนาที
- การตรวจสอบความสมบูรณ์ของเซิร์ฟเวอร์
- การตรวจสอบสถานะการชำระเงิน (ไม่บ่อยนัก)
Polling ใช้ได้ดีเมื่อ:
- การอัปเดตไม่บ่อยนัก
- ความล่าช้าเล็กน้อยยอมรับได้
- คุณต้องการการใช้งานที่เรียบง่าย
- ทรัพยากรมีขนาดเล็ก
ควรใช้ Webhooks เมื่อใด
เหมาะสำหรับ:
- การอัปเดตแบบเรียลไทม์
- มีทรัพยากรจำนวนมากที่ต้องตรวจสอบ
- เหตุการณ์ที่ต้องทันเวลา
- การผสานรวมกับบุคคลที่สาม
- การอัปเดตความถี่สูง
ตัวอย่าง:
- การยืนยันการชำระเงิน
- ข้อความแชท
- การแจ้งเตือนราคาหุ้น
- การเปลี่ยนแปลงสถานะคำสั่งซื้อ
- การแจ้งเตือน CI/CD
Webhooks ดีกว่าเมื่อ:
- การอัปเดตต้องเกิดขึ้นทันที
- Polling ไม่มีประสิทธิภาพ
- มี Client จำนวนมากตรวจสอบทรัพยากรเดียวกัน
- คุณต้องการลดโหลดของเซิร์ฟเวอร์
ตารางเปรียบเทียบ
<!--kg-card-begin: html-->
| ปัจจัย | Polling | Webhooks |
|---|---|---|
| ความหน่วง (Latency) | ขึ้นอยู่กับช่วงเวลา Polling | เรียลไทม์ |
| โหลดเซิร์ฟเวอร์ | สูง (คำขอเปล่าจำนวนมาก) | ต่ำ (เฉพาะเหตุการณ์จริง) |
| ความซับซ้อน | ง่าย | ซับซ้อน |
| ความน่าเชื่อถือ | สูง (Client ควบคุมการลองใหม่) | ปานกลาง (ต้องมี Logic การลองใหม่) |
| การตั้งค่า | ไม่มี | การลงทะเบียน Endpoint |
| ปัญหาไฟร์วอลล์ | ไม่มี (ขาออกเท่านั้น) | อาจต้อง Whitelisting |
| ค่าใช้จ่าย | สูงกว่า (คำขอมากกว่า) | ต่ำกว่า (คำขอน้อยกว่า) |
| ดีที่สุดสำหรับ | การตรวจสอบที่ไม่บ่อยนัก | การอัปเดตแบบเรียลไทม์ |
การนำ Polling ไปใช้งาน
Basic Polling (การ Polling ขั้นพื้นฐาน)
async function pollOrderStatus(orderId, callback) {
let lastStatus = null;
const poll = async () => {
try {
const response = await fetch(`https://petstoreapi.com/api/v1/orders/${orderId}`);
const order = await response.json();
// Only callback if status changed (เรียก Callback เฉพาะเมื่อสถานะเปลี่ยน)
if (order.status !== lastStatus) {
lastStatus = order.status;
callback(order);
}
// Stop polling if terminal state (หยุด Polling หากถึงสถานะสุดท้าย)
if (['completed', 'cancelled'].includes(order.status)) {
return;
}
// Continue polling (ดำเนินการ Polling ต่อไป)
setTimeout(poll, 5000);
} catch (error) {
console.error('Polling error:', error);
setTimeout(poll, 30000); // Back off on error (รอมากขึ้นเมื่อเกิดข้อผิดพลาด)
}
};
poll();
}
// Usage (การใช้งาน)
pollOrderStatus('order-123', (order) => {
console.log(`Order status: ${order.status}`);
});
Smart Polling (Exponential Backoff) (การ Polling อัจฉริยะ (การถอยกลับแบบทวีคูณ))
async function smartPoll(url, callback, options = {}) {
const {
maxRetries = 10,
initialInterval = 1000,
maxInterval = 60000,
stopCondition = () => false
} = options;
let retries = 0;
let interval = initialInterval;
let lastData = null;
const poll = async () => {
try {
const response = await fetch(url);
const data = await response.json();
// Callback if data changed (เรียก Callback หากข้อมูลเปลี่ยน)
if (JSON.stringify(data) !== JSON.stringify(lastData)) {
lastData = data;
callback(data);
}
// Stop if condition met (หยุดหากตรงตามเงื่อนไข)
if (stopCondition(data)) {
return;
}
// Reset interval on successful request (รีเซ็ตช่วงเวลาเมื่อคำขอสำเร็จ)
interval = initialInterval;
} catch (error) {
retries++;
if (retries >= maxRetries) {
throw new Error('Max retries exceeded');
}
}
// Schedule next poll with exponential backoff (กำหนดเวลา Polling ครั้งถัดไปด้วยการถอยกลับแบบทวีคูณ)
setTimeout(poll, interval);
interval = Math.min(interval * 2, maxInterval);
};
poll();
}
// Usage: Poll order until completed (การใช้งาน: Polling คำสั่งซื้อจนกว่าจะเสร็จสมบูรณ์)
smartPoll('https://petstoreapi.com/api/v1/orders/123',
(order) => console.log('Order:', order),
{
stopCondition: (order) => ['completed', 'cancelled'].includes(order.status),
initialInterval: 2000,
maxInterval: 30000
}
);
Polling with ETag (การ Polling ด้วย ETag)
async function pollWithEtag(url, callback) {
let etag = null;
const poll = async () => {
const headers = {};
if (etag) {
headers['If-None-Match'] = etag;
}
const response = await fetch(url, { headers });
if (response.status === 304) {
// Not modified, continue polling (ไม่มีการแก้ไข ดำเนินการ Polling ต่อไป)
setTimeout(poll, 30000);
return;
}
const data = await response.json();
etag = response.headers.get('etag');
callback(data);
setTimeout(poll, 30000);
};
poll();
}
การนำ Webhooks ไปใช้งาน
Registering Webhooks (การลงทะเบียน Webhooks)
// Register webhook endpoint (ลงทะเบียน webhook endpoint)
async function registerWebhook(url, events) {
const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
url,
events,
secret: generateSecret()
})
});
return response.json();
}
function generateSecret() {
return 'whsec_' + crypto.randomBytes(32).toString('hex');
}
Receiving Webhooks (การรับ Webhooks)
const express = require('express');
const crypto = require('crypto');
const app = express();
// Raw body parser for signature verification (ตัวแยกวิเคราะห์เนื้อหาดิบสำหรับการตรวจสอบลายเซ็น)
app.use('/webhooks', express.raw({ type: 'application/json' }));
app.post('/webhooks/petstore', async (req, res) => {
const signature = req.headers['x-petstore-signature'];
const body = req.body;
// Verify signature (ตรวจสอบลายเซ็น)
const isValid = verifySignature(body, signature, process.env.WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(body.toString());
// Process event (ประมวลผลเหตุการณ์)
switch (event.type) {
case 'order.created':
await handleOrderCreated(event.data);
break;
case 'order.completed':
await handleOrderCompleted(event.data);
break;
case 'order.cancelled':
await handleOrderCancelled(event.data);
break;
}
// Acknowledge receipt (ยืนยันการรับ)
res.status(200).json({ received: true });
});
function verifySignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
Testing Webhooks Locally (การทดสอบ Webhooks ในเครื่อง)
# Use ngrok to expose local endpoint (ใช้ ngrok เพื่อเปิดเผย endpoint ในเครื่อง)
ngrok http 3000
# Register ngrok URL as webhook endpoint (ลงทะเบียน URL ของ ngrok เป็น webhook endpoint)
curl -X POST https://petstoreapi.com/api/v1/webhooks \
-H "Authorization: Bearer $TOKEN" \
-d '{
"url": "https://abc123.ngrok.io/webhooks/petstore",
"events": ["order.created", "order.completed"]
}'
Reliable Webhook Delivery (การส่ง Webhook ที่เชื่อถือได้)
Webhooks อาจล้มเหลวได้ ควรใช้ Logic การลองใหม่
Sender Side (Server) (ฝั่งผู้ส่ง (Server))
// Queue webhooks for delivery (จัดคิว webhooks สำหรับการจัดส่ง)
const webhookQueue = [];
async function sendWebhook(event) {
const webhooks = await db.webhooks.findMany({
where: { events: { contains: event.type } }
});
for (const webhook of webhooks) {
webhookQueue.push({
webhook,
event,
attempts: 0,
nextAttempt: Date.now()
});
}
processQueue();
}
async function processQueue() {
const now = Date.now();
for (const item of webhookQueue) {
if (item.nextAttempt > now) continue;
try {
await deliverWebhook(item);
// Remove from queue on success (ลบออกจากคิวเมื่อสำเร็จ)
webhookQueue.splice(webhookQueue.indexOf(item), 1);
} catch (error) {
// Schedule retry with exponential backoff (กำหนดเวลาลองใหม่ด้วยการถอยกลับแบบทวีคูณ)
item.attempts++;
item.nextAttempt = now + getBackoff(item.attempts);
if (item.attempts >= 5) {
// Mark as failed after 5 attempts (ทำเครื่องหมายว่าล้มเหลวหลังจาก 5 ครั้ง)
await markWebhookFailed(item);
webhookQueue.splice(webhookQueue.indexOf(item), 1);
}
}
}
setTimeout(processQueue, 5000);
}
function getBackoff(attempt) {
// 1min, 5min, 15min, 1hr, 4hr
const delays = [60000, 300000, 900000, 3600000, 14400000];
return delays[attempt - 1] || delays[delays.length - 1];
}
async function deliverWebhook({ webhook, event }) {
const signature = generateSignature(event, webhook.secret);
const response = await fetch(webhook.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Petstore-Signature': signature,
'X-Petstore-Event': event.type
},
body: JSON.stringify(event),
timeout: 10000
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
}
Receiver Side (Client) (ฝั่งผู้รับ (Client))
// Idempotent webhook handling (การจัดการ Webhook แบบ Idempotent)
const processedEvents = new Set();
app.post('/webhooks/petstore', async (req, res) => {
const event = JSON.parse(req.body.toString());
// Skip if already processed (idempotency) (ข้ามหากประมวลผลไปแล้ว (idempotency))
if (processedEvents.has(event.id)) {
return res.status(200).json({ received: true });
}
try {
await processEvent(event);
processedEvents.add(event.id);
// Clean up old event IDs (keep last 1000) (ล้าง ID เหตุการณ์เก่า (เก็บ 1000 รายการล่าสุด))
if (processedEvents.size > 1000) {
const arr = Array.from(processedEvents);
arr.slice(0, arr.length - 1000).forEach(id => processedEvents.delete(id));
}
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook processing error:', error);
// Return 5xx to trigger retry (ส่งคืน 5xx เพื่อเรียกการลองใหม่)
res.status(500).json({ error: 'Processing failed' });
}
});
async function processEvent(event) {
// Process the event (ประมวลผลเหตุการณ์)
switch (event.type) {
case 'order.created':
await handleOrderCreated(event.data);
break;
// ... handle other events (...จัดการเหตุการณ์อื่นๆ)
}
}
Hybrid Approach (แนวทางแบบผสมผสาน)
ใช้ทั้ง Polling และ Webhooks สำหรับการอัปเดตที่สำคัญ
class OrderMonitor {
constructor(orderId, callback) {
this.orderId = orderId;
this.callback = callback;
this.pollInterval = null;
}
async start() {
// Start with polling for immediate feedback (เริ่มต้นด้วย Polling เพื่อรับการตอบรับทันที)
this.startPolling();
// Register webhook for real-time update (ลงทะเบียน webhook สำหรับการอัปเดตแบบเรียลไทม์)
await this.registerWebhook();
}
startPolling() {
this.pollInterval = setInterval(async () => {
const order = await this.fetchOrder();
this.callback(order);
if (['completed', 'cancelled'].includes(order.status)) {
this.stop();
}
}, 10000);
}
async registerWebhook() {
const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
method: 'POST',
headers: { 'Authorization': `Bearer ${TOKEN}` },
body: JSON.stringify({
url: 'https://myapp.com/webhooks/petstore',
events: [`order.${this.orderId}`],
oneTime: true // Auto-delete after first delivery (ลบอัตโนมัติหลังจากการส่งครั้งแรก)
})
});
this.webhookId = (await response.json()).id;
}
stop() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
if (this.webhookId) {
fetch(`https://petstoreapi.com/api/v1/webhooks/${this.webhookId}`, {
method: 'DELETE'
});
}
}
}
คำถามที่พบบ่อย
ถาม: ฉันควร Polling บ่อยแค่ไหน?ขึ้นอยู่กับความเร่งด่วน 30 วินาทีสำหรับการอัปเดตเกือบเรียลไทม์ 5 นาทีสำหรับเรื่องที่ไม่เร่งด่วน ควรสร้างสมดุลระหว่างความสดใหม่ของข้อมูลกับโหลดของเซิร์ฟเวอร์
ถาม: จะเกิดอะไรขึ้นถ้า webhook endpoint ของฉันล่ม?ผู้ให้บริการ Webhook ที่ดีจะลองใหม่ด้วย Exponential backoff ควรใช้ Idempotency เพื่อจัดการกับการส่งซ้ำ
ถาม: ฉันจะรักษาความปลอดภัยของ Webhooks ได้อย่างไร?ตรวจสอบลายเซ็นโดยใช้ Shared secrets ใช้อะไรรองรับ HTTPS เท่านั้น และตรวจสอบความถูกต้องของข้อมูลเหตุการณ์
ถาม: ฉันสามารถใช้ Webhooks สำหรับข้อมูลในอดีตได้หรือไม่?ไม่ได้ Webhooks มีไว้สำหรับเหตุการณ์ใหม่เท่านั้น ใช้ Polling หรือ Batch API สำหรับข้อมูลในอดีต
ถาม: ฉันควรใช้ Polling หรือ Webhooks สำหรับแอปพลิเคชันมือถือ?Polling นั้นง่ายกว่าสำหรับมือถือ Webhooks ต้องใช้ Push Notifications เป็นตัวกลาง
ถาม: ฉันจะดีบักปัญหา Webhook ได้อย่างไร?ใช้เครื่องมือเช่น webhook.site สำหรับการทดสอบ บันทึกการส่ง Webhook ทั้งหมด และจัดเตรียมประวัติเหตุการณ์ Webhook ใน API ของคุณ
Modern PetstoreAPI รองรับทั้ง Polling และ Webhooks ดู คู่มือ Webhooks สำหรับรายละเอียดการนำไปใช้งาน ทดสอบการผสานรวม Webhook ด้วย Apidog
Top comments (0)