Network Services — จาก Socket สู่ API
HTTP และ REST API คือการยกระดับแนวคิดจาก socket และ protocol ที่ออกแบบเองในหัวข้อก่อนหน้า ให้กลายเป็นมาตรฐานที่ระบบต่างภาษา ต่างทีม ต่างองค์กร คุยกันได้
1HTTP Request/Response ในมุมของ Distributed Application
หัวข้อก่อนหน้าออกแบบ protocol เองทั้งหมด (framing, command format) — HTTP คือตัวอย่างของ protocol ที่มีคนออกแบบไว้ให้แล้วและใช้กันทั่วโลก มันแก้ปัญหาเดียวกับที่เจอในหัวข้อที่ 1 ทุกอย่าง เพียงแต่เป็นมาตรฐาน:
| ปัญหาจากหัวข้อที่ 1 | วิธีที่ HTTP แก้ |
|---|---|
| Message framing | Header ระบุ Content-Length (คล้าย length-prefixed) หรือ Transfer-Encoding: chunked, บรรทัดว่างคั่น header กับ body (คล้าย delimiter-based) |
| Serialization | ไม่ผูกติดรูปแบบเดียว — Content-Type บอกว่า body เป็น JSON, HTML, form-data ฯลฯ |
| Command format ที่ออกแบบเอง (SET/GET/DELETE) | Method มาตรฐาน (GET/POST/PUT/DELETE) ที่ทุกระบบเข้าใจตรงกันโดยไม่ต้องตกลงกันใหม่ |
requests, browser, web framework) จัดการให้หมดแล้ว2API Contract
SET key value) เป็นทางการและตรวจสอบได้Contract ที่ดีทำให้ทีมพัฒนา client และทีมพัฒนา server ทำงานคู่ขนานกันได้โดยไม่ต้องรอกัน — ตราบใดที่ทั้งสองฝั่งยึด contract เดียวกัน ระบบก็ทำงานร่วมกันได้แม้ implementation ภายในจะต่างกันโดยสิ้นเชิง (คนละภาษา คนละทีม)
3REST API
doSetCourse, doGetCourse แบบ RPC| Method | ความหมาย | เทียบกับ Lab หัวข้อที่ 1 |
|---|---|---|
GET | อ่าน resource — ไม่มีผลข้างเคียง (safe) | เทียบเท่า GET key |
POST | สร้าง resource ใหม่ หรือกระทำที่ไม่ idempotent | ใกล้เคียง SET key value แบบสร้างใหม่ |
PUT | สร้างหรือแทนที่ resource ทั้งชุดแบบ idempotent (เรียกซ้ำผลลัพธ์เหมือนเดิม) | เทียบเท่า SET key value |
PATCH | แก้ไข resource บางส่วน | - |
DELETE | ลบ resource | เทียบเท่า DELETE key |
PUT /keys/course body: {"value": "NetworkComputing"} → 200/201
GET /keys/course → 200 {"value": "NetworkComputing"}
DELETE /keys/course → 204 หรือ 404 ถ้าไม่มี key นี้
โครงสร้าง URL /keys/{key} ทำให้ "key" เป็น resource ที่ระบุตัวตนได้ผ่าน URL โดยตรง — ต่างจาก protocol เดิมที่ key เป็นแค่ argument ตัวหนึ่งใน text command
HTTP Status Code
| กลุ่ม | ความหมาย | ตัวอย่างที่พบบ่อย |
|---|---|---|
| 2xx | สำเร็จ | 200 OK, 201 Created, 204 No Content |
| 3xx | redirect | 301 Moved Permanently, 304 Not Modified |
| 4xx | ผิดพลาดจากฝั่ง client | 400 Bad Request, 401 Unauthorized, 404 Not Found, 409 Conflict |
| 5xx | ผิดพลาดจากฝั่ง server | 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable |
4JSON Serialization และ Stateless Service
REST API ส่วนใหญ่ใช้ JSON เป็น body format (เหมือนที่ทำใน NDJSON ของหัวข้อที่ 1) — แต่ HTTP มี Content-Type: application/json ระบุชัดเจนแทนที่ต้องตกลงรูปแบบกันเอง
ทำไม statelessness สำคัญ: ถ้า server ตัวหนึ่งล่ม request ถัดไปจาก client เดิมส่งไป server ตัวอื่นได้ทันทีโดยไม่เสียข้อมูล (สำคัญมากสำหรับ load balancing และ scaling ในหัวข้อถัดไป) — เทียบกับ protocol ใน lab หัวข้อที่ 1 ที่ connection เดียวคุยกันยาวและ server จำ state ของ connection นั้นไว้ (stateful ในระดับ connection)
5Synchronous Communication
recv() ใน socket ที่บล็อกรอข้อมูล) ต่างจาก asynchronous ที่ client ส่งแล้วทำงานอื่นต่อ ไม่รอ response ทันที (เช่น message queue)Synchronous เข้าใจง่าย debug ง่าย แต่มีข้อเสียสำคัญที่จะเห็นชัดในหัวข้อที่ 3: ถ้า service ที่ถูกเรียกช้าหรือค้าง service ที่เรียกก็ค้างตามไปด้วย (cascading latency/failure)
6Authentication เบื้องต้น
| วิธี | กลไก | จุดอ่อน/ข้อควรระวัง |
|---|---|---|
| Basic Auth | ส่ง username:password เข้ารหัส Base64 ใน header Authorization | Base64 ไม่ใช่การเข้ารหัส (ถอดกลับได้ทันที) — ต้องใช้คู่กับ TLS/HTTPS เสมอ (ดู Network Security) |
| API Key | ส่ง token คงที่ผ่าน header เช่น X-API-Key | ถ้า key รั่วใช้แทนตัวได้ทันที ควร rotate ได้และจำกัดสิทธิ์ |
| Token-based (Bearer / JWT) | Server ออก token ให้หลัง login สำเร็จ client แนบ Authorization: Bearer <token> ทุก request ถัดไป | เข้ากับ stateless service ได้ดี (token มีข้อมูลครบในตัว ไม่ต้องเก็บ session ฝั่ง server) แต่ revoke token ก่อนหมดอายุทำได้ยากกว่า session |
7REST เทียบกับ RPC
| REST | RPC (เช่น gRPC, JSON-RPC) | |
|---|---|---|
| แนวคิด | กระทำต่อ resource ผ่าน HTTP method มาตรฐาน | เรียก "ฟังก์ชัน" บนเครื่องอื่นราวกับเรียกฟังก์ชัน local (getCourse(key)) |
| URL/endpoint | ผูกกับ resource (/keys/course) | ผูกกับ action/method (/GetCourse) |
| Schema | มักหลวม (JSON เปิดกว้าง) มี OpenAPI ช่วยกำหนด schema เพิ่ม | มักเข้มงวด กำหนด schema ล่วงหน้าเป็นไฟล์ (เช่น Protobuf `.proto`) แล้ว generate โค้ดทั้งสองฝั่ง |
| Performance | Text-based (JSON) ขนาดใหญ่กว่า | มักใช้ binary encoding (เช่น Protobuf) เร็วและเล็กกว่า |
| เหมาะกับ | Public API, web/mobile client ที่หลากหลาย | การสื่อสารภายในระหว่าง microservice ที่ควบคุมทั้งสองฝั่งเอง ต้องการ performance สูง |
8WebSocket และ Message Queue (ภาพรวม)
9Lab: ระบบสองบริการ — Service ที่เป็นทั้ง Server และ Client
สร้างสอง service ที่คุยกันเป็นทอด: Service A (order service) รับ request จาก external client แล้วต้องเรียก Service B (inventory service) เพื่อตรวจสต๊อกก่อนตอบกลับ
External Client → [Service A: /orders] → [Service B: /inventory/{item}]
(server) (server)
(client เมื่อเรียก B)
# service_a.py
from flask import Flask, request, jsonify
import requests
app = Flask(__name__)
INVENTORY_URL = "http://service-b:5001" # ดู nc-3 เรื่อง Docker service name
@app.post("/orders")
def create_order():
body = request.get_json()
item = body.get("item")
# Service A เป็น "client" ตอนเรียก Service B
resp = requests.get(f"{INVENTORY_URL}/inventory/{item}", timeout=3)
if resp.status_code != 200:
return jsonify({"error": "inventory check failed"}), 502
stock = resp.json()["stock"]
if stock <= 0:
return jsonify({"error": "out of stock"}), 409
return jsonify({"order_id": 1, "item": item, "status": "confirmed"}), 201
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
# service_b.py
from flask import Flask, jsonify
app = Flask(__name__)
stock_db = {"laptop": 5, "mouse": 0}
@app.get("/inventory/<item>")
def get_stock(item):
return jsonify({"item": item, "stock": stock_db.get(item, 0)})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5001)
- Service A เป็น server เมื่อรับ HTTP request จาก external client ที่
/orders - Service A เป็น client พร้อมกันในตัว เมื่อมันเรียก Service B ผ่าน
requests.get(...)— บทบาทเดียวกับที่เจอในหัวข้อ 2 (Client–Server Model) ของหัวข้อก่อนหน้า เพียงแต่ตอนนี้เป็นระบบสองบริการที่แยก process/แยกเครื่องกันจริง - ถ้า Service B ตอบช้าหรือไม่ตอบเลย Service A จะค้างตาม (synchronous) — ปูทางไปสู่ปัญหา partial failure ในหัวข้อที่ 3 โดยตรง
- อธิบายว่า HTTP แก้ปัญหา message framing และ serialization ที่เจอในหัวข้อที่ 1 อย่างไร
- เปรียบเทียบ REST กับ RPC — สถานการณ์แบบไหนควรเลือกใช้แบบไหน
- อธิบายว่าทำไม stateless service ถึงสำคัญต่อการ scale ระบบ
- ในระบบสองบริการ (Service A เรียก Service B) อธิบายว่า Service A มีบทบาทเป็นอะไรบ้าง ในสถานการณ์ไหน
- อธิบายความแตกต่างระหว่าง synchronous REST call กับ message queue แบบ asynchronous
10ชุดการทดลอง: จาก HTTP หนึ่งคำขอไปสู่ระบบสองบริการ
การทดลองที่ 1 — อ่าน HTTP แบบไม่ผ่าน Browser
- เปิด service แล้วใช้
curl -v http://localhost:5000/healthแยกบรรทัดที่เป็น request header, response header และ body - ใช้
curl -iและcurl -Iเปรียบเทียบ แล้วอธิบายว่า HEAD ช่วยตรวจ metadata โดยไม่รับ body ได้อย่างไร - ส่ง JSON ด้วยและไม่มี
Content-Type: application/jsonจดความต่าง ห้ามให้ server เดาสุ่มว่าข้อมูลเป็นชนิดใด - จับ packet แล้วหา TCP connection เดียวกับ request จากคู่ IP/port จากนั้นเชื่อมสิ่งที่เห็นใน HTTP กับ byte stream จากแล็ปก่อน
curl -v -X POST http://localhost:5000/orders \
-H "Content-Type: application/json" \
-H "X-Request-ID: lab-001" \
-d '{"item":"laptop","quantity":1}'
การทดลองที่ 2 — Contract, Validation และ Status Code
เขียนตาราง contract ก่อนลงมือแก้โค้ด ระบุ field ที่จำเป็น ชนิดข้อมูล ช่วงค่า response และ error ทุกแบบ จากนั้นสร้าง test case อย่างน้อย 10 กรณี รวม body ว่าง JSON เสีย quantity เป็นข้อความ จำนวนติดลบ item ไม่มีอยู่ และ request ซ้ำ
| เหตุการณ์ | รหัสที่ควรพิจารณา | เหตุผล |
|---|---|---|
| สร้าง order สำเร็จ | 201 | เกิด resource ใหม่ |
| ข้อมูลผิดรูปแบบ | 400 | server อ่าน request ได้แต่ใช้ไม่ได้ |
| ยังไม่ยืนยันตัวตน | 401 | ต้องแสดง credential ที่ใช้ได้ |
| ยืนยันแล้วแต่ไม่มีสิทธิ์ | 403 | รู้ว่าเป็นใครแต่ห้ามทำรายการ |
| สินค้าไม่มี | 404 | resource ที่อ้างถึงไม่พบ |
| สินค้าหมด | 409 | request ขัดกับสถานะปัจจุบัน |
| service ปลายทางพัง | 502/503/504 | แยกคำตอบเสีย เวลาหมด และยังไม่พร้อม |
การทดลองที่ 3 — Stateless ไม่ได้แปลว่าไม่มีข้อมูล
- รัน service สอง instance คนละ port แล้วสลับส่ง request ไปทั้งคู่
- เก็บ session ไว้ใน memory ของแต่ละ process แล้วสังเกตว่า login “หาย” เมื่อ request ไปอีก instance
- ย้าย state ที่ต้องแชร์ไปยัง database/cache หรือใช้ token ที่ตรวจสอบได้ทุก instance แล้วทดลองใหม่
- อธิบายว่า stateless หมายถึงแต่ละ request มีข้อมูลพอให้ประมวลผล ไม่ใช่ระบบไม่เก็บ order หรือ user เลย
การทดลองที่ 4 — Service A เป็นทั้ง Server และ Client
ใช้ตัวอย่าง order/inventory ด้านบน เพิ่ม log เวลาเริ่มและจบการเรียกแต่ละช่วง ทดลอง inventory ตอบใน 50 ms, 1 s และ 5 s วาดเส้นเวลาว่าเวลาของผู้ใช้ถูกใช้ที่ A หรือ B เท่าใด แล้วตั้ง timeout ฝั่ง A เป็น 2 s
การทดลองที่ 5 — Idempotency และคำขอที่ส่งซ้ำ
- ให้ client ส่ง
POST /ordersแล้วตัด connection ก่อนอ่าน response จากนั้นส่งซ้ำ นักศึกษาต้องตอบให้ได้ว่าคำสั่งแรกสำเร็จหรือไม่ - เพิ่ม
Idempotency-Keyให้ server เก็บผลลัพธ์ตาม key ถ้าได้ key เดิมให้คืนผลเดิมโดยไม่สร้าง order ใหม่ - เปรียบเทียบกับ
PUTที่กำหนด resource ID ล่วงหน้า และอธิบายว่า HTTP method ที่ควร idempotent ยังต้องพึ่ง implementation ที่ถูกต้อง
การทดลองที่ 6 — REST, RPC, WebSocket และ Queue เลือกตามงาน
อย่าเลือกจากความทันสมัย ให้เลือกจากรูปแบบการสื่อสาร ทดลองออกแบบงานเดียวกันสี่แบบ: ดู stock, คำนวณราคา, ส่งสถานะสด และส่งอีเมลภายหลัง แล้วกรอกตาราง
| คำถาม | REST | RPC | WebSocket | Queue |
|---|---|---|---|---|
| ต้องตอบทันทีหรือไม่ | มักใช่ | ใช่ | ผลักข้อมูลได้ต่อเนื่อง | ไม่จำเป็น |
| ผู้ส่งต้องรู้ว่าผู้รับพร้อมไหม | ต้องรู้ | ต้องรู้ | connection ต้องอยู่ | ไม่ต้องพร้อมพร้อมกัน |
| หลักฐานที่ควรเก็บ | status/body | method/error | message timeline | queue depth/ack |
ชิ้นงานและเกณฑ์ตรวจ
- OpenAPI หรือ contract ที่สอดคล้องกับโค้ดจริง พร้อมตัวอย่าง success/error
- ชุดทดสอบที่รันซ้ำได้ ไม่ใช่คลิกทดลองด้วยมืออย่างเดียว
- เส้นเวลาของ request ข้ามสอง service พร้อม request ID เดียวกัน
- รายงาน failure อย่างน้อย 5 แบบ ระบุสิ่งที่ผู้ใช้เห็น สิ่งที่ log เห็น และการแก้ที่เลือก
- บันทึกสั้น ๆ ว่าถ้าระบบนี้โตขึ้น จุดใดควรยัง synchronous และจุดใดควรเปลี่ยนเป็น queue
11สรุปและขั้นตอนถัดไป
เราเริ่มจากข้อความ HTTP หนึ่งฉบับ แล้วเห็นว่าเมื่อบริการหนึ่งเรียกอีกบริการ ความช้าและความล้มเหลวจะไหลย้อนกลับมาหาผู้ใช้ ขั้นถัดไปคือทำให้ failure เหล่านี้เกิดซ้ำได้ วัดได้ และ deploy ระบบโดยไม่ต้องหวังว่าทุกส่วนจะทำงานพร้อมกันเสมอ