Network Computing · แล็ปโน้ต 2 จาก 5

Network Services — จาก Socket สู่ API

HTTP และ REST API คือการยกระดับแนวคิดจาก socket และ protocol ที่ออกแบบเองในหัวข้อก่อนหน้า ให้กลายเป็นมาตรฐานที่ระบบต่างภาษา ต่างทีม ต่างองค์กร คุยกันได้

🔗
ต่อยอดจาก: หัวข้อที่ 1 — Network Programming (framing, serialization) และ OSI L7 Application Layer (HTTP พื้นฐาน) — บทความนี้เจาะลึกมุมมอง "distributed application" ของ HTTP มากกว่า

1HTTP Request/Response ในมุมของ Distributed Application

หัวข้อก่อนหน้าออกแบบ protocol เองทั้งหมด (framing, command format) — HTTP คือตัวอย่างของ protocol ที่มีคนออกแบบไว้ให้แล้วและใช้กันทั่วโลก มันแก้ปัญหาเดียวกับที่เจอในหัวข้อที่ 1 ทุกอย่าง เพียงแต่เป็นมาตรฐาน:

ปัญหาจากหัวข้อที่ 1วิธีที่ HTTP แก้
Message framingHeader ระบุ 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) ที่ทุกระบบเข้าใจตรงกันโดยไม่ต้องตกลงกันใหม่
HTTP วิ่งอยู่บนอะไร
HTTP เป็น application-layer protocol ที่ใช้ TCP เป็นพาหนะ (HTTP/1.1, HTTP/2) — ดังนั้นทุกปัญหาที่เจอในหัวข้อที่ 1 (byte stream ไม่ใช่ message, ต้อง framing เอง) ก็ยังอยู่จริงข้างใต้ เพียงแต่ library ของ HTTP (เช่น requests, browser, web framework) จัดการให้หมดแล้ว

2API Contract

นิยาม
API contract คือข้อตกลงที่ชัดเจนระหว่าง service ผู้ให้บริการกับ client ผู้เรียกใช้ ว่า: endpoint ไหนมีอยู่บ้าง, รับ input แบบไหน (format, field ที่จำเป็น), คืน output แบบไหน, error กรณีไหนตอบกลับอย่างไร — เป็นการทำให้ "protocol" ที่ออกแบบเองในหัวข้อที่ 1 (เช่น SET key value) เป็นทางการและตรวจสอบได้

Contract ที่ดีทำให้ทีมพัฒนา client และทีมพัฒนา server ทำงานคู่ขนานกันได้โดยไม่ต้องรอกัน — ตราบใดที่ทั้งสองฝั่งยึด contract เดียวกัน ระบบก็ทำงานร่วมกันได้แม้ implementation ภายในจะต่างกันโดยสิ้นเชิง (คนละภาษา คนละทีม)

3REST API

แนวคิดหลักของ REST
REST (Representational State Transfer) มอง API เป็นการกระทำต่อ resource (สิ่งของ/ข้อมูลที่ระบุตัวตนได้ด้วย URL) ผ่าน HTTP method มาตรฐาน แทนที่จะออกแบบ "คำสั่ง" เอง เช่น 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
Key-Value Server หัวข้อที่ 1 เขียนใหม่เป็น REST
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
3xxredirect301 Moved Permanently, 304 Not Modified
4xxผิดพลาดจากฝั่ง client400 Bad Request, 401 Unauthorized, 404 Not Found, 409 Conflict
5xxผิดพลาดจากฝั่ง server500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable
ข้อควรจำ
Status code ไม่ใช่แค่ "รายละเอียดสวยงาม" — มันคือส่วนหนึ่งของ contract ที่ client (และ retry logic ในหัวข้อที่ 3) ใช้ตัดสินใจว่าควร retry (5xx บางกรณี) หรือไม่ควร retry เลย (4xx ส่วนใหญ่ เพราะ request ผิดเองซ้ำไปก็ยังผิด)

4JSON Serialization และ Stateless Service

REST API ส่วนใหญ่ใช้ JSON เป็น body format (เหมือนที่ทำใน NDJSON ของหัวข้อที่ 1) — แต่ HTTP มี Content-Type: application/json ระบุชัดเจนแทนที่ต้องตกลงรูปแบบกันเอง

Stateless Service คืออะไร
Service เป็น stateless เมื่อแต่ละ request มีข้อมูลครบถ้วนในตัวเองสำหรับ process ได้ทันที โดย server ไม่ต้องจำว่า request ก่อนหน้าจาก client รายเดียวกันคืออะไร (ไม่มี session state ค้างใน memory ของ server ตัวใดตัวหนึ่ง)

ทำไม statelessness สำคัญ: ถ้า server ตัวหนึ่งล่ม request ถัดไปจาก client เดิมส่งไป server ตัวอื่นได้ทันทีโดยไม่เสียข้อมูล (สำคัญมากสำหรับ load balancing และ scaling ในหัวข้อถัดไป) — เทียบกับ protocol ใน lab หัวข้อที่ 1 ที่ connection เดียวคุยกันยาวและ server จำ state ของ connection นั้นไว้ (stateful ในระดับ connection)

5Synchronous Communication

นิยาม
REST call แบบทั่วไปเป็น synchronous — client ส่ง request แล้วบล็อกรอ response ก่อนทำงานขั้นถัดไป (เหมือน recv() ใน socket ที่บล็อกรอข้อมูล) ต่างจาก asynchronous ที่ client ส่งแล้วทำงานอื่นต่อ ไม่รอ response ทันที (เช่น message queue)

Synchronous เข้าใจง่าย debug ง่าย แต่มีข้อเสียสำคัญที่จะเห็นชัดในหัวข้อที่ 3: ถ้า service ที่ถูกเรียกช้าหรือค้าง service ที่เรียกก็ค้างตามไปด้วย (cascading latency/failure)

6Authentication เบื้องต้น

วิธีกลไกจุดอ่อน/ข้อควรระวัง
Basic Authส่ง username:password เข้ารหัส Base64 ใน header AuthorizationBase64 ไม่ใช่การเข้ารหัส (ถอดกลับได้ทันที) — ต้องใช้คู่กับ 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

RESTRPC (เช่น 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 โค้ดทั้งสองฝั่ง
PerformanceText-based (JSON) ขนาดใหญ่กว่ามักใช้ binary encoding (เช่น Protobuf) เร็วและเล็กกว่า
เหมาะกับPublic API, web/mobile client ที่หลากหลายการสื่อสารภายในระหว่าง microservice ที่ควบคุมทั้งสองฝั่งเอง ต้องการ performance สูง

8WebSocket และ Message Queue (ภาพรวม)

WebSocketเปิด TCP connection เดียวค้างไว้ (persistent) ให้ทั้งสองฝ่ายส่งข้อความหากันได้ตลอดเวลาแบบ full-duplex โดยไม่ต้องเปิด request/response ใหม่ทุกครั้งแบบ HTTP — เหมาะกับข้อมูล real-time (chat, live update, เกม) ตรงกับ TCP chat ที่เขียนเองในหัวข้อที่ 1 เพียงแต่ห่อด้วย handshake มาตรฐานที่ browser รองรับ
Message Queue(เช่น RabbitMQ, Kafka) ทำให้การสื่อสารเป็น asynchronous — ผู้ส่ง (producer) วางข้อความไว้ใน queue แล้วทำงานต่อทันทีโดยไม่รอผู้รับ (consumer) มาอ่าน ผู้รับดึงข้อความไปประมวลผลเมื่อพร้อม ช่วยลดปัญหา cascading failure จาก synchronous call ที่กล่าวถึงในหัวข้อ 5
ขอบเขตของบทนี้
ทั้งสองหัวข้อกล่าวถึงในระดับภาพรวมเพื่อให้เห็นแผนที่กว้างของการสื่อสารแบบ distributed — รายละเอียดเชิงลึก (WebSocket frame format, queue delivery guarantee) อยู่นอกขอบเขตวิชานี้

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 (order service) — Flask
# 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 (inventory service) — Flask
# 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)
สิ่งที่ Lab นี้ต้องแสดงให้เห็น
  • 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 โดยตรง
แนวคิดสำคัญก่อนเข้า Distributed Systems
ระบบที่ดูเหมือน "client" จากมุมหนึ่งเสมอเป็น "server" จากอีกมุมหนึ่งได้พร้อมกัน — สถาปัตยกรรม microservice ทั้งหมดสร้างจากหน่วยที่สลับบทบาทแบบนี้ต่อกันเป็นทอดๆ (chain of calls) ความเข้าใจนี้คือรากฐานก่อนจะเข้าใจปัญหา partial failure, cascading timeout, distributed tracing ในหัวข้อถัดไป
คำถามซ้อมสอบ
  1. อธิบายว่า HTTP แก้ปัญหา message framing และ serialization ที่เจอในหัวข้อที่ 1 อย่างไร
  2. เปรียบเทียบ REST กับ RPC — สถานการณ์แบบไหนควรเลือกใช้แบบไหน
  3. อธิบายว่าทำไม stateless service ถึงสำคัญต่อการ scale ระบบ
  4. ในระบบสองบริการ (Service A เรียก Service B) อธิบายว่า Service A มีบทบาทเป็นอะไรบ้าง ในสถานการณ์ไหน
  5. อธิบายความแตกต่างระหว่าง synchronous REST call กับ message queue แบบ asynchronous

10ชุดการทดลอง: จาก HTTP หนึ่งคำขอไปสู่ระบบสองบริการ

เป้าหมาย
แล็ปนี้ให้มอง API เป็นข้อตกลงระหว่างคนสองทีม ไม่ใช่เพียง URL ที่เรียกติด HTTP เปรียบเหมือนเคาน์เตอร์รับเรื่อง: method บอกว่าจะมาทำอะไร URL บอกว่าจะทำกับเรื่องไหน header คือข้อมูลกำกับ ซอง body คือเอกสาร และ status code คือใบตอบรับที่ทำให้ผู้ยื่นเรื่องรู้ว่าจะทำอย่างไรต่อ

การทดลองที่ 1 — อ่าน HTTP แบบไม่ผ่าน Browser

  1. เปิด service แล้วใช้ curl -v http://localhost:5000/health แยกบรรทัดที่เป็น request header, response header และ body
  2. ใช้ curl -i และ curl -I เปรียบเทียบ แล้วอธิบายว่า HEAD ช่วยตรวจ metadata โดยไม่รับ body ได้อย่างไร
  3. ส่ง JSON ด้วยและไม่มี Content-Type: application/json จดความต่าง ห้ามให้ server เดาสุ่มว่าข้อมูลเป็นชนิดใด
  4. จับ 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 ใหม่
ข้อมูลผิดรูปแบบ400server อ่าน request ได้แต่ใช้ไม่ได้
ยังไม่ยืนยันตัวตน401ต้องแสดง credential ที่ใช้ได้
ยืนยันแล้วแต่ไม่มีสิทธิ์403รู้ว่าเป็นใครแต่ห้ามทำรายการ
สินค้าไม่มี404resource ที่อ้างถึงไม่พบ
สินค้าหมด409request ขัดกับสถานะปัจจุบัน
service ปลายทางพัง502/503/504แยกคำตอบเสีย เวลาหมด และยังไม่พร้อม

การทดลองที่ 3 — Stateless ไม่ได้แปลว่าไม่มีข้อมูล

  1. รัน service สอง instance คนละ port แล้วสลับส่ง request ไปทั้งคู่
  2. เก็บ session ไว้ใน memory ของแต่ละ process แล้วสังเกตว่า login “หาย” เมื่อ request ไปอีก instance
  3. ย้าย state ที่ต้องแชร์ไปยัง database/cache หรือใช้ token ที่ตรวจสอบได้ทุก instance แล้วทดลองใหม่
  4. อธิบายว่า 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

ทำให้พัง
หยุด Service B, ใช้ port ผิด, ให้ B คืน JSON ผิด schema, คืน 500, และหน่วงเกิน timeout แต่ละกรณีต้องได้คำตอบจาก A ที่มีรูปแบบแน่นอน ไม่ควรปล่อย stack trace หรือค้างไม่สิ้นสุด การเรียก service ต่อกันเหมือนฝากเพื่อนไปซื้อของ: คนรอไม่ควรรอไปตลอด และต้องรู้ว่าของหมด ร้านปิด หรือเพื่อนหายไปกลางทางเป็นคนละเรื่อง

การทดลองที่ 5 — Idempotency และคำขอที่ส่งซ้ำ

  1. ให้ client ส่ง POST /orders แล้วตัด connection ก่อนอ่าน response จากนั้นส่งซ้ำ นักศึกษาต้องตอบให้ได้ว่าคำสั่งแรกสำเร็จหรือไม่
  2. เพิ่ม Idempotency-Key ให้ server เก็บผลลัพธ์ตาม key ถ้าได้ key เดิมให้คืนผลเดิมโดยไม่สร้าง order ใหม่
  3. เปรียบเทียบกับ PUT ที่กำหนด resource ID ล่วงหน้า และอธิบายว่า HTTP method ที่ควร idempotent ยังต้องพึ่ง implementation ที่ถูกต้อง

การทดลองที่ 6 — REST, RPC, WebSocket และ Queue เลือกตามงาน

อย่าเลือกจากความทันสมัย ให้เลือกจากรูปแบบการสื่อสาร ทดลองออกแบบงานเดียวกันสี่แบบ: ดู stock, คำนวณราคา, ส่งสถานะสด และส่งอีเมลภายหลัง แล้วกรอกตาราง

คำถามRESTRPCWebSocketQueue
ต้องตอบทันทีหรือไม่มักใช่ใช่ผลักข้อมูลได้ต่อเนื่องไม่จำเป็น
ผู้ส่งต้องรู้ว่าผู้รับพร้อมไหมต้องรู้ต้องรู้connection ต้องอยู่ไม่ต้องพร้อมพร้อมกัน
หลักฐานที่ควรเก็บstatus/bodymethod/errormessage timelinequeue depth/ack

ชิ้นงานและเกณฑ์ตรวจ

11สรุปและขั้นตอนถัดไป

เราเริ่มจากข้อความ HTTP หนึ่งฉบับ แล้วเห็นว่าเมื่อบริการหนึ่งเรียกอีกบริการ ความช้าและความล้มเหลวจะไหลย้อนกลับมาหาผู้ใช้ ขั้นถัดไปคือทำให้ failure เหล่านี้เกิดซ้ำได้ วัดได้ และ deploy ระบบโดยไม่ต้องหวังว่าทุกส่วนจะทำงานพร้อมกันเสมอ